diff --git a/Config/DefaultGame.ini b/Config/DefaultGame.ini index c3d15654f..6d8ee3b57 100644 --- a/Config/DefaultGame.ini +++ b/Config/DefaultGame.ini @@ -11,3 +11,17 @@ bRequiresPushToTalk=true InputData=/CommonUI/GenericInputData.GenericInputData_C bEnableEnhancedInputSupport=True + + +[CommonInputPlatformSettings_Windows CommonInputPlatformSettings] +DefaultInputType=MouseAndKeyboard +bSupportsMouseAndKeyboard=True +bSupportsTouch=False +bSupportsGamepad=True +DefaultGamepadName=Generic +bCanChangeGamepadType=True ++ControllerData=/Game/UI/Foundation/Platform/Input/GamepadPS5/CommonInput_Gamepad_PS5.CommonInput_Gamepad_PS5_C ++ControllerData=/Game/UI/Foundation/Platform/Input/GamepadXboxOne/CommonInput_Gamepad_XboxOne.CommonInput_Gamepad_XboxOne_C ++ControllerData=None ++ControllerData=None + diff --git a/Content/Blueprint/Lobby/TG_PlayerControllerLobby.uasset b/Content/Blueprint/Lobby/TG_PlayerControllerLobby.uasset index bd3bed1ba..145dbb1b7 100644 Binary files a/Content/Blueprint/Lobby/TG_PlayerControllerLobby.uasset and b/Content/Blueprint/Lobby/TG_PlayerControllerLobby.uasset differ diff --git a/Content/Maps/Login.umap b/Content/Maps/Login.umap index 362ce30f6..4a38f020f 100644 Binary files a/Content/Maps/Login.umap and b/Content/Maps/Login.umap differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Animations/Jog_Fwd.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Animations/Jog_Fwd.uasset new file mode 100644 index 000000000..f14dd7d6b Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Animations/Jog_Fwd.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Materials/Layers/ML_Latex_Black.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Materials/Layers/ML_Latex_Black.uasset new file mode 100644 index 000000000..855f72f3e Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Materials/Layers/ML_Latex_Black.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Materials/Layers/ML_ShinyPlastic_Beige.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Materials/Layers/ML_ShinyPlastic_Beige.uasset new file mode 100644 index 000000000..eca4b9f68 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Materials/Layers/ML_ShinyPlastic_Beige.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Materials/Layers/ML_ShinyPlastic_Beige_Logo.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Materials/Layers/ML_ShinyPlastic_Beige_Logo.uasset new file mode 100644 index 000000000..559a151a9 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Materials/Layers/ML_ShinyPlastic_Beige_Logo.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Materials/Layers/ML_SoftMetal.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Materials/Layers/ML_SoftMetal.uasset new file mode 100644 index 000000000..006da0206 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Materials/Layers/ML_SoftMetal.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Materials/M_MannequinUE4_Body.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Materials/M_MannequinUE4_Body.uasset new file mode 100644 index 000000000..215c7f18e Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Materials/M_MannequinUE4_Body.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Materials/M_MannequinUE4_ChestLogo.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Materials/M_MannequinUE4_ChestLogo.uasset new file mode 100644 index 000000000..e775ba023 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Materials/M_MannequinUE4_ChestLogo.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Meshes/SK_Mannequin.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Meshes/SK_Mannequin.uasset new file mode 100644 index 000000000..aba3b8fe6 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Meshes/SK_Mannequin.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Meshes/SK_Mannequin_PhysicsAsset.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Meshes/SK_Mannequin_PhysicsAsset.uasset new file mode 100644 index 000000000..2a8df827d Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Meshes/SK_Mannequin_PhysicsAsset.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Meshes/SK_Mannequin_Skeleton.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Meshes/SK_Mannequin_Skeleton.uasset new file mode 100644 index 000000000..5d864d04a Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Meshes/SK_Mannequin_Skeleton.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Rigs/IK_UE4_Mannequin.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Rigs/IK_UE4_Mannequin.uasset new file mode 100644 index 000000000..091ab7f01 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Rigs/IK_UE4_Mannequin.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Rigs/RTG_UE4Manny_UE5Manny.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Rigs/RTG_UE4Manny_UE5Manny.uasset new file mode 100644 index 000000000..887545f5b Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Rigs/RTG_UE4Manny_UE5Manny.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Rigs/RTG_UE5Manny_UE4Manny.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Rigs/RTG_UE5Manny_UE4Manny.uasset new file mode 100644 index 000000000..e2f50575c Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Rigs/RTG_UE5Manny_UE4Manny.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Textures/T_ML_Aluminum01.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Textures/T_ML_Aluminum01.uasset new file mode 100644 index 000000000..c9c9e7226 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Textures/T_ML_Aluminum01.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Textures/T_ML_Aluminum01_N.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Textures/T_ML_Aluminum01_N.uasset new file mode 100644 index 000000000..5d8b70fb2 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Textures/T_ML_Aluminum01_N.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Textures/T_ML_Rubber_Blue_01_D.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Textures/T_ML_Rubber_Blue_01_D.uasset new file mode 100644 index 000000000..55e1a3478 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Textures/T_ML_Rubber_Blue_01_D.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Textures/T_ML_Rubber_Blue_01_N.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Textures/T_ML_Rubber_Blue_01_N.uasset new file mode 100644 index 000000000..25feb3c5d Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Textures/T_ML_Rubber_Blue_01_N.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Textures/T_UE4_Mannequin_MAT_MASKA.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Textures/T_UE4_Mannequin_MAT_MASKA.uasset new file mode 100644 index 000000000..d42d1e875 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Textures/T_UE4_Mannequin_MAT_MASKA.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Textures/T_UE4_Mannequin__normals.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Textures/T_UE4_Mannequin__normals.uasset new file mode 100644 index 000000000..eba5d4741 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Textures/T_UE4_Mannequin__normals.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Textures/T_UELogo_Mask.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Textures/T_UELogo_Mask.uasset new file mode 100644 index 000000000..d501bb4f1 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Textures/T_UELogo_Mask.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Textures/T_UELogo_N_TGA.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Textures/T_UELogo_N_TGA.uasset new file mode 100644 index 000000000..aa3041356 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequin_UE4/Textures/T_UELogo_N_TGA.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/ABP_Manny.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/ABP_Manny.uasset new file mode 100644 index 000000000..4f27ecfc2 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/ABP_Manny.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/ABP_Quinn.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/ABP_Quinn.uasset new file mode 100644 index 000000000..69bc7b0ee Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/ABP_Quinn.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Manny/BS_MM_WalkRun.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Manny/BS_MM_WalkRun.uasset new file mode 100644 index 000000000..aad5e1f45 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Manny/BS_MM_WalkRun.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Manny/MM_Fall_Loop.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Manny/MM_Fall_Loop.uasset new file mode 100644 index 000000000..37b00fedc Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Manny/MM_Fall_Loop.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Manny/MM_Idle.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Manny/MM_Idle.uasset new file mode 100644 index 000000000..d854e4273 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Manny/MM_Idle.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Manny/MM_Jump.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Manny/MM_Jump.uasset new file mode 100644 index 000000000..cc9edaddd Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Manny/MM_Jump.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Manny/MM_Land.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Manny/MM_Land.uasset new file mode 100644 index 000000000..873bac4f6 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Manny/MM_Land.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Manny/MM_Run_Fwd.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Manny/MM_Run_Fwd.uasset new file mode 100644 index 000000000..6bc4b044e Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Manny/MM_Run_Fwd.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Manny/MM_T_Pose.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Manny/MM_T_Pose.uasset new file mode 100644 index 000000000..058cc0c03 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Manny/MM_T_Pose.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Manny/MM_Walk_Fwd.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Manny/MM_Walk_Fwd.uasset new file mode 100644 index 000000000..b6f1fdeb2 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Manny/MM_Walk_Fwd.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Manny/MM_Walk_InPlace.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Manny/MM_Walk_InPlace.uasset new file mode 100644 index 000000000..e7978272b Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Manny/MM_Walk_InPlace.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Quinn/BS_MF_Unarmed_WalkRun.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Quinn/BS_MF_Unarmed_WalkRun.uasset new file mode 100644 index 000000000..8d1889294 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Quinn/BS_MF_Unarmed_WalkRun.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Quinn/MF_Idle.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Quinn/MF_Idle.uasset new file mode 100644 index 000000000..4b8aeabaf Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Quinn/MF_Idle.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Quinn/MF_Run_Fwd.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Quinn/MF_Run_Fwd.uasset new file mode 100644 index 000000000..ee4f0ff4c Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Quinn/MF_Run_Fwd.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Quinn/MF_Walk_Fwd.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Quinn/MF_Walk_Fwd.uasset new file mode 100644 index 000000000..a12c12fd6 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Animations/Quinn/MF_Walk_Fwd.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Materials/Functions/CA_Mannequin.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Materials/Functions/CA_Mannequin.uasset new file mode 100644 index 000000000..1f408cd58 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Materials/Functions/CA_Mannequin.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Materials/Functions/ChromaticCurve.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Materials/Functions/ChromaticCurve.uasset new file mode 100644 index 000000000..5b1d50552 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Materials/Functions/ChromaticCurve.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Materials/Functions/MF_Diffraction.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Materials/Functions/MF_Diffraction.uasset new file mode 100644 index 000000000..bc19d0f37 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Materials/Functions/MF_Diffraction.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Materials/Functions/MF_logo3layers.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Materials/Functions/MF_logo3layers.uasset new file mode 100644 index 000000000..313459bf6 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Materials/Functions/MF_logo3layers.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Materials/Functions/ML_BaseColorFallOff.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Materials/Functions/ML_BaseColorFallOff.uasset new file mode 100644 index 000000000..dac26343a Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Materials/Functions/ML_BaseColorFallOff.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Materials/Instances/Manny/MI_Manny_01.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Materials/Instances/Manny/MI_Manny_01.uasset new file mode 100644 index 000000000..7f9c1b312 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Materials/Instances/Manny/MI_Manny_01.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Materials/Instances/Manny/MI_Manny_02.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Materials/Instances/Manny/MI_Manny_02.uasset new file mode 100644 index 000000000..3ecd77dd2 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Materials/Instances/Manny/MI_Manny_02.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Materials/Instances/Quinn/MI_Quinn_01.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Materials/Instances/Quinn/MI_Quinn_01.uasset new file mode 100644 index 000000000..baf119eb5 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Materials/Instances/Quinn/MI_Quinn_01.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Materials/Instances/Quinn/MI_Quinn_02.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Materials/Instances/Quinn/MI_Quinn_02.uasset new file mode 100644 index 000000000..f174d00bb Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Materials/Instances/Quinn/MI_Quinn_02.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Materials/M_Mannequin.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Materials/M_Mannequin.uasset new file mode 100644 index 000000000..05c63f675 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Materials/M_Mannequin.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Meshes/Mannequin_LODSettings.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Meshes/Mannequin_LODSettings.uasset new file mode 100644 index 000000000..0bffa66fb Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Meshes/Mannequin_LODSettings.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Meshes/SKM_Manny.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Meshes/SKM_Manny.uasset new file mode 100644 index 000000000..a8aa475c5 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Meshes/SKM_Manny.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Meshes/SKM_Manny_Simple.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Meshes/SKM_Manny_Simple.uasset new file mode 100644 index 000000000..f4d3b37a9 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Meshes/SKM_Manny_Simple.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Meshes/SKM_Quinn.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Meshes/SKM_Quinn.uasset new file mode 100644 index 000000000..e179c4daa Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Meshes/SKM_Quinn.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Meshes/SKM_Quinn_Simple.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Meshes/SKM_Quinn_Simple.uasset new file mode 100644 index 000000000..c519dcea0 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Meshes/SKM_Quinn_Simple.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Meshes/SK_Mannequin.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Meshes/SK_Mannequin.uasset new file mode 100644 index 000000000..c9cdb8145 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Meshes/SK_Mannequin.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/ABP_Manny_PostProcess.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/ABP_Manny_PostProcess.uasset new file mode 100644 index 000000000..f2a2f6f8d Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/ABP_Manny_PostProcess.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/ABP_Quinn_PostProcess.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/ABP_Quinn_PostProcess.uasset new file mode 100644 index 000000000..2dd56fcb4 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/ABP_Quinn_PostProcess.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/CR_Mannequin_BasicFootIK.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/CR_Mannequin_BasicFootIK.uasset new file mode 100644 index 000000000..ff6414455 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/CR_Mannequin_BasicFootIK.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/CR_Mannequin_Body.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/CR_Mannequin_Body.uasset new file mode 100644 index 000000000..36ebdebd1 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/CR_Mannequin_Body.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/CR_Mannequin_Procedural.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/CR_Mannequin_Procedural.uasset new file mode 100644 index 000000000..2671b8206 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/CR_Mannequin_Procedural.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/IK_Mannequin.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/IK_Mannequin.uasset new file mode 100644 index 000000000..0dc3d3c7b Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/IK_Mannequin.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/PA_Mannequin.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/PA_Mannequin.uasset new file mode 100644 index 000000000..95aee9e5b Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/PA_Mannequin.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_calf_l_anim.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_calf_l_anim.uasset new file mode 100644 index 000000000..6f852f715 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_calf_l_anim.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_calf_l_pose.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_calf_l_pose.uasset new file mode 100644 index 000000000..6df9e30c6 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_calf_l_pose.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_calf_r_anim.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_calf_r_anim.uasset new file mode 100644 index 000000000..5b10b52ef Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_calf_r_anim.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_calf_r_pose.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_calf_r_pose.uasset new file mode 100644 index 000000000..94114aa8d Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_calf_r_pose.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_clavicle_l_anim.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_clavicle_l_anim.uasset new file mode 100644 index 000000000..6bd29d110 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_clavicle_l_anim.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_clavicle_l_pose.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_clavicle_l_pose.uasset new file mode 100644 index 000000000..385d15cda Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_clavicle_l_pose.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_clavicle_r_anim.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_clavicle_r_anim.uasset new file mode 100644 index 000000000..e93dc378b Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_clavicle_r_anim.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_clavicle_r_pose.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_clavicle_r_pose.uasset new file mode 100644 index 000000000..402e4be75 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_clavicle_r_pose.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_foot_l_anim.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_foot_l_anim.uasset new file mode 100644 index 000000000..caa6cc2a9 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_foot_l_anim.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_foot_l_pose.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_foot_l_pose.uasset new file mode 100644 index 000000000..22d754952 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_foot_l_pose.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_foot_r_anim.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_foot_r_anim.uasset new file mode 100644 index 000000000..7c3b552e1 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_foot_r_anim.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_foot_r_pose.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_foot_r_pose.uasset new file mode 100644 index 000000000..a8bfffd0a Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_foot_r_pose.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_hand_l_anim.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_hand_l_anim.uasset new file mode 100644 index 000000000..35ee11f41 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_hand_l_anim.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_hand_l_pose.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_hand_l_pose.uasset new file mode 100644 index 000000000..d16c6599f Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_hand_l_pose.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_hand_r_anim.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_hand_r_anim.uasset new file mode 100644 index 000000000..47e0bbbd0 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_hand_r_anim.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_hand_r_pose.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_hand_r_pose.uasset new file mode 100644 index 000000000..3ebac9581 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_hand_r_pose.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_lowerarm_l_anim.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_lowerarm_l_anim.uasset new file mode 100644 index 000000000..ce5634d82 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_lowerarm_l_anim.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_lowerarm_l_pose.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_lowerarm_l_pose.uasset new file mode 100644 index 000000000..b84cabd0f Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_lowerarm_l_pose.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_lowerarm_r_anim.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_lowerarm_r_anim.uasset new file mode 100644 index 000000000..de1407571 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_lowerarm_r_anim.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_lowerarm_r_pose.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_lowerarm_r_pose.uasset new file mode 100644 index 000000000..0275ddca7 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_lowerarm_r_pose.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_thigh_l_anim.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_thigh_l_anim.uasset new file mode 100644 index 000000000..acf706925 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_thigh_l_anim.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_thigh_l_pose.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_thigh_l_pose.uasset new file mode 100644 index 000000000..5d5e20ae6 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_thigh_l_pose.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_thigh_r_anim.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_thigh_r_anim.uasset new file mode 100644 index 000000000..4c917096c Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_thigh_r_anim.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_thigh_r_pose.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_thigh_r_pose.uasset new file mode 100644 index 000000000..41edbb2e7 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_thigh_r_pose.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_upperarm_l_anim.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_upperarm_l_anim.uasset new file mode 100644 index 000000000..cb25cf617 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_upperarm_l_anim.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_upperarm_l_pose.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_upperarm_l_pose.uasset new file mode 100644 index 000000000..1358958ac Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_upperarm_l_pose.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_upperarm_r_anim.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_upperarm_r_anim.uasset new file mode 100644 index 000000000..c32ea6635 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_upperarm_r_anim.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_upperarm_r_pose.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_upperarm_r_pose.uasset new file mode 100644 index 000000000..f6cdc3b99 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Manny/Manny_upperarm_r_pose.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_calf_l_anim.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_calf_l_anim.uasset new file mode 100644 index 000000000..a600e7125 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_calf_l_anim.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_calf_l_pose.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_calf_l_pose.uasset new file mode 100644 index 000000000..468a59274 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_calf_l_pose.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_calf_r_anim.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_calf_r_anim.uasset new file mode 100644 index 000000000..d04e4ea9d Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_calf_r_anim.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_calf_r_pose.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_calf_r_pose.uasset new file mode 100644 index 000000000..6e38b5a52 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_calf_r_pose.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_clavicle_l_anim.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_clavicle_l_anim.uasset new file mode 100644 index 000000000..d38969fac Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_clavicle_l_anim.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_clavicle_l_pose.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_clavicle_l_pose.uasset new file mode 100644 index 000000000..112123c37 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_clavicle_l_pose.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_clavicle_r_anim.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_clavicle_r_anim.uasset new file mode 100644 index 000000000..159a47490 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_clavicle_r_anim.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_clavicle_r_pose.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_clavicle_r_pose.uasset new file mode 100644 index 000000000..38ef8bf45 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_clavicle_r_pose.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_foot_l_anim.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_foot_l_anim.uasset new file mode 100644 index 000000000..ee799650c Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_foot_l_anim.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_foot_l_pose.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_foot_l_pose.uasset new file mode 100644 index 000000000..f36f7be67 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_foot_l_pose.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_foot_r_anim.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_foot_r_anim.uasset new file mode 100644 index 000000000..7c396f544 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_foot_r_anim.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_foot_r_pose.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_foot_r_pose.uasset new file mode 100644 index 000000000..18be357da Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_foot_r_pose.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_hand_l_anim.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_hand_l_anim.uasset new file mode 100644 index 000000000..0393a86da Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_hand_l_anim.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_hand_l_pose.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_hand_l_pose.uasset new file mode 100644 index 000000000..9a239ea03 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_hand_l_pose.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_hand_r_anim.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_hand_r_anim.uasset new file mode 100644 index 000000000..67e5993bd Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_hand_r_anim.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_hand_r_pose.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_hand_r_pose.uasset new file mode 100644 index 000000000..0d446ba93 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_hand_r_pose.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_lowerarm_l_anim.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_lowerarm_l_anim.uasset new file mode 100644 index 000000000..8059e1b03 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_lowerarm_l_anim.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_lowerarm_l_pose.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_lowerarm_l_pose.uasset new file mode 100644 index 000000000..3d73cc08d Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_lowerarm_l_pose.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_lowerarm_r_anim.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_lowerarm_r_anim.uasset new file mode 100644 index 000000000..e0a6bd513 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_lowerarm_r_anim.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_lowerarm_r_pose.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_lowerarm_r_pose.uasset new file mode 100644 index 000000000..e64101c0f Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_lowerarm_r_pose.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_thigh_l_anim.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_thigh_l_anim.uasset new file mode 100644 index 000000000..518dedfd5 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_thigh_l_anim.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_thigh_l_pose.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_thigh_l_pose.uasset new file mode 100644 index 000000000..2813702bd Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_thigh_l_pose.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_thigh_r_anim.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_thigh_r_anim.uasset new file mode 100644 index 000000000..17bb5da10 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_thigh_r_anim.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_thigh_r_pose.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_thigh_r_pose.uasset new file mode 100644 index 000000000..86cf4eb93 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_thigh_r_pose.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_upperarm_l_anim.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_upperarm_l_anim.uasset new file mode 100644 index 000000000..7518a8b34 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_upperarm_l_anim.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_upperarm_l_pose.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_upperarm_l_pose.uasset new file mode 100644 index 000000000..5d914ab27 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_upperarm_l_pose.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_upperarm_r_anim.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_upperarm_r_anim.uasset new file mode 100644 index 000000000..7d1686f02 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_upperarm_r_anim.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_upperarm_r_pose.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_upperarm_r_pose.uasset new file mode 100644 index 000000000..7d677c94a Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/Poses/Quinn/Quinn_upperarm_r_pose.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/RTG_Mannequin.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/RTG_Mannequin.uasset new file mode 100644 index 000000000..e19f4e344 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Rigs/RTG_Mannequin.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_01_ASAOPMASK_MSK.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_01_ASAOPMASK_MSK.uasset new file mode 100644 index 000000000..f1faebef0 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_01_ASAOPMASK_MSK.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_01_BN.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_01_BN.uasset new file mode 100644 index 000000000..e6c04513d Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_01_BN.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_01_CCRCCPlastic_MSK.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_01_CCRCCPlastic_MSK.uasset new file mode 100644 index 000000000..f65b39970 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_01_CCRCCPlastic_MSK.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_01_D.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_01_D.uasset new file mode 100644 index 000000000..87e0e1e1f Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_01_D.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_01_MSR_MSK.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_01_MSR_MSK.uasset new file mode 100644 index 000000000..065367e27 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_01_MSR_MSK.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_01_N.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_01_N.uasset new file mode 100644 index 000000000..6eec6cf85 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_01_N.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_01_Tan.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_01_Tan.uasset new file mode 100644 index 000000000..f22540d53 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_01_Tan.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_02_ASAOPMASK_MSK.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_02_ASAOPMASK_MSK.uasset new file mode 100644 index 000000000..911ed3989 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_02_ASAOPMASK_MSK.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_02_BN.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_02_BN.uasset new file mode 100644 index 000000000..9b63d8838 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_02_BN.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_02_CCRCCPlastic_MSK.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_02_CCRCCPlastic_MSK.uasset new file mode 100644 index 000000000..ca2683e03 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_02_CCRCCPlastic_MSK.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_02_D.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_02_D.uasset new file mode 100644 index 000000000..895d5dce1 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_02_D.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_02_MSR_MSK.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_02_MSR_MSK.uasset new file mode 100644 index 000000000..fc558410b Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_02_MSR_MSK.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_02_N.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_02_N.uasset new file mode 100644 index 000000000..80d26bde3 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_02_N.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_02_Tan.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_02_Tan.uasset new file mode 100644 index 000000000..d0a81302b Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Manny/T_Manny_02_Tan.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_01ID_BN.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_01ID_BN.uasset new file mode 100644 index 000000000..a468c6cf3 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_01ID_BN.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_01ID_D.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_01ID_D.uasset new file mode 100644 index 000000000..04417f4a6 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_01ID_D.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_01ID_MSR_MSK.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_01ID_MSR_MSK.uasset new file mode 100644 index 000000000..ecb048216 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_01ID_MSR_MSK.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_01ID_N.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_01ID_N.uasset new file mode 100644 index 000000000..445a69605 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_01ID_N.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_01ID_Tan.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_01ID_Tan.uasset new file mode 100644 index 000000000..4a1f17eb8 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_01ID_Tan.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_01_ASAOMASK_MSK.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_01_ASAOMASK_MSK.uasset new file mode 100644 index 000000000..109feebeb Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_01_ASAOMASK_MSK.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_01_CCRCCPlastic_MSK.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_01_CCRCCPlastic_MSK.uasset new file mode 100644 index 000000000..222163199 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_01_CCRCCPlastic_MSK.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_02ID_BN.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_02ID_BN.uasset new file mode 100644 index 000000000..5ae2586a5 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_02ID_BN.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_02ID_D.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_02ID_D.uasset new file mode 100644 index 000000000..f67d7a673 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_02ID_D.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_02ID_MSR_MSK.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_02ID_MSR_MSK.uasset new file mode 100644 index 000000000..a106f2a66 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_02ID_MSR_MSK.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_02ID_N.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_02ID_N.uasset new file mode 100644 index 000000000..b02efd57b Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_02ID_N.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_02ID_Tan.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_02ID_Tan.uasset new file mode 100644 index 000000000..a32f36950 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_02ID_Tan.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_02_ASAOMASK_MSK.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_02_ASAOMASK_MSK.uasset new file mode 100644 index 000000000..95cd646b1 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_02_ASAOMASK_MSK.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_02_CCRCCPlastic_MSK.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_02_CCRCCPlastic_MSK.uasset new file mode 100644 index 000000000..5e3f1fce7 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Quinn/T_Quinn_02_CCRCCPlastic_MSK.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Shared/T_UE_Logo_M.uasset b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Shared/T_UE_Logo_M.uasset new file mode 100644 index 000000000..d91d602a6 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/Characters/Mannequins/Textures/Shared/T_UE_Logo_M.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/StarterContent/Audio/Starter_Birds01.uasset b/Content/TokyoStylizedEnvironment/DemoContent/StarterContent/Audio/Starter_Birds01.uasset new file mode 100644 index 000000000..3f7af773a Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/StarterContent/Audio/Starter_Birds01.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/StarterContent/Audio/Starter_Wind05.uasset b/Content/TokyoStylizedEnvironment/DemoContent/StarterContent/Audio/Starter_Wind05.uasset new file mode 100644 index 000000000..14f194023 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/StarterContent/Audio/Starter_Wind05.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/StarterContent/Audio/Starter_Wind06.uasset b/Content/TokyoStylizedEnvironment/DemoContent/StarterContent/Audio/Starter_Wind06.uasset new file mode 100644 index 000000000..1189dcd69 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/StarterContent/Audio/Starter_Wind06.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/StarterContent/HDRI/HDRI_Epic_Courtyard_Daylight.uasset b/Content/TokyoStylizedEnvironment/DemoContent/StarterContent/HDRI/HDRI_Epic_Courtyard_Daylight.uasset new file mode 100644 index 000000000..767fe8f69 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/StarterContent/HDRI/HDRI_Epic_Courtyard_Daylight.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/ThirdPerson/Blueprints/BP_ThirdPersonCharacter.uasset b/Content/TokyoStylizedEnvironment/DemoContent/ThirdPerson/Blueprints/BP_ThirdPersonCharacter.uasset new file mode 100644 index 000000000..5c875fa82 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/ThirdPerson/Blueprints/BP_ThirdPersonCharacter.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/ThirdPerson/Blueprints/BP_ThirdPersonGameMode.uasset b/Content/TokyoStylizedEnvironment/DemoContent/ThirdPerson/Blueprints/BP_ThirdPersonGameMode.uasset new file mode 100644 index 000000000..ea36e031a Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/ThirdPerson/Blueprints/BP_ThirdPersonGameMode.uasset differ diff --git a/Content/TokyoStylizedEnvironment/DemoContent/ThirdPerson/Maps/ThirdPersonMap.umap b/Content/TokyoStylizedEnvironment/DemoContent/ThirdPerson/Maps/ThirdPersonMap.umap new file mode 100644 index 000000000..9fc84a634 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/DemoContent/ThirdPerson/Maps/ThirdPersonMap.umap differ diff --git a/Content/TokyoStylizedEnvironment/Maps/Assets_Overview.umap b/Content/TokyoStylizedEnvironment/Maps/Assets_Overview.umap new file mode 100644 index 000000000..0d4f1b684 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/Maps/Assets_Overview.umap differ diff --git a/Content/TokyoStylizedEnvironment/Materials/SM_Bike/MI_Bike_Orange.uasset b/Content/TokyoStylizedEnvironment/Materials/SM_Bike/MI_Bike_Orange.uasset index 88431f85d..023089001 100644 Binary files a/Content/TokyoStylizedEnvironment/Materials/SM_Bike/MI_Bike_Orange.uasset and b/Content/TokyoStylizedEnvironment/Materials/SM_Bike/MI_Bike_Orange.uasset differ diff --git a/Content/TokyoStylizedEnvironment/Materials/SM_Bike/M_Bike.uasset b/Content/TokyoStylizedEnvironment/Materials/SM_Bike/M_Bike.uasset index 701d88a45..d1a3c8ba7 100644 Binary files a/Content/TokyoStylizedEnvironment/Materials/SM_Bike/M_Bike.uasset and b/Content/TokyoStylizedEnvironment/Materials/SM_Bike/M_Bike.uasset differ diff --git a/Content/TokyoStylizedEnvironment/Materials/Tile_Glass/MI_Glass_Clear.uasset b/Content/TokyoStylizedEnvironment/Materials/Tile_Glass/MI_Glass_Clear.uasset new file mode 100644 index 000000000..ae138cfc6 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/Materials/Tile_Glass/MI_Glass_Clear.uasset differ diff --git a/Content/TokyoStylizedEnvironment/Materials/Tile_Glass/M_Glass_Opaque.uasset b/Content/TokyoStylizedEnvironment/Materials/Tile_Glass/M_Glass_Opaque.uasset index 354c23259..7fa6280ee 100644 Binary files a/Content/TokyoStylizedEnvironment/Materials/Tile_Glass/M_Glass_Opaque.uasset and b/Content/TokyoStylizedEnvironment/Materials/Tile_Glass/M_Glass_Opaque.uasset differ diff --git a/Content/TokyoStylizedEnvironment/Materials/Tile_Panel02/MI_Panel02_Metallic_Red.uasset b/Content/TokyoStylizedEnvironment/Materials/Tile_Panel02/MI_Panel02_Metallic_Red.uasset new file mode 100644 index 000000000..07677b320 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/Materials/Tile_Panel02/MI_Panel02_Metallic_Red.uasset differ diff --git a/Content/TokyoStylizedEnvironment/Materials/Tile_Road_Asphalt/MI_RoadAsphalt_Dark.uasset b/Content/TokyoStylizedEnvironment/Materials/Tile_Road_Asphalt/MI_RoadAsphalt_Dark.uasset new file mode 100644 index 000000000..39707a5f9 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/Materials/Tile_Road_Asphalt/MI_RoadAsphalt_Dark.uasset differ diff --git a/Content/TokyoStylizedEnvironment/Materials/Trim_Decal01/MI_Decal01_Dark01.uasset b/Content/TokyoStylizedEnvironment/Materials/Trim_Decal01/MI_Decal01_Dark01.uasset new file mode 100644 index 000000000..3ce303900 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/Materials/Trim_Decal01/MI_Decal01_Dark01.uasset differ diff --git a/Content/TokyoStylizedEnvironment/Materials/Trim_Street_Trash/M_StreetTrash.uasset b/Content/TokyoStylizedEnvironment/Materials/Trim_Street_Trash/M_StreetTrash.uasset index 97221ba2a..d8042f16f 100644 Binary files a/Content/TokyoStylizedEnvironment/Materials/Trim_Street_Trash/M_StreetTrash.uasset and b/Content/TokyoStylizedEnvironment/Materials/Trim_Street_Trash/M_StreetTrash.uasset differ diff --git a/Content/TokyoStylizedEnvironment/Meshes/Foliages/SM_SakuraTree01.uasset b/Content/TokyoStylizedEnvironment/Meshes/Foliages/SM_SakuraTree01.uasset new file mode 100644 index 000000000..8a294d534 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/Meshes/Foliages/SM_SakuraTree01.uasset differ diff --git a/Content/TokyoStylizedEnvironment/Meshes/Foliages/SM_SakuraTree02.uasset b/Content/TokyoStylizedEnvironment/Meshes/Foliages/SM_SakuraTree02.uasset new file mode 100644 index 000000000..6f00e5dbc Binary files /dev/null and b/Content/TokyoStylizedEnvironment/Meshes/Foliages/SM_SakuraTree02.uasset differ diff --git a/Content/TokyoStylizedEnvironment/Meshes/Modular_BuildingBlocks/SM_Wall_50x100.uasset b/Content/TokyoStylizedEnvironment/Meshes/Modular_BuildingBlocks/SM_Wall_50x100.uasset new file mode 100644 index 000000000..428c13a57 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/Meshes/Modular_BuildingBlocks/SM_Wall_50x100.uasset differ diff --git a/Content/TokyoStylizedEnvironment/Meshes/Modular_BuildingBlocks/SM_Wall_FrontOpen03_400x400.uasset b/Content/TokyoStylizedEnvironment/Meshes/Modular_BuildingBlocks/SM_Wall_FrontOpen03_400x400.uasset new file mode 100644 index 000000000..51c2c91ab Binary files /dev/null and b/Content/TokyoStylizedEnvironment/Meshes/Modular_BuildingBlocks/SM_Wall_FrontOpen03_400x400.uasset differ diff --git a/Content/TokyoStylizedEnvironment/Meshes/Modular_BuildingBlocks/SM_Wall_FrontOpen03_500x500.uasset b/Content/TokyoStylizedEnvironment/Meshes/Modular_BuildingBlocks/SM_Wall_FrontOpen03_500x500.uasset new file mode 100644 index 000000000..dc7865e9c Binary files /dev/null and b/Content/TokyoStylizedEnvironment/Meshes/Modular_BuildingBlocks/SM_Wall_FrontOpen03_500x500.uasset differ diff --git a/Content/TokyoStylizedEnvironment/Meshes/Modular_BuildingBlocks/SM_Wall_FrontOpen04_400x400.uasset b/Content/TokyoStylizedEnvironment/Meshes/Modular_BuildingBlocks/SM_Wall_FrontOpen04_400x400.uasset new file mode 100644 index 000000000..baead8ad2 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/Meshes/Modular_BuildingBlocks/SM_Wall_FrontOpen04_400x400.uasset differ diff --git a/Content/TokyoStylizedEnvironment/Meshes/Modular_BuildingBlocks/SM_Wall_FrontOpen04_500x500.uasset b/Content/TokyoStylizedEnvironment/Meshes/Modular_BuildingBlocks/SM_Wall_FrontOpen04_500x500.uasset new file mode 100644 index 000000000..20c805087 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/Meshes/Modular_BuildingBlocks/SM_Wall_FrontOpen04_500x500.uasset differ diff --git a/Content/TokyoStylizedEnvironment/Meshes/Modular_BuildingBlocks/SM_Wall_Slanted_400x400.uasset b/Content/TokyoStylizedEnvironment/Meshes/Modular_BuildingBlocks/SM_Wall_Slanted_400x400.uasset new file mode 100644 index 000000000..74efb038a Binary files /dev/null and b/Content/TokyoStylizedEnvironment/Meshes/Modular_BuildingBlocks/SM_Wall_Slanted_400x400.uasset differ diff --git a/Content/TokyoStylizedEnvironment/Meshes/Road_Props/SM_Road_Drain.uasset b/Content/TokyoStylizedEnvironment/Meshes/Road_Props/SM_Road_Drain.uasset new file mode 100644 index 000000000..2d8562b7d Binary files /dev/null and b/Content/TokyoStylizedEnvironment/Meshes/Road_Props/SM_Road_Drain.uasset differ diff --git a/Content/TokyoStylizedEnvironment/Meshes/Road_Props/SM_Road_Manhole.uasset b/Content/TokyoStylizedEnvironment/Meshes/Road_Props/SM_Road_Manhole.uasset new file mode 100644 index 000000000..79c334f15 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/Meshes/Road_Props/SM_Road_Manhole.uasset differ diff --git a/Content/TokyoStylizedEnvironment/Meshes/Signages/Characters_Flat01/SM_Text16.uasset b/Content/TokyoStylizedEnvironment/Meshes/Signages/Characters_Flat01/SM_Text16.uasset new file mode 100644 index 000000000..eb321c8d6 Binary files /dev/null and b/Content/TokyoStylizedEnvironment/Meshes/Signages/Characters_Flat01/SM_Text16.uasset differ diff --git a/Content/TokyoStylizedEnvironment/Meshes/Signages/Characters_Flat02/SM_Text03.uasset b/Content/TokyoStylizedEnvironment/Meshes/Signages/Characters_Flat02/SM_Text03.uasset new file mode 100644 index 000000000..a27e2a95b Binary files /dev/null and b/Content/TokyoStylizedEnvironment/Meshes/Signages/Characters_Flat02/SM_Text03.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadPS4/CommonInput_Gamepad_PS4.uasset b/Content/UI/Foundation/Platform/Input/GamepadPS4/CommonInput_Gamepad_PS4.uasset new file mode 100644 index 000000000..dd5624e0a Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadPS4/CommonInput_Gamepad_PS4.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadPS4/ControllerConfig.uasset b/Content/UI/Foundation/Platform/Input/GamepadPS4/ControllerConfig.uasset new file mode 100644 index 000000000..47322684d Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadPS4/ControllerConfig.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadPS4/T_PS4_DPad.uasset b/Content/UI/Foundation/Platform/Input/GamepadPS4/T_PS4_DPad.uasset new file mode 100644 index 000000000..572000677 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadPS4/T_PS4_DPad.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadPS4/T_PS4_Dpad_LeftRight.uasset b/Content/UI/Foundation/Platform/Input/GamepadPS4/T_PS4_Dpad_LeftRight.uasset new file mode 100644 index 000000000..d7be90651 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadPS4/T_PS4_Dpad_LeftRight.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadPS4/T_PS4_Dpad_UpDown.uasset b/Content/UI/Foundation/Platform/Input/GamepadPS4/T_PS4_Dpad_UpDown.uasset new file mode 100644 index 000000000..d710c42fc Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadPS4/T_PS4_Dpad_UpDown.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadPS4/T_PS4_TouchPad.uasset b/Content/UI/Foundation/Platform/Input/GamepadPS4/T_PS4_TouchPad.uasset new file mode 100644 index 000000000..811618b0c Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadPS4/T_PS4_TouchPad.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadPS5/ControllerConfig.uasset b/Content/UI/Foundation/Platform/Input/GamepadPS5/ControllerConfig.uasset new file mode 100644 index 000000000..ca328c9e3 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadPS5/ControllerConfig.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/CommonInput_Gamepad_Switch.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/CommonInput_Gamepad_Switch.uasset new file mode 100644 index 000000000..ca264199f Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/CommonInput_Gamepad_Switch.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/ControllerConfig.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/ControllerConfig.uasset new file mode 100644 index 000000000..e6024b73f Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/ControllerConfig.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_A.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_A.uasset new file mode 100644 index 000000000..48f3acfd3 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_A.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_B.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_B.uasset new file mode 100644 index 000000000..c175c0225 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_B.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_Capture.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_Capture.uasset new file mode 100644 index 000000000..5fb04985d Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_Capture.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_DPad.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_DPad.uasset new file mode 100644 index 000000000..d39a482eb Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_DPad.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_DPad_Down.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_DPad_Down.uasset new file mode 100644 index 000000000..62a987d65 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_DPad_Down.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_DPad_Left.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_DPad_Left.uasset new file mode 100644 index 000000000..f730d6494 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_DPad_Left.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_DPad_LeftRight.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_DPad_LeftRight.uasset new file mode 100644 index 000000000..5e94f68d2 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_DPad_LeftRight.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_DPad_Right.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_DPad_Right.uasset new file mode 100644 index 000000000..1b361e9d2 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_DPad_Right.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_DPad_Up.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_DPad_Up.uasset new file mode 100644 index 000000000..5ce3f5874 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_DPad_Up.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_DPad_UpDown.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_DPad_UpDown.uasset new file mode 100644 index 000000000..4d4d85ac7 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_DPad_UpDown.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_Home.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_Home.uasset new file mode 100644 index 000000000..c91bd4c9c Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_Home.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_LB.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_LB.uasset new file mode 100644 index 000000000..aa05c527c Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_LB.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_LStick.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_LStick.uasset new file mode 100644 index 000000000..4259ff600 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_LStick.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_LStick_Down.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_LStick_Down.uasset new file mode 100644 index 000000000..cf12705b6 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_LStick_Down.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_LStick_Left.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_LStick_Left.uasset new file mode 100644 index 000000000..2d8e1c3e7 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_LStick_Left.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_LStick_Pan.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_LStick_Pan.uasset new file mode 100644 index 000000000..8c04b2834 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_LStick_Pan.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_LStick_Press.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_LStick_Press.uasset new file mode 100644 index 000000000..7881ed3b6 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_LStick_Press.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_LStick_Right.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_LStick_Right.uasset new file mode 100644 index 000000000..199ac61b2 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_LStick_Right.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_LStick_Scroll.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_LStick_Scroll.uasset new file mode 100644 index 000000000..013bc0e30 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_LStick_Scroll.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_LStick_Up.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_LStick_Up.uasset new file mode 100644 index 000000000..13e116e11 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_LStick_Up.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_Minus.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_Minus.uasset new file mode 100644 index 000000000..101ea41c9 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_Minus.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_Plus.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_Plus.uasset new file mode 100644 index 000000000..5f0d40bdd Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_Plus.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_R.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_R.uasset new file mode 100644 index 000000000..2cce56e49 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_R.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_RStick.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_RStick.uasset new file mode 100644 index 000000000..04f4f3cc5 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_RStick.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_RStick_Down.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_RStick_Down.uasset new file mode 100644 index 000000000..fd0b02650 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_RStick_Down.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_RStick_Left.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_RStick_Left.uasset new file mode 100644 index 000000000..232a3ca5b Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_RStick_Left.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_RStick_Pan.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_RStick_Pan.uasset new file mode 100644 index 000000000..26215d08e Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_RStick_Pan.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_RStick_Press.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_RStick_Press.uasset new file mode 100644 index 000000000..59f195001 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_RStick_Press.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_RStick_Right.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_RStick_Right.uasset new file mode 100644 index 000000000..2543de79e Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_RStick_Right.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_RStick_Scroll.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_RStick_Scroll.uasset new file mode 100644 index 000000000..6d732f298 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_RStick_Scroll.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_RStick_Up.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_RStick_Up.uasset new file mode 100644 index 000000000..b6a36f64f Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_RStick_Up.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_X.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_X.uasset new file mode 100644 index 000000000..614b30ef6 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_X.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_Y.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_Y.uasset new file mode 100644 index 000000000..17018ed48 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_Y.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_ZL.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_ZL.uasset new file mode 100644 index 000000000..530c2c76b Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_ZL.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_ZR.uasset b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_ZR.uasset new file mode 100644 index 000000000..045f1987a Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadSwitch/T_NS_ZR.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadXboxOne/ControllerConfig.uasset b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/ControllerConfig.uasset index 9884853a5..bf9653952 100644 Binary files a/Content/UI/Foundation/Platform/Input/GamepadXboxOne/ControllerConfig.uasset and b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/ControllerConfig.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_LStick_Press.uasset b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_LStick_Press.uasset new file mode 100644 index 000000000..fdafdbe9e Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_LStick_Press.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_LStick_Right.uasset b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_LStick_Right.uasset new file mode 100644 index 000000000..2a80f529a Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_LStick_Right.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_LStick_Scroll.uasset b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_LStick_Scroll.uasset new file mode 100644 index 000000000..71cd8d884 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_LStick_Scroll.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_LStick_Up.uasset b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_LStick_Up.uasset new file mode 100644 index 000000000..e8f101805 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_LStick_Up.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_LT.uasset b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_LT.uasset new file mode 100644 index 000000000..f192683a2 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_LT.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_Menu.uasset b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_Menu.uasset new file mode 100644 index 000000000..2eb521037 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_Menu.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_RB.uasset b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_RB.uasset new file mode 100644 index 000000000..816dde6bf Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_RB.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_RStick.uasset b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_RStick.uasset new file mode 100644 index 000000000..448c7acb6 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_RStick.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_RStick_Down.uasset b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_RStick_Down.uasset new file mode 100644 index 000000000..19d9564cb Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_RStick_Down.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_RStick_Left.uasset b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_RStick_Left.uasset new file mode 100644 index 000000000..4cab19928 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_RStick_Left.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_RStick_Pan.uasset b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_RStick_Pan.uasset new file mode 100644 index 000000000..e377d110a Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_RStick_Pan.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_RStick_Press.uasset b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_RStick_Press.uasset new file mode 100644 index 000000000..acffef33f Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_RStick_Press.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_RStick_Right.uasset b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_RStick_Right.uasset new file mode 100644 index 000000000..8b95d2f4d Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_RStick_Right.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_RStick_Scroll.uasset b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_RStick_Scroll.uasset new file mode 100644 index 000000000..58eca93f7 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_RStick_Scroll.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_RStick_Up.uasset b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_RStick_Up.uasset new file mode 100644 index 000000000..de8419d27 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_RStick_Up.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_RT.uasset b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_RT.uasset new file mode 100644 index 000000000..075068363 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_RT.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_Windows.uasset b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_Windows.uasset new file mode 100644 index 000000000..6a1e5dc83 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_Windows.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_X.uasset b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_X.uasset new file mode 100644 index 000000000..5343089a0 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_X.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_Y.uasset b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_Y.uasset new file mode 100644 index 000000000..5bf059bde Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadXboxOne/T_XB1_Y.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadXboxSeriesX/CommonInput_Gamepad_XSX.uasset b/Content/UI/Foundation/Platform/Input/GamepadXboxSeriesX/CommonInput_Gamepad_XSX.uasset new file mode 100644 index 000000000..7eaeb811e Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadXboxSeriesX/CommonInput_Gamepad_XSX.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadXboxSeriesX/ControllerConfig.uasset b/Content/UI/Foundation/Platform/Input/GamepadXboxSeriesX/ControllerConfig.uasset new file mode 100644 index 000000000..3d59cfd4e Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadXboxSeriesX/ControllerConfig.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/GamepadXboxSeriesX/T_XB1_Share.uasset b/Content/UI/Foundation/Platform/Input/GamepadXboxSeriesX/T_XB1_Share.uasset new file mode 100644 index 000000000..975bc3ece Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/GamepadXboxSeriesX/T_XB1_Share.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/KeyboardMouse/CommonInput_KeyboardMouse.uasset b/Content/UI/Foundation/Platform/Input/KeyboardMouse/CommonInput_KeyboardMouse.uasset new file mode 100644 index 000000000..00b207f04 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/KeyboardMouse/CommonInput_KeyboardMouse.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/KeyboardMouse/Mouse1_T.uasset b/Content/UI/Foundation/Platform/Input/KeyboardMouse/Mouse1_T.uasset new file mode 100644 index 000000000..cf4af3ec4 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/KeyboardMouse/Mouse1_T.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/KeyboardMouse/Mouse2_T.uasset b/Content/UI/Foundation/Platform/Input/KeyboardMouse/Mouse2_T.uasset new file mode 100644 index 000000000..33983c98d Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/KeyboardMouse/Mouse2_T.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/KeyboardMouse/Mouse3_T.uasset b/Content/UI/Foundation/Platform/Input/KeyboardMouse/Mouse3_T.uasset new file mode 100644 index 000000000..cd8dc4746 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/KeyboardMouse/Mouse3_T.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/KeyboardMouse/Mouse4_T.uasset b/Content/UI/Foundation/Platform/Input/KeyboardMouse/Mouse4_T.uasset new file mode 100644 index 000000000..d93833b5a Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/KeyboardMouse/Mouse4_T.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/KeyboardMouse/Mouse5_T.uasset b/Content/UI/Foundation/Platform/Input/KeyboardMouse/Mouse5_T.uasset new file mode 100644 index 000000000..35f461706 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/KeyboardMouse/Mouse5_T.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/KeyboardMouse/MouseAxis-X.uasset b/Content/UI/Foundation/Platform/Input/KeyboardMouse/MouseAxis-X.uasset new file mode 100644 index 000000000..a34d22120 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/KeyboardMouse/MouseAxis-X.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/KeyboardMouse/MouseScrollAxis_T.uasset b/Content/UI/Foundation/Platform/Input/KeyboardMouse/MouseScrollAxis_T.uasset new file mode 100644 index 000000000..64dc27eb9 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/KeyboardMouse/MouseScrollAxis_T.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/KeyboardMouse/MouseWheelDown_T.uasset b/Content/UI/Foundation/Platform/Input/KeyboardMouse/MouseWheelDown_T.uasset new file mode 100644 index 000000000..b0fad838e Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/KeyboardMouse/MouseWheelDown_T.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/KeyboardMouse/MouseWheelUp_T.uasset b/Content/UI/Foundation/Platform/Input/KeyboardMouse/MouseWheelUp_T.uasset new file mode 100644 index 000000000..ef37e619e Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/KeyboardMouse/MouseWheelUp_T.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/Misc/Keybind-Frame-L.uasset b/Content/UI/Foundation/Platform/Input/Misc/Keybind-Frame-L.uasset new file mode 100644 index 000000000..6f36e195f Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/Misc/Keybind-Frame-L.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/Misc/Keybind-Frame-M.uasset b/Content/UI/Foundation/Platform/Input/Misc/Keybind-Frame-M.uasset new file mode 100644 index 000000000..932e174a7 Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/Misc/Keybind-Frame-M.uasset differ diff --git a/Content/UI/Foundation/Platform/Input/Misc/Keybind-Frame-S.uasset b/Content/UI/Foundation/Platform/Input/Misc/Keybind-Frame-S.uasset new file mode 100644 index 000000000..6487982fd Binary files /dev/null and b/Content/UI/Foundation/Platform/Input/Misc/Keybind-Frame-S.uasset differ diff --git a/Content/Widget/Comm/CW_BaseHUD_Lobby.uasset b/Content/Widget/Comm/CW_BaseHUD_Lobby.uasset new file mode 100644 index 000000000..39b57acb0 Binary files /dev/null and b/Content/Widget/Comm/CW_BaseHUD_Lobby.uasset differ diff --git a/Content/Widget/Lobby/WB_Main_Lobby.uasset b/Content/Widget/Lobby/WB_Main_Lobby.uasset index aa349d935..e3604f22c 100644 Binary files a/Content/Widget/Lobby/WB_Main_Lobby.uasset and b/Content/Widget/Lobby/WB_Main_Lobby.uasset differ diff --git a/Content/Widget/MainMenu/WB_MainMenu.uasset b/Content/Widget/MainMenu/WB_MainMenu.uasset index d5b2270fe..f8f2c3782 100644 Binary files a/Content/Widget/MainMenu/WB_MainMenu.uasset and b/Content/Widget/MainMenu/WB_MainMenu.uasset differ diff --git a/Plugins/CommonGame/CommonGame.uplugin b/Plugins/CommonGame/CommonGame.uplugin new file mode 100644 index 000000000..d47f95acc --- /dev/null +++ b/Plugins/CommonGame/CommonGame.uplugin @@ -0,0 +1,42 @@ +{ + "FileVersion": 3, + "Version": 1, + "VersionName": "1.0", + "FriendlyName": "CommonGame", + "Description": "Generic gameplay classes that use the other Common plugins.", + "Category": "Gameplay", + "CreatedBy": "Epic Games, Inc.", + "CreatedByURL": "https://www.epicgames.com", + "DocsURL": "", + "MarketplaceURL": "", + "SupportURL": "", + "CanContainContent": false, + "IsBetaVersion": false, + "Installed": false, + + "Modules": [ + { + "Name": "CommonGame", + "Type": "Runtime", + "LoadingPhase": "Default" + } + ], + "Plugins": [ + { + "Name": "CommonUI", + "Enabled": true + }, + { + "Name": "CommonUser", + "Enabled": true + }, + { + "Name": "ModularGameplayActors", + "Enabled": true + }, + { + "Name": "OnlineFramework", + "Enabled": true + } + ] +} \ No newline at end of file diff --git a/Plugins/CommonGame/Source/CommonGame.Build.cs b/Plugins/CommonGame/Source/CommonGame.Build.cs new file mode 100644 index 000000000..6521ea320 --- /dev/null +++ b/Plugins/CommonGame/Source/CommonGame.Build.cs @@ -0,0 +1,43 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; + +public class CommonGame : ModuleRules +{ + public CommonGame(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "CoreUObject", + "InputCore", + "Engine", + "Slate", + "SlateCore", + "UMG", + "CommonInput", + "CommonUI", + "CommonUser", + "GameplayTags", + "ModularGameplayActors", + } + ); + + + PrivateDependencyModuleNames.AddRange( + new string[] + { + } + ); + + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + } + ); + } +} diff --git a/Plugins/CommonGame/Source/Private/Actions/AsyncAction_CreateWidgetAsync.cpp b/Plugins/CommonGame/Source/Private/Actions/AsyncAction_CreateWidgetAsync.cpp new file mode 100644 index 000000000..2d1b9e09e --- /dev/null +++ b/Plugins/CommonGame/Source/Private/Actions/AsyncAction_CreateWidgetAsync.cpp @@ -0,0 +1,97 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "Actions/AsyncAction_CreateWidgetAsync.h" + +#include "Blueprint/UserWidget.h" +#include "Blueprint/WidgetBlueprintLibrary.h" +#include "CommonUIExtensions.h" +#include "Engine/AssetManager.h" +#include "Engine/Engine.h" +#include "Engine/GameInstance.h" +#include "Engine/StreamableManager.h" +#include "UObject/Stack.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(AsyncAction_CreateWidgetAsync) + +class UUserWidget; + +static const FName InputFilterReason_Template = FName(TEXT("CreatingWidgetAsync")); + +UAsyncAction_CreateWidgetAsync::UAsyncAction_CreateWidgetAsync(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , bSuspendInputUntilComplete(true) +{ +} + +UAsyncAction_CreateWidgetAsync* UAsyncAction_CreateWidgetAsync::CreateWidgetAsync(UObject* InWorldContextObject, TSoftClassPtr InUserWidgetSoftClass, APlayerController* InOwningPlayer, bool bSuspendInputUntilComplete) +{ + if (InUserWidgetSoftClass.IsNull()) + { + FFrame::KismetExecutionMessage(TEXT("CreateWidgetAsync was passed a null UserWidgetSoftClass"), ELogVerbosity::Error); + return nullptr; + } + + UWorld* World = GEngine->GetWorldFromContextObject(InWorldContextObject, EGetWorldErrorMode::LogAndReturnNull); + + UAsyncAction_CreateWidgetAsync* Action = NewObject(); + Action->UserWidgetSoftClass = InUserWidgetSoftClass; + Action->OwningPlayer = InOwningPlayer; + Action->World = World; + Action->GameInstance = World->GetGameInstance(); + Action->bSuspendInputUntilComplete = bSuspendInputUntilComplete; + Action->RegisterWithGameInstance(World); + + return Action; +} + +void UAsyncAction_CreateWidgetAsync::Activate() +{ + SuspendInputToken = bSuspendInputUntilComplete ? UCommonUIExtensions::SuspendInputForPlayer(OwningPlayer.Get(), InputFilterReason_Template) : NAME_None; + + TWeakObjectPtr LocalWeakThis(this); + StreamingHandle = UAssetManager::Get().GetStreamableManager().RequestAsyncLoad( + UserWidgetSoftClass.ToSoftObjectPath(), + FStreamableDelegate::CreateUObject(this, &ThisClass::OnWidgetLoaded), + FStreamableManager::AsyncLoadHighPriority + ); + + // Setup a cancel delegate so that we can resume input if this handler is canceled. + StreamingHandle->BindCancelDelegate(FStreamableDelegate::CreateWeakLambda(this, + [this]() + { + UCommonUIExtensions::ResumeInputForPlayer(OwningPlayer.Get(), SuspendInputToken); + }) + ); +} + +void UAsyncAction_CreateWidgetAsync::Cancel() +{ + Super::Cancel(); + + if (StreamingHandle.IsValid()) + { + StreamingHandle->CancelHandle(); + StreamingHandle.Reset(); + } +} + +void UAsyncAction_CreateWidgetAsync::OnWidgetLoaded() +{ + if (bSuspendInputUntilComplete) + { + UCommonUIExtensions::ResumeInputForPlayer(OwningPlayer.Get(), SuspendInputToken); + } + + // If the load as successful, create it, otherwise don't complete this. + TSubclassOf UserWidgetClass = UserWidgetSoftClass.Get(); + if (UserWidgetClass) + { + UUserWidget* UserWidget = UWidgetBlueprintLibrary::Create(World.Get(), UserWidgetClass, OwningPlayer.Get()); + OnComplete.Broadcast(UserWidget); + } + + StreamingHandle.Reset(); + + SetReadyToDestroy(); +} + diff --git a/Plugins/CommonGame/Source/Private/Actions/AsyncAction_PushContentToLayerForPlayer.cpp b/Plugins/CommonGame/Source/Private/Actions/AsyncAction_PushContentToLayerForPlayer.cpp new file mode 100644 index 000000000..340d5212d --- /dev/null +++ b/Plugins/CommonGame/Source/Private/Actions/AsyncAction_PushContentToLayerForPlayer.cpp @@ -0,0 +1,81 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "Actions/AsyncAction_PushContentToLayerForPlayer.h" + +#include "Engine/Engine.h" +#include "PrimaryGameLayout.h" +#include "UObject/Stack.h" +#include "Widgets/CommonActivatableWidgetContainer.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(AsyncAction_PushContentToLayerForPlayer) + +UAsyncAction_PushContentToLayerForPlayer::UAsyncAction_PushContentToLayerForPlayer(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} + +UAsyncAction_PushContentToLayerForPlayer* UAsyncAction_PushContentToLayerForPlayer::PushContentToLayerForPlayer(APlayerController* InOwningPlayer, TSoftClassPtr InWidgetClass, FGameplayTag InLayerName, bool bSuspendInputUntilComplete) +{ + if (InWidgetClass.IsNull()) + { + FFrame::KismetExecutionMessage(TEXT("PushContentToLayerForPlayer was passed a null WidgetClass"), ELogVerbosity::Error); + return nullptr; + } + + if (UWorld* World = GEngine->GetWorldFromContextObject(InOwningPlayer, EGetWorldErrorMode::LogAndReturnNull)) + { + UAsyncAction_PushContentToLayerForPlayer* Action = NewObject(); + Action->WidgetClass = InWidgetClass; + Action->OwningPlayerPtr = InOwningPlayer; + Action->LayerName = InLayerName; + Action->bSuspendInputUntilComplete = bSuspendInputUntilComplete; + Action->RegisterWithGameInstance(World); + + return Action; + } + + return nullptr; +} + +void UAsyncAction_PushContentToLayerForPlayer::Cancel() +{ + Super::Cancel(); + + if (StreamingHandle.IsValid()) + { + StreamingHandle->CancelHandle(); + StreamingHandle.Reset(); + } +} + +void UAsyncAction_PushContentToLayerForPlayer::Activate() +{ + if (UPrimaryGameLayout* RootLayout = UPrimaryGameLayout::GetPrimaryGameLayout(OwningPlayerPtr.Get())) + { + TWeakObjectPtr WeakThis = this; + StreamingHandle = RootLayout->PushWidgetToLayerStackAsync(LayerName, bSuspendInputUntilComplete, WidgetClass, [this, WeakThis](EAsyncWidgetLayerState State, UCommonActivatableWidget* Widget) { + if (WeakThis.IsValid()) + { + switch (State) + { + case EAsyncWidgetLayerState::Initialize: + BeforePush.Broadcast(Widget); + break; + case EAsyncWidgetLayerState::AfterPush: + AfterPush.Broadcast(Widget); + SetReadyToDestroy(); + break; + case EAsyncWidgetLayerState::Canceled: + SetReadyToDestroy(); + break; + } + } + SetReadyToDestroy(); + }); + } + else + { + SetReadyToDestroy(); + } +} + diff --git a/Plugins/CommonGame/Source/Private/Actions/AsyncAction_ShowConfirmation.cpp b/Plugins/CommonGame/Source/Private/Actions/AsyncAction_ShowConfirmation.cpp new file mode 100644 index 000000000..04d516a84 --- /dev/null +++ b/Plugins/CommonGame/Source/Private/Actions/AsyncAction_ShowConfirmation.cpp @@ -0,0 +1,88 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "Actions/AsyncAction_ShowConfirmation.h" + +#include "Engine/GameInstance.h" +#include "Messaging/CommonGameDialog.h" +#include "Messaging/CommonMessagingSubsystem.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(AsyncAction_ShowConfirmation) + +UAsyncAction_ShowConfirmation::UAsyncAction_ShowConfirmation(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} + +UAsyncAction_ShowConfirmation* UAsyncAction_ShowConfirmation::ShowConfirmationYesNo(UObject* InWorldContextObject, FText Title, FText Message) +{ + UAsyncAction_ShowConfirmation* Action = NewObject(); + Action->WorldContextObject = InWorldContextObject; + Action->Descriptor = UCommonGameDialogDescriptor::CreateConfirmationYesNo(Title, Message); + Action->RegisterWithGameInstance(InWorldContextObject); + + return Action; +} + +UAsyncAction_ShowConfirmation* UAsyncAction_ShowConfirmation::ShowConfirmationOkCancel(UObject* InWorldContextObject, FText Title, FText Message) +{ + UAsyncAction_ShowConfirmation* Action = NewObject(); + Action->WorldContextObject = InWorldContextObject; + Action->Descriptor = UCommonGameDialogDescriptor::CreateConfirmationOkCancel(Title, Message); + Action->RegisterWithGameInstance(InWorldContextObject); + + return Action; +} + +UAsyncAction_ShowConfirmation* UAsyncAction_ShowConfirmation::ShowConfirmationCustom(UObject* InWorldContextObject, UCommonGameDialogDescriptor* Descriptor) +{ + UAsyncAction_ShowConfirmation* Action = NewObject(); + Action->WorldContextObject = InWorldContextObject; + Action->Descriptor = Descriptor; + Action->RegisterWithGameInstance(InWorldContextObject); + + return Action; +} + +void UAsyncAction_ShowConfirmation::Activate() +{ + if (WorldContextObject && !TargetLocalPlayer) + { + if (UUserWidget* UserWidget = Cast(WorldContextObject)) + { + TargetLocalPlayer = UserWidget->GetOwningLocalPlayer(); + } + else if (APlayerController* PC = Cast(WorldContextObject)) + { + TargetLocalPlayer = PC->GetLocalPlayer(); + } + else if (UWorld* World = WorldContextObject->GetWorld()) + { + if (UGameInstance* GameInstance = World->GetGameInstance()) + { + TargetLocalPlayer = GameInstance->GetPrimaryPlayerController(false)->GetLocalPlayer(); + } + } + } + + if (TargetLocalPlayer) + { + if (UCommonMessagingSubsystem* Messaging = TargetLocalPlayer->GetSubsystem()) + { + FCommonMessagingResultDelegate ResultCallback = FCommonMessagingResultDelegate::CreateUObject(this, &UAsyncAction_ShowConfirmation::HandleConfirmationResult); + Messaging->ShowConfirmation(Descriptor, ResultCallback); + return; + } + } + + // If we couldn't make the confirmation, just handle an unknown result and broadcast nothing + HandleConfirmationResult(ECommonMessagingResult::Unknown); +} + +void UAsyncAction_ShowConfirmation::HandleConfirmationResult(ECommonMessagingResult ConfirmationResult) +{ + OnResult.Broadcast(ConfirmationResult); + + SetReadyToDestroy(); +} + + diff --git a/Plugins/CommonGame/Source/Private/CommonGameInstance.cpp b/Plugins/CommonGame/Source/Private/CommonGameInstance.cpp new file mode 100644 index 000000000..5c1f567ab --- /dev/null +++ b/Plugins/CommonGame/Source/Private/CommonGameInstance.cpp @@ -0,0 +1,194 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "CommonGameInstance.h" + +#include "CommonLocalPlayer.h" +#include "CommonSessionSubsystem.h" +#include "CommonUISettings.h" +#include "CommonUserSubsystem.h" +#include "GameUIManagerSubsystem.h" +#include "ICommonUIModule.h" +#include "LogCommonGame.h" +#include "Messaging/CommonGameDialog.h" +#include "Messaging/CommonMessagingSubsystem.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(CommonGameInstance) + +UCommonGameInstance::UCommonGameInstance(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + +} + +void UCommonGameInstance::HandleSystemMessage(FGameplayTag MessageType, FText Title, FText Message) +{ + ULocalPlayer* FirstPlayer = GetFirstGamePlayer(); + // Forward severe ones to the error dialog for the first player + if (FirstPlayer && MessageType.MatchesTag(FCommonUserTags::SystemMessage_Error)) + { + if (UCommonMessagingSubsystem* Messaging = FirstPlayer->GetSubsystem()) + { + Messaging->ShowError(UCommonGameDialogDescriptor::CreateConfirmationOk(Title, Message)); + } + } +} + +void UCommonGameInstance::HandlePrivilegeChanged(const UCommonUserInfo* UserInfo, ECommonUserPrivilege Privilege, ECommonUserAvailability OldAvailability, ECommonUserAvailability NewAvailability) +{ + // By default show errors and disconnect if play privilege for first player is lost + if (Privilege == ECommonUserPrivilege::CanPlay && OldAvailability == ECommonUserAvailability::NowAvailable && NewAvailability != ECommonUserAvailability::NowAvailable) + { + UE_LOG(LogCommonGame, Error, TEXT("HandlePrivilegeChanged: Player %d no longer has permission to play the game!"), UserInfo->LocalPlayerIndex); + // TODO: Games can do something specific in subclass + // ReturnToMainMenu(); + } +} + +int32 UCommonGameInstance::AddLocalPlayer(ULocalPlayer* NewPlayer, FPlatformUserId UserId) +{ + int32 ReturnVal = Super::AddLocalPlayer(NewPlayer, UserId); + if (ReturnVal != INDEX_NONE) + { + if (!PrimaryPlayer.IsValid()) + { + UE_LOG(LogCommonGame, Log, TEXT("AddLocalPlayer: Set %s to Primary Player"), *NewPlayer->GetName()); + PrimaryPlayer = NewPlayer; + } + + GetSubsystem()->NotifyPlayerAdded(Cast(NewPlayer)); + } + + return ReturnVal; +} + +bool UCommonGameInstance::RemoveLocalPlayer(ULocalPlayer* ExistingPlayer) +{ + if (PrimaryPlayer == ExistingPlayer) + { + //TODO: do we want to fall back to another player? + PrimaryPlayer.Reset(); + UE_LOG(LogCommonGame, Log, TEXT("RemoveLocalPlayer: Unsetting Primary Player from %s"), *ExistingPlayer->GetName()); + } + GetSubsystem()->NotifyPlayerDestroyed(Cast(ExistingPlayer)); + + return Super::RemoveLocalPlayer(ExistingPlayer); +} + +void UCommonGameInstance::Init() +{ + Super::Init(); + + // After subsystems are initialized, hook them together + FGameplayTagContainer PlatformTraits = ICommonUIModule::GetSettings().GetPlatformTraits(); + + UCommonUserSubsystem* UserSubsystem = GetSubsystem(); + if (ensure(UserSubsystem)) + { + UserSubsystem->SetTraitTags(PlatformTraits); + UserSubsystem->OnHandleSystemMessage.AddDynamic(this, &UCommonGameInstance::HandleSystemMessage); + UserSubsystem->OnUserPrivilegeChanged.AddDynamic(this, &UCommonGameInstance::HandlePrivilegeChanged); + } + + UCommonSessionSubsystem* SessionSubsystem = GetSubsystem(); + if (ensure(SessionSubsystem)) + { + SessionSubsystem->OnUserRequestedSessionEvent.AddUObject(this, &UCommonGameInstance::OnUserRequestedSession); + } +} + +void UCommonGameInstance::ResetUserAndSessionState() +{ + UCommonUserSubsystem* UserSubsystem = GetSubsystem(); + if (ensure(UserSubsystem)) + { + UserSubsystem->ResetUserState(); + } + + UCommonSessionSubsystem* SessionSubsystem = GetSubsystem(); + if (ensure(SessionSubsystem)) + { + SessionSubsystem->CleanUpSessions(); + } +} + +void UCommonGameInstance::ReturnToMainMenu() +{ + // By default when returning to main menu we should reset everything + ResetUserAndSessionState(); + + Super::ReturnToMainMenu(); +} + +void UCommonGameInstance::OnUserRequestedSession(const FPlatformUserId& PlatformUserId, UCommonSession_SearchResult* InRequestedSession, const FOnlineResultInformation& RequestedSessionResult) +{ + if (InRequestedSession) + { + SetRequestedSession(InRequestedSession); + } + else + { + HandleSystemMessage(FCommonUserTags::SystemMessage_Error, NSLOCTEXT("CommonGame", "Warning_RequestedSessionFailed", "Requested Session Failed"), RequestedSessionResult.ErrorText); + } +} + +void UCommonGameInstance::SetRequestedSession(UCommonSession_SearchResult* InRequestedSession) +{ + RequestedSession = InRequestedSession; + if (RequestedSession) + { + if (CanJoinRequestedSession()) + { + JoinRequestedSession(); + } + else + { + ResetGameAndJoinRequestedSession(); + } + } +} + +bool UCommonGameInstance::CanJoinRequestedSession() const +{ + // Default behavior is always allow joining the requested session + return true; +} + +void UCommonGameInstance::JoinRequestedSession() +{ + if (RequestedSession) + { + if (ULocalPlayer* const FirstPlayer = GetFirstGamePlayer()) + { + UCommonSessionSubsystem* SessionSubsystem = GetSubsystem(); + if (ensure(SessionSubsystem)) + { + // Clear our current requested session since we are now acting on it. + UCommonSession_SearchResult* LocalRequestedSession = RequestedSession; + RequestedSession = nullptr; + SessionSubsystem->JoinSession(FirstPlayer->PlayerController, LocalRequestedSession); + } + } + } +} + +void UCommonGameInstance::ResetGameAndJoinRequestedSession() +{ + // Default behavior is to return to the main menu. The game must call JoinRequestedSession when the game is in a ready state. + ReturnToMainMenu(); +} + + +//void UCommonGameInstance::OnPreLoadMap(const FString& MapName) +//{ +// if (!IsDedicatedServerInstance()) +// { +// if (!bWasInLoadMap) +// { +// UGameUIManagerSubsystem* UIManager = GetSubsystem(); +// for (ULocalPlayer* LocalPlayer : LocalPlayers) +// { +// UIManager->NotifyPlayerAdded(Cast(LocalPlayer)); +// } +// } +// } +//} diff --git a/Plugins/CommonGame/Source/Private/CommonGameModule.cpp b/Plugins/CommonGame/Source/Private/CommonGameModule.cpp new file mode 100644 index 000000000..368f54ad0 --- /dev/null +++ b/Plugins/CommonGame/Source/Private/CommonGameModule.cpp @@ -0,0 +1,32 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "Modules/ModuleManager.h" + +/** + * Implements the FCommonGameModule module. + */ +class FCommonGameModule : public IModuleInterface +{ +public: + FCommonGameModule(); + virtual void StartupModule() override; + virtual void ShutdownModule() override; + +private: + +}; + + +FCommonGameModule::FCommonGameModule() +{ +} + +void FCommonGameModule::StartupModule() +{ +} + +void FCommonGameModule::ShutdownModule() +{ +} + +IMPLEMENT_MODULE(FCommonGameModule, CommonGame); diff --git a/Plugins/CommonGame/Source/Private/CommonLocalPlayer.cpp b/Plugins/CommonGame/Source/Private/CommonLocalPlayer.cpp new file mode 100644 index 000000000..0af17e00f --- /dev/null +++ b/Plugins/CommonGame/Source/Private/CommonLocalPlayer.cpp @@ -0,0 +1,81 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "CommonLocalPlayer.h" + +#include "Engine/GameInstance.h" +#include "GameFramework/PlayerController.h" +#include "GameUIManagerSubsystem.h" +#include "GameUIPolicy.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(CommonLocalPlayer) + +class APawn; +class APlayerState; +class FViewport; +struct FSceneViewProjectionData; + +UCommonLocalPlayer::UCommonLocalPlayer() + : Super(FObjectInitializer::Get()) +{ +} + +FDelegateHandle UCommonLocalPlayer::CallAndRegister_OnPlayerControllerSet(FPlayerControllerSetDelegate::FDelegate Delegate) +{ + APlayerController* PC = GetPlayerController(GetWorld()); + + if (PC) + { + Delegate.Execute(this, PC); + } + + return OnPlayerControllerSet.Add(Delegate); +} + +FDelegateHandle UCommonLocalPlayer::CallAndRegister_OnPlayerStateSet(FPlayerStateSetDelegate::FDelegate Delegate) +{ + APlayerController* PC = GetPlayerController(GetWorld()); + APlayerState* PlayerState = PC ? PC->PlayerState : nullptr; + + if (PlayerState) + { + Delegate.Execute(this, PlayerState); + } + + return OnPlayerStateSet.Add(Delegate); +} + +FDelegateHandle UCommonLocalPlayer::CallAndRegister_OnPlayerPawnSet(FPlayerPawnSetDelegate::FDelegate Delegate) +{ + APlayerController* PC = GetPlayerController(GetWorld()); + APawn* Pawn = PC ? PC->GetPawn() : nullptr; + + if (Pawn) + { + Delegate.Execute(this, Pawn); + } + + return OnPlayerPawnSet.Add(Delegate); +} + +bool UCommonLocalPlayer::GetProjectionData(FViewport* Viewport, FSceneViewProjectionData& ProjectionData, int32 StereoViewIndex) const +{ + if (!bIsPlayerViewEnabled) + { + return false; + } + + return Super::GetProjectionData(Viewport, ProjectionData, StereoViewIndex); +} + +UPrimaryGameLayout* UCommonLocalPlayer::GetRootUILayout() const +{ + if (UGameUIManagerSubsystem* UIManager = GetGameInstance()->GetSubsystem()) + { + if (UGameUIPolicy* Policy = UIManager->GetCurrentUIPolicy()) + { + return Policy->GetRootLayout(this); + } + } + + return nullptr; +} diff --git a/Plugins/CommonGame/Source/Private/CommonPlayerController.cpp b/Plugins/CommonGame/Source/Private/CommonPlayerController.cpp new file mode 100644 index 000000000..a63d8ee01 --- /dev/null +++ b/Plugins/CommonGame/Source/Private/CommonPlayerController.cpp @@ -0,0 +1,72 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "CommonPlayerController.h" + +#include "CommonLocalPlayer.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(CommonPlayerController) + +class APawn; + +ACommonPlayerController::ACommonPlayerController(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} + +void ACommonPlayerController::ReceivedPlayer() +{ + Super::ReceivedPlayer(); + + if (UCommonLocalPlayer* LocalPlayer = Cast(Player)) + { + LocalPlayer->OnPlayerControllerSet.Broadcast(LocalPlayer, this); + + if (PlayerState) + { + LocalPlayer->OnPlayerStateSet.Broadcast(LocalPlayer, PlayerState); + } + } +} + +void ACommonPlayerController::SetPawn(APawn* InPawn) +{ + Super::SetPawn(InPawn); + + if (UCommonLocalPlayer* LocalPlayer = Cast(Player)) + { + LocalPlayer->OnPlayerPawnSet.Broadcast(LocalPlayer, InPawn); + } +} + +void ACommonPlayerController::OnPossess(APawn* APawn) +{ + Super::OnPossess(APawn); + + if (UCommonLocalPlayer* LocalPlayer = Cast(Player)) + { + LocalPlayer->OnPlayerPawnSet.Broadcast(LocalPlayer, APawn); + } +} + +void ACommonPlayerController::OnUnPossess() +{ + Super::OnUnPossess(); + + if (UCommonLocalPlayer* LocalPlayer = Cast(Player)) + { + LocalPlayer->OnPlayerPawnSet.Broadcast(LocalPlayer, nullptr); + } +} + +void ACommonPlayerController::OnRep_PlayerState() +{ + Super::OnRep_PlayerState(); + + if (PlayerState) + { + if (UCommonLocalPlayer* LocalPlayer = Cast(Player)) + { + LocalPlayer->OnPlayerStateSet.Broadcast(LocalPlayer, PlayerState); + } + } +} diff --git a/Plugins/CommonGame/Source/Private/CommonPlayerInputKey.cpp b/Plugins/CommonGame/Source/Private/CommonPlayerInputKey.cpp new file mode 100644 index 000000000..2d7ad0c1e --- /dev/null +++ b/Plugins/CommonGame/Source/Private/CommonPlayerInputKey.cpp @@ -0,0 +1,543 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "CommonPlayerInputKey.h" + +#include "CommonInputSubsystem.h" +#include "CommonInputTypeEnum.h" +#include "CommonLocalPlayer.h" +#include "CommonPlayerController.h" +#include "Fonts/FontMeasure.h" +#include "Framework/Application/SlateApplication.h" +#include "Materials/Material.h" +#include "Materials/MaterialInstanceDynamic.h" +#include "Rendering/SlateRenderer.h" +#include "TimerManager.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(CommonPlayerInputKey) + +class FPaintArgs; +class FSlateRect; + +#define LOCTEXT_NAMESPACE "CommonKeybindWidget" + +DECLARE_LOG_CATEGORY_EXTERN(LogCommonPlayerInput, Log, All); +DEFINE_LOG_CATEGORY(LogCommonPlayerInput); + +struct FSlateDrawUtil +{ + static void DrawBrushCenterFit( + FSlateWindowElementList& ElementList, + uint32 InLayer, + const FGeometry& InAllottedGeometry, + const FSlateBrush* InBrush, + const FLinearColor& InTint = FLinearColor::White) + { + DrawBrushCenterFitWithOffset + ( + ElementList, + InLayer, + InAllottedGeometry, + InBrush, + InTint, + FVector2D(0, 0) + ); + } + + static void DrawBrushCenterFitWithOffset( + FSlateWindowElementList& ElementList, + uint32 InLayer, + const FGeometry& InAllottedGeometry, + const FSlateBrush* InBrush, + const FLinearColor& InTint, + const FVector2D InOffset) + { + if (!InBrush) + { + return; + } + + const FVector2D AreaSize = InAllottedGeometry.GetLocalSize(); + const FVector2D ProgressSize = InBrush->GetImageSize(); + const float FitScale = FMath::Min(FMath::Min(AreaSize.X / ProgressSize.X, AreaSize.Y / ProgressSize.Y), 1.0f); + const FVector2D FinalSize = FitScale * ProgressSize; + + const FVector2D Offset = (InAllottedGeometry.GetLocalSize() * 0.5f) - (FinalSize * 0.5f) + InOffset; + + FSlateDrawElement::MakeBox + ( + ElementList, + InLayer, + InAllottedGeometry.ToPaintGeometry(FinalSize, FSlateLayoutTransform(Offset)), + InBrush, + ESlateDrawEffect::None, + InTint + ); + } +}; + + + +void FMeasuredText::SetText(const FText& InText) +{ + CachedText = InText; + bTextDirty = true; +} + +FVector2D FMeasuredText::UpdateTextSize(const FSlateFontInfo &InFontInfo, float FontScale) const +{ + if (bTextDirty) + { + bTextDirty = false; + CachedTextSize = FSlateApplication::Get().GetRenderer()->GetFontMeasureService()->Measure(CachedText, InFontInfo, FontScale); + } + + return CachedTextSize; +} + +UCommonPlayerInputKey::UCommonPlayerInputKey(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , BoundKeyFallback(EKeys::Invalid) + , InputTypeOverride(ECommonInputType::Count) +{ + FrameSize = FVector2D(0, 0); +} + +void UCommonPlayerInputKey::NativePreConstruct() +{ + Super::NativePreConstruct(); + + UpdateKeybindWidget(); + + if (IsDesignTime()) + { + ShowHoldBackPlate(); + RecalculateDesiredSize(); + } +} + +void UCommonPlayerInputKey::NativeConstruct() +{ + Super::NativeConstruct(); +} + +void UCommonPlayerInputKey::NativeDestruct() +{ + if (ProgressPercentageMID) + { + // Need to restore the material on the brush before we kill off the MID. + HoldProgressBrush.SetResourceObject(ProgressPercentageMID->GetMaterial()); + + ProgressPercentageMID->MarkAsGarbage(); + ProgressPercentageMID = nullptr; + } + + Super::NativeDestruct(); +} + +int32 UCommonPlayerInputKey::NativePaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const +{ + int32 MaxLayer = Super::NativePaint(Args, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled); + + if (bDrawProgress) + { + FSlateDrawUtil::DrawBrushCenterFit + ( + OutDrawElements, + ++MaxLayer, + AllottedGeometry, + &HoldProgressBrush, + FLinearColor(InWidgetStyle.GetColorAndOpacityTint() * HoldProgressBrush.GetTint(InWidgetStyle)) + ); + } + + if (bDrawCountdownText) + { + const FVector2D CountdownTextOffset = (AllottedGeometry.GetLocalSize() - CountdownText.GetTextSize()) * 0.5f; + + FSlateDrawElement::MakeText + ( + OutDrawElements, + ++MaxLayer, + AllottedGeometry.ToOffsetPaintGeometry(CountdownTextOffset), + CountdownText.GetText(), + CountdownTextFont, + ESlateDrawEffect::None, + FLinearColor(InWidgetStyle.GetColorAndOpacityTint()) + ); + } + else if (bDrawBrushForKey) + { + // Draw Shadow + FSlateDrawUtil::DrawBrushCenterFitWithOffset + ( + OutDrawElements, + ++MaxLayer, + AllottedGeometry, + &CachedKeyBrush, + FLinearColor(InWidgetStyle.GetColorAndOpacityTint() * FLinearColor::Black), + FVector2D(1, 1) + ); + + FSlateDrawUtil::DrawBrushCenterFit + ( + OutDrawElements, + ++MaxLayer, + AllottedGeometry, + &CachedKeyBrush, + FLinearColor(InWidgetStyle.GetColorAndOpacityTint() * CachedKeyBrush.GetTint(InWidgetStyle)) + ); + } + else if (KeybindText.GetTextSize().X > 0) + { + const FVector2D FrameOffset = (AllottedGeometry.GetLocalSize() - FrameSize) * 0.5f; + + FSlateDrawElement::MakeBox + ( + OutDrawElements, + ++MaxLayer, + AllottedGeometry.ToPaintGeometry(FrameSize, FSlateLayoutTransform(FrameOffset)), + &KeyBindTextBorder, + ESlateDrawEffect::None, + FLinearColor(InWidgetStyle.GetColorAndOpacityTint() * KeyBindTextBorder.GetTint(InWidgetStyle)) + ); + + const FVector2D ActionTextOffset = (AllottedGeometry.GetLocalSize() - KeybindText.GetTextSize()) * 0.5f; + + FSlateDrawElement::MakeText + ( + OutDrawElements, + ++MaxLayer, + AllottedGeometry.ToOffsetPaintGeometry(ActionTextOffset), + KeybindText.GetText(), + KeyBindTextFont, + ESlateDrawEffect::None, + FLinearColor(InWidgetStyle.GetColorAndOpacityTint()) + ); + } + + return MaxLayer; +} + +void UCommonPlayerInputKey::StartHoldProgress(FName HoldActionName, float HoldDuration) +{ + if (HoldActionName == BoundAction && ensureMsgf(HoldDuration > 0.0f, TEXT("Trying to perform hold action \"%s\" with no HoldDuration"), *BoundAction.ToString())) + { + HoldKeybindDuration = HoldDuration; + HoldKeybindStartTime = GetWorld()->GetRealTimeSeconds(); + + UpdateHoldProgress(); + } +} + +void UCommonPlayerInputKey::StopHoldProgress(FName HoldActionName, bool bCompletedSuccessfully) +{ + if (HoldActionName == BoundAction) + { + HoldKeybindStartTime = 0.f; + HoldKeybindDuration = 0.f; + + if (ensure(ProgressPercentageMID)) + { + ProgressPercentageMID->SetScalarParameterValue(PercentageMaterialParameterName, 0.f); + } + + if (bDrawCountdownText) + { + bDrawCountdownText = false; + Invalidate(EInvalidateWidget::Paint); + RecalculateDesiredSize(); + } + } +} + +void UCommonPlayerInputKey::SyncHoldProgress() +{ + // If we had an active hold action, stop it + if (HoldKeybindStartTime > 0.f) + { + StopHoldProgress(BoundAction, false); + } +} + +void UCommonPlayerInputKey::UpdateHoldProgress() +{ + if (HoldKeybindStartTime != 0.f && HoldKeybindDuration > 0.f) + { + const float CurrentTime = GetWorld()->GetRealTimeSeconds(); + const float ElapsedTime = FMath::Min(CurrentTime - HoldKeybindStartTime, HoldKeybindDuration); + const float RemainingTime = FMath::Max(0.0f, HoldKeybindDuration - ElapsedTime); + + if (ElapsedTime < HoldKeybindDuration && ensure(ProgressPercentageMID)) + { + const float HoldKeybindPercentage = ElapsedTime / HoldKeybindDuration; + ProgressPercentageMID->SetScalarParameterValue(PercentageMaterialParameterName, HoldKeybindPercentage); + + // Schedule a callback for next tick to update the hold progress again. + GetWorld()->GetTimerManager().SetTimerForNextTick(this, &ThisClass::UpdateHoldProgress); + } + + if (bShowTimeCountDown) + { + FNumberFormattingOptions Options; + Options.MinimumFractionalDigits = 1; + Options.MaximumFractionalDigits = 1; + CountdownText.SetText(FText::AsNumber(RemainingTime, &Options)); + + bDrawCountdownText = true; + Invalidate(EInvalidateWidget::Paint); + RecalculateDesiredSize(); + } + } +} + +void UCommonPlayerInputKey::UpdateKeybindWidget() +{ + if (!GetOwningPlayer()) + { + bWaitingForPlayerController = true; + return; + } + + UCommonInputSubsystem* CommonInputSubsystem = GetInputSubsystem(); + + if (CommonInputSubsystem && !CommonInputSubsystem->ShouldShowInputKeys()) + { + SetVisibility(ESlateVisibility::Collapsed); + return; + } + + const bool bIsUsingGamepad = (InputTypeOverride == ECommonInputType::Gamepad) || ((CommonInputSubsystem != nullptr) && (CommonInputSubsystem->GetCurrentInputType() == ECommonInputType::Gamepad)) ; + + if (!BoundKey.IsValid()) + { + BoundKey = BoundKeyFallback; + } + UE_LOG(LogCommonPlayerInput, Verbose, TEXT("UCommonKeybindWidget::UpdateKeybindWidget: Action: %s Key: %s"), *(BoundAction.ToString()), *(BoundKey.ToString())); + + // Must be called before Update, due to the creation of ProgressPercentageMID which will be used in Update + SetupHoldKeybind(); + + bool NewDrawBrushForKey = false; + bool NeedToRecalcSize = false; + + if (BoundKey.IsValid()) + { + SetVisibility(ESlateVisibility::HitTestInvisible); + + ShowHoldBackPlate(); + + NeedToRecalcSize = true; + } + else + { + if (bShowUnboundStatus) + { + SetVisibility(ESlateVisibility::HitTestInvisible); + NewDrawBrushForKey = false; + + KeybindText.SetText(LOCTEXT("Unbound", "Unbound")); + + NeedToRecalcSize = true; + } + else + { + SetVisibility(ESlateVisibility::Collapsed); + } + } + + if (bDrawBrushForKey != NewDrawBrushForKey) + { + bDrawBrushForKey = NewDrawBrushForKey; + Invalidate(EInvalidateWidget::Paint); + } + + // As RecalculateDesiredSize relies on the bDrawBrushForKey + // we shouldn't call it until that value has been finalized + // for the update + if (NeedToRecalcSize) + { + RecalculateDesiredSize(); + } +} + +void UCommonPlayerInputKey::SetBoundKey(FKey NewKey) +{ + if (NewKey != BoundKey) + { + BoundKeyFallback = NewKey; + BoundAction = NAME_None; + UpdateKeybindWidget(); + } +} + +void UCommonPlayerInputKey::SetBoundAction(FName NewBoundAction) +{ + bool bUpdateWidget = true; + + if (BoundAction != NewBoundAction) + { + BoundAction = NewBoundAction; + } + + if (bUpdateWidget) + { + UpdateKeybindWidget(); + } +} + +void UCommonPlayerInputKey::NativeOnInitialized() +{ + Super::NativeOnInitialized(); + + if (UCommonLocalPlayer* CommonLocalPlayer = GetOwningLocalPlayer()) + { + CommonLocalPlayer->OnPlayerControllerSet.AddUObject(this, &ThisClass::HandlePlayerControllerSet); + } +} + +void UCommonPlayerInputKey::SetForcedHoldKeybind(bool InForcedHoldKeybind) +{ + if (InForcedHoldKeybind) + { + SetForcedHoldKeybindStatus(ECommonKeybindForcedHoldStatus::ForcedHold); + } + else + { + SetForcedHoldKeybindStatus(ECommonKeybindForcedHoldStatus::NoForcedHold); + } +} + +void UCommonPlayerInputKey::SetForcedHoldKeybindStatus(ECommonKeybindForcedHoldStatus InForcedHoldKeybindStatus) +{ + ForcedHoldKeybindStatus = InForcedHoldKeybindStatus; + + UpdateKeybindWidget(); +} + +void UCommonPlayerInputKey::SetShowProgressCountDown(bool bShow) +{ + bShowTimeCountDown = bShow; +} + +void UCommonPlayerInputKey::SetupHoldKeybind() +{ + ACommonPlayerController* OwningCommonPlayer = Cast(GetOwningPlayer()); + + // Setup the hold + if (ForcedHoldKeybindStatus == ECommonKeybindForcedHoldStatus::ForcedHold) + { + bIsHoldKeybind = true; + } + else if (ForcedHoldKeybindStatus == ECommonKeybindForcedHoldStatus::NeverShowHold) + { + bIsHoldKeybind = false; + } + + if (ensure(OwningCommonPlayer)) + { + if (bIsHoldKeybind) + { + // Setup the ProgressPercentageMID + if (ProgressPercentageMID == nullptr) + { + if (UMaterialInterface* Material = Cast(HoldProgressBrush.GetResourceObject())) + { + ProgressPercentageMID = UMaterialInstanceDynamic::Create(Material, this); + HoldProgressBrush.SetResourceObject(ProgressPercentageMID); + } + } + SyncHoldProgress(); + } + } +} + +void UCommonPlayerInputKey::ShowHoldBackPlate() +{ + bool bDirty = false; + + if (IsHoldKeybind()) + { + float BrushSizeAsValue = 32.0f; + + float DesiredBoxSize = BrushSizeAsValue + 10.0f; + if (!bDrawBrushForKey) + { + DesiredBoxSize += 14.0f; + } + + const FVector2D NewDesiredBrushSize(DesiredBoxSize, DesiredBoxSize); + if (HoldProgressBrush.GetImageSize() != NewDesiredBrushSize) + { + HoldProgressBrush.SetImageSize(NewDesiredBrushSize); + bDirty = true; + } + + if (!bDrawProgress) + { + bDrawProgress = true; + bDirty = true; + } + + static const FName BackAlphaName = TEXT("BackAlpha"); + static const FName OutlineAlphaName = TEXT("OutlineAlpha"); + + if (ProgressPercentageMID) + { + ProgressPercentageMID->SetScalarParameterValue(BackAlphaName, 0.2f); + ProgressPercentageMID->SetScalarParameterValue(OutlineAlphaName, 0.4f); + } + } + else + { + if (bDrawProgress) + { + bDrawProgress = false; + bDirty = true; + } + } + + if (bDirty) + { + Invalidate(EInvalidateWidget::Paint); + } +} + +void UCommonPlayerInputKey::HandlePlayerControllerSet(UCommonLocalPlayer* LocalPlayer, APlayerController* PlayerController) +{ + if (bWaitingForPlayerController && GetOwningPlayer()) + { + UpdateKeybindWidget(); + bWaitingForPlayerController = false; + } +} + +void UCommonPlayerInputKey::RecalculateDesiredSize() +{ + FVector2D MaximumDesiredSize(0, 0); + float LayoutScale = 1; + + if (bDrawProgress) + { + MaximumDesiredSize = FVector2D::Max(MaximumDesiredSize, HoldProgressBrush.GetImageSize()); + } + + if (bDrawCountdownText) + { + MaximumDesiredSize = FVector2D::Max(MaximumDesiredSize, CountdownText.UpdateTextSize(CountdownTextFont, LayoutScale)); + } + else if (bDrawBrushForKey) + { + MaximumDesiredSize = FVector2D::Max(MaximumDesiredSize, CachedKeyBrush.GetImageSize()); + } + else + { + const FVector2D KeybindTextSize = KeybindText.UpdateTextSize(KeyBindTextFont, LayoutScale); + FrameSize = FVector2D::Max(KeybindTextSize, KeybindFrameMinimumSize) + KeybindTextPadding.GetDesiredSize(); + MaximumDesiredSize = FVector2D::Max(MaximumDesiredSize, FrameSize); + } + + SetMinimumDesiredSize(MaximumDesiredSize); +} + +#undef LOCTEXT_NAMESPACE + diff --git a/Plugins/CommonGame/Source/Private/CommonUIExtensions.cpp b/Plugins/CommonGame/Source/Private/CommonUIExtensions.cpp new file mode 100644 index 000000000..c5d1b4576 --- /dev/null +++ b/Plugins/CommonGame/Source/Private/CommonUIExtensions.cpp @@ -0,0 +1,171 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "CommonUIExtensions.h" + +#include "CommonInputSubsystem.h" +#include "CommonInputTypeEnum.h" +#include "CommonLocalPlayer.h" +#include "Engine/GameInstance.h" +#include "GameUIManagerSubsystem.h" +#include "GameUIPolicy.h" +#include "PrimaryGameLayout.h" +#include "Widgets/CommonActivatableWidgetContainer.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(CommonUIExtensions) + +int32 UCommonUIExtensions::InputSuspensions = 0; + +ECommonInputType UCommonUIExtensions::GetOwningPlayerInputType(const UUserWidget* WidgetContextObject) +{ + if (WidgetContextObject) + { + if (const UCommonInputSubsystem* InputSubsystem = UCommonInputSubsystem::Get(WidgetContextObject->GetOwningLocalPlayer())) + { + return InputSubsystem->GetCurrentInputType(); + } + } + + return ECommonInputType::Count; +} + +bool UCommonUIExtensions::IsOwningPlayerUsingTouch(const UUserWidget* WidgetContextObject) +{ + if (WidgetContextObject) + { + if (const UCommonInputSubsystem* InputSubsystem = UCommonInputSubsystem::Get(WidgetContextObject->GetOwningLocalPlayer())) + { + return InputSubsystem->GetCurrentInputType() == ECommonInputType::Touch; + } + } + return false; +} + +bool UCommonUIExtensions::IsOwningPlayerUsingGamepad(const UUserWidget* WidgetContextObject) +{ + if (WidgetContextObject) + { + if (const UCommonInputSubsystem* InputSubsystem = UCommonInputSubsystem::Get(WidgetContextObject->GetOwningLocalPlayer())) + { + return InputSubsystem->GetCurrentInputType() == ECommonInputType::Gamepad; + } + } + return false; +} + +UCommonActivatableWidget* UCommonUIExtensions::PushContentToLayer_ForPlayer(const ULocalPlayer* LocalPlayer, FGameplayTag LayerName, TSubclassOf WidgetClass) +{ + if (!ensure(LocalPlayer) || !ensure(WidgetClass != nullptr)) + { + return nullptr; + } + + if (UGameUIManagerSubsystem* UIManager = LocalPlayer->GetGameInstance()->GetSubsystem()) + { + if (UGameUIPolicy* Policy = UIManager->GetCurrentUIPolicy()) + { + if (UPrimaryGameLayout* RootLayout = Policy->GetRootLayout(CastChecked(LocalPlayer))) + { + return RootLayout->PushWidgetToLayerStack(LayerName, WidgetClass); + } + } + } + + return nullptr; +} + +void UCommonUIExtensions::PushStreamedContentToLayer_ForPlayer(const ULocalPlayer* LocalPlayer, FGameplayTag LayerName, TSoftClassPtr WidgetClass) +{ + if (!ensure(LocalPlayer) || !ensure(!WidgetClass.IsNull())) + { + return; + } + + if (UGameUIManagerSubsystem* UIManager = LocalPlayer->GetGameInstance()->GetSubsystem()) + { + if (UGameUIPolicy* Policy = UIManager->GetCurrentUIPolicy()) + { + if (UPrimaryGameLayout* RootLayout = Policy->GetRootLayout(CastChecked(LocalPlayer))) + { + const bool bSuspendInputUntilComplete = true; + RootLayout->PushWidgetToLayerStackAsync(LayerName, bSuspendInputUntilComplete, WidgetClass); + } + } + } +} + +void UCommonUIExtensions::PopContentFromLayer(UCommonActivatableWidget* ActivatableWidget) +{ + if (!ActivatableWidget) + { + // Ignore request to pop an already deleted widget + return; + } + + if (const ULocalPlayer* LocalPlayer = ActivatableWidget->GetOwningLocalPlayer()) + { + if (const UGameUIManagerSubsystem* UIManager = LocalPlayer->GetGameInstance()->GetSubsystem()) + { + if (const UGameUIPolicy* Policy = UIManager->GetCurrentUIPolicy()) + { + if (UPrimaryGameLayout* RootLayout = Policy->GetRootLayout(CastChecked(LocalPlayer))) + { + RootLayout->FindAndRemoveWidgetFromLayer(ActivatableWidget); + } + } + } + } +} + +ULocalPlayer* UCommonUIExtensions::GetLocalPlayerFromController(APlayerController* PlayerController) +{ + if (PlayerController) + { + return Cast(PlayerController->Player); + } + + return nullptr; +} + +FName UCommonUIExtensions::SuspendInputForPlayer(APlayerController* PlayerController, FName SuspendReason) +{ + return SuspendInputForPlayer(PlayerController ? PlayerController->GetLocalPlayer() : nullptr, SuspendReason); +} + +FName UCommonUIExtensions::SuspendInputForPlayer(ULocalPlayer* LocalPlayer, FName SuspendReason) +{ + if (UCommonInputSubsystem* CommonInputSubsystem = UCommonInputSubsystem::Get(LocalPlayer)) + { + InputSuspensions++; + FName SuspendToken = SuspendReason; + SuspendToken.SetNumber(InputSuspensions); + + CommonInputSubsystem->SetInputTypeFilter(ECommonInputType::MouseAndKeyboard, SuspendToken, true); + CommonInputSubsystem->SetInputTypeFilter(ECommonInputType::Gamepad, SuspendToken, true); + CommonInputSubsystem->SetInputTypeFilter(ECommonInputType::Touch, SuspendToken, true); + + return SuspendToken; + } + + return NAME_None; +} + +void UCommonUIExtensions::ResumeInputForPlayer(APlayerController* PlayerController, FName SuspendToken) +{ + ResumeInputForPlayer(PlayerController ? PlayerController->GetLocalPlayer() : nullptr, SuspendToken); +} + +void UCommonUIExtensions::ResumeInputForPlayer(ULocalPlayer* LocalPlayer, FName SuspendToken) +{ + if (SuspendToken == NAME_None) + { + return; + } + + if (UCommonInputSubsystem* CommonInputSubsystem = UCommonInputSubsystem::Get(LocalPlayer)) + { + CommonInputSubsystem->SetInputTypeFilter(ECommonInputType::MouseAndKeyboard, SuspendToken, false); + CommonInputSubsystem->SetInputTypeFilter(ECommonInputType::Gamepad, SuspendToken, false); + CommonInputSubsystem->SetInputTypeFilter(ECommonInputType::Touch, SuspendToken, false); + } +} + diff --git a/Plugins/CommonGame/Source/Private/GameUIManagerSubsystem.cpp b/Plugins/CommonGame/Source/Private/GameUIManagerSubsystem.cpp new file mode 100644 index 000000000..eb1754ade --- /dev/null +++ b/Plugins/CommonGame/Source/Private/GameUIManagerSubsystem.cpp @@ -0,0 +1,76 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "GameUIManagerSubsystem.h" + +#include "Engine/GameInstance.h" +#include "GameUIPolicy.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GameUIManagerSubsystem) + +class FSubsystemCollectionBase; +class UClass; + +void UGameUIManagerSubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); + + if (!CurrentPolicy && !DefaultUIPolicyClass.IsNull()) + { + TSubclassOf PolicyClass = DefaultUIPolicyClass.LoadSynchronous(); + SwitchToPolicy(NewObject(this, PolicyClass)); + } +} + +void UGameUIManagerSubsystem::Deinitialize() +{ + Super::Deinitialize(); + + SwitchToPolicy(nullptr); +} + +bool UGameUIManagerSubsystem::ShouldCreateSubsystem(UObject* Outer) const +{ + if (!CastChecked(Outer)->IsDedicatedServerInstance()) + { + TArray ChildClasses; + GetDerivedClasses(GetClass(), ChildClasses, false); + + // Only create an instance if there is no override implementation defined elsewhere + return ChildClasses.Num() == 0; + } + + return false; +} + +void UGameUIManagerSubsystem::NotifyPlayerAdded(UCommonLocalPlayer* LocalPlayer) +{ + if (ensure(LocalPlayer) && CurrentPolicy) + { + CurrentPolicy->NotifyPlayerAdded(LocalPlayer); + } +} + +void UGameUIManagerSubsystem::NotifyPlayerRemoved(UCommonLocalPlayer* LocalPlayer) +{ + if (LocalPlayer && CurrentPolicy) + { + CurrentPolicy->NotifyPlayerRemoved(LocalPlayer); + } +} + +void UGameUIManagerSubsystem::NotifyPlayerDestroyed(UCommonLocalPlayer* LocalPlayer) +{ + if (LocalPlayer && CurrentPolicy) + { + CurrentPolicy->NotifyPlayerDestroyed(LocalPlayer); + } +} + +void UGameUIManagerSubsystem::SwitchToPolicy(UGameUIPolicy* InPolicy) +{ + if (CurrentPolicy != InPolicy) + { + CurrentPolicy = InPolicy; + } +} + diff --git a/Plugins/CommonGame/Source/Private/GameUIPolicy.cpp b/Plugins/CommonGame/Source/Private/GameUIPolicy.cpp new file mode 100644 index 000000000..55b90bc37 --- /dev/null +++ b/Plugins/CommonGame/Source/Private/GameUIPolicy.cpp @@ -0,0 +1,203 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "GameUIPolicy.h" +#include "Engine/GameInstance.h" +#include "Framework/Application/SlateApplication.h" +#include "GameUIManagerSubsystem.h" +#include "CommonLocalPlayer.h" +#include "PrimaryGameLayout.h" +#include "Engine/Engine.h" +#include "LogCommonGame.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(GameUIPolicy) + +// Static +UGameUIPolicy* UGameUIPolicy::GetGameUIPolicy(const UObject* WorldContextObject) +{ + if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull)) + { + if (UGameInstance* GameInstance = World->GetGameInstance()) + { + if (UGameUIManagerSubsystem* UIManager = UGameInstance::GetSubsystem(GameInstance)) + { + return UIManager->GetCurrentUIPolicy(); + } + } + } + + return nullptr; +} + +UGameUIManagerSubsystem* UGameUIPolicy::GetOwningUIManager() const +{ + return CastChecked(GetOuter()); +} + +UWorld* UGameUIPolicy::GetWorld() const +{ + return GetOwningUIManager()->GetGameInstance()->GetWorld(); +} + +UPrimaryGameLayout* UGameUIPolicy::GetRootLayout(const UCommonLocalPlayer* LocalPlayer) const +{ + const FRootViewportLayoutInfo* LayoutInfo = RootViewportLayouts.FindByKey(LocalPlayer); + return LayoutInfo ? LayoutInfo->RootLayout : nullptr; +} + +void UGameUIPolicy::NotifyPlayerAdded(UCommonLocalPlayer* LocalPlayer) +{ + LocalPlayer->OnPlayerControllerSet.AddWeakLambda(this, [this](UCommonLocalPlayer* LocalPlayer, APlayerController* PlayerController) + { + NotifyPlayerRemoved(LocalPlayer); + + if (FRootViewportLayoutInfo* LayoutInfo = RootViewportLayouts.FindByKey(LocalPlayer)) + { + AddLayoutToViewport(LocalPlayer, LayoutInfo->RootLayout); + LayoutInfo->bAddedToViewport = true; + } + else + { + CreateLayoutWidget(LocalPlayer); + } + }); + + if (FRootViewportLayoutInfo* LayoutInfo = RootViewportLayouts.FindByKey(LocalPlayer)) + { + AddLayoutToViewport(LocalPlayer, LayoutInfo->RootLayout); + LayoutInfo->bAddedToViewport = true; + } + else + { + CreateLayoutWidget(LocalPlayer); + } +} + +void UGameUIPolicy::NotifyPlayerRemoved(UCommonLocalPlayer* LocalPlayer) +{ + if (FRootViewportLayoutInfo* LayoutInfo = RootViewportLayouts.FindByKey(LocalPlayer)) + { + RemoveLayoutFromViewport(LocalPlayer, LayoutInfo->RootLayout); + LayoutInfo->bAddedToViewport = false; + + if (LocalMultiplayerInteractionMode == ELocalMultiplayerInteractionMode::SingleToggle && !LocalPlayer->IsPrimaryPlayer()) + { + UPrimaryGameLayout* RootLayout = LayoutInfo->RootLayout; + if (RootLayout && !RootLayout->IsDormant()) + { + // We're removing a secondary player's root while it's in control - transfer control back to the primary player's root + RootLayout->SetIsDormant(true); + for (const FRootViewportLayoutInfo& RootLayoutInfo : RootViewportLayouts) + { + if (RootLayoutInfo.LocalPlayer->IsPrimaryPlayer()) + { + if (UPrimaryGameLayout* PrimaryRootLayout = RootLayoutInfo.RootLayout) + { + PrimaryRootLayout->SetIsDormant(false); + } + } + } + } + } + } +} + +void UGameUIPolicy::NotifyPlayerDestroyed(UCommonLocalPlayer* LocalPlayer) +{ + NotifyPlayerRemoved(LocalPlayer); + LocalPlayer->OnPlayerControllerSet.RemoveAll(this); + const int32 LayoutInfoIdx = RootViewportLayouts.IndexOfByKey(LocalPlayer); + if (LayoutInfoIdx != INDEX_NONE) + { + UPrimaryGameLayout* Layout = RootViewportLayouts[LayoutInfoIdx].RootLayout; + RootViewportLayouts.RemoveAt(LayoutInfoIdx); + + RemoveLayoutFromViewport(LocalPlayer, Layout); + + OnRootLayoutReleased(LocalPlayer, Layout); + } +} + +void UGameUIPolicy::AddLayoutToViewport(UCommonLocalPlayer* LocalPlayer, UPrimaryGameLayout* Layout) +{ + UE_LOG(LogCommonGame, Log, TEXT("[%s] is adding player [%s]'s root layout [%s] to the viewport"), *GetName(), *GetNameSafe(LocalPlayer), *GetNameSafe(Layout)); + + Layout->SetPlayerContext(FLocalPlayerContext(LocalPlayer)); + Layout->AddToPlayerScreen(1000); + + OnRootLayoutAddedToViewport(LocalPlayer, Layout); +} + +void UGameUIPolicy::RemoveLayoutFromViewport(UCommonLocalPlayer* LocalPlayer, UPrimaryGameLayout* Layout) +{ + TWeakPtr LayoutSlateWidget = Layout->GetCachedWidget(); + if (LayoutSlateWidget.IsValid()) + { + UE_LOG(LogCommonGame, Log, TEXT("[%s] is removing player [%s]'s root layout [%s] from the viewport"), *GetName(), *GetNameSafe(LocalPlayer), *GetNameSafe(Layout)); + + Layout->RemoveFromParent(); + if (LayoutSlateWidget.IsValid()) + { + UE_LOG(LogCommonGame, Log, TEXT("Player [%s]'s root layout [%s] has been removed from the viewport, but other references to its underlying Slate widget still exist. Noting in case we leak it."), *GetNameSafe(LocalPlayer), *GetNameSafe(Layout)); + } + + OnRootLayoutRemovedFromViewport(LocalPlayer, Layout); + } +} + +void UGameUIPolicy::OnRootLayoutAddedToViewport(UCommonLocalPlayer* LocalPlayer, UPrimaryGameLayout* Layout) +{ +#if WITH_EDITOR + if (GIsEditor && LocalPlayer->IsPrimaryPlayer()) + { + // So our controller will work in PIE without needing to click in the viewport + FSlateApplication::Get().SetUserFocusToGameViewport(0); + } +#endif +} + +void UGameUIPolicy::OnRootLayoutRemovedFromViewport(UCommonLocalPlayer* LocalPlayer, UPrimaryGameLayout* Layout) +{ + +} + +void UGameUIPolicy::OnRootLayoutReleased(UCommonLocalPlayer* LocalPlayer, UPrimaryGameLayout* Layout) +{ + +} + +void UGameUIPolicy::RequestPrimaryControl(UPrimaryGameLayout* Layout) +{ + if (LocalMultiplayerInteractionMode == ELocalMultiplayerInteractionMode::SingleToggle && Layout->IsDormant()) + { + for (const FRootViewportLayoutInfo& LayoutInfo : RootViewportLayouts) + { + UPrimaryGameLayout* RootLayout = LayoutInfo.RootLayout; + if (RootLayout && !RootLayout->IsDormant()) + { + RootLayout->SetIsDormant(true); + break; + } + } + Layout->SetIsDormant(false); + } +} + +void UGameUIPolicy::CreateLayoutWidget(UCommonLocalPlayer* LocalPlayer) +{ + if (APlayerController* PlayerController = LocalPlayer->GetPlayerController(GetWorld())) + { + TSubclassOf LayoutWidgetClass = GetLayoutWidgetClass(LocalPlayer); + if (ensure(LayoutWidgetClass && !LayoutWidgetClass->HasAnyClassFlags(CLASS_Abstract))) + { + UPrimaryGameLayout* NewLayoutObject = CreateWidget(PlayerController, LayoutWidgetClass); + RootViewportLayouts.Emplace(LocalPlayer, NewLayoutObject, true); + + AddLayoutToViewport(LocalPlayer, NewLayoutObject); + } + } +} + +TSubclassOf UGameUIPolicy::GetLayoutWidgetClass(UCommonLocalPlayer* LocalPlayer) +{ + return LayoutClass.LoadSynchronous(); +} diff --git a/Plugins/CommonGame/Source/Private/LogCommonGame.cpp b/Plugins/CommonGame/Source/Private/LogCommonGame.cpp new file mode 100644 index 000000000..bff69306a --- /dev/null +++ b/Plugins/CommonGame/Source/Private/LogCommonGame.cpp @@ -0,0 +1,5 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "LogCommonGame.h" + +DEFINE_LOG_CATEGORY(LogCommonGame); \ No newline at end of file diff --git a/Plugins/CommonGame/Source/Private/LogCommonGame.h b/Plugins/CommonGame/Source/Private/LogCommonGame.h new file mode 100644 index 000000000..463c0e896 --- /dev/null +++ b/Plugins/CommonGame/Source/Private/LogCommonGame.h @@ -0,0 +1,7 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "Logging/LogMacros.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogCommonGame, Log, All); diff --git a/Plugins/CommonGame/Source/Private/Messaging/CommonGameDialog.cpp b/Plugins/CommonGame/Source/Private/Messaging/CommonGameDialog.cpp new file mode 100644 index 000000000..6f19c0085 --- /dev/null +++ b/Plugins/CommonGame/Source/Private/Messaging/CommonGameDialog.cpp @@ -0,0 +1,106 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "Messaging/CommonGameDialog.h" + +#include "Messaging/CommonMessagingSubsystem.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(CommonGameDialog) + +#define LOCTEXT_NAMESPACE "Messaging" + +UCommonGameDialogDescriptor* UCommonGameDialogDescriptor::CreateConfirmationOk(const FText& Header, const FText& Body) +{ + UCommonGameDialogDescriptor* Descriptor = NewObject(); + Descriptor->Header = Header; + Descriptor->Body = Body; + + FConfirmationDialogAction ConfirmAction; + ConfirmAction.Result = ECommonMessagingResult::Confirmed; + ConfirmAction.OptionalDisplayText = LOCTEXT("Ok", "Ok"); + + Descriptor->ButtonActions.Add(ConfirmAction); + + return Descriptor; +} + +UCommonGameDialogDescriptor* UCommonGameDialogDescriptor::CreateConfirmationOkCancel(const FText& Header, const FText& Body) +{ + UCommonGameDialogDescriptor* Descriptor = NewObject(); + Descriptor->Header = Header; + Descriptor->Body = Body; + + FConfirmationDialogAction ConfirmAction; + ConfirmAction.Result = ECommonMessagingResult::Confirmed; + ConfirmAction.OptionalDisplayText = LOCTEXT("Ok", "Ok"); + + FConfirmationDialogAction CancelAction; + CancelAction.Result = ECommonMessagingResult::Cancelled; + CancelAction.OptionalDisplayText = LOCTEXT("Cancel", "Cancel"); + + Descriptor->ButtonActions.Add(ConfirmAction); + Descriptor->ButtonActions.Add(CancelAction); + + return Descriptor; +} + +UCommonGameDialogDescriptor* UCommonGameDialogDescriptor::CreateConfirmationYesNo(const FText& Header, const FText& Body) +{ + UCommonGameDialogDescriptor* Descriptor = NewObject(); + Descriptor->Header = Header; + Descriptor->Body = Body; + + FConfirmationDialogAction ConfirmAction; + ConfirmAction.Result = ECommonMessagingResult::Confirmed; + ConfirmAction.OptionalDisplayText = LOCTEXT("Yes", "Yes"); + + FConfirmationDialogAction DeclineAction; + DeclineAction.Result = ECommonMessagingResult::Declined; + DeclineAction.OptionalDisplayText = LOCTEXT("No", "No"); + + Descriptor->ButtonActions.Add(ConfirmAction); + Descriptor->ButtonActions.Add(DeclineAction); + + return Descriptor; +} + +UCommonGameDialogDescriptor* UCommonGameDialogDescriptor::CreateConfirmationYesNoCancel(const FText& Header, const FText& Body) +{ + UCommonGameDialogDescriptor* Descriptor = NewObject(); + Descriptor->Header = Header; + Descriptor->Body = Body; + + FConfirmationDialogAction ConfirmAction; + ConfirmAction.Result = ECommonMessagingResult::Confirmed; + ConfirmAction.OptionalDisplayText = LOCTEXT("Yes", "Yes"); + + FConfirmationDialogAction DeclineAction; + DeclineAction.Result = ECommonMessagingResult::Declined; + DeclineAction.OptionalDisplayText = LOCTEXT("No", "No"); + + FConfirmationDialogAction CancelAction; + CancelAction.Result = ECommonMessagingResult::Cancelled; + CancelAction.OptionalDisplayText = LOCTEXT("Cancel", "Cancel"); + + Descriptor->ButtonActions.Add(ConfirmAction); + Descriptor->ButtonActions.Add(DeclineAction); + Descriptor->ButtonActions.Add(CancelAction); + + return Descriptor; +} + +UCommonGameDialog::UCommonGameDialog() +{ + +} + +void UCommonGameDialog::SetupDialog(UCommonGameDialogDescriptor* Descriptor, FCommonMessagingResultDelegate ResultCallback) +{ + +} + +void UCommonGameDialog::KillDialog() +{ + +} + +#undef LOCTEXT_NAMESPACE diff --git a/Plugins/CommonGame/Source/Private/Messaging/CommonMessagingSubsystem.cpp b/Plugins/CommonGame/Source/Private/Messaging/CommonMessagingSubsystem.cpp new file mode 100644 index 000000000..3e9999a44 --- /dev/null +++ b/Plugins/CommonGame/Source/Private/Messaging/CommonMessagingSubsystem.cpp @@ -0,0 +1,46 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "Messaging/CommonMessagingSubsystem.h" + +#include "Engine/GameInstance.h" +#include "Engine/LocalPlayer.h" +#include "UObject/UObjectHash.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(CommonMessagingSubsystem) + +class FSubsystemCollectionBase; +class UClass; + +void UCommonMessagingSubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); +} + +void UCommonMessagingSubsystem::Deinitialize() +{ + Super::Deinitialize(); +} + +bool UCommonMessagingSubsystem::ShouldCreateSubsystem(UObject* Outer) const +{ + if (!CastChecked(Outer)->GetGameInstance()->IsDedicatedServerInstance()) + { + TArray ChildClasses; + GetDerivedClasses(GetClass(), ChildClasses, false); + + // Only create an instance if there is no override implementation defined elsewhere + return ChildClasses.Num() == 0; + } + + return false; +} + +void UCommonMessagingSubsystem::ShowConfirmation(UCommonGameDialogDescriptor* DialogDescriptor, FCommonMessagingResultDelegate ResultCallback) +{ + +} + +void UCommonMessagingSubsystem::ShowError(UCommonGameDialogDescriptor* DialogDescriptor, FCommonMessagingResultDelegate ResultCallback) +{ + +} diff --git a/Plugins/CommonGame/Source/Private/PrimaryGameLayout.cpp b/Plugins/CommonGame/Source/Private/PrimaryGameLayout.cpp new file mode 100644 index 000000000..004cdf3e3 --- /dev/null +++ b/Plugins/CommonGame/Source/Private/PrimaryGameLayout.cpp @@ -0,0 +1,132 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "PrimaryGameLayout.h" + +#include "CommonLocalPlayer.h" +#include "Engine/GameInstance.h" +#include "GameUIManagerSubsystem.h" +#include "GameUIPolicy.h" +#include "Kismet/GameplayStatics.h" +#include "LogCommonGame.h" +#include "Widgets/CommonActivatableWidgetContainer.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(PrimaryGameLayout) + +class UObject; + +/*static*/ UPrimaryGameLayout* UPrimaryGameLayout::GetPrimaryGameLayoutForPrimaryPlayer(const UObject* WorldContextObject) +{ + UGameInstance* GameInstance = UGameplayStatics::GetGameInstance(WorldContextObject); + APlayerController* PlayerController = GameInstance->GetPrimaryPlayerController(false); + return GetPrimaryGameLayout(PlayerController); +} + +/*static*/ UPrimaryGameLayout* UPrimaryGameLayout::GetPrimaryGameLayout(APlayerController* PlayerController) +{ + return PlayerController ? GetPrimaryGameLayout(Cast(PlayerController->Player)) : nullptr; +} + +/*static*/ UPrimaryGameLayout* UPrimaryGameLayout::GetPrimaryGameLayout(ULocalPlayer* LocalPlayer) +{ + if (LocalPlayer) + { + const UCommonLocalPlayer* CommonLocalPlayer = CastChecked(LocalPlayer); + if (const UGameInstance* GameInstance = CommonLocalPlayer->GetGameInstance()) + { + if (UGameUIManagerSubsystem* UIManager = GameInstance->GetSubsystem()) + { + if (const UGameUIPolicy* Policy = UIManager->GetCurrentUIPolicy()) + { + if (UPrimaryGameLayout* RootLayout = Policy->GetRootLayout(CommonLocalPlayer)) + { + return RootLayout; + } + } + } + } + } + + return nullptr; +} + +UPrimaryGameLayout::UPrimaryGameLayout(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} + +void UPrimaryGameLayout::SetIsDormant(bool InDormant) +{ + if (bIsDormant != InDormant) + { + const ULocalPlayer* LP = GetOwningLocalPlayer(); + const int32 PlayerId = LP ? LP->GetControllerId() : -1; + const TCHAR* OldDormancyStr = bIsDormant ? TEXT("Dormant") : TEXT("Not-Dormant"); + const TCHAR* NewDormancyStr = InDormant ? TEXT("Dormant") : TEXT("Not-Dormant"); + const TCHAR* PrimaryPlayerStr = LP && LP->IsPrimaryPlayer() ? TEXT("[Primary]") : TEXT("[Non-Primary]"); + UE_LOG(LogCommonGame, Display, TEXT("%s PrimaryGameLayout Dormancy changed for [%d] from [%s] to [%s]"), PrimaryPlayerStr, PlayerId, OldDormancyStr, NewDormancyStr); + + bIsDormant = InDormant; + OnIsDormantChanged(); + } +} + +void UPrimaryGameLayout::OnIsDormantChanged() +{ + //@TODO NDarnell Determine what to do with dormancy, in the past we treated dormancy as a way to shutoff rendering + //and the view for the other local players when we force multiple players to use the player view of a single player. + + //if (UCommonLocalPlayer* LocalPlayer = GetOwningLocalPlayer()) + //{ + // // When the root layout is dormant, we don't want to render anything from the owner's view either + // LocalPlayer->SetIsPlayerViewEnabled(!bIsDormant); + //} + + //SetVisibility(bIsDormant ? ESlateVisibility::Collapsed : ESlateVisibility::SelfHitTestInvisible); + + //OnLayoutDormancyChanged().Broadcast(bIsDormant); +} + +void UPrimaryGameLayout::RegisterLayer(FGameplayTag LayerTag, UCommonActivatableWidgetContainerBase* LayerWidget) +{ + if (!IsDesignTime()) + { + LayerWidget->OnTransitioningChanged.AddUObject(this, &UPrimaryGameLayout::OnWidgetStackTransitioning); + // TODO: Consider allowing a transition duration, we currently set it to 0, because if it's not 0, the + // transition effect will cause focus to not transition properly to the new widgets when using + // gamepad always. + LayerWidget->SetTransitionDuration(0.0); + + Layers.Add(LayerTag, LayerWidget); + } +} + +void UPrimaryGameLayout::OnWidgetStackTransitioning(UCommonActivatableWidgetContainerBase* Widget, bool bIsTransitioning) +{ + if (bIsTransitioning) + { + const FName SuspendToken = UCommonUIExtensions::SuspendInputForPlayer(GetOwningLocalPlayer(), TEXT("GlobalStackTransion")); + SuspendInputTokens.Add(SuspendToken); + } + else + { + if (ensure(SuspendInputTokens.Num() > 0)) + { + const FName SuspendToken = SuspendInputTokens.Pop(); + UCommonUIExtensions::ResumeInputForPlayer(GetOwningLocalPlayer(), SuspendToken); + } + } +} + +void UPrimaryGameLayout::FindAndRemoveWidgetFromLayer(UCommonActivatableWidget* ActivatableWidget) +{ + // We're not sure what layer the widget is on so go searching. + for (const auto& LayerKVP : Layers) + { + LayerKVP.Value->RemoveWidget(*ActivatableWidget); + } +} + +UCommonActivatableWidgetContainerBase* UPrimaryGameLayout::GetLayerWidget(FGameplayTag LayerName) +{ + return Layers.FindRef(LayerName); +} diff --git a/Plugins/CommonGame/Source/Public/Actions/AsyncAction_CreateWidgetAsync.h b/Plugins/CommonGame/Source/Public/Actions/AsyncAction_CreateWidgetAsync.h new file mode 100644 index 000000000..a2fb0c32e --- /dev/null +++ b/Plugins/CommonGame/Source/Public/Actions/AsyncAction_CreateWidgetAsync.h @@ -0,0 +1,51 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "Engine/CancellableAsyncAction.h" +#include "UObject/SoftObjectPtr.h" + +#include "AsyncAction_CreateWidgetAsync.generated.h" + +class APlayerController; +class UGameInstance; +class UUserWidget; +class UWorld; +struct FFrame; +struct FStreamableHandle; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FCreateWidgetAsyncDelegate, UUserWidget*, UserWidget); + +/** + * Load the widget class asynchronously, the instance the widget after the loading completes, and return it on OnComplete. + */ +UCLASS(BlueprintType) +class COMMONGAME_API UAsyncAction_CreateWidgetAsync : public UCancellableAsyncAction +{ + GENERATED_UCLASS_BODY() + +public: + virtual void Cancel() override; + + UFUNCTION(BlueprintCallable, BlueprintCosmetic, meta=(WorldContext = "WorldContextObject", BlueprintInternalUseOnly="true")) + static UAsyncAction_CreateWidgetAsync* CreateWidgetAsync(UObject* WorldContextObject, TSoftClassPtr UserWidgetSoftClass, APlayerController* OwningPlayer, bool bSuspendInputUntilComplete = true); + + virtual void Activate() override; + +public: + + UPROPERTY(BlueprintAssignable) + FCreateWidgetAsyncDelegate OnComplete; + +private: + + void OnWidgetLoaded(); + + FName SuspendInputToken; + TWeakObjectPtr OwningPlayer; + TWeakObjectPtr World; + TWeakObjectPtr GameInstance; + bool bSuspendInputUntilComplete; + TSoftClassPtr UserWidgetSoftClass; + TSharedPtr StreamingHandle; +}; diff --git a/Plugins/CommonGame/Source/Public/Actions/AsyncAction_PushContentToLayerForPlayer.h b/Plugins/CommonGame/Source/Public/Actions/AsyncAction_PushContentToLayerForPlayer.h new file mode 100644 index 000000000..1ffad4289 --- /dev/null +++ b/Plugins/CommonGame/Source/Public/Actions/AsyncAction_PushContentToLayerForPlayer.h @@ -0,0 +1,51 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "Engine/CancellableAsyncAction.h" +#include "GameplayTagContainer.h" +#include "UObject/SoftObjectPtr.h" + +#include "AsyncAction_PushContentToLayerForPlayer.generated.h" + +class APlayerController; +class UCommonActivatableWidget; +class UObject; +struct FFrame; +struct FStreamableHandle; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FPushContentToLayerForPlayerAsyncDelegate, UCommonActivatableWidget*, UserWidget); + +/** + * + */ +UCLASS(BlueprintType) +class COMMONGAME_API UAsyncAction_PushContentToLayerForPlayer : public UCancellableAsyncAction +{ + GENERATED_UCLASS_BODY() + +public: + virtual void Cancel() override; + + UFUNCTION(BlueprintCallable, BlueprintCosmetic, meta=(WorldContext = "WorldContextObject", BlueprintInternalUseOnly="true")) + static UAsyncAction_PushContentToLayerForPlayer* PushContentToLayerForPlayer(APlayerController* OwningPlayer, UPARAM(meta = (AllowAbstract=false)) TSoftClassPtr WidgetClass, UPARAM(meta = (Categories = "UI.Layer")) FGameplayTag LayerName, bool bSuspendInputUntilComplete = true); + + virtual void Activate() override; + +public: + + UPROPERTY(BlueprintAssignable) + FPushContentToLayerForPlayerAsyncDelegate BeforePush; + + UPROPERTY(BlueprintAssignable) + FPushContentToLayerForPlayerAsyncDelegate AfterPush; + +private: + + FGameplayTag LayerName; + bool bSuspendInputUntilComplete = false; + TWeakObjectPtr OwningPlayerPtr; + TSoftClassPtr WidgetClass; + + TSharedPtr StreamingHandle; +}; diff --git a/Plugins/CommonGame/Source/Public/Actions/AsyncAction_ShowConfirmation.h b/Plugins/CommonGame/Source/Public/Actions/AsyncAction_ShowConfirmation.h new file mode 100644 index 000000000..12f53966f --- /dev/null +++ b/Plugins/CommonGame/Source/Public/Actions/AsyncAction_ShowConfirmation.h @@ -0,0 +1,60 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "Kismet/BlueprintAsyncActionBase.h" + +#include "UObject/ObjectPtr.h" +#include "AsyncAction_ShowConfirmation.generated.h" + +enum class ECommonMessagingResult : uint8; + +class FText; +class UCommonGameDialogDescriptor; +class ULocalPlayer; +struct FFrame; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FCommonMessagingResultMCDelegate, ECommonMessagingResult, Result); + +/** + * Allows easily triggering an async confirmation dialog in blueprints that you can then wait on the result. + */ +UCLASS() +class UAsyncAction_ShowConfirmation : public UBlueprintAsyncActionBase +{ + GENERATED_UCLASS_BODY() + +public: + UFUNCTION(BlueprintCallable, BlueprintCosmetic, meta = (BlueprintInternalUseOnly = "true", WorldContext = "InWorldContextObject")) + static UAsyncAction_ShowConfirmation* ShowConfirmationYesNo( + UObject* InWorldContextObject, FText Title, FText Message + ); + + UFUNCTION(BlueprintCallable, BlueprintCosmetic, meta = (BlueprintInternalUseOnly = "true", WorldContext = "InWorldContextObject")) + static UAsyncAction_ShowConfirmation* ShowConfirmationOkCancel( + UObject* InWorldContextObject, FText Title, FText Message + ); + + UFUNCTION(BlueprintCallable, BlueprintCosmetic, meta = (BlueprintInternalUseOnly = "true", WorldContext = "InWorldContextObject")) + static UAsyncAction_ShowConfirmation* ShowConfirmationCustom( + UObject* InWorldContextObject, UCommonGameDialogDescriptor* Descriptor + ); + + virtual void Activate() override; + +public: + UPROPERTY(BlueprintAssignable) + FCommonMessagingResultMCDelegate OnResult; + +private: + void HandleConfirmationResult(ECommonMessagingResult ConfirmationResult); + + UPROPERTY(Transient) + TObjectPtr WorldContextObject; + + UPROPERTY(Transient) + TObjectPtr TargetLocalPlayer; + + UPROPERTY(Transient) + TObjectPtr Descriptor; +}; diff --git a/Plugins/CommonGame/Source/Public/CommonGameInstance.h b/Plugins/CommonGame/Source/Public/CommonGameInstance.h new file mode 100644 index 000000000..44c34b2bd --- /dev/null +++ b/Plugins/CommonGame/Source/Public/CommonGameInstance.h @@ -0,0 +1,72 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "Engine/GameInstance.h" + +#include "CommonGameInstance.generated.h" + +enum class ECommonUserAvailability : uint8; +enum class ECommonUserPrivilege : uint8; + +class FText; +class UCommonUserInfo; +class UCommonSession_SearchResult; +struct FOnlineResultInformation; +class ULocalPlayer; +class USocialManager; +class UObject; +struct FFrame; +struct FGameplayTag; + +UCLASS(Abstract, Config = Game) +class COMMONGAME_API UCommonGameInstance : public UGameInstance +{ + GENERATED_BODY() + +public: + UCommonGameInstance(const FObjectInitializer& ObjectInitializer); + + /** Handles errors/warnings from CommonUser, can be overridden per game */ + UFUNCTION() + virtual void HandleSystemMessage(FGameplayTag MessageType, FText Title, FText Message); + + UFUNCTION() + virtual void HandlePrivilegeChanged(const UCommonUserInfo* UserInfo, ECommonUserPrivilege Privilege, ECommonUserAvailability OldAvailability, ECommonUserAvailability NewAvailability); + + /** Call to reset user and session state, usually because a player has been disconnected */ + virtual void ResetUserAndSessionState(); + + /** + * Requested Session Flow + * Something requests the user to join a specific session (for example, a platform overlay via OnUserRequestedSession). + * This request is handled in SetRequestedSession. + * Check if we can join the requested session immediately (CanJoinRequestedSession). If we can, join the requested session (JoinRequestedSession) + * If not, cache the requested session and instruct the game to get into a state where the session can be joined (ResetGameAndJoinRequestedSession) + */ + /** Handles user accepting a session invite from an external source (for example, a platform overlay). Intended to be overridden per game. */ + virtual void OnUserRequestedSession(const FPlatformUserId& PlatformUserId, UCommonSession_SearchResult* InRequestedSession, const FOnlineResultInformation& RequestedSessionResult); + + /** Get the requested session */ + UCommonSession_SearchResult* GetRequestedSession() const { return RequestedSession; } + /** Set (or clear) the requested session. When this is set, the requested session flow begins. */ + virtual void SetRequestedSession(UCommonSession_SearchResult* InRequestedSession); + /** Checks if the requested session can be joined. Can be overridden per game. */ + virtual bool CanJoinRequestedSession() const; + /** Join the requested session */ + virtual void JoinRequestedSession(); + /** Get the game into a state to join the requested session */ + virtual void ResetGameAndJoinRequestedSession(); + + virtual int32 AddLocalPlayer(ULocalPlayer* NewPlayer, FPlatformUserId UserId) override; + virtual bool RemoveLocalPlayer(ULocalPlayer* ExistingPlayer) override; + virtual void Init() override; + virtual void ReturnToMainMenu() override; + +private: + /** This is the primary player*/ + TWeakObjectPtr PrimaryPlayer; + /** Session the player has requested to join */ + UPROPERTY() + TObjectPtr RequestedSession; +}; diff --git a/Plugins/CommonGame/Source/Public/CommonLocalPlayer.h b/Plugins/CommonGame/Source/Public/CommonLocalPlayer.h new file mode 100644 index 000000000..e627c7d7e --- /dev/null +++ b/Plugins/CommonGame/Source/Public/CommonLocalPlayer.h @@ -0,0 +1,51 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "Engine/LocalPlayer.h" + +#include "CommonLocalPlayer.generated.h" + +class APawn; +class APlayerController; +class APlayerState; +class FViewport; +class UObject; +class UPrimaryGameLayout; +struct FSceneViewProjectionData; + +UCLASS(config=Engine, transient) +class COMMONGAME_API UCommonLocalPlayer : public ULocalPlayer +{ + GENERATED_BODY() + +public: + UCommonLocalPlayer(); + + /** Called when the local player is assigned a player controller */ + DECLARE_MULTICAST_DELEGATE_TwoParams(FPlayerControllerSetDelegate, UCommonLocalPlayer* LocalPlayer, APlayerController* PlayerController); + FPlayerControllerSetDelegate OnPlayerControllerSet; + + /** Called when the local player is assigned a player state */ + DECLARE_MULTICAST_DELEGATE_TwoParams(FPlayerStateSetDelegate, UCommonLocalPlayer* LocalPlayer, APlayerState* PlayerState); + FPlayerStateSetDelegate OnPlayerStateSet; + + /** Called when the local player is assigned a player pawn */ + DECLARE_MULTICAST_DELEGATE_TwoParams(FPlayerPawnSetDelegate, UCommonLocalPlayer* LocalPlayer, APawn* Pawn); + FPlayerPawnSetDelegate OnPlayerPawnSet; + + FDelegateHandle CallAndRegister_OnPlayerControllerSet(FPlayerControllerSetDelegate::FDelegate Delegate); + FDelegateHandle CallAndRegister_OnPlayerStateSet(FPlayerStateSetDelegate::FDelegate Delegate); + FDelegateHandle CallAndRegister_OnPlayerPawnSet(FPlayerPawnSetDelegate::FDelegate Delegate); + +public: + virtual bool GetProjectionData(FViewport* Viewport, FSceneViewProjectionData& ProjectionData, int32 StereoViewIndex) const override; + + bool IsPlayerViewEnabled() const { return bIsPlayerViewEnabled; } + void SetIsPlayerViewEnabled(bool bInIsPlayerViewEnabled) { bIsPlayerViewEnabled = bInIsPlayerViewEnabled; } + + UPrimaryGameLayout* GetRootUILayout() const; + +private: + bool bIsPlayerViewEnabled = true; +}; diff --git a/Plugins/CommonGame/Source/Public/CommonPlayerController.h b/Plugins/CommonGame/Source/Public/CommonPlayerController.h new file mode 100644 index 000000000..f01fa0b7f --- /dev/null +++ b/Plugins/CommonGame/Source/Public/CommonPlayerController.h @@ -0,0 +1,27 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "ModularPlayerController.h" + +#include "CommonPlayerController.generated.h" + +class APawn; +class UObject; + +UCLASS(config=Game) +class COMMONGAME_API ACommonPlayerController : public AModularPlayerController +{ + GENERATED_BODY() + +public: + ACommonPlayerController(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + virtual void ReceivedPlayer() override; + virtual void SetPawn(APawn* InPawn) override; + virtual void OnPossess(class APawn* APawn) override; + virtual void OnUnPossess() override; + +protected: + virtual void OnRep_PlayerState() override; +}; diff --git a/Plugins/CommonGame/Source/Public/CommonPlayerInputKey.h b/Plugins/CommonGame/Source/Public/CommonPlayerInputKey.h new file mode 100644 index 000000000..6b09adf8f --- /dev/null +++ b/Plugins/CommonGame/Source/Public/CommonPlayerInputKey.h @@ -0,0 +1,228 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CommonUserWidget.h" +#include "Fonts/SlateFontInfo.h" + +#include "CommonPlayerInputKey.generated.h" + +enum class ECommonInputType : uint8; + +class APlayerController; +class FPaintArgs; +class FSlateRect; +class FSlateWindowElementList; +class FWidgetStyle; +class UCommonLocalPlayer; +class UMaterialInstanceDynamic; +class UObject; +struct FFrame; +struct FGeometry; + +UENUM(BlueprintType) +enum class ECommonKeybindForcedHoldStatus : uint8 +{ + NoForcedHold, + ForcedHold, + NeverShowHold +}; + +USTRUCT() +struct FMeasuredText +{ + GENERATED_BODY() + +public: + FText GetText() const { return CachedText; } + void SetText(const FText& InText); + + FVector2D GetTextSize() const { return CachedTextSize; } + FVector2D UpdateTextSize(const FSlateFontInfo &InFontInfo, float FontScale = 1.0f) const; + +private: + + FText CachedText; + mutable FVector2D CachedTextSize; + mutable bool bTextDirty = true; +}; + +UCLASS(Abstract, BlueprintType, Blueprintable, meta = (DisableNativeTick)) +class COMMONGAME_API UCommonPlayerInputKey : public UCommonUserWidget +{ + GENERATED_BODY() + +public: + UCommonPlayerInputKey(const FObjectInitializer& ObjectInitializer); + + /** Update the key and associated display based on our current Boundaction */ + UFUNCTION(BlueprintCallable, Category = "Keybind Widget") + void UpdateKeybindWidget(); + + /** Set the bound key for our keybind */ + UFUNCTION(BlueprintCallable, Category = "Keybind Widget") + void SetBoundKey(FKey NewBoundAction); + + /** Set the bound action for our keybind */ + UFUNCTION(BlueprintCallable, Category = "Keybind Widget") + void SetBoundAction(FName NewBoundAction); + + /** Force this keybind to be a hold keybind */ + UFUNCTION(BlueprintCallable, Category = "Keybind Widget", meta=(DeprecatedFunction, DeprecationMessage = "Use SetForcedHoldKeybindStatus instead")) + void SetForcedHoldKeybind(bool InForcedHoldKeybind); + + /** Force this keybind to be a hold keybind */ + UFUNCTION(BlueprintCallable, Category = "Keybind Widget") + void SetForcedHoldKeybindStatus(ECommonKeybindForcedHoldStatus InForcedHoldKeybindStatus); + + /** Force this keybind to be a hold keybind */ + UFUNCTION(BlueprintCallable, Category = "Keybind Widget") + void SetShowProgressCountDown(bool bShow); + + /** Set the axis scale value for this keybind */ + UFUNCTION(BlueprintCallable, Category = "Keybind Widget") + void SetAxisScale(const float NewValue) { AxisScale = NewValue; } + + /** Set the preset name override value for this keybind. */ + UFUNCTION(BlueprintCallable, Category = "Keybind Widget") + void SetPresetNameOverride(const FName NewValue) { PresetNameOverride = NewValue; } + + /** Our current BoundAction */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Keybind Widget") + FName BoundAction; + + /** Scale to read when using an axis Mapping */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Keybind Widget") + float AxisScale; + + /** Key this widget is bound to set directly in blueprint. Used when we want to reference a specific key instead of an action. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Keybind Widget") + FKey BoundKeyFallback; + + /** Allows us to set the input type explicitly for the keybind widget. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Keybind Widget") + ECommonInputType InputTypeOverride; + + /** Allows us to set the preset name explicitly for the keybind widget. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Keybind Widget") + FName PresetNameOverride; + + /** Setting that can show this keybind as a hold or never show it as a hold (even if it is) */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Keybind Widget") + ECommonKeybindForcedHoldStatus ForcedHoldKeybindStatus; + + /** Called through a delegate when we start hold progress */ + UFUNCTION() + void StartHoldProgress(FName HoldActionName, float HoldDuration); + + /** Called through a delegate when we stop hold progress */ + UFUNCTION() + void StopHoldProgress(FName HoldActionName, bool bCompletedSuccessfully); + + /** Get whether this keybind is a hold action. */ + UFUNCTION(BlueprintCallable, Category = "Keybind Widget") + bool IsHoldKeybind() const { return bIsHoldKeybind; } + + UFUNCTION() + bool IsBoundKeyValid() const { return BoundKey.IsValid(); } + +protected: + virtual void NativePreConstruct() override; + virtual void NativeConstruct() override; + virtual int32 NativePaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override; + void RecalculateDesiredSize(); + + /** Overridden to destroy our MID */ + virtual void NativeDestruct() override; + + /** Whether or not this keybind widget is currently set to be a hold keybind */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Keybind Widget", meta=(ScriptName = "IsHoldKeybindValue")) + bool bIsHoldKeybind; + + /** */ + UPROPERTY(Transient) + bool bShowKeybindBorder; + + UPROPERTY(Transient) + FVector2D FrameSize; + + UPROPERTY(BlueprintReadOnly, Category = "Keybind Widget") + bool bShowTimeCountDown; + + /** Derived Key this widget is bound to */ + UPROPERTY(BlueprintReadOnly, Category = "Keybind Widget") + FKey BoundKey; + + /** Material for showing Progress */ + UPROPERTY(EditDefaultsOnly, Category = "Keybind Widget") + FSlateBrush HoldProgressBrush; + + /** The key bind text border. */ + UPROPERTY(EditDefaultsOnly, Category = "Keybind Widget") + FSlateBrush KeyBindTextBorder; + + /** Should this keybinding widget display information that it is currently unbound? */ + UPROPERTY(EditAnywhere, Category = "Keybind Widget") + bool bShowUnboundStatus = false; + + /** The font to apply at each size */ + UPROPERTY(EditDefaultsOnly, Category = "Font") + FSlateFontInfo KeyBindTextFont; + + /** The font to apply at each size */ + UPROPERTY(EditDefaultsOnly, Category = "Font") + FSlateFontInfo CountdownTextFont; + + UPROPERTY(Transient) + FMeasuredText CountdownText; + + UPROPERTY(Transient) + FMeasuredText KeybindText; + + UPROPERTY(Transient) + FMargin KeybindTextPadding; + + UPROPERTY(Transient) + FVector2D KeybindFrameMinimumSize; + + /** The material parameter name for hold percentage in the HoldKeybindImage */ + UPROPERTY(EditDefaultsOnly, Category = "Keybind Widget") + FName PercentageMaterialParameterName; + + /** MID for the progress percentage */ + UPROPERTY(Transient) + TObjectPtr ProgressPercentageMID; + + virtual void NativeOnInitialized() override; + +private: + /** + * Synchronizes the hold progress to whatever is currently set in the + * owning player controller. + */ + void SyncHoldProgress(); + + /** Called for updating the HoldKeybindImage during a hold keybind */ + void UpdateHoldProgress(); + + /** Called when we want to set up this keybind widget as a hold keybind */ + void SetupHoldKeybind(); + + void ShowHoldBackPlate(); + + void HandlePlayerControllerSet(UCommonLocalPlayer* LocalPlayer, APlayerController* PlayerController); + + /** Time when we started using a hold keybind */ + float HoldKeybindStartTime = 0; + + /** How long, in seconds, we will be doing a hold keybind */ + float HoldKeybindDuration = 0; + + bool bDrawProgress = false; + bool bDrawBrushForKey = false; + bool bDrawCountdownText = false; + bool bWaitingForPlayerController = false; + + UPROPERTY(Transient) + FSlateBrush CachedKeyBrush; +}; diff --git a/Plugins/CommonGame/Source/Public/CommonUIExtensions.h b/Plugins/CommonGame/Source/Public/CommonUIExtensions.h new file mode 100644 index 000000000..0df2e3398 --- /dev/null +++ b/Plugins/CommonGame/Source/Public/CommonUIExtensions.h @@ -0,0 +1,62 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "Kismet/BlueprintFunctionLibrary.h" +#include "UObject/SoftObjectPtr.h" + +#include "CommonUIExtensions.generated.h" + +enum class ECommonInputType : uint8; +template class TSubclassOf; + +class APlayerController; +class UCommonActivatableWidget; +class ULocalPlayer; +class UObject; +class UUserWidget; +struct FFrame; +struct FGameplayTag; + +UCLASS() +class COMMONGAME_API UCommonUIExtensions : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + UCommonUIExtensions() { } + + UFUNCTION(BlueprintPure, BlueprintCosmetic, Category = "Global UI Extensions", meta = (WorldContext = "WidgetContextObject")) + static ECommonInputType GetOwningPlayerInputType(const UUserWidget* WidgetContextObject); + + UFUNCTION(BlueprintPure, BlueprintCosmetic, Category = "Global UI Extensions", meta = (WorldContext = "WidgetContextObject")) + static bool IsOwningPlayerUsingTouch(const UUserWidget* WidgetContextObject); + + UFUNCTION(BlueprintPure, BlueprintCosmetic, Category = "Global UI Extensions", meta = (WorldContext = "WidgetContextObject")) + static bool IsOwningPlayerUsingGamepad(const UUserWidget* WidgetContextObject); + + UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category = "Global UI Extensions") + static UCommonActivatableWidget* PushContentToLayer_ForPlayer(const ULocalPlayer* LocalPlayer, UPARAM(meta = (Categories = "UI.Layer")) FGameplayTag LayerName, UPARAM(meta = (AllowAbstract = false)) TSubclassOf WidgetClass); + + UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category = "Global UI Extensions") + static void PushStreamedContentToLayer_ForPlayer(const ULocalPlayer* LocalPlayer, UPARAM(meta = (Categories = "UI.Layer")) FGameplayTag LayerName, UPARAM(meta = (AllowAbstract = false)) TSoftClassPtr WidgetClass); + + UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category = "Global UI Extensions") + static void PopContentFromLayer(UCommonActivatableWidget* ActivatableWidget); + + UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category = "Global UI Extensions") + static ULocalPlayer* GetLocalPlayerFromController(APlayerController* PlayerController); + + UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category = "Global UI Extensions") + static FName SuspendInputForPlayer(APlayerController* PlayerController, FName SuspendReason); + + static FName SuspendInputForPlayer(ULocalPlayer* LocalPlayer, FName SuspendReason); + + UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category = "Global UI Extensions") + static void ResumeInputForPlayer(APlayerController* PlayerController, FName SuspendToken); + + static void ResumeInputForPlayer(ULocalPlayer* LocalPlayer, FName SuspendToken); + +private: + static int32 InputSuspensions; +}; diff --git a/Plugins/CommonGame/Source/Public/GameUIManagerSubsystem.h b/Plugins/CommonGame/Source/Public/GameUIManagerSubsystem.h new file mode 100644 index 000000000..cfce845f0 --- /dev/null +++ b/Plugins/CommonGame/Source/Public/GameUIManagerSubsystem.h @@ -0,0 +1,50 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "Subsystems/GameInstanceSubsystem.h" +#include "UObject/SoftObjectPtr.h" + +#include "GameUIManagerSubsystem.generated.h" + +class FSubsystemCollectionBase; +class UCommonLocalPlayer; +class UGameUIPolicy; +class UObject; + +/** + * This manager is intended to be replaced by whatever your game needs to + * actually create, so this class is abstract to prevent it from being created. + * + * If you just need the basic functionality you will start by sublcassing this + * subsystem in your own game. + */ +UCLASS(Abstract, config = Game) +class COMMONGAME_API UGameUIManagerSubsystem : public UGameInstanceSubsystem +{ + GENERATED_BODY() + +public: + UGameUIManagerSubsystem() { } + + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + virtual bool ShouldCreateSubsystem(UObject* Outer) const override; + + const UGameUIPolicy* GetCurrentUIPolicy() const { return CurrentPolicy; } + UGameUIPolicy* GetCurrentUIPolicy() { return CurrentPolicy; } + + virtual void NotifyPlayerAdded(UCommonLocalPlayer* LocalPlayer); + virtual void NotifyPlayerRemoved(UCommonLocalPlayer* LocalPlayer); + virtual void NotifyPlayerDestroyed(UCommonLocalPlayer* LocalPlayer); + +protected: + void SwitchToPolicy(UGameUIPolicy* InPolicy); + +private: + UPROPERTY(Transient) + TObjectPtr CurrentPolicy = nullptr; + + UPROPERTY(config, EditAnywhere) + TSoftClassPtr DefaultUIPolicyClass; +}; diff --git a/Plugins/CommonGame/Source/Public/GameUIPolicy.h b/Plugins/CommonGame/Source/Public/GameUIPolicy.h new file mode 100644 index 000000000..f3cef57a0 --- /dev/null +++ b/Plugins/CommonGame/Source/Public/GameUIPolicy.h @@ -0,0 +1,103 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "Engine/World.h" + +#include "GameUIPolicy.generated.h" + +class UCommonLocalPlayer; +class UGameUIManagerSubsystem; +class ULocalPlayer; +class UPrimaryGameLayout; + +/** + * + */ +UENUM() +enum class ELocalMultiplayerInteractionMode : uint8 +{ + // Fullscreen viewport for the primary player only, regardless of the other player's existence + PrimaryOnly, + + // Fullscreen viewport for one player, but players can swap control over who's is displayed and who's is dormant + SingleToggle, + + // Viewports displayed simultaneously for both players + Simultaneous +}; + +USTRUCT() +struct FRootViewportLayoutInfo +{ + GENERATED_BODY() +public: + UPROPERTY(Transient) + TObjectPtr LocalPlayer = nullptr; + + UPROPERTY(Transient) + TObjectPtr RootLayout = nullptr; + + UPROPERTY(Transient) + bool bAddedToViewport = false; + + FRootViewportLayoutInfo() {} + FRootViewportLayoutInfo(ULocalPlayer* InLocalPlayer, UPrimaryGameLayout* InRootLayout, bool bIsInViewport) + : LocalPlayer(InLocalPlayer) + , RootLayout(InRootLayout) + , bAddedToViewport(bIsInViewport) + {} + + bool operator==(const ULocalPlayer* OtherLocalPlayer) const { return LocalPlayer == OtherLocalPlayer; } +}; + +UCLASS(Abstract, Blueprintable, Within = GameUIManagerSubsystem) +class COMMONGAME_API UGameUIPolicy : public UObject +{ + GENERATED_BODY() + +public: + template + static GameUIPolicyClass* GetGameUIPolicyAs(const UObject* WorldContextObject) + { + return Cast(GetGameUIPolicy(WorldContextObject)); + } + + static UGameUIPolicy* GetGameUIPolicy(const UObject* WorldContextObject); + +public: + virtual UWorld* GetWorld() const override; + UGameUIManagerSubsystem* GetOwningUIManager() const; + UPrimaryGameLayout* GetRootLayout(const UCommonLocalPlayer* LocalPlayer) const; + + ELocalMultiplayerInteractionMode GetLocalMultiplayerInteractionMode() const { return LocalMultiplayerInteractionMode; } + + void RequestPrimaryControl(UPrimaryGameLayout* Layout); + +protected: + void AddLayoutToViewport(UCommonLocalPlayer* LocalPlayer, UPrimaryGameLayout* Layout); + void RemoveLayoutFromViewport(UCommonLocalPlayer* LocalPlayer, UPrimaryGameLayout* Layout); + + virtual void OnRootLayoutAddedToViewport(UCommonLocalPlayer* LocalPlayer, UPrimaryGameLayout* Layout); + virtual void OnRootLayoutRemovedFromViewport(UCommonLocalPlayer* LocalPlayer, UPrimaryGameLayout* Layout); + virtual void OnRootLayoutReleased(UCommonLocalPlayer* LocalPlayer, UPrimaryGameLayout* Layout); + + void CreateLayoutWidget(UCommonLocalPlayer* LocalPlayer); + TSubclassOf GetLayoutWidgetClass(UCommonLocalPlayer* LocalPlayer); + +private: + ELocalMultiplayerInteractionMode LocalMultiplayerInteractionMode = ELocalMultiplayerInteractionMode::PrimaryOnly; + + UPROPERTY(EditAnywhere) + TSoftClassPtr LayoutClass; + + UPROPERTY(Transient) + TArray RootViewportLayouts; + +private: + void NotifyPlayerAdded(UCommonLocalPlayer* LocalPlayer); + void NotifyPlayerRemoved(UCommonLocalPlayer* LocalPlayer); + void NotifyPlayerDestroyed(UCommonLocalPlayer* LocalPlayer); + + friend class UGameUIManagerSubsystem; +}; diff --git a/Plugins/CommonGame/Source/Public/Messaging/CommonGameDialog.h b/Plugins/CommonGame/Source/Public/Messaging/CommonGameDialog.h new file mode 100644 index 000000000..c3a142dc1 --- /dev/null +++ b/Plugins/CommonGame/Source/Public/Messaging/CommonGameDialog.h @@ -0,0 +1,68 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CommonActivatableWidget.h" +#include "CommonMessagingSubsystem.h" + +#include "CommonGameDialog.generated.h" + +USTRUCT(BlueprintType) +struct FConfirmationDialogAction +{ + GENERATED_BODY() + +public: + /** Required: The dialog option to provide. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite) + ECommonMessagingResult Result = ECommonMessagingResult::Unknown; + + /** Optional: Display Text to use instead of the action name associated with the result. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite) + FText OptionalDisplayText; + + bool operator==(const FConfirmationDialogAction& Other) const + { + return Result == Other.Result && + OptionalDisplayText.EqualTo(Other.OptionalDisplayText); + } +}; + +UCLASS() +class COMMONGAME_API UCommonGameDialogDescriptor : public UObject +{ + GENERATED_BODY() + +public: + static UCommonGameDialogDescriptor* CreateConfirmationOk(const FText& Header, const FText& Body); + static UCommonGameDialogDescriptor* CreateConfirmationOkCancel(const FText& Header, const FText& Body); + static UCommonGameDialogDescriptor* CreateConfirmationYesNo(const FText& Header, const FText& Body); + static UCommonGameDialogDescriptor* CreateConfirmationYesNoCancel(const FText& Header, const FText& Body); + +public: + /** The header of the message to display */ + UPROPERTY(EditAnywhere, BlueprintReadWrite) + FText Header; + + /** The body of the message to display */ + UPROPERTY(EditAnywhere, BlueprintReadWrite) + FText Body; + + /** The confirm button's input action to use. */ + UPROPERTY(BlueprintReadWrite) + TArray ButtonActions; +}; + + +UCLASS(Abstract) +class COMMONGAME_API UCommonGameDialog : public UCommonActivatableWidget +{ + GENERATED_BODY() + +public: + UCommonGameDialog(); + + virtual void SetupDialog(UCommonGameDialogDescriptor* Descriptor, FCommonMessagingResultDelegate ResultCallback); + + virtual void KillDialog(); +}; diff --git a/Plugins/CommonGame/Source/Public/Messaging/CommonMessagingSubsystem.h b/Plugins/CommonGame/Source/Public/Messaging/CommonMessagingSubsystem.h new file mode 100644 index 000000000..838babc1d --- /dev/null +++ b/Plugins/CommonGame/Source/Public/Messaging/CommonMessagingSubsystem.h @@ -0,0 +1,50 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "Subsystems/LocalPlayerSubsystem.h" + +#include "CommonMessagingSubsystem.generated.h" + +class FSubsystemCollectionBase; +class UCommonGameDialogDescriptor; +class UObject; + +/** Possible results from a dialog */ +UENUM(BlueprintType) +enum class ECommonMessagingResult : uint8 +{ + /** The "yes" button was pressed */ + Confirmed, + /** The "no" button was pressed */ + Declined, + /** The "ignore/cancel" button was pressed */ + Cancelled, + /** The dialog was explicitly killed (no user input) */ + Killed, + Unknown UMETA(Hidden) +}; + +DECLARE_DELEGATE_OneParam(FCommonMessagingResultDelegate, ECommonMessagingResult /* Result */); + +/** + * + */ +UCLASS(config = Game) +class COMMONGAME_API UCommonMessagingSubsystem : public ULocalPlayerSubsystem +{ + GENERATED_BODY() + +public: + UCommonMessagingSubsystem() { } + + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + virtual bool ShouldCreateSubsystem(UObject* Outer) const override; + + virtual void ShowConfirmation(UCommonGameDialogDescriptor* DialogDescriptor, FCommonMessagingResultDelegate ResultCallback = FCommonMessagingResultDelegate()); + virtual void ShowError(UCommonGameDialogDescriptor* DialogDescriptor, FCommonMessagingResultDelegate ResultCallback = FCommonMessagingResultDelegate()); + +private: + +}; diff --git a/Plugins/CommonGame/Source/Public/PrimaryGameLayout.h b/Plugins/CommonGame/Source/Public/PrimaryGameLayout.h new file mode 100644 index 000000000..8db1e75a0 --- /dev/null +++ b/Plugins/CommonGame/Source/Public/PrimaryGameLayout.h @@ -0,0 +1,137 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CommonActivatableWidget.h" +#include "CommonUIExtensions.h" +#include "Engine/AssetManager.h" +#include "Engine/StreamableManager.h" +#include "GameplayTagContainer.h" +#include "Widgets/CommonActivatableWidgetContainer.h" // IWYU pragma: keep + +#include "PrimaryGameLayout.generated.h" + +class APlayerController; +class UClass; +class UCommonActivatableWidgetContainerBase; +class ULocalPlayer; +class UObject; +struct FFrame; + +/** + * The state of an async load operation for the UI. + */ +enum class EAsyncWidgetLayerState : uint8 +{ + Canceled, + Initialize, + AfterPush +}; + +/** + * The primary game UI layout of your game. This widget class represents how to layout, push and display all layers + * of the UI for a single player. Each player in a split-screen game will receive their own primary game layout. + */ +UCLASS(Abstract, meta = (DisableNativeTick)) +class COMMONGAME_API UPrimaryGameLayout : public UCommonUserWidget +{ + GENERATED_BODY() + +public: + static UPrimaryGameLayout* GetPrimaryGameLayoutForPrimaryPlayer(const UObject* WorldContextObject); + static UPrimaryGameLayout* GetPrimaryGameLayout(APlayerController* PlayerController); + static UPrimaryGameLayout* GetPrimaryGameLayout(ULocalPlayer* LocalPlayer); + +public: + UPrimaryGameLayout(const FObjectInitializer& ObjectInitializer); + + /** A dormant root layout is collapsed and responds only to persistent actions registered by the owning player */ + void SetIsDormant(bool Dormant); + bool IsDormant() const { return bIsDormant; } + +public: + template + TSharedPtr PushWidgetToLayerStackAsync(FGameplayTag LayerName, bool bSuspendInputUntilComplete, TSoftClassPtr ActivatableWidgetClass) + { + return PushWidgetToLayerStackAsync(LayerName, bSuspendInputUntilComplete, ActivatableWidgetClass, [](EAsyncWidgetLayerState, ActivatableWidgetT*) {}); + } + + template + TSharedPtr PushWidgetToLayerStackAsync(FGameplayTag LayerName, bool bSuspendInputUntilComplete, TSoftClassPtr ActivatableWidgetClass, TFunction StateFunc) + { + static_assert(TIsDerivedFrom::IsDerived, "Only CommonActivatableWidgets can be used here"); + + static FName NAME_PushingWidgetToLayer("PushingWidgetToLayer"); + const FName SuspendInputToken = bSuspendInputUntilComplete ? UCommonUIExtensions::SuspendInputForPlayer(GetOwningPlayer(), NAME_PushingWidgetToLayer) : NAME_None; + + FStreamableManager& StreamableManager = UAssetManager::Get().GetStreamableManager(); + TSharedPtr StreamingHandle = StreamableManager.RequestAsyncLoad(ActivatableWidgetClass.ToSoftObjectPath(), FStreamableDelegate::CreateWeakLambda(this, + [this, LayerName, ActivatableWidgetClass, StateFunc, SuspendInputToken]() + { + UCommonUIExtensions::ResumeInputForPlayer(GetOwningPlayer(), SuspendInputToken); + + ActivatableWidgetT* Widget = PushWidgetToLayerStack(LayerName, ActivatableWidgetClass.Get(), [StateFunc](ActivatableWidgetT& WidgetToInit) { + StateFunc(EAsyncWidgetLayerState::Initialize, &WidgetToInit); + }); + + StateFunc(EAsyncWidgetLayerState::AfterPush, Widget); + }) + ); + + // Setup a cancel delegate so that we can resume input if this handler is canceled. + StreamingHandle->BindCancelDelegate(FStreamableDelegate::CreateWeakLambda(this, + [this, StateFunc, SuspendInputToken]() + { + UCommonUIExtensions::ResumeInputForPlayer(GetOwningPlayer(), SuspendInputToken); + StateFunc(EAsyncWidgetLayerState::Canceled, nullptr); + }) + ); + + return StreamingHandle; + } + + template + ActivatableWidgetT* PushWidgetToLayerStack(FGameplayTag LayerName, UClass* ActivatableWidgetClass) + { + return PushWidgetToLayerStack(LayerName, ActivatableWidgetClass, [](ActivatableWidgetT&) {}); + } + + template + ActivatableWidgetT* PushWidgetToLayerStack(FGameplayTag LayerName, UClass* ActivatableWidgetClass, TFunctionRef InitInstanceFunc) + { + static_assert(TIsDerivedFrom::IsDerived, "Only CommonActivatableWidgets can be used here"); + + if (UCommonActivatableWidgetContainerBase* Layer = GetLayerWidget(LayerName)) + { + return Layer->AddWidget(ActivatableWidgetClass, InitInstanceFunc); + } + + return nullptr; + } + + // Find the widget if it exists on any of the layers and remove it from the layer. + void FindAndRemoveWidgetFromLayer(UCommonActivatableWidget* ActivatableWidget); + + // Get the layer widget for the given layer tag. + UCommonActivatableWidgetContainerBase* GetLayerWidget(FGameplayTag LayerName); + +protected: + /** Register a layer that widgets can be pushed onto. */ + UFUNCTION(BlueprintCallable, Category="Layer") + void RegisterLayer(UPARAM(meta = (Categories = "UI.Layer")) FGameplayTag LayerTag, UCommonActivatableWidgetContainerBase* LayerWidget); + + virtual void OnIsDormantChanged(); + + void OnWidgetStackTransitioning(UCommonActivatableWidgetContainerBase* Widget, bool bIsTransitioning); + +private: + bool bIsDormant = false; + + // Lets us keep track of all suspended input tokens so that multiple async UIs can be loading and we correctly suspend + // for the duration of all of them. + TArray SuspendInputTokens; + + // The registered layers for the primary layout. + UPROPERTY(Transient, meta = (Categories = "UI.Layer")) + TMap> Layers; +}; diff --git a/Plugins/CommonLoadingScreen/CommonLoadingScreen.uplugin b/Plugins/CommonLoadingScreen/CommonLoadingScreen.uplugin new file mode 100644 index 000000000..69f4f1062 --- /dev/null +++ b/Plugins/CommonLoadingScreen/CommonLoadingScreen.uplugin @@ -0,0 +1,29 @@ +{ + "FileVersion": 3, + "Version": 1, + "VersionName": "1.0", + "FriendlyName": "CommonLoadingScreen", + "Description": "Loading screen manager handling creation and display of a project-specified loading screen widget", + "Category": "Gameplay", + "CreatedBy": "Epic Games, Inc.", + "CreatedByURL": "https://www.epicgames.com", + "DocsURL": "", + "MarketplaceURL": "", + "SupportURL": "", + "CanContainContent": false, + "IsBetaVersion": false, + "IsExperimentalVersion": false, + "Installed": false, + "Modules": [ + { + "Name": "CommonLoadingScreen", + "Type": "Runtime", + "LoadingPhase": "Default" + }, + { + "Name": "CommonStartupLoadingScreen", + "Type": "ClientOnly", + "LoadingPhase": "PreLoadingScreen" + } + ] +} \ No newline at end of file diff --git a/Plugins/CommonLoadingScreen/Resources/Icon128.png b/Plugins/CommonLoadingScreen/Resources/Icon128.png new file mode 100644 index 000000000..1231d4aad Binary files /dev/null and b/Plugins/CommonLoadingScreen/Resources/Icon128.png differ diff --git a/Plugins/CommonLoadingScreen/Source/CommonLoadingScreen/CommonLoadingScreen.Build.cs b/Plugins/CommonLoadingScreen/Source/CommonLoadingScreen/CommonLoadingScreen.Build.cs new file mode 100644 index 000000000..5c9fc0790 --- /dev/null +++ b/Plugins/CommonLoadingScreen/Source/CommonLoadingScreen/CommonLoadingScreen.Build.cs @@ -0,0 +1,57 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; + +public class CommonLoadingScreen : ModuleRules +{ + public CommonLoadingScreen(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicIncludePaths.AddRange( + new string[] { + // ... add public include paths required here ... + } + ); + + + PrivateIncludePaths.AddRange( + new string[] { + // ... add other private include paths required here ... + } + ); + + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + // ... add other public dependencies that you statically link with here ... + } + ); + + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + "Engine", + "Slate", + "SlateCore", + "InputCore", + "PreLoadScreen", + "RenderCore", + "DeveloperSettings", + "UMG" + } + ); + + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + // ... add any modules that your module loads dynamically here ... + } + ); + } +} diff --git a/Plugins/CommonLoadingScreen/Source/CommonLoadingScreen/Private/CommonLoadingScreenModule.cpp b/Plugins/CommonLoadingScreen/Source/CommonLoadingScreen/Private/CommonLoadingScreenModule.cpp new file mode 100644 index 000000000..7a559700f --- /dev/null +++ b/Plugins/CommonLoadingScreen/Source/CommonLoadingScreen/Private/CommonLoadingScreenModule.cpp @@ -0,0 +1,5 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "Modules/ModuleManager.h" + +IMPLEMENT_MODULE(FDefaultModuleImpl, CommonLoadingScreen) \ No newline at end of file diff --git a/Plugins/CommonLoadingScreen/Source/CommonLoadingScreen/Private/CommonLoadingScreenSettings.cpp b/Plugins/CommonLoadingScreen/Source/CommonLoadingScreen/Private/CommonLoadingScreenSettings.cpp new file mode 100644 index 000000000..bfa8735b0 --- /dev/null +++ b/Plugins/CommonLoadingScreen/Source/CommonLoadingScreen/Private/CommonLoadingScreenSettings.cpp @@ -0,0 +1,12 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "CommonLoadingScreenSettings.h" + + +#include UE_INLINE_GENERATED_CPP_BY_NAME(CommonLoadingScreenSettings) + +UCommonLoadingScreenSettings::UCommonLoadingScreenSettings() +{ + CategoryName = TEXT("Game"); +} + diff --git a/Plugins/CommonLoadingScreen/Source/CommonLoadingScreen/Private/CommonLoadingScreenSettings.h b/Plugins/CommonLoadingScreen/Source/CommonLoadingScreen/Private/CommonLoadingScreenSettings.h new file mode 100644 index 000000000..33746aa37 --- /dev/null +++ b/Plugins/CommonLoadingScreen/Source/CommonLoadingScreen/Private/CommonLoadingScreenSettings.h @@ -0,0 +1,67 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "Engine/DeveloperSettingsBackedByCVars.h" +#include "UObject/SoftObjectPath.h" + +#include "CommonLoadingScreenSettings.generated.h" + +class UObject; + +/** + * Settings for a loading screen system. + */ +UCLASS(config=Game, defaultconfig, meta=(DisplayName="Common Loading Screen")) +class UCommonLoadingScreenSettings : public UDeveloperSettingsBackedByCVars +{ + GENERATED_BODY() + +public: + UCommonLoadingScreenSettings(); + +public: + + // The widget to load for the loading screen. + UPROPERTY(config, EditAnywhere, Category=Display, meta=(MetaClass="/Script/UMG.UserWidget")) + FSoftClassPath LoadingScreenWidget; + + // The z-order of the loading screen widget in the viewport stack + UPROPERTY(config, EditAnywhere, Category=Display) + int32 LoadingScreenZOrder = 10000; + + // How long to hold the loading screen up after other loading finishes (in seconds) to + // try to give texture streaming a chance to avoid blurriness + // + // Note: This is not normally applied in the editor for iteration time, but can be + // enabled via HoldLoadingScreenAdditionalSecsEvenInEditor + UPROPERTY(config, EditAnywhere, Category=Configuration, meta=(ForceUnits=s, ConsoleVariable="CommonLoadingScreen.HoldLoadingScreenAdditionalSecs")) + float HoldLoadingScreenAdditionalSecs = 2.0f; + + // The interval in seconds beyond which the loading screen is considered permanently hung (if non-zero). + UPROPERTY(config, EditAnywhere, Category=Configuration, meta=(ForceUnits=s)) + float LoadingScreenHeartbeatHangDuration = 0.0f; + + // The interval in seconds between each log of what is keeping a loading screen up (if non-zero). + UPROPERTY(config, EditAnywhere, Category=Configuration, meta=(ForceUnits=s)) + float LogLoadingScreenHeartbeatInterval = 5.0f; + + // When true, the reason the loading screen is shown or hidden will be printed to the log every frame. + UPROPERTY(Transient, EditAnywhere, Category=Debugging, meta=(ConsoleVariable="CommonLoadingScreen.LogLoadingScreenReasonEveryFrame")) + bool LogLoadingScreenReasonEveryFrame = 0; + + // Force the loading screen to be displayed (useful for debugging) + UPROPERTY(Transient, EditAnywhere, Category=Debugging, meta=(ConsoleVariable="CommonLoadingScreen.AlwaysShow")) + bool ForceLoadingScreenVisible = false; + + // Should we apply the additional HoldLoadingScreenAdditionalSecs delay even in the editor + // (useful when iterating on loading screens) + UPROPERTY(Transient, EditAnywhere, Category=Debugging) + bool HoldLoadingScreenAdditionalSecsEvenInEditor = false; + + // Should we apply the additional HoldLoadingScreenAdditionalSecs delay even in the editor + // (useful when iterating on loading screens) + UPROPERTY(config, EditAnywhere, Category=Configuration) + bool ForceTickLoadingScreenEvenInEditor = true; +}; + diff --git a/Plugins/CommonLoadingScreen/Source/CommonLoadingScreen/Private/LoadingScreenManager.cpp b/Plugins/CommonLoadingScreen/Source/CommonLoadingScreen/Private/LoadingScreenManager.cpp new file mode 100644 index 000000000..9b7f3448b --- /dev/null +++ b/Plugins/CommonLoadingScreen/Source/CommonLoadingScreen/Private/LoadingScreenManager.cpp @@ -0,0 +1,631 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "LoadingScreenManager.h" + +#include "HAL/ThreadHeartBeat.h" + +#include "Engine/GameInstance.h" +#include "Engine/GameViewportClient.h" +#include "Engine/Engine.h" +#include "GameFramework/GameStateBase.h" +#include "GameFramework/WorldSettings.h" +#include "Misc/CommandLine.h" +#include "Misc/ConfigCacheIni.h" + +#include "LoadingProcessInterface.h" + +#include "Framework/Application/IInputProcessor.h" +#include "Framework/Application/SlateApplication.h" + +#include "PreLoadScreen.h" +#include "PreLoadScreenManager.h" + +#include "ShaderPipelineCache.h" +#include "CommonLoadingScreenSettings.h" + +//@TODO: Used as the placeholder widget in error cases, should probably create a wrapper that at least centers it/etc... +#include "Widgets/Images/SThrobber.h" +#include "Blueprint/UserWidget.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(LoadingScreenManager) + +DECLARE_LOG_CATEGORY_EXTERN(LogLoadingScreen, Log, All); +DEFINE_LOG_CATEGORY(LogLoadingScreen); + +//@TODO: Why can GetLocalPlayers() have nullptr entries? Can it really? +//@TODO: Test with PIE mode set to simulate and decide how much (if any) loading screen action should occur +//@TODO: Allow other things implementing ILoadingProcessInterface besides GameState/PlayerController (and owned components) to register as interested parties +//@TODO: ChangeMusicSettings (either here or using the LoadingScreenVisibilityChanged delegate) +//@TODO: Studio analytics (FireEvent_PIEFinishedLoading / tracking PIE startup time for regressions, either here or using the LoadingScreenVisibilityChanged delegate) + +// Profiling category for loading screens +CSV_DEFINE_CATEGORY(LoadingScreen, true); + +////////////////////////////////////////////////////////////////////// + +bool ILoadingProcessInterface::ShouldShowLoadingScreen(UObject* TestObject, FString& OutReason) +{ + if (TestObject != nullptr) + { + if (ILoadingProcessInterface* LoadObserver = Cast(TestObject)) + { + FString ObserverReason; + if (LoadObserver->ShouldShowLoadingScreen(/*out*/ ObserverReason)) + { + if (ensureMsgf(!ObserverReason.IsEmpty(), TEXT("%s failed to set a reason why it wants to show the loading screen"), *GetPathNameSafe(TestObject))) + { + OutReason = ObserverReason; + } + return true; + } + } + } + + return false; +} + +////////////////////////////////////////////////////////////////////// + +namespace LoadingScreenCVars +{ + // CVars + static float HoldLoadingScreenAdditionalSecs = 2.0f; + static FAutoConsoleVariableRef CVarHoldLoadingScreenUpAtLeastThisLongInSecs( + TEXT("CommonLoadingScreen.HoldLoadingScreenAdditionalSecs"), + HoldLoadingScreenAdditionalSecs, + TEXT("How long to hold the loading screen up after other loading finishes (in seconds) to try to give texture streaming a chance to avoid blurriness"), + ECVF_Default | ECVF_Preview); + + static bool LogLoadingScreenReasonEveryFrame = false; + static FAutoConsoleVariableRef CVarLogLoadingScreenReasonEveryFrame( + TEXT("CommonLoadingScreen.LogLoadingScreenReasonEveryFrame"), + LogLoadingScreenReasonEveryFrame, + TEXT("When true, the reason the loading screen is shown or hidden will be printed to the log every frame."), + ECVF_Default); + + static bool ForceLoadingScreenVisible = false; + static FAutoConsoleVariableRef CVarForceLoadingScreenVisible( + TEXT("CommonLoadingScreen.AlwaysShow"), + ForceLoadingScreenVisible, + TEXT("Force the loading screen to show."), + ECVF_Default); +} + +////////////////////////////////////////////////////////////////////// +// FLoadingScreenInputPreProcessor + +// Input processor to throw in when loading screen is shown +// This will capture any inputs, so active menus under the loading screen will not interact +class FLoadingScreenInputPreProcessor : public IInputProcessor +{ +public: + FLoadingScreenInputPreProcessor() { } + virtual ~FLoadingScreenInputPreProcessor() { } + + bool CanEatInput() const + { + return !GIsEditor; + } + + //~IInputProcess interface + virtual void Tick(const float DeltaTime, FSlateApplication& SlateApp, TSharedRef Cursor) override { } + + virtual bool HandleKeyDownEvent(FSlateApplication& SlateApp, const FKeyEvent& InKeyEvent) override { return CanEatInput(); } + virtual bool HandleKeyUpEvent(FSlateApplication& SlateApp, const FKeyEvent& InKeyEvent) override { return CanEatInput(); } + virtual bool HandleAnalogInputEvent(FSlateApplication& SlateApp, const FAnalogInputEvent& InAnalogInputEvent) override { return CanEatInput(); } + virtual bool HandleMouseMoveEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override { return CanEatInput(); } + virtual bool HandleMouseButtonDownEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override { return CanEatInput(); } + virtual bool HandleMouseButtonUpEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override { return CanEatInput(); } + virtual bool HandleMouseButtonDoubleClickEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override { return CanEatInput(); } + virtual bool HandleMouseWheelOrGestureEvent(FSlateApplication& SlateApp, const FPointerEvent& InWheelEvent, const FPointerEvent* InGestureEvent) override { return CanEatInput(); } + virtual bool HandleMotionDetectedEvent(FSlateApplication& SlateApp, const FMotionEvent& MotionEvent) override { return CanEatInput(); } + //~End of IInputProcess interface +}; + +////////////////////////////////////////////////////////////////////// +// ULoadingScreenManager + +void ULoadingScreenManager::Initialize(FSubsystemCollectionBase& Collection) +{ + FCoreUObjectDelegates::PreLoadMapWithContext.AddUObject(this, &ThisClass::HandlePreLoadMap); + FCoreUObjectDelegates::PostLoadMapWithWorld.AddUObject(this, &ThisClass::HandlePostLoadMap); + + const UGameInstance* LocalGameInstance = GetGameInstance(); + check(LocalGameInstance); +} + +void ULoadingScreenManager::Deinitialize() +{ + StopBlockingInput(); + + RemoveWidgetFromViewport(); + + FCoreUObjectDelegates::PreLoadMap.RemoveAll(this); + FCoreUObjectDelegates::PostLoadMapWithWorld.RemoveAll(this); +} + +bool ULoadingScreenManager::ShouldCreateSubsystem(UObject* Outer) const +{ + // Only clients have loading screens + const UGameInstance* GameInstance = CastChecked(Outer); + const bool bIsServerWorld = GameInstance->IsDedicatedServerInstance(); + return !bIsServerWorld; +} + +void ULoadingScreenManager::Tick(float DeltaTime) +{ + UpdateLoadingScreen(); + + TimeUntilNextLogHeartbeatSeconds = FMath::Max(TimeUntilNextLogHeartbeatSeconds - DeltaTime, 0.0); +} + +ETickableTickType ULoadingScreenManager::GetTickableTickType() const +{ + return ETickableTickType::Conditional; +} + +bool ULoadingScreenManager::IsTickable() const +{ + return !HasAnyFlags(RF_ClassDefaultObject); +} + +TStatId ULoadingScreenManager::GetStatId() const +{ + RETURN_QUICK_DECLARE_CYCLE_STAT(ULoadingScreenManager, STATGROUP_Tickables); +} + +UWorld* ULoadingScreenManager::GetTickableGameObjectWorld() const +{ + return GetGameInstance()->GetWorld(); +} + +void ULoadingScreenManager::RegisterLoadingProcessor(TScriptInterface Interface) +{ + ExternalLoadingProcessors.Add(Interface.GetObject()); +} + +void ULoadingScreenManager::UnregisterLoadingProcessor(TScriptInterface Interface) +{ + ExternalLoadingProcessors.Remove(Interface.GetObject()); +} + +void ULoadingScreenManager::HandlePreLoadMap(const FWorldContext& WorldContext, const FString& MapName) +{ + if (WorldContext.OwningGameInstance == GetGameInstance()) + { + bCurrentlyInLoadMap = true; + + // Update the loading screen immediately if the engine is initialized + if (GEngine->IsInitialized()) + { + UpdateLoadingScreen(); + } + } +} + +void ULoadingScreenManager::HandlePostLoadMap(UWorld* World) +{ + if ((World != nullptr) && (World->GetGameInstance() == GetGameInstance())) + { + bCurrentlyInLoadMap = false; + } +} + +void ULoadingScreenManager::UpdateLoadingScreen() +{ + bool bLogLoadingScreenStatus = LoadingScreenCVars::LogLoadingScreenReasonEveryFrame; + + if (ShouldShowLoadingScreen()) + { + const UCommonLoadingScreenSettings* Settings = GetDefault(); + + // If we don't make it to the specified checkpoint in the given time will trigger the hang detector so we can better determine where progress stalled. + FThreadHeartBeat::Get().MonitorCheckpointStart(GetFName(), Settings->LoadingScreenHeartbeatHangDuration); + + ShowLoadingScreen(); + + if ((Settings->LogLoadingScreenHeartbeatInterval > 0.0f) && (TimeUntilNextLogHeartbeatSeconds <= 0.0)) + { + bLogLoadingScreenStatus = true; + TimeUntilNextLogHeartbeatSeconds = Settings->LogLoadingScreenHeartbeatInterval; + } + } + else + { + HideLoadingScreen(); + + FThreadHeartBeat::Get().MonitorCheckpointEnd(GetFName()); + } + + if (bLogLoadingScreenStatus) + { + UE_LOG(LogLoadingScreen, Log, TEXT("Loading screen showing: %d. Reason: %s"), bCurrentlyShowingLoadingScreen ? 1 : 0, *DebugReasonForShowingOrHidingLoadingScreen); + } +} + +bool ULoadingScreenManager::CheckForAnyNeedToShowLoadingScreen() +{ + // Start out with 'unknown' reason in case someone forgets to put a reason when changing this in the future. + DebugReasonForShowingOrHidingLoadingScreen = TEXT("Reason for Showing/Hiding LoadingScreen is unknown!"); + + const UGameInstance* LocalGameInstance = GetGameInstance(); + + if (LoadingScreenCVars::ForceLoadingScreenVisible) + { + DebugReasonForShowingOrHidingLoadingScreen = FString(TEXT("CommonLoadingScreen.AlwaysShow is true")); + return true; + } + + const FWorldContext* Context = LocalGameInstance->GetWorldContext(); + if (Context == nullptr) + { + // We don't have a world context right now... better show a loading screen + DebugReasonForShowingOrHidingLoadingScreen = FString(TEXT("The game instance has a null WorldContext")); + return true; + } + + UWorld* World = Context->World(); + if (World == nullptr) + { + DebugReasonForShowingOrHidingLoadingScreen = FString(TEXT("We have no world (FWorldContext's World() is null)")); + return true; + } + + AGameStateBase* GameState = World->GetGameState(); + if (GameState == nullptr) + { + // The game state has not yet replicated. + DebugReasonForShowingOrHidingLoadingScreen = FString(TEXT("GameState hasn't yet replicated (it's null)")); + return true; + } + + if (bCurrentlyInLoadMap) + { + // Show a loading screen if we are in LoadMap + DebugReasonForShowingOrHidingLoadingScreen = FString(TEXT("bCurrentlyInLoadMap is true")); + return true; + } + + if (!Context->TravelURL.IsEmpty()) + { + // Show a loading screen when pending travel + DebugReasonForShowingOrHidingLoadingScreen = FString(TEXT("We have pending travel (the TravelURL is not empty)")); + return true; + } + + if (Context->PendingNetGame != nullptr) + { + // Connecting to another server + DebugReasonForShowingOrHidingLoadingScreen = FString(TEXT("We are connecting to another server (PendingNetGame != nullptr)")); + return true; + } + + if (!World->HasBegunPlay()) + { + DebugReasonForShowingOrHidingLoadingScreen = FString(TEXT("World hasn't begun play")); + return true; + } + + if (World->IsInSeamlessTravel()) + { + // Show a loading screen during seamless travel + DebugReasonForShowingOrHidingLoadingScreen = FString(TEXT("We are in seamless travel")); + return true; + } + + // Ask the game state if it needs a loading screen + if (ILoadingProcessInterface::ShouldShowLoadingScreen(GameState, /*out*/ DebugReasonForShowingOrHidingLoadingScreen)) + { + return true; + } + + // Ask any game state components if they need a loading screen + for (UActorComponent* TestComponent : GameState->GetComponents()) + { + if (ILoadingProcessInterface::ShouldShowLoadingScreen(TestComponent, /*out*/ DebugReasonForShowingOrHidingLoadingScreen)) + { + return true; + } + } + + // Ask any of the external loading processors that may have been registered. These might be actors or components + // that were registered by game code to tell us to keep the loading screen up while perhaps something finishes + // streaming in. + for (const TWeakInterfacePtr& Processor : ExternalLoadingProcessors) + { + if (ILoadingProcessInterface::ShouldShowLoadingScreen(Processor.GetObject(), /*out*/ DebugReasonForShowingOrHidingLoadingScreen)) + { + return true; + } + } + + // Check each local player + bool bFoundAnyLocalPC = false; + bool bMissingAnyLocalPC = false; + + for (ULocalPlayer* LP : LocalGameInstance->GetLocalPlayers()) + { + if (LP != nullptr) + { + if (APlayerController* PC = LP->PlayerController) + { + bFoundAnyLocalPC = true; + + // Ask the PC itself if it needs a loading screen + if (ILoadingProcessInterface::ShouldShowLoadingScreen(PC, /*out*/ DebugReasonForShowingOrHidingLoadingScreen)) + { + return true; + } + + // Ask any PC components if they need a loading screen + for (UActorComponent* TestComponent : PC->GetComponents()) + { + if (ILoadingProcessInterface::ShouldShowLoadingScreen(TestComponent, /*out*/ DebugReasonForShowingOrHidingLoadingScreen)) + { + return true; + } + } + } + else + { + bMissingAnyLocalPC = true; + } + } + } + + UGameViewportClient* GameViewportClient = LocalGameInstance->GetGameViewportClient(); + const bool bIsInSplitscreen = GameViewportClient->GetCurrentSplitscreenConfiguration() != ESplitScreenType::None; + + // In splitscreen we need all player controllers to be present + if (bIsInSplitscreen && bMissingAnyLocalPC) + { + DebugReasonForShowingOrHidingLoadingScreen = FString(TEXT("At least one missing local player controller in splitscreen")); + return true; + } + + // And in non-splitscreen we need at least one player controller to be present + if (!bIsInSplitscreen && !bFoundAnyLocalPC) + { + DebugReasonForShowingOrHidingLoadingScreen = FString(TEXT("Need at least one local player controller")); + return true; + } + + // Victory! The loading screen can go away now + DebugReasonForShowingOrHidingLoadingScreen = TEXT("(nothing wants to show it anymore)"); + return false; +} + +bool ULoadingScreenManager::ShouldShowLoadingScreen() +{ + const UCommonLoadingScreenSettings* Settings = GetDefault(); + + // Check debugging commands that force the state one way or another +#if !UE_BUILD_SHIPPING + static bool bCmdLineNoLoadingScreen = FParse::Param(FCommandLine::Get(), TEXT("NoLoadingScreen")); + if (bCmdLineNoLoadingScreen) + { + DebugReasonForShowingOrHidingLoadingScreen = FString(TEXT("CommandLine has 'NoLoadingScreen'")); + return false; + } +#endif + + // Check for a need to show the loading screen + const bool bNeedToShowLoadingScreen = CheckForAnyNeedToShowLoadingScreen(); + + // Keep the loading screen up a bit longer if desired + bool bWantToForceShowLoadingScreen = false; + if (bNeedToShowLoadingScreen) + { + // Still need to show it + TimeLoadingScreenLastDismissed = -1.0; + } + else + { + // Don't *need* to show the screen anymore, but might still want to for a bit + const double CurrentTime = FPlatformTime::Seconds(); + const bool bCanHoldLoadingScreen = (!GIsEditor || Settings->HoldLoadingScreenAdditionalSecsEvenInEditor); + const double HoldLoadingScreenAdditionalSecs = bCanHoldLoadingScreen ? LoadingScreenCVars::HoldLoadingScreenAdditionalSecs : 0.0; + + if (TimeLoadingScreenLastDismissed < 0.0) + { + TimeLoadingScreenLastDismissed = CurrentTime; + } + const double TimeSinceScreenDismissed = CurrentTime - TimeLoadingScreenLastDismissed; + + // hold for an extra X seconds, to cover up streaming + if ((HoldLoadingScreenAdditionalSecs > 0.0) && (TimeSinceScreenDismissed < HoldLoadingScreenAdditionalSecs)) + { + // Make sure we're rendering the world at this point, so that textures will actually stream in + //@TODO: If bNeedToShowLoadingScreen bounces back true during this window, we won't turn this off again... + UGameViewportClient* GameViewportClient = GetGameInstance()->GetGameViewportClient(); + GameViewportClient->bDisableWorldRendering = false; + + DebugReasonForShowingOrHidingLoadingScreen = FString::Printf(TEXT("Keeping loading screen up for an additional %.2f seconds to allow texture streaming"), HoldLoadingScreenAdditionalSecs); + bWantToForceShowLoadingScreen = true; + } + } + + return bNeedToShowLoadingScreen || bWantToForceShowLoadingScreen; +} + +bool ULoadingScreenManager::IsShowingInitialLoadingScreen() const +{ + FPreLoadScreenManager* PreLoadScreenManager = FPreLoadScreenManager::Get(); + return (PreLoadScreenManager != nullptr) && PreLoadScreenManager->HasValidActivePreLoadScreen(); +} + +void ULoadingScreenManager::ShowLoadingScreen() +{ + if (bCurrentlyShowingLoadingScreen) + { + return; + } + + // Unable to show loading screen if the engine is still loading with its loading screen. + if (FPreLoadScreenManager::Get() && FPreLoadScreenManager::Get()->HasActivePreLoadScreenType(EPreLoadScreenTypes::EngineLoadingScreen)) + { + return; + } + + TimeLoadingScreenShown = FPlatformTime::Seconds(); + + bCurrentlyShowingLoadingScreen = true; + + CSV_EVENT(LoadingScreen, TEXT("Show")); + + const UCommonLoadingScreenSettings* Settings = GetDefault(); + + if (IsShowingInitialLoadingScreen()) + { + UE_LOG(LogLoadingScreen, Log, TEXT("Showing loading screen when 'IsShowingInitialLoadingScreen()' is true.")); + UE_LOG(LogLoadingScreen, Log, TEXT("%s"), *DebugReasonForShowingOrHidingLoadingScreen); + } + else + { + UE_LOG(LogLoadingScreen, Log, TEXT("Showing loading screen when 'IsShowingInitialLoadingScreen()' is false.")); + UE_LOG(LogLoadingScreen, Log, TEXT("%s"), *DebugReasonForShowingOrHidingLoadingScreen); + + UGameInstance* LocalGameInstance = GetGameInstance(); + + // Eat input while the loading screen is displayed + StartBlockingInput(); + + LoadingScreenVisibilityChanged.Broadcast(/*bIsVisible=*/ true); + + // Create the loading screen widget + TSubclassOf LoadingScreenWidgetClass = Settings->LoadingScreenWidget.TryLoadClass(); + if (UUserWidget* UserWidget = UUserWidget::CreateWidgetInstance(*LocalGameInstance, LoadingScreenWidgetClass, NAME_None)) + { + LoadingScreenWidget = UserWidget->TakeWidget(); + } + else + { + UE_LOG(LogLoadingScreen, Error, TEXT("Failed to load the loading screen widget %s, falling back to placeholder."), *Settings->LoadingScreenWidget.ToString()); + LoadingScreenWidget = SNew(SThrobber); + } + + // Add to the viewport at a high ZOrder to make sure it is on top of most things + UGameViewportClient* GameViewportClient = LocalGameInstance->GetGameViewportClient(); + GameViewportClient->AddViewportWidgetContent(LoadingScreenWidget.ToSharedRef(), Settings->LoadingScreenZOrder); + + ChangePerformanceSettings(/*bEnableLoadingScreen=*/ true); + + if (!GIsEditor || Settings->ForceTickLoadingScreenEvenInEditor) + { + // Tick Slate to make sure the loading screen is displayed immediately + FSlateApplication::Get().Tick(); + } + } +} + +void ULoadingScreenManager::HideLoadingScreen() +{ + if (!bCurrentlyShowingLoadingScreen) + { + return; + } + + StopBlockingInput(); + + if (IsShowingInitialLoadingScreen()) + { + UE_LOG(LogLoadingScreen, Log, TEXT("Hiding loading screen when 'IsShowingInitialLoadingScreen()' is true.")); + UE_LOG(LogLoadingScreen, Log, TEXT("%s"), *DebugReasonForShowingOrHidingLoadingScreen); + } + else + { + UE_LOG(LogLoadingScreen, Log, TEXT("Hiding loading screen when 'IsShowingInitialLoadingScreen()' is false.")); + UE_LOG(LogLoadingScreen, Log, TEXT("%s"), *DebugReasonForShowingOrHidingLoadingScreen); + + UE_LOG(LogLoadingScreen, Log, TEXT("Garbage Collecting before dropping load screen")); + GEngine->ForceGarbageCollection(true); + + RemoveWidgetFromViewport(); + + ChangePerformanceSettings(/*bEnableLoadingScreen=*/ false); + + // Let observers know that the loading screen is done + LoadingScreenVisibilityChanged.Broadcast(/*bIsVisible=*/ false); + } + + CSV_EVENT(LoadingScreen, TEXT("Hide")); + + const double LoadingScreenDuration = FPlatformTime::Seconds() - TimeLoadingScreenShown; + UE_LOG(LogLoadingScreen, Log, TEXT("LoadingScreen was visible for %.2fs"), LoadingScreenDuration); + + bCurrentlyShowingLoadingScreen = false; +} + +void ULoadingScreenManager::RemoveWidgetFromViewport() +{ + UGameInstance* LocalGameInstance = GetGameInstance(); + if (LoadingScreenWidget.IsValid()) + { + if (UGameViewportClient* GameViewportClient = LocalGameInstance->GetGameViewportClient()) + { + GameViewportClient->RemoveViewportWidgetContent(LoadingScreenWidget.ToSharedRef()); + } + LoadingScreenWidget.Reset(); + } +} + +void ULoadingScreenManager::StartBlockingInput() +{ + if (!InputPreProcessor.IsValid()) + { + InputPreProcessor = MakeShareable(new FLoadingScreenInputPreProcessor()); + FSlateApplication::Get().RegisterInputPreProcessor(InputPreProcessor, 0); + } +} + +void ULoadingScreenManager::StopBlockingInput() +{ + if (InputPreProcessor.IsValid()) + { + FSlateApplication::Get().UnregisterInputPreProcessor(InputPreProcessor); + InputPreProcessor.Reset(); + } +} + +void ULoadingScreenManager::ChangePerformanceSettings(bool bEnabingLoadingScreen) +{ + UGameInstance* LocalGameInstance = GetGameInstance(); + UGameViewportClient* GameViewportClient = LocalGameInstance->GetGameViewportClient(); + + FShaderPipelineCache::SetBatchMode(bEnabingLoadingScreen ? FShaderPipelineCache::BatchMode::Fast : FShaderPipelineCache::BatchMode::Background); + + // Don't bother drawing the 3D world while we're loading + GameViewportClient->bDisableWorldRendering = bEnabingLoadingScreen; + + // Make sure to prioritize streaming in levels if the loading screen is up + if (UWorld* ViewportWorld = GameViewportClient->GetWorld()) + { + if (AWorldSettings* WorldSettings = ViewportWorld->GetWorldSettings(false, false)) + { + WorldSettings->bHighPriorityLoadingLocal = bEnabingLoadingScreen; + } + } + + if (bEnabingLoadingScreen) + { + // Set a new hang detector timeout multiplier when the loading screen is visible. + double HangDurationMultiplier; + if (!GConfig || !GConfig->GetDouble(TEXT("Core.System"), TEXT("LoadingScreenHangDurationMultiplier"), /*out*/ HangDurationMultiplier, GEngineIni)) + { + HangDurationMultiplier = 1.0; + } + FThreadHeartBeat::Get().SetDurationMultiplier(HangDurationMultiplier); + + // Do not report hitches while the loading screen is up + FGameThreadHitchHeartBeat::Get().SuspendHeartBeat(); + } + else + { + // Restore the hang detector timeout when we hide the loading screen + FThreadHeartBeat::Get().SetDurationMultiplier(1.0); + + // Resume reporting hitches now that the loading screen is down + FGameThreadHitchHeartBeat::Get().ResumeHeartBeat(); + } +} + diff --git a/Plugins/CommonLoadingScreen/Source/CommonLoadingScreen/Public/LoadingProcessInterface.h b/Plugins/CommonLoadingScreen/Source/CommonLoadingScreen/Public/LoadingProcessInterface.h new file mode 100644 index 000000000..ed6cc4e3a --- /dev/null +++ b/Plugins/CommonLoadingScreen/Source/CommonLoadingScreen/Public/LoadingProcessInterface.h @@ -0,0 +1,30 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Interface.h" + +#include "LoadingProcessInterface.generated.h" + +/** Interface for things that might cause loading to happen which requires a loading screen to be displayed */ +UINTERFACE(BlueprintType) +class COMMONLOADINGSCREEN_API ULoadingProcessInterface : public UInterface +{ + GENERATED_BODY() +}; + +class COMMONLOADINGSCREEN_API ILoadingProcessInterface +{ + GENERATED_BODY() + +public: + // Checks to see if this object implements the interface, and if so asks whether or not we should + // be currently showing a loading screen + static bool ShouldShowLoadingScreen(UObject* TestObject, FString& OutReason); + + virtual bool ShouldShowLoadingScreen(FString& OutReason) const + { + return false; + } +}; diff --git a/Plugins/CommonLoadingScreen/Source/CommonLoadingScreen/Public/LoadingProcessTask.cpp b/Plugins/CommonLoadingScreen/Source/CommonLoadingScreen/Public/LoadingProcessTask.cpp new file mode 100644 index 000000000..5d36a9686 --- /dev/null +++ b/Plugins/CommonLoadingScreen/Source/CommonLoadingScreen/Public/LoadingProcessTask.cpp @@ -0,0 +1,47 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "LoadingProcessTask.h" + +#include "Engine/Engine.h" +#include "Engine/GameInstance.h" +#include "Engine/World.h" +#include "LoadingScreenManager.h" +#include "UObject/ScriptInterface.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(LoadingProcessTask) + +/*static*/ ULoadingProcessTask* ULoadingProcessTask::CreateLoadingScreenProcessTask(UObject* WorldContextObject, const FString& ShowLoadingScreenReason) +{ + UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull); + UGameInstance* GameInstance = World ? World->GetGameInstance() : nullptr; + ULoadingScreenManager* LoadingScreenManager = GameInstance ? GameInstance->GetSubsystem() : nullptr; + + if (LoadingScreenManager) + { + ULoadingProcessTask* NewLoadingTask = NewObject(LoadingScreenManager); + NewLoadingTask->SetShowLoadingScreenReason(ShowLoadingScreenReason); + + LoadingScreenManager->RegisterLoadingProcessor(NewLoadingTask); + + return NewLoadingTask; + } + + return nullptr; +} + +void ULoadingProcessTask::Unregister() +{ + ULoadingScreenManager* LoadingScreenManager = Cast(GetOuter()); + LoadingScreenManager->UnregisterLoadingProcessor(this); +} + +void ULoadingProcessTask::SetShowLoadingScreenReason(const FString& InReason) +{ + Reason = InReason; +} + +bool ULoadingProcessTask::ShouldShowLoadingScreen(FString& OutReason) const +{ + OutReason = Reason; + return true; +} diff --git a/Plugins/CommonLoadingScreen/Source/CommonLoadingScreen/Public/LoadingProcessTask.h b/Plugins/CommonLoadingScreen/Source/CommonLoadingScreen/Public/LoadingProcessTask.h new file mode 100644 index 000000000..d4ae02756 --- /dev/null +++ b/Plugins/CommonLoadingScreen/Source/CommonLoadingScreen/Public/LoadingProcessTask.h @@ -0,0 +1,33 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "LoadingProcessInterface.h" +#include "UObject/Object.h" + +#include "LoadingProcessTask.generated.h" + +struct FFrame; + +UCLASS(BlueprintType) +class COMMONLOADINGSCREEN_API ULoadingProcessTask : public UObject, public ILoadingProcessInterface +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, meta=(WorldContext = "WorldContextObject")) + static ULoadingProcessTask* CreateLoadingScreenProcessTask(UObject* WorldContextObject, const FString& ShowLoadingScreenReason); + +public: + ULoadingProcessTask() { } + + UFUNCTION(BlueprintCallable) + void Unregister(); + + UFUNCTION(BlueprintCallable) + void SetShowLoadingScreenReason(const FString& InReason); + + virtual bool ShouldShowLoadingScreen(FString& OutReason) const override; + + FString Reason; +}; diff --git a/Plugins/CommonLoadingScreen/Source/CommonLoadingScreen/Public/LoadingScreenManager.h b/Plugins/CommonLoadingScreen/Source/CommonLoadingScreen/Public/LoadingScreenManager.h new file mode 100644 index 000000000..c037cde38 --- /dev/null +++ b/Plugins/CommonLoadingScreen/Source/CommonLoadingScreen/Public/LoadingScreenManager.h @@ -0,0 +1,127 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "Subsystems/GameInstanceSubsystem.h" +#include "Tickable.h" +#include "UObject/WeakInterfacePtr.h" + +#include "LoadingScreenManager.generated.h" + +template class TScriptInterface; + +class FSubsystemCollectionBase; +class IInputProcessor; +class ILoadingProcessInterface; +class SWidget; +class UObject; +class UWorld; +struct FFrame; +struct FWorldContext; + +/** + * Handles showing/hiding the loading screen + */ +UCLASS() +class COMMONLOADINGSCREEN_API ULoadingScreenManager : public UGameInstanceSubsystem, public FTickableGameObject +{ + GENERATED_BODY() + +public: + //~USubsystem interface + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + virtual bool ShouldCreateSubsystem(UObject* Outer) const override; + //~End of USubsystem interface + + //~FTickableObjectBase interface + virtual void Tick(float DeltaTime) override; + virtual ETickableTickType GetTickableTickType() const override; + virtual bool IsTickable() const override; + virtual TStatId GetStatId() const override; + virtual UWorld* GetTickableGameObjectWorld() const override; + //~End of FTickableObjectBase interface + + UFUNCTION(BlueprintCallable, Category=LoadingScreen) + FString GetDebugReasonForShowingOrHidingLoadingScreen() const + { + return DebugReasonForShowingOrHidingLoadingScreen; + } + + /** Returns True when the loading screen is currently being shown */ + bool GetLoadingScreenDisplayStatus() const + { + return bCurrentlyShowingLoadingScreen; + } + + /** Called when the loading screen visibility changes */ + DECLARE_MULTICAST_DELEGATE_OneParam(FOnLoadingScreenVisibilityChangedDelegate, bool); + FORCEINLINE FOnLoadingScreenVisibilityChangedDelegate& OnLoadingScreenVisibilityChangedDelegate() { return LoadingScreenVisibilityChanged; } + + void RegisterLoadingProcessor(TScriptInterface Interface); + void UnregisterLoadingProcessor(TScriptInterface Interface); + +private: + void HandlePreLoadMap(const FWorldContext& WorldContext, const FString& MapName); + void HandlePostLoadMap(UWorld* World); + + /** Determines if we should show or hide the loading screen. Called every frame. */ + void UpdateLoadingScreen(); + + /** Returns true if we need to be showing the loading screen. */ + bool CheckForAnyNeedToShowLoadingScreen(); + + /** Returns true if we want to be showing the loading screen (if we need to or are artificially forcing it on for other reasons). */ + bool ShouldShowLoadingScreen(); + + /** Returns true if we are in the initial loading flow before this screen should be used */ + bool IsShowingInitialLoadingScreen() const; + + /** Shows the loading screen. Sets up the loading screen widget on the viewport */ + void ShowLoadingScreen(); + + /** Hides the loading screen. The loading screen widget will be destroyed */ + void HideLoadingScreen(); + + /** Removes the widget from the viewport */ + void RemoveWidgetFromViewport(); + + /** Prevents input from being used in-game while the loading screen is visible */ + void StartBlockingInput(); + + /** Resumes in-game input, if blocking */ + void StopBlockingInput(); + + void ChangePerformanceSettings(bool bEnabingLoadingScreen); + +private: + /** Delegate broadcast when the loading screen visibility changes */ + FOnLoadingScreenVisibilityChangedDelegate LoadingScreenVisibilityChanged; + + /** A reference to the loading screen widget we are displaying (if any) */ + TSharedPtr LoadingScreenWidget; + + /** Input processor to eat all input while the loading screen is shown */ + TSharedPtr InputPreProcessor; + + /** External loading processors, components maybe actors that delay the loading. */ + TArray> ExternalLoadingProcessors; + + /** The reason why the loading screen is up (or not) */ + FString DebugReasonForShowingOrHidingLoadingScreen; + + /** The time when we started showing the loading screen */ + double TimeLoadingScreenShown = 0.0; + + /** The time the loading screen most recently wanted to be dismissed (might still be up due to a min display duration requirement) **/ + double TimeLoadingScreenLastDismissed = -1.0; + + /** The time until the next log for why the loading screen is still up */ + double TimeUntilNextLogHeartbeatSeconds = 0.0; + + /** True when we are between PreLoadMap and PostLoadMap */ + bool bCurrentlyInLoadMap = false; + + /** True when the loading screen is currently being shown */ + bool bCurrentlyShowingLoadingScreen = false; +}; diff --git a/Plugins/CommonLoadingScreen/Source/CommonStartupLoadingScreen/CommonStartupLoadingScreen.Build.cs b/Plugins/CommonLoadingScreen/Source/CommonStartupLoadingScreen/CommonStartupLoadingScreen.Build.cs new file mode 100644 index 000000000..72eb5531f --- /dev/null +++ b/Plugins/CommonLoadingScreen/Source/CommonStartupLoadingScreen/CommonStartupLoadingScreen.Build.cs @@ -0,0 +1,55 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; + +public class CommonStartupLoadingScreen : ModuleRules +{ + public CommonStartupLoadingScreen(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicIncludePaths.AddRange( + new string[] { + // ... add public include paths required here ... + } + ); + + + PrivateIncludePaths.AddRange( + new string[] { + // ... add other private include paths required here ... + } + ); + + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + // ... add other public dependencies that you statically link with here ... + } + ); + + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + "Engine", + "Slate", + "SlateCore", + "MoviePlayer", + "PreLoadScreen", + "DeveloperSettings" + } + ); + + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + // ... add any modules that your module loads dynamically here ... + } + ); + } +} diff --git a/Plugins/CommonLoadingScreen/Source/CommonStartupLoadingScreen/Private/CommonPreLoadScreen.cpp b/Plugins/CommonLoadingScreen/Source/CommonStartupLoadingScreen/Private/CommonPreLoadScreen.cpp new file mode 100644 index 000000000..32a2b6b1d --- /dev/null +++ b/Plugins/CommonLoadingScreen/Source/CommonStartupLoadingScreen/Private/CommonPreLoadScreen.cpp @@ -0,0 +1,18 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "CommonPreLoadScreen.h" + +#include "Misc/App.h" +#include "SCommonPreLoadingScreenWidget.h" + +#define LOCTEXT_NAMESPACE "CommonPreLoadingScreen" + +void FCommonPreLoadScreen::Init() +{ + if (!GIsEditor && FApp::CanEverRender()) + { + EngineLoadingWidget = SNew(SCommonPreLoadingScreenWidget); + } +} + +#undef LOCTEXT_NAMESPACE diff --git a/Plugins/CommonLoadingScreen/Source/CommonStartupLoadingScreen/Private/CommonPreLoadScreen.h b/Plugins/CommonLoadingScreen/Source/CommonStartupLoadingScreen/Private/CommonPreLoadScreen.h new file mode 100644 index 000000000..286f99186 --- /dev/null +++ b/Plugins/CommonLoadingScreen/Source/CommonStartupLoadingScreen/Private/CommonPreLoadScreen.h @@ -0,0 +1,20 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "PreLoadScreenBase.h" + +class SWidget; + +class FCommonPreLoadScreen : public FPreLoadScreenBase +{ +public: + + /*** IPreLoadScreen Implementation ***/ + virtual void Init() override; + virtual EPreLoadScreenTypes GetPreLoadScreenType() const override { return EPreLoadScreenTypes::EngineLoadingScreen; } + virtual TSharedPtr GetWidget() override { return EngineLoadingWidget; } +private: + + TSharedPtr EngineLoadingWidget; +}; diff --git a/Plugins/CommonLoadingScreen/Source/CommonStartupLoadingScreen/Private/CommonStartupLoadingScreen.cpp b/Plugins/CommonLoadingScreen/Source/CommonStartupLoadingScreen/Private/CommonStartupLoadingScreen.cpp new file mode 100644 index 000000000..93cb7bde6 --- /dev/null +++ b/Plugins/CommonLoadingScreen/Source/CommonStartupLoadingScreen/Private/CommonStartupLoadingScreen.cpp @@ -0,0 +1,62 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "CommonPreLoadScreen.h" +#include "Misc/App.h" +#include "Modules/ModuleManager.h" +#include "PreLoadScreenManager.h" + +#define LOCTEXT_NAMESPACE "FCommonLoadingScreenModule" + +class FCommonStartupLoadingScreenModule : public IModuleInterface +{ +public: + + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; + bool IsGameModule() const override; + +private: + void OnPreLoadScreenManagerCleanUp(); + + TSharedPtr PreLoadingScreen; +}; + + +void FCommonStartupLoadingScreenModule::StartupModule() +{ + // No need to load these assets on dedicated servers. + // Still want to load them in commandlets so cook catches them + if (!IsRunningDedicatedServer()) + { + PreLoadingScreen = MakeShared(); + PreLoadingScreen->Init(); + + if (!GIsEditor && FApp::CanEverRender() && FPreLoadScreenManager::Get()) + { + FPreLoadScreenManager::Get()->RegisterPreLoadScreen(PreLoadingScreen); + FPreLoadScreenManager::Get()->OnPreLoadScreenManagerCleanUp.AddRaw(this, &FCommonStartupLoadingScreenModule::OnPreLoadScreenManagerCleanUp); + } + } +} + +void FCommonStartupLoadingScreenModule::OnPreLoadScreenManagerCleanUp() +{ + //Once the PreLoadScreenManager is cleaning up, we can get rid of all our resources too + PreLoadingScreen.Reset(); + ShutdownModule(); +} + +void FCommonStartupLoadingScreenModule::ShutdownModule() +{ + +} + +bool FCommonStartupLoadingScreenModule::IsGameModule() const +{ + return true; +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FCommonStartupLoadingScreenModule, CommonStartupLoadingScreen) diff --git a/Plugins/CommonLoadingScreen/Source/CommonStartupLoadingScreen/Private/SCommonPreLoadingScreenWidget.cpp b/Plugins/CommonLoadingScreen/Source/CommonStartupLoadingScreen/Private/SCommonPreLoadingScreenWidget.cpp new file mode 100644 index 000000000..b5cf8e6e9 --- /dev/null +++ b/Plugins/CommonLoadingScreen/Source/CommonStartupLoadingScreen/Private/SCommonPreLoadingScreenWidget.cpp @@ -0,0 +1,32 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "SCommonPreLoadingScreenWidget.h" + +#include "Widgets/Layout/SBorder.h" + +class FReferenceCollector; + +#define LOCTEXT_NAMESPACE "SCommonPreLoadingScreenWidget" + +void SCommonPreLoadingScreenWidget::Construct(const FArguments& InArgs) +{ + ChildSlot + [ + SNew(SBorder) + .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) + .BorderBackgroundColor(FLinearColor::Black) + .Padding(0) + ]; +} + +void SCommonPreLoadingScreenWidget::AddReferencedObjects(FReferenceCollector& Collector) +{ + //WidgetAssets.AddReferencedObjects(Collector); +} + +FString SCommonPreLoadingScreenWidget::GetReferencerName() const +{ + return TEXT("SCommonPreLoadingScreenWidget"); +} + +#undef LOCTEXT_NAMESPACE diff --git a/Plugins/CommonLoadingScreen/Source/CommonStartupLoadingScreen/Private/SCommonPreLoadingScreenWidget.h b/Plugins/CommonLoadingScreen/Source/CommonStartupLoadingScreen/Private/SCommonPreLoadingScreenWidget.h new file mode 100644 index 000000000..ba1bd568d --- /dev/null +++ b/Plugins/CommonLoadingScreen/Source/CommonStartupLoadingScreen/Private/SCommonPreLoadingScreenWidget.h @@ -0,0 +1,26 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "UObject/GCObject.h" +#include "Widgets/Accessibility/SlateWidgetAccessibleTypes.h" +#include "Widgets/SCompoundWidget.h" + +class FReferenceCollector; + +class SCommonPreLoadingScreenWidget : public SCompoundWidget, public FGCObject +{ +public: + SLATE_BEGIN_ARGS(SCommonPreLoadingScreenWidget) {} + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs); + + //~ Begin FGCObject interface + virtual void AddReferencedObjects(FReferenceCollector& Collector) override; + virtual FString GetReferencerName() const override; + //~ End FGCObject interface + +private: + +}; diff --git a/Plugins/CommonUser/CommonUser.uplugin b/Plugins/CommonUser/CommonUser.uplugin new file mode 100644 index 000000000..889ddbfdc --- /dev/null +++ b/Plugins/CommonUser/CommonUser.uplugin @@ -0,0 +1,38 @@ +{ + "FileVersion": 3, + "Version": 1, + "VersionName": "1.0", + "FriendlyName": "CommonUser", + "Description": "Provides gameplay code and blueprint wrappers for online and platform operations.", + "Category": "Gameplay", + "CreatedBy": "Epic Games, Inc.", + "CreatedByURL": "https://www.epicgames.com", + "DocsURL": "", + "MarketplaceURL": "", + "SupportURL": "", + "CanContainContent": false, + "IsBetaVersion": false, + "IsExperimentalVersion": false, + "Installed": false, + "Modules": [ + { + "Name": "CommonUser", + "Type": "Runtime", + "LoadingPhase": "Default" + } + ], + "Plugins": [ + { + "Name": "OnlineSubsystem", + "Enabled": true + }, + { + "Name": "OnlineSubsystemUtils", + "Enabled": true + }, + { + "Name": "OnlineServices", + "Enabled": true + } + ] +} \ No newline at end of file diff --git a/Plugins/CommonUser/Resources/Icon128.png b/Plugins/CommonUser/Resources/Icon128.png new file mode 100644 index 000000000..1231d4aad Binary files /dev/null and b/Plugins/CommonUser/Resources/Icon128.png differ diff --git a/Plugins/CommonUser/Source/CommonUser/CommonUser.Build.cs b/Plugins/CommonUser/Source/CommonUser/CommonUser.Build.cs new file mode 100644 index 000000000..1b409c118 --- /dev/null +++ b/Plugins/CommonUser/Source/CommonUser/CommonUser.Build.cs @@ -0,0 +1,70 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; + +public class CommonUser : ModuleRules +{ + public CommonUser(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + bool bUseOnlineSubsystemV1 = true; + + PublicIncludePaths.AddRange( + new string[] { + // ... add public include paths required here ... + } + ); + + + PrivateIncludePaths.AddRange( + new string[] { + // ... add other private include paths required here ... + } + ); + + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "CoreOnline", + "GameplayTags", + // ... add other public dependencies that you statically link with here ... + } + ); + + if (bUseOnlineSubsystemV1) + { + PublicDependencyModuleNames.Add("OnlineSubsystem"); + } + else + { + PublicDependencyModuleNames.Add("OnlineServicesInterface"); + } + PrivateDependencyModuleNames.Add("OnlineSubsystemUtils"); + PublicDefinitions.Add("COMMONUSER_OSSV1=" + (bUseOnlineSubsystemV1 ? "1" : "0")); + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreOnline", + "CoreUObject", + "Engine", + "Slate", + "SlateCore", + "ApplicationCore", + "InputCore", + // ... add private dependencies that you statically link with here ... + } + ); + + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + // ... add any modules that your module loads dynamically here ... + } + ); + } +} diff --git a/Plugins/CommonUser/Source/CommonUser/Private/AsyncAction_CommonUserInitialize.cpp b/Plugins/CommonUser/Source/CommonUser/Private/AsyncAction_CommonUserInitialize.cpp new file mode 100644 index 000000000..0c3a70996 --- /dev/null +++ b/Plugins/CommonUser/Source/CommonUser/Private/AsyncAction_CommonUserInitialize.cpp @@ -0,0 +1,105 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "AsyncAction_CommonUserInitialize.h" + +#include "GenericPlatform/GenericPlatformInputDeviceMapper.h" +#include "TimerManager.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(AsyncAction_CommonUserInitialize) + +UAsyncAction_CommonUserInitialize* UAsyncAction_CommonUserInitialize::InitializeForLocalPlay(UCommonUserSubsystem* Target, int32 LocalPlayerIndex, FInputDeviceId PrimaryInputDevice, bool bCanUseGuestLogin) +{ + if (!PrimaryInputDevice.IsValid()) + { + // Set to default device + PrimaryInputDevice = IPlatformInputDeviceMapper::Get().GetDefaultInputDevice(); + } + + UAsyncAction_CommonUserInitialize* Action = NewObject(); + + Action->RegisterWithGameInstance(Target); + + if (Target && Action->IsRegistered()) + { + Action->Subsystem = Target; + + Action->Params.RequestedPrivilege = ECommonUserPrivilege::CanPlay; + Action->Params.LocalPlayerIndex = LocalPlayerIndex; + Action->Params.PrimaryInputDevice = PrimaryInputDevice; + Action->Params.bCanUseGuestLogin = bCanUseGuestLogin; + Action->Params.bCanCreateNewLocalPlayer = true; + } + else + { + Action->SetReadyToDestroy(); + } + + return Action; +} + +UAsyncAction_CommonUserInitialize* UAsyncAction_CommonUserInitialize::LoginForOnlinePlay(UCommonUserSubsystem* Target, int32 LocalPlayerIndex) +{ + UAsyncAction_CommonUserInitialize* Action = NewObject(); + + Action->RegisterWithGameInstance(Target); + + if (Target && Action->IsRegistered()) + { + Action->Subsystem = Target; + + Action->Params.RequestedPrivilege = ECommonUserPrivilege::CanPlayOnline; + Action->Params.LocalPlayerIndex = LocalPlayerIndex; + Action->Params.bCanCreateNewLocalPlayer = false; + } + else + { + Action->SetReadyToDestroy(); + } + + return Action; +} + +void UAsyncAction_CommonUserInitialize::HandleFailure() +{ + const UCommonUserInfo* UserInfo = nullptr; + if (Subsystem.IsValid()) + { + UserInfo = Subsystem->GetUserInfoForLocalPlayerIndex(Params.LocalPlayerIndex); + } + HandleInitializationComplete(UserInfo, false, NSLOCTEXT("CommonUser", "LoginFailedEarly", "Unable to start login process"), Params.RequestedPrivilege, Params.OnlineContext); +} + +void UAsyncAction_CommonUserInitialize::HandleInitializationComplete(const UCommonUserInfo* UserInfo, bool bSuccess, FText Error, ECommonUserPrivilege RequestedPrivilege, ECommonUserOnlineContext OnlineContext) +{ + if (ShouldBroadcastDelegates()) + { + OnInitializationComplete.Broadcast(UserInfo, bSuccess, Error, RequestedPrivilege, OnlineContext); + } + + SetReadyToDestroy(); +} + +void UAsyncAction_CommonUserInitialize::Activate() +{ + if (Subsystem.IsValid()) + { + Params.OnUserInitializeComplete.BindUFunction(this, GET_FUNCTION_NAME_CHECKED(UAsyncAction_CommonUserInitialize, HandleInitializationComplete)); + bool bSuccess = Subsystem->TryToInitializeUser(Params); + + if (!bSuccess) + { + // Call failure next frame + FTimerManager* TimerManager = GetTimerManager(); + + if (TimerManager) + { + TimerManager->SetTimerForNextTick(FTimerDelegate::CreateUObject(this, &UAsyncAction_CommonUserInitialize::HandleFailure)); + } + } + } + else + { + SetReadyToDestroy(); + } +} + diff --git a/Plugins/CommonUser/Source/CommonUser/Private/CommonSessionSubsystem.cpp b/Plugins/CommonUser/Source/CommonUser/Private/CommonSessionSubsystem.cpp new file mode 100644 index 000000000..56b1b4e94 --- /dev/null +++ b/Plugins/CommonUser/Source/CommonUser/Private/CommonSessionSubsystem.cpp @@ -0,0 +1,1426 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "CommonSessionSubsystem.h" +#include "AssetRegistry/AssetData.h" +#include "CommonUserTypes.h" +#include "Engine/AssetManager.h" +#include "Engine/GameInstance.h" +#include "Engine/LocalPlayer.h" +#include "GameFramework/PlayerController.h" +#include "Interfaces/OnlineSessionDelegates.h" +#include "Online/OnlineSessionNames.h" +#include "OnlineSessionSettings.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(CommonSessionSubsystem) + +#if COMMONUSER_OSSV1 +#include "Engine/World.h" +#include "OnlineSubsystemUtils.h" + +FName SETTING_ONLINESUBSYSTEM_VERSION(TEXT("OSSv1")); +#else +#include "Online/OnlineSessionNames.h" +#include "Interfaces/OnlineSessionDelegates.h" +#include "Online/OnlineServicesEngineUtils.h" + +FName SETTING_ONLINESUBSYSTEM_VERSION(TEXT("OSSv2")); +using namespace UE::Online; +#endif // COMMONUSER_OSSV1 + + +DECLARE_LOG_CATEGORY_EXTERN(LogCommonSession, Log, All); +DEFINE_LOG_CATEGORY(LogCommonSession); + +#define LOCTEXT_NAMESPACE "CommonUser" + +////////////////////////////////////////////////////////////////////// +//UCommonSession_SearchSessionRequest + +void UCommonSession_SearchSessionRequest::NotifySearchFinished(bool bSucceeded, const FText& ErrorMessage) +{ + OnSearchFinished.Broadcast(bSucceeded, ErrorMessage); + K2_OnSearchFinished.Broadcast(bSucceeded, ErrorMessage); +} + + +////////////////////////////////////////////////////////////////////// +//UCommonSession_SearchResult + +#if COMMONUSER_OSSV1 +FString UCommonSession_SearchResult::GetDescription() const +{ + return Result.GetSessionIdStr(); +} + +void UCommonSession_SearchResult::GetStringSetting(FName Key, FString& Value, bool& bFoundValue) const +{ + bFoundValue = Result.Session.SessionSettings.Get(Key, /*out*/ Value); +} + +void UCommonSession_SearchResult::GetIntSetting(FName Key, int32& Value, bool& bFoundValue) const +{ + bFoundValue = Result.Session.SessionSettings.Get(Key, /*out*/ Value); +} + +int32 UCommonSession_SearchResult::GetNumOpenPrivateConnections() const +{ + return Result.Session.NumOpenPrivateConnections; +} + +int32 UCommonSession_SearchResult::GetNumOpenPublicConnections() const +{ + return Result.Session.NumOpenPublicConnections; +} + +int32 UCommonSession_SearchResult::GetMaxPublicConnections() const +{ + return Result.Session.SessionSettings.NumPublicConnections; +} + +int32 UCommonSession_SearchResult::GetPingInMs() const +{ + return Result.PingInMs; +} +#else +FString UCommonSession_SearchResult::GetDescription() const +{ + return ToLogString(Lobby->LobbyId); +} + +void UCommonSession_SearchResult::GetStringSetting(FName Key, FString& Value, bool& bFoundValue) const +{ + if (const FSchemaVariant* VariantValue = Lobby->Attributes.Find(Key)) + { + bFoundValue = true; + Value = VariantValue->GetString(); + } + else + { + bFoundValue = false; + } +} + +void UCommonSession_SearchResult::GetIntSetting(FName Key, int32& Value, bool& bFoundValue) const +{ + if (const FSchemaVariant* VariantValue = Lobby->Attributes.Find(Key)) + { + bFoundValue = true; + Value = (int32)VariantValue->GetInt64(); + } + else + { + bFoundValue = false; + } +} + +int32 UCommonSession_SearchResult::GetNumOpenPrivateConnections() const +{ + // TODO: Private connections + return 0; +} + +int32 UCommonSession_SearchResult::GetNumOpenPublicConnections() const +{ + return Lobby->MaxMembers - Lobby->Members.Num(); +} + +int32 UCommonSession_SearchResult::GetMaxPublicConnections() const +{ + return Lobby->MaxMembers; +} + +int32 UCommonSession_SearchResult::GetPingInMs() const +{ + // TODO: Not a property of lobbies. Need to implement with sessions. + return 0; +} +#endif //COMMONUSER_OSSV1 + + +class FCommonOnlineSearchSettingsBase : public FGCObject +{ +public: + FCommonOnlineSearchSettingsBase(UCommonSession_SearchSessionRequest* InSearchRequest) + { + SearchRequest = InSearchRequest; + } + + virtual ~FCommonOnlineSearchSettingsBase() {} + + virtual void AddReferencedObjects(FReferenceCollector& Collector) override + { + Collector.AddReferencedObject(SearchRequest); + } + + virtual FString GetReferencerName() const override + { + static const FString NameString = TEXT("FCommonOnlineSearchSettings"); + return NameString; + } + +public: + UCommonSession_SearchSessionRequest* SearchRequest = nullptr; +}; + +#if COMMONUSER_OSSV1 +////////////////////////////////////////////////////////////////////// +// FCommonSession_OnlineSessionSettings + +class FCommonSession_OnlineSessionSettings : public FOnlineSessionSettings +{ +public: + + FCommonSession_OnlineSessionSettings(bool bIsLAN = false, bool bIsPresence = false, int32 MaxNumPlayers = 4) + { + NumPublicConnections = MaxNumPlayers; + if (NumPublicConnections < 0) + { + NumPublicConnections = 0; + } + NumPrivateConnections = 0; + bIsLANMatch = bIsLAN; + bShouldAdvertise = true; + bAllowJoinInProgress = true; + bAllowInvites = true; + bUsesPresence = bIsPresence; + bAllowJoinViaPresence = true; + bAllowJoinViaPresenceFriendsOnly = false; + } + + virtual ~FCommonSession_OnlineSessionSettings() {} +}; + +////////////////////////////////////////////////////////////////////// +// FCommonOnlineSearchSettingsOSSv1 + +class FCommonOnlineSearchSettingsOSSv1 : public FOnlineSessionSearch, public FCommonOnlineSearchSettingsBase +{ +public: + FCommonOnlineSearchSettingsOSSv1(UCommonSession_SearchSessionRequest* InSearchRequest) + : FCommonOnlineSearchSettingsBase(InSearchRequest) + { + bIsLanQuery = (InSearchRequest->OnlineMode == ECommonSessionOnlineMode::LAN); + MaxSearchResults = 10; + PingBucketSize = 50; + + QuerySettings.Set(SETTING_ONLINESUBSYSTEM_VERSION, true, EOnlineComparisonOp::Equals); + if (InSearchRequest->bUseLobbies) + { + QuerySettings.Set(SEARCH_PRESENCE, true, EOnlineComparisonOp::Equals); + QuerySettings.Set(SEARCH_LOBBIES, true, EOnlineComparisonOp::Equals); + } + } + + virtual ~FCommonOnlineSearchSettingsOSSv1() {} +}; +#else + +class FCommonOnlineSearchSettingsOSSv2 : public FCommonOnlineSearchSettingsBase +{ +public: + FCommonOnlineSearchSettingsOSSv2(UCommonSession_SearchSessionRequest* InSearchRequest) + : FCommonOnlineSearchSettingsBase(InSearchRequest) + { + FindLobbyParams.MaxResults = 10; + + FindLobbyParams.Filters.Emplace(FFindLobbySearchFilter{ SETTING_ONLINESUBSYSTEM_VERSION, ESchemaAttributeComparisonOp::Equals, true }); + + if (InSearchRequest->bUseLobbies) + { + FindLobbyParams.Filters.Emplace(FFindLobbySearchFilter{ SEARCH_PRESENCE, ESchemaAttributeComparisonOp::Equals, true }); + } + } +public: + FFindLobbies::Params FindLobbyParams; +}; + +#endif // COMMONUSER_OSSV1 + +////////////////////////////////////////////////////////////////////// +// UCommonSession_HostSessionRequest + +FString UCommonSession_HostSessionRequest::GetMapName() const +{ + FAssetData MapAssetData; + if (UAssetManager::Get().GetPrimaryAssetData(MapID, /*out*/ MapAssetData)) + { + return MapAssetData.PackageName.ToString(); + } + else + { + return FString(); + } +} + +FString UCommonSession_HostSessionRequest::ConstructTravelURL() const +{ + FString CombinedExtraArgs; + + if (OnlineMode == ECommonSessionOnlineMode::LAN) + { + CombinedExtraArgs += TEXT("?bIsLanMatch"); + } + + if (OnlineMode != ECommonSessionOnlineMode::Offline) + { + CombinedExtraArgs += TEXT("?listen"); + } + + for (const auto& KVP : ExtraArgs) + { + if (!KVP.Key.IsEmpty()) + { + if (KVP.Value.IsEmpty()) + { + CombinedExtraArgs += FString::Printf(TEXT("?%s"), *KVP.Key); + } + else + { + CombinedExtraArgs += FString::Printf(TEXT("?%s=%s"), *KVP.Key, *KVP.Value); + } + } + } + + //bIsRecordingDemo ? TEXT("?DemoRec") : TEXT("")); + + return FString::Printf(TEXT("%s%s"), + *GetMapName(), + *CombinedExtraArgs); +} + +bool UCommonSession_HostSessionRequest::ValidateAndLogErrors(FText& OutError) const +{ +#if WITH_SERVER_CODE + if (GetMapName().IsEmpty()) + { + OutError = FText::Format(NSLOCTEXT("NetworkErrors", "InvalidMapFormat", "Can't find asset data for MapID {0}, hosting request failed."), FText::FromString(MapID.ToString())); + return false; + } + + return true; +#else + // Client builds are only meant to connect to dedicated servers, they are missing the code to host a session by default + // You can change this behavior in subclasses to handle something like a tutorial + OutError = NSLOCTEXT("NetworkErrors", "ClientBuildCannotHost", "Client builds cannot host game sessions."); + return false; +#endif +} + +int32 UCommonSession_HostSessionRequest::GetMaxPlayers() const +{ + return MaxPlayerCount; +} + +////////////////////////////////////////////////////////////////////// +// UCommonSessionSubsystem + +void UCommonSessionSubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); + BindOnlineDelegates(); + GEngine->OnTravelFailure().AddUObject(this, &UCommonSessionSubsystem::TravelLocalSessionFailure); + + FCoreUObjectDelegates::PostLoadMapWithWorld.AddUObject(this, &UCommonSessionSubsystem::HandlePostLoadMap); + + UGameInstance* GameInstance = GetGameInstance(); + bIsDedicatedServer = GameInstance->IsDedicatedServerInstance(); +} + +void UCommonSessionSubsystem::BindOnlineDelegates() +{ +#if COMMONUSER_OSSV1 + BindOnlineDelegatesOSSv1(); +#else + BindOnlineDelegatesOSSv2(); +#endif +} + +#if COMMONUSER_OSSV1 +void UCommonSessionSubsystem::BindOnlineDelegatesOSSv1() +{ + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + check(OnlineSub); + + const IOnlineSessionPtr SessionInterface = OnlineSub->GetSessionInterface(); + check(SessionInterface.IsValid()); + + SessionInterface->AddOnCreateSessionCompleteDelegate_Handle(FOnCreateSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnCreateSessionComplete)); + SessionInterface->AddOnStartSessionCompleteDelegate_Handle(FOnStartSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnStartSessionComplete)); + SessionInterface->AddOnUpdateSessionCompleteDelegate_Handle(FOnUpdateSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnUpdateSessionComplete)); + SessionInterface->AddOnEndSessionCompleteDelegate_Handle(FOnEndSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnEndSessionComplete)); + SessionInterface->AddOnDestroySessionCompleteDelegate_Handle(FOnDestroySessionCompleteDelegate::CreateUObject(this, &ThisClass::OnDestroySessionComplete)); + +// SessionInterface->AddOnMatchmakingCompleteDelegate_Handle(FOnMatchmakingCompleteDelegate::CreateUObject(this, &ThisClass::OnMatchmakingComplete)); +// SessionInterface->AddOnCancelMatchmakingCompleteDelegate_Handle(FOnCancelMatchmakingCompleteDelegate::CreateUObject(this, &ThisClass::OnCancelMatchmakingComplete)); + + SessionInterface->AddOnFindSessionsCompleteDelegate_Handle(FOnFindSessionsCompleteDelegate::CreateUObject(this, &ThisClass::OnFindSessionsComplete)); +// SessionInterface->AddOnCancelFindSessionsCompleteDelegate_Handle(FOnCancelFindSessionsCompleteDelegate::CreateUObject(this, &ThisClass::OnCancelFindSessionsComplete)); +// SessionInterface->AddOnPingSearchResultsCompleteDelegate_Handle(FOnPingSearchResultsCompleteDelegate::CreateUObject(this, &ThisClass::OnPingSearchResultsComplete)); + SessionInterface->AddOnJoinSessionCompleteDelegate_Handle(FOnJoinSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnJoinSessionComplete)); + +// TWO_PARAM(OnSessionParticipantJoined, FName, const FUniqueNetId&); +// THREE_PARAM(OnSessionParticipantLeft, FName, const FUniqueNetId&, EOnSessionParticipantLeftReason); +// ONE_PARAM(OnQosDataRequested, FName); +// TWO_PARAM(OnSessionCustomDataChanged, FName, const FOnlineSessionSettings&); +// TWO_PARAM(OnSessionSettingsUpdated, FName, const FOnlineSessionSettings&); +// THREE_PARAM(OnSessionParticipantSettingsUpdated, FName, const FUniqueNetId&, const FOnlineSessionSettings&); +// FOUR_PARAM(OnSessionInviteReceived, const FUniqueNetId& /*UserId*/, const FUniqueNetId& /*FromId*/, const FString& /*AppId*/, const FOnlineSessionSearchResult& /*InviteResult*/); +// THREE_PARAM(OnRegisterPlayersComplete, FName, const TArray< FUniqueNetIdRef >&, bool); +// THREE_PARAM(OnUnregisterPlayersComplete, FName, const TArray< FUniqueNetIdRef >&, bool); + + SessionInterface->AddOnSessionUserInviteAcceptedDelegate_Handle(FOnSessionUserInviteAcceptedDelegate::CreateUObject(this, &ThisClass::HandleSessionUserInviteAccepted)); + SessionInterface->AddOnSessionFailureDelegate_Handle(FOnSessionFailureDelegate::CreateUObject(this, &ThisClass::HandleSessionFailure)); +} + +#else + +void UCommonSessionSubsystem::BindOnlineDelegatesOSSv2() +{ + // TODO: Bind OSSv2 delegates when they are available + // Note that most OSSv1 delegates above are implemented as completion delegates in OSSv2 and don't need to be subscribed to + TSharedPtr OnlineServices = GetServices(GetWorld()); + check(OnlineServices); + ILobbiesPtr Lobbies = OnlineServices->GetLobbiesInterface(); + check(Lobbies); + + LobbyJoinRequestedHandle = Lobbies->OnUILobbyJoinRequested().Add(this, &UCommonSessionSubsystem::OnSessionJoinRequested); +} +#endif + +void UCommonSessionSubsystem::Deinitialize() +{ +#if COMMONUSER_OSSV1 + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + + if (OnlineSub) + { + // During shutdown this may not be valid + const IOnlineSessionPtr SessionInterface = OnlineSub->GetSessionInterface(); + if (SessionInterface) + { + SessionInterface->ClearOnSessionFailureDelegates(this); + } + } +#endif // COMMONUSER_OSSV1 + + if (GEngine) + { + GEngine->OnTravelFailure().RemoveAll(this); + } + + FCoreUObjectDelegates::PostLoadMapWithWorld.RemoveAll(this); + + Super::Deinitialize(); +} + +bool UCommonSessionSubsystem::ShouldCreateSubsystem(UObject* Outer) const +{ + TArray ChildClasses; + GetDerivedClasses(GetClass(), ChildClasses, false); + + // Only create an instance if there is not a game-specific subclass + return ChildClasses.Num() == 0; +} + +UCommonSession_HostSessionRequest* UCommonSessionSubsystem::CreateOnlineHostSessionRequest() +{ + /** Game-specific subsystems can override this or you can modify after creation */ + + UCommonSession_HostSessionRequest* NewRequest = NewObject(this); + NewRequest->OnlineMode = ECommonSessionOnlineMode::Online; + NewRequest->bUseLobbies = true; + + return NewRequest; +} + +UCommonSession_SearchSessionRequest* UCommonSessionSubsystem::CreateOnlineSearchSessionRequest() +{ + /** Game-specific subsystems can override this or you can modify after creation */ + + UCommonSession_SearchSessionRequest* NewRequest = NewObject(this); + NewRequest->OnlineMode = ECommonSessionOnlineMode::Online; + NewRequest->bUseLobbies = true; + + return NewRequest; +} + +void UCommonSessionSubsystem::HostSession(APlayerController* HostingPlayer, UCommonSession_HostSessionRequest* Request) +{ + if (Request == nullptr) + { + SetCreateSessionError(NSLOCTEXT("NetworkErrors", "InvalidRequest", "HostSession passed an invalid request.")); + OnCreateSessionComplete(NAME_None, false); + return; + } + + ULocalPlayer* LocalPlayer = (HostingPlayer != nullptr) ? HostingPlayer->GetLocalPlayer() : nullptr; + if (LocalPlayer == nullptr && !bIsDedicatedServer) + { + SetCreateSessionError(NSLOCTEXT("NetworkErrors", "InvalidHostingPlayer", "HostingPlayer is invalid.")); + OnCreateSessionComplete(NAME_None, false); + return; + } + + FText OutError; + if (!Request->ValidateAndLogErrors(OutError)) + { + SetCreateSessionError(OutError); + OnCreateSessionComplete(NAME_None, false); + return; + } + + if (Request->OnlineMode == ECommonSessionOnlineMode::Offline) + { + if (GetWorld()->GetNetMode() == NM_Client) + { + SetCreateSessionError(NSLOCTEXT("NetworkErrors", "CannotHostAsClient", "Cannot host offline game as client.")); + OnCreateSessionComplete(NAME_None, false); + return; + } + else + { + // Offline so travel to the specified match URL immediately + GetWorld()->ServerTravel(Request->ConstructTravelURL()); + } + } + else + { + CreateOnlineSessionInternal(LocalPlayer, Request); + } +} + +void UCommonSessionSubsystem::CreateOnlineSessionInternal(ULocalPlayer* LocalPlayer, UCommonSession_HostSessionRequest* Request) +{ + CreateSessionResult = FOnlineResultInformation(); + PendingTravelURL = Request->ConstructTravelURL(); + +#if COMMONUSER_OSSV1 + CreateOnlineSessionInternalOSSv1(LocalPlayer, Request); +#else + CreateOnlineSessionInternalOSSv2(LocalPlayer, Request); +#endif +} + +#if COMMONUSER_OSSV1 +void UCommonSessionSubsystem::CreateOnlineSessionInternalOSSv1(ULocalPlayer* LocalPlayer, UCommonSession_HostSessionRequest* Request) +{ + const FName SessionName(NAME_GameSession); + const int32 MaxPlayers = Request->GetMaxPlayers(); + const bool bIsPresence = Request->bUseLobbies; // Using lobbies implies presence + + IOnlineSubsystem* const OnlineSub = Online::GetSubsystem(GetWorld()); + check(OnlineSub); + + IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface(); + check(Sessions); + + FUniqueNetIdPtr UserId; + if (LocalPlayer) + { + UserId = LocalPlayer->GetPreferredUniqueNetId().GetUniqueNetId(); + } + else if (bIsDedicatedServer) + { + UserId = OnlineSub->GetIdentityInterface()->GetUniquePlayerId(0); + } + + //@TODO: You can get here on some platforms while trying to do a LAN session, does that require a valid user id? + if (ensure(UserId.IsValid())) + { + HostSettings = MakeShareable(new FCommonSession_OnlineSessionSettings(Request->OnlineMode == ECommonSessionOnlineMode::LAN, bIsPresence, MaxPlayers)); + HostSettings->bUseLobbiesIfAvailable = Request->bUseLobbies; + HostSettings->Set(SETTING_GAMEMODE, Request->ModeNameForAdvertisement, EOnlineDataAdvertisementType::ViaOnlineService); + HostSettings->Set(SETTING_MAPNAME, Request->GetMapName(), EOnlineDataAdvertisementType::ViaOnlineService); + //@TODO: HostSettings->Set(SETTING_MATCHING_HOPPER, FString("TeamDeathmatch"), EOnlineDataAdvertisementType::DontAdvertise); + HostSettings->Set(SETTING_MATCHING_TIMEOUT, 120.0f, EOnlineDataAdvertisementType::ViaOnlineService); + HostSettings->Set(SETTING_SESSION_TEMPLATE_NAME, FString(TEXT("GameSession")), EOnlineDataAdvertisementType::DontAdvertise); + HostSettings->Set(SETTING_ONLINESUBSYSTEM_VERSION, true, EOnlineDataAdvertisementType::ViaOnlineService); + + FSessionSettings& UserSettings = HostSettings->MemberSettings.Add(UserId.ToSharedRef(), FSessionSettings()); + UserSettings.Add(SETTING_GAMEMODE, FOnlineSessionSetting(FString("GameSession"), EOnlineDataAdvertisementType::ViaOnlineService)); + + Sessions->CreateSession(*UserId, SessionName, *HostSettings); + } + else + { + OnCreateSessionComplete(SessionName, false); + } +} + +#else + +void UCommonSessionSubsystem::CreateOnlineSessionInternalOSSv2(ULocalPlayer* LocalPlayer, UCommonSession_HostSessionRequest* Request) +{ + // Only lobbies are supported for now + if (!ensureMsgf(Request->bUseLobbies, TEXT("Only Lobbies are supported in this release"))) + { + Request->bUseLobbies = true; + } + + const FName SessionName(NAME_GameSession); + const int32 MaxPlayers = Request->GetMaxPlayers(); + const bool bIsPresence = Request->bUseLobbies; // Using lobbies implies presence + + IOnlineServicesPtr OnlineServices = GetServices(GetWorld()); + check(OnlineServices); + ILobbiesPtr Lobbies = OnlineServices->GetLobbiesInterface(); + check(Lobbies); + FCreateLobby::Params CreateParams; + + if (LocalPlayer) + { + CreateParams.LocalAccountId = LocalPlayer->GetPreferredUniqueNetId().GetV2(); + } + else if (bIsDedicatedServer) + { + // TODO what should this do for v2? + } + + CreateParams.LocalName = SessionName; + CreateParams.SchemaId = FSchemaId(TEXT("GameLobby")); // TODO: make a parameter + CreateParams.MaxMembers = MaxPlayers; + CreateParams.JoinPolicy = ELobbyJoinPolicy::PublicAdvertised; // TODO: Check parameters + + CreateParams.Attributes.Emplace(SETTING_GAMEMODE, Request->ModeNameForAdvertisement); + CreateParams.Attributes.Emplace(SETTING_MAPNAME, Request->GetMapName()); + //@TODO: CreateParams.Attributes.Emplace(SETTING_MATCHING_HOPPER, FString("TeamDeathmatch")); + CreateParams.Attributes.Emplace(SETTING_MATCHING_TIMEOUT, 120.0f); + CreateParams.Attributes.Emplace(SETTING_SESSION_TEMPLATE_NAME, FString(TEXT("GameSession"))); + CreateParams.Attributes.Emplace(SETTING_ONLINESUBSYSTEM_VERSION, true); + if (bIsPresence) + { + // Add presence setting so it can be searched for + CreateParams.Attributes.Emplace(SEARCH_PRESENCE, true); + } + + CreateParams.UserAttributes.Emplace(SETTING_GAMEMODE, FString(TEXT("GameSession"))); + + // TODO: Add splitscreen players + + Lobbies->CreateLobby(MoveTemp(CreateParams)).OnComplete(this, [this, SessionName](const TOnlineResult& CreateResult) + { + OnCreateSessionComplete(SessionName, CreateResult.IsOk()); + }); +} + +#endif + +void UCommonSessionSubsystem::OnCreateSessionComplete(FName SessionName, bool bWasSuccessful) +{ + UE_LOG(LogCommonSession, Log, TEXT("OnCreateSessionComplete(SessionName: %s, bWasSuccessful: %d)"), *SessionName.ToString(), bWasSuccessful); + +#if COMMONUSER_OSSV1 // OSSv2 joins splitscreen players as part of the create call + // Add the splitscreen player if one exists +#if 0 //@TODO: + if (bWasSuccessful && LocalPlayers.Num() > 1) + { + IOnlineSessionPtr Sessions = Online::GetSessionInterface(GetWorld()); + if (Sessions.IsValid() && LocalPlayers[1]->GetPreferredUniqueNetId().IsValid()) + { + Sessions->RegisterLocalPlayer(*LocalPlayers[1]->GetPreferredUniqueNetId(), NAME_GameSession, + FOnRegisterLocalPlayerCompleteDelegate::CreateUObject(this, &ThisClass::OnRegisterLocalPlayerComplete_CreateSession)); + } + } + else +#endif +#endif + { + // We either failed or there is only a single local user + FinishSessionCreation(bWasSuccessful); + } +} + +#if COMMONUSER_OSSV1 +void UCommonSessionSubsystem::OnRegisterLocalPlayerComplete_CreateSession(const FUniqueNetId& PlayerId, EOnJoinSessionCompleteResult::Type Result) +{ + FinishSessionCreation(Result == EOnJoinSessionCompleteResult::Success); +} + +void UCommonSessionSubsystem::OnStartSessionComplete(FName SessionName, bool bWasSuccessful) +{ + UE_LOG(LogCommonSession, Log, TEXT("OnStartSessionComplete(SessionName: %s, bWasSuccessful: %d)"), *SessionName.ToString(), bWasSuccessful); + + if (bWantToDestroyPendingSession) + { + CleanUpSessions(); + } +} +#endif // COMMONUSER_OSSV1 + +void UCommonSessionSubsystem::FinishSessionCreation(bool bWasSuccessful) +{ + if (bWasSuccessful) + { + //@TODO Synchronize timing of this with join callbacks, modify both places and the comments if plan changes + CreateSessionResult = FOnlineResultInformation(); + CreateSessionResult.bWasSuccessful = true; + + NotifyCreateSessionComplete(CreateSessionResult); + + // Travel to the specified match URL + GetWorld()->ServerTravel(PendingTravelURL); + } + else + { + if (CreateSessionResult.bWasSuccessful || CreateSessionResult.ErrorText.IsEmpty()) + { + FString ReturnError = TEXT("GenericFailure"); // TODO: No good way to get session error codes out of OSSV1 + FText ReturnReason = NSLOCTEXT("NetworkErrors", "CreateSessionFailed", "Failed to create session."); + + CreateSessionResult.bWasSuccessful = false; + CreateSessionResult.ErrorId = ReturnError; + CreateSessionResult.ErrorText = ReturnReason; + } + + UE_LOG(LogCommonSession, Error, TEXT("FinishSessionCreation(%s): %s"), *CreateSessionResult.ErrorId, *CreateSessionResult.ErrorText.ToString()); + + NotifyCreateSessionComplete(CreateSessionResult); + } +} + +#if COMMONUSER_OSSV1 +void UCommonSessionSubsystem::OnUpdateSessionComplete(FName SessionName, bool bWasSuccessful) +{ + UE_LOG(LogCommonSession, Log, TEXT("OnUpdateSessionComplete(SessionName: %s, bWasSuccessful: %d"), *SessionName.ToString(), bWasSuccessful ? TEXT("true") : TEXT("false")); +} + +void UCommonSessionSubsystem::OnEndSessionComplete(FName SessionName, bool bWasSuccessful) +{ + UE_LOG(LogCommonSession, Log, TEXT("OnEndSessionComplete(SessionName: %s, bWasSuccessful: %s)"), *SessionName.ToString(), bWasSuccessful ? TEXT("true") : TEXT("false")); + CleanUpSessions(); +} + +void UCommonSessionSubsystem::OnDestroySessionComplete(FName SessionName, bool bWasSuccessful) +{ + UE_LOG(LogCommonSession, Log, TEXT("OnDestroySessionComplete(SessionName: %s, bWasSuccessful: %s)"), *SessionName.ToString(), bWasSuccessful ? TEXT("true") : TEXT("false")); + bWantToDestroyPendingSession = false; +} +#endif // COMMONUSER_OSSV1 + +void UCommonSessionSubsystem::FindSessions(APlayerController* SearchingPlayer, UCommonSession_SearchSessionRequest* Request) +{ + if (Request == nullptr) + { + UE_LOG(LogCommonSession, Error, TEXT("FindSessions passed a null request")); + return; + } + +#if COMMONUSER_OSSV1 + FindSessionsInternal(SearchingPlayer, MakeShared(Request)); +#else + FindSessionsInternal(SearchingPlayer, MakeShared(Request)); +#endif // COMMONUSER_OSSV1 +} + +void UCommonSessionSubsystem::FindSessionsInternal(APlayerController* SearchingPlayer, const TSharedRef& InSearchSettings) +{ + if (SearchSettings.IsValid()) + { + //@TODO: This is a poor user experience for the API user, we should let the additional search piggyback and + // just give it the same results as the currently pending one + // (or enqueue the request and service it when the previous one finishes or fails) + UE_LOG(LogCommonSession, Error, TEXT("A previous FindSessions call is still in progress, aborting")); + SearchSettings->SearchRequest->NotifySearchFinished(false, LOCTEXT("Error_FindSessionAlreadyInProgress", "Session search already in progress")); + } + + ULocalPlayer* LocalPlayer = (SearchingPlayer != nullptr) ? SearchingPlayer->GetLocalPlayer() : nullptr; + if (LocalPlayer == nullptr) + { + UE_LOG(LogCommonSession, Error, TEXT("SearchingPlayer is invalid")); + InSearchSettings->SearchRequest->NotifySearchFinished(false, LOCTEXT("Error_FindSessionBadPlayer", "Session search was not provided a local player")); + return; + } + + SearchSettings = InSearchSettings; +#if COMMONUSER_OSSV1 + FindSessionsInternalOSSv1(LocalPlayer); +#else + FindSessionsInternalOSSv2(LocalPlayer); +#endif +} + +#if COMMONUSER_OSSV1 +void UCommonSessionSubsystem::FindSessionsInternalOSSv1(ULocalPlayer* LocalPlayer) +{ + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + check(OnlineSub); + IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface(); + check(Sessions); + + if (!Sessions->FindSessions(*LocalPlayer->GetPreferredUniqueNetId().GetUniqueNetId(), StaticCastSharedRef(SearchSettings.ToSharedRef()))) + { + // Some session search failures will call this delegate inside the function, others will not + OnFindSessionsComplete(false); + } +} + +#else + +void UCommonSessionSubsystem::FindSessionsInternalOSSv2(ULocalPlayer* LocalPlayer) +{ + IOnlineServicesPtr OnlineServices = GetServices(GetWorld()); + check(OnlineServices); + ILobbiesPtr Lobbies = OnlineServices->GetLobbiesInterface(); + check(Lobbies); + + FFindLobbies::Params FindLobbyParams = StaticCastSharedPtr(SearchSettings)->FindLobbyParams; + FindLobbyParams.LocalAccountId = LocalPlayer->GetPreferredUniqueNetId().GetV2(); + + Lobbies->FindLobbies(MoveTemp(FindLobbyParams)).OnComplete(this, [this, LocalSearchSettings = SearchSettings](const TOnlineResult& FindResult) + { + if (LocalSearchSettings != SearchSettings) + { + // This was an abandoned search, ignore + return; + } + const bool bWasSuccessful = FindResult.IsOk(); + UE_LOG(LogCommonSession, Log, TEXT("FindLobbies(bWasSuccessful: %s)"), *LexToString(bWasSuccessful)); + check(SearchSettings.IsValid()); + if (bWasSuccessful) + { + const FFindLobbies::Result& FindResults = FindResult.GetOkValue(); + SearchSettings->SearchRequest->Results.Reset(FindResults.Lobbies.Num()); + + for (const TSharedRef& Lobby : FindResults.Lobbies) + { + if (!Lobby->OwnerAccountId.IsValid()) + { + UE_LOG(LogCommonSession, Verbose, TEXT("\tIgnoring Lobby with no owner (LobbyId: %s)"), + *ToLogString(Lobby->LobbyId)); + } + else if (Lobby->Members.Num() == 0) + { + UE_LOG(LogCommonSession, Verbose, TEXT("\tIgnoring Lobby with no members (UserId: %s)"), + *ToLogString(Lobby->OwnerAccountId)); + } + else + { + UCommonSession_SearchResult* Entry = NewObject(SearchSettings->SearchRequest); + Entry->Lobby = Lobby; + SearchSettings->SearchRequest->Results.Add(Entry); + + UE_LOG(LogCommonSession, Log, TEXT("\tFound lobby (UserId: %s, NumOpenConns: %d)"), + *ToLogString(Lobby->OwnerAccountId), Lobby->MaxMembers - Lobby->Members.Num()); + } + } + } + else + { + SearchSettings->SearchRequest->Results.Empty(); + } + + const FText ResultText = bWasSuccessful ? FText() : FindResult.GetErrorValue().GetText(); + + SearchSettings->SearchRequest->NotifySearchFinished(bWasSuccessful, ResultText); + SearchSettings.Reset(); + }); +} +#endif // COMMONUSER_OSSV1 + +void UCommonSessionSubsystem::QuickPlaySession(APlayerController* JoiningOrHostingPlayer, UCommonSession_HostSessionRequest* HostRequest) +{ + UE_LOG(LogCommonSession, Log, TEXT("QuickPlay Requested")); + + if (HostRequest == nullptr) + { + UE_LOG(LogCommonSession, Error, TEXT("QuickPlaySession passed a null request")); + return; + } + + TStrongObjectPtr HostRequestPtr = TStrongObjectPtr(HostRequest); + TWeakObjectPtr JoiningOrHostingPlayerPtr = TWeakObjectPtr(JoiningOrHostingPlayer); + + UCommonSession_SearchSessionRequest* QuickPlayRequest = CreateOnlineSearchSessionRequest(); + QuickPlayRequest->OnSearchFinished.AddUObject(this, &UCommonSessionSubsystem::HandleQuickPlaySearchFinished, JoiningOrHostingPlayerPtr, HostRequestPtr); + + FindSessionsInternal(JoiningOrHostingPlayer, CreateQuickPlaySearchSettings(HostRequest, QuickPlayRequest)); +} + +TSharedRef UCommonSessionSubsystem::CreateQuickPlaySearchSettings(UCommonSession_HostSessionRequest* HostRequest, UCommonSession_SearchSessionRequest* SearchRequest) +{ +#if COMMONUSER_OSSV1 + return CreateQuickPlaySearchSettingsOSSv1(HostRequest, SearchRequest); +#else + return CreateQuickPlaySearchSettingsOSSv2(HostRequest, SearchRequest); +#endif // COMMONUSER_OSSV1 +} + +#if COMMONUSER_OSSV1 +TSharedRef UCommonSessionSubsystem::CreateQuickPlaySearchSettingsOSSv1(UCommonSession_HostSessionRequest* HostRequest, UCommonSession_SearchSessionRequest* SearchRequest) +{ + TSharedRef QuickPlaySearch = MakeShared(SearchRequest); + + /** By default quick play does not want to include the map or game mode, games can fill this in as desired + if (!HostRequest->ModeNameForAdvertisement.IsEmpty()) + { + QuickPlaySearch->QuerySettings.Set(SETTING_GAMEMODE, HostRequest->ModeNameForAdvertisement, EOnlineComparisonOp::Equals); + } + + if (!HostRequest->GetMapName().IsEmpty()) + { + QuickPlaySearch->QuerySettings.Set(SETTING_MAPNAME, HostRequest->GetMapName(), EOnlineComparisonOp::Equals); + } + */ + + // QuickPlaySearch->QuerySettings.Set(SEARCH_DEDICATED_ONLY, true, EOnlineComparisonOp::Equals); + return QuickPlaySearch; +} + +#else + +TSharedRef UCommonSessionSubsystem::CreateQuickPlaySearchSettingsOSSv2(UCommonSession_HostSessionRequest* HostRequest, UCommonSession_SearchSessionRequest* SearchRequest) +{ + TSharedRef QuickPlaySearch = MakeShared(SearchRequest); + + /** By default quick play does not want to include the map or game mode, games can fill this in as desired + if (!HostRequest->ModeNameForAdvertisement.IsEmpty()) + { + QuickPlaySearch->FindLobbyParams.Filters.Emplace(FFindLobbySearchFilter{SETTING_GAMEMODE, ESchemaAttributeComparisonOp::Equals, HostRequest->ModeNameForAdvertisement}); + } + if (!HostRequest->GetMapName().IsEmpty()) + { + QuickPlaySearch->FindLobbyParams.Filters.Emplace(FFindLobbySearchFilter{SETTING_MAPNAME, ESchemaAttributeComparisonOp::Equals, HostRequest->GetMapName()}); + } + */ + + return QuickPlaySearch; +} + +#endif // COMMONUSER_OSSV1 + +void UCommonSessionSubsystem::HandleQuickPlaySearchFinished(bool bSucceeded, const FText& ErrorMessage, TWeakObjectPtr JoiningOrHostingPlayer, TStrongObjectPtr HostRequest) +{ + const int32 ResultCount = SearchSettings->SearchRequest->Results.Num(); + UE_LOG(LogCommonSession, Log, TEXT("QuickPlay Search Finished %s (Results %d) (Error: %s)"), bSucceeded ? TEXT("Success") : TEXT("Failed"), ResultCount, *ErrorMessage.ToString()); + + //@TODO: We have to check if the error message is empty because some OSS layers report a failure just because there are no sessions. Please fix with OSS 2.0. + if (bSucceeded || ErrorMessage.IsEmpty()) + { + // Join the best search result. + if (ResultCount > 0) + { + //@TODO: We should probably look at ping? maybe some other factors to find the best. Idk if they come pre-sorted or not. + for (UCommonSession_SearchResult* Result : SearchSettings->SearchRequest->Results) + { + JoinSession(JoiningOrHostingPlayer.Get(), Result); + return; + } + } + else + { + HostSession(JoiningOrHostingPlayer.Get(), HostRequest.Get()); + } + } + else + { + //@TODO: This sucks, need to tell someone. + } +} + +void UCommonSessionSubsystem::CleanUpSessions() +{ + bWantToDestroyPendingSession = true; + HostSettings.Reset(); +#if COMMONUSER_OSSV1 + CleanUpSessionsOSSv1(); +#else + CleanUpSessionsOSSv2(); +#endif // COMMONUSER_OSSV1 +} + +#if COMMONUSER_OSSV1 +void UCommonSessionSubsystem::CleanUpSessionsOSSv1() +{ + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + check(OnlineSub); + IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface(); + check(Sessions); + + EOnlineSessionState::Type SessionState = Sessions->GetSessionState(NAME_GameSession); + UE_LOG(LogCommonSession, Log, TEXT("Session state is %s"), EOnlineSessionState::ToString(SessionState)); + + if (EOnlineSessionState::InProgress == SessionState) + { + UE_LOG(LogCommonSession, Log, TEXT("Ending session because of return to front end")); + Sessions->EndSession(NAME_GameSession); + } + else if (EOnlineSessionState::Ending == SessionState) + { + UE_LOG(LogCommonSession, Log, TEXT("Waiting for session to end on return to main menu")); + } + else if (EOnlineSessionState::Ended == SessionState || EOnlineSessionState::Pending == SessionState) + { + UE_LOG(LogCommonSession, Log, TEXT("Destroying session on return to main menu")); + Sessions->DestroySession(NAME_GameSession); + } + else if (EOnlineSessionState::Starting == SessionState || EOnlineSessionState::Creating == SessionState) + { + UE_LOG(LogCommonSession, Log, TEXT("Waiting for session to start, and then we will end it to return to main menu")); + } +} + +#else +void UCommonSessionSubsystem::CleanUpSessionsOSSv2() +{ + IOnlineServicesPtr OnlineServices = GetServices(GetWorld()); + check(OnlineServices); + ILobbiesPtr Lobbies = OnlineServices->GetLobbiesInterface(); + check(Lobbies); + + FAccountId LocalPlayerId = GetAccountId(GetGameInstance()->GetFirstLocalPlayerController()); + FLobbyId LobbyId = GetLobbyId(NAME_GameSession); + + if (!LocalPlayerId.IsValid() || !LobbyId.IsValid()) + { + return; + } + // TODO: Include all local players leave the lobby + Lobbies->LeaveLobby({LocalPlayerId, LobbyId}); +} + +#endif // COMMONUSER_OSSV1 + +#if COMMONUSER_OSSV1 +void UCommonSessionSubsystem::OnFindSessionsComplete(bool bWasSuccessful) +{ + UE_LOG(LogCommonSession, Log, TEXT("OnFindSessionsComplete(bWasSuccessful: %s)"), bWasSuccessful ? TEXT("true") : TEXT("false")); + + if (!SearchSettings.IsValid()) + { + // This could get called twice for failed session searches, or for a search requested by a different system + return; + } + + FCommonOnlineSearchSettingsOSSv1& SearchSettingsV1 = *StaticCastSharedPtr(SearchSettings); + if (SearchSettingsV1.SearchState == EOnlineAsyncTaskState::InProgress) + { + UE_LOG(LogCommonSession, Error, TEXT("OnFindSessionsComplete called when search is still in progress!")); + return; + } + + if (!ensure(SearchSettingsV1.SearchRequest)) + { + UE_LOG(LogCommonSession, Error, TEXT("OnFindSessionsComplete called with invalid search request object!")); + return; + } + + if (bWasSuccessful) + { + SearchSettingsV1.SearchRequest->Results.Reset(SearchSettingsV1.SearchResults.Num()); + + for (const FOnlineSessionSearchResult& Result : SearchSettingsV1.SearchResults) + { + UCommonSession_SearchResult* Entry = NewObject(SearchSettingsV1.SearchRequest); + Entry->Result = Result; + SearchSettingsV1.SearchRequest->Results.Add(Entry); + FString OwningUserId = TEXT("Unknown"); + if (Result.Session.OwningUserId.IsValid()) + { + OwningUserId = Result.Session.OwningUserId->ToString(); + } + + UE_LOG(LogCommonSession, Log, TEXT("\tFound session (UserId: %s, UserName: %s, NumOpenPrivConns: %d, NumOpenPubConns: %d, Ping: %d ms"), + *OwningUserId, + *Result.Session.OwningUserName, + Result.Session.NumOpenPrivateConnections, + Result.Session.NumOpenPublicConnections, + Result.PingInMs + ); + } + } + else + { + SearchSettingsV1.SearchRequest->Results.Empty(); + } + + if (0) + { + // Fake Sessions OSSV1 + for (int i = 0; i < 10; i++) + { + UCommonSession_SearchResult* Entry = NewObject(SearchSettings->SearchRequest); + FOnlineSessionSearchResult FakeResult; + FakeResult.Session.OwningUserName = TEXT("Fake User"); + FakeResult.Session.SessionSettings.NumPublicConnections = 10; + FakeResult.Session.SessionSettings.bShouldAdvertise = true; + FakeResult.Session.SessionSettings.bAllowJoinInProgress = true; + FakeResult.PingInMs=99; + Entry->Result = FakeResult; + SearchSettingsV1.SearchRequest->Results.Add(Entry); + } + } + + SearchSettingsV1.SearchRequest->NotifySearchFinished(bWasSuccessful, bWasSuccessful ? FText() : LOCTEXT("Error_FindSessionV1Failed", "Find session failed")); + SearchSettings.Reset(); +} +#endif // COMMONUSER_OSSV1 + + +void UCommonSessionSubsystem::JoinSession(APlayerController* JoiningPlayer, UCommonSession_SearchResult* Request) +{ + if (Request == nullptr) + { + UE_LOG(LogCommonSession, Error, TEXT("JoinSession passed a null request")); + return; + } + + ULocalPlayer* LocalPlayer = (JoiningPlayer != nullptr) ? JoiningPlayer->GetLocalPlayer() : nullptr; + if (LocalPlayer == nullptr) + { + UE_LOG(LogCommonSession, Error, TEXT("JoiningPlayer is invalid")); + return; + } + + JoinSessionInternal(LocalPlayer, Request); +} + +void UCommonSessionSubsystem::JoinSessionInternal(ULocalPlayer* LocalPlayer, UCommonSession_SearchResult* Request) +{ +#if COMMONUSER_OSSV1 + JoinSessionInternalOSSv1(LocalPlayer, Request); +#else + JoinSessionInternalOSSv2(LocalPlayer, Request); +#endif // COMMONUSER_OSSV1 +} + +#if COMMONUSER_OSSV1 +void UCommonSessionSubsystem::JoinSessionInternalOSSv1(ULocalPlayer* LocalPlayer, UCommonSession_SearchResult* Request) +{ + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + check(OnlineSub); + IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface(); + check(Sessions); + + Sessions->JoinSession(*LocalPlayer->GetPreferredUniqueNetId().GetUniqueNetId(), NAME_GameSession, Request->Result); +} + +void UCommonSessionSubsystem::OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result) +{ + // Add any splitscreen players if they exist + //@TODO: +// if (Result == EOnJoinSessionCompleteResult::Success && LocalPlayers.Num() > 1) +// { +// IOnlineSessionPtr Sessions = Online::GetSessionInterface(GetWorld()); +// if (Sessions.IsValid() && LocalPlayers[1]->GetPreferredUniqueNetId().IsValid()) +// { +// Sessions->RegisterLocalPlayer(*LocalPlayers[1]->GetPreferredUniqueNetId(), NAME_GameSession, +// FOnRegisterLocalPlayerCompleteDelegate::CreateUObject(this, &UShooterGameInstance::OnRegisterJoiningLocalPlayerComplete)); +// } +// } +// else + { + FinishJoinSession(Result); + } +} + +void UCommonSessionSubsystem::OnRegisterJoiningLocalPlayerComplete(const FUniqueNetId& PlayerId, EOnJoinSessionCompleteResult::Type Result) +{ + FinishJoinSession(Result); +} + +void UCommonSessionSubsystem::FinishJoinSession(EOnJoinSessionCompleteResult::Type Result) +{ + if (Result == EOnJoinSessionCompleteResult::Success) + { + //@TODO Synchronize timing of this with create callbacks, modify both places and the comments if plan changes + FOnlineResultInformation JoinSessionResult; + JoinSessionResult.bWasSuccessful = true; + NotifyJoinSessionComplete(JoinSessionResult); + + InternalTravelToSession(NAME_GameSession); + } + else + { + FText ReturnReason; + switch (Result) + { + case EOnJoinSessionCompleteResult::SessionIsFull: + ReturnReason = NSLOCTEXT("NetworkErrors", "SessionIsFull", "Game is full."); + break; + case EOnJoinSessionCompleteResult::SessionDoesNotExist: + ReturnReason = NSLOCTEXT("NetworkErrors", "SessionDoesNotExist", "Game no longer exists."); + break; + default: + ReturnReason = NSLOCTEXT("NetworkErrors", "JoinSessionFailed", "Join failed."); + break; + } + + //@TODO: Error handling + UE_LOG(LogCommonSession, Error, TEXT("FinishJoinSession(Failed with Result: %s)"), *ReturnReason.ToString()); + + // No FOnlineError to initialize from + FOnlineResultInformation JoinSessionResult; + JoinSessionResult.bWasSuccessful = false; + JoinSessionResult.ErrorId = LexToString(Result); // This is not robust but there is no extended information available + JoinSessionResult.ErrorText = ReturnReason; + NotifyJoinSessionComplete(JoinSessionResult); + } +} + +#else + +void UCommonSessionSubsystem::JoinSessionInternalOSSv2(ULocalPlayer* LocalPlayer, UCommonSession_SearchResult* Request) +{ + const FName SessionName(NAME_GameSession); + IOnlineServicesPtr OnlineServices = GetServices(GetWorld()); + check(OnlineServices); + ILobbiesPtr Lobbies = OnlineServices->GetLobbiesInterface(); + check(Lobbies); + + FJoinLobby::Params JoinParams; + JoinParams.LocalAccountId = LocalPlayer->GetPreferredUniqueNetId().GetV2(); + JoinParams.LocalName = SessionName; + JoinParams.LobbyId = Request->Lobby->LobbyId; + + // Add any splitscreen players if they exist //@TODO: See UCommonSessionSubsystem::OnJoinSessionComplete + + Lobbies->JoinLobby(MoveTemp(JoinParams)).OnComplete(this, [this, SessionName](const TOnlineResult& JoinResult) + { + if (JoinResult.IsOk()) + { + InternalTravelToSession(SessionName); + } + else + { + //@TODO: Error handling + UE_LOG(LogCommonSession, Error, TEXT("JoinLobby Failed with Result: %s"), *ToLogString(JoinResult.GetErrorValue())); + } + }); +} + +void UCommonSessionSubsystem::OnSessionJoinRequested(const UE::Online::FUILobbyJoinRequested& EventParams) +{ + TSharedPtr OnlineServices = GetServices(GetWorld()); + check(OnlineServices); + IAuthPtr Auth = OnlineServices->GetAuthInterface(); + check(Auth); + TOnlineResult Account = Auth->GetLocalOnlineUserByOnlineAccountId({ EventParams.LocalAccountId }); + if (Account.IsOk()) + { + FPlatformUserId PlatformUserId = Account.GetOkValue().AccountInfo->PlatformUserId; + UCommonSession_SearchResult* RequestedSession = nullptr; + FOnlineResultInformation ResultInfo; + if (EventParams.Result.IsOk()) + { + RequestedSession = NewObject(this); + RequestedSession->Lobby = EventParams.Result.GetOkValue(); + } + else + { + ResultInfo.FromOnlineError(EventParams.Result.GetErrorValue()); + } + NotifyUserRequestedSession(PlatformUserId, RequestedSession, ResultInfo); + } + else + { + UE_LOG(LogCommonSession, Error, TEXT("OnJoinLobbyRequested: Failed to get account by local user id %s"), *UE::Online::ToLogString(EventParams.LocalAccountId)); + } +} + +UE::Online::FAccountId UCommonSessionSubsystem::GetAccountId(APlayerController* PlayerController) const +{ + if (const ULocalPlayer* const LocalPlayer = PlayerController->GetLocalPlayer()) + { + FUniqueNetIdRepl LocalPlayerIdRepl = LocalPlayer->GetPreferredUniqueNetId(); + if (LocalPlayerIdRepl.IsValid()) + { + return LocalPlayerIdRepl.GetV2(); + } + } + return FAccountId(); +} + +UE::Online::FLobbyId UCommonSessionSubsystem::GetLobbyId(const FName SessionName) const +{ + FAccountId LocalUserId = GetAccountId(GetGameInstance()->GetFirstLocalPlayerController()); + if (LocalUserId.IsValid()) + { + IOnlineServicesPtr OnlineServices = GetServices(GetWorld()); + check(OnlineServices); + ILobbiesPtr Lobbies = OnlineServices->GetLobbiesInterface(); + check(Lobbies); + TOnlineResult JoinedLobbies = Lobbies->GetJoinedLobbies({ LocalUserId }); + if (JoinedLobbies.IsOk()) + { + for (const TSharedRef& Lobby : JoinedLobbies.GetOkValue().Lobbies) + { + if (Lobby->LocalName == SessionName) + { + return Lobby->LobbyId; + } + } + } + } + return FLobbyId(); +} + +#endif // COMMONUSER_OSSV1 + +void UCommonSessionSubsystem::InternalTravelToSession(const FName SessionName) +{ + //@TODO: Ideally we'd use triggering player instead of first (they're all gonna go at once so it probably doesn't matter) + APlayerController* const PlayerController = GetGameInstance()->GetFirstLocalPlayerController(); + if (PlayerController == nullptr) + { + FText ReturnReason = NSLOCTEXT("NetworkErrors", "InvalidPlayerController", "Invalid Player Controller"); + UE_LOG(LogCommonSession, Error, TEXT("InternalTravelToSession(Failed due to %s)"), *ReturnReason.ToString()); + return; + } + + FString URL; +#if COMMONUSER_OSSV1 + // travel to session + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + check(OnlineSub); + + IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface(); + check(Sessions.IsValid()); + + if (!Sessions->GetResolvedConnectString(SessionName, URL)) + { + FText FailReason = NSLOCTEXT("NetworkErrors", "TravelSessionFailed", "Travel to Session failed."); + UE_LOG(LogCommonSession, Error, TEXT("InternalTravelToSession(%s)"), *FailReason.ToString()); + return; + } +#else + TSharedPtr OnlineServices = GetServices(GetWorld(), EOnlineServices::Default); + check(OnlineServices); + + FAccountId LocalUserId = GetAccountId(PlayerController); + if (LocalUserId.IsValid()) + { + TOnlineResult Result = OnlineServices->GetResolvedConnectString({LocalUserId, GetLobbyId(SessionName)}); + if (ensure(Result.IsOk())) + { + URL = Result.GetOkValue().ResolvedConnectString; + } + } +#endif // COMMONUSER_OSSV1 + + // Allow modification of the URL prior to travel + OnPreClientTravelEvent.Broadcast(URL); + + PlayerController->ClientTravel(URL, TRAVEL_Absolute); +} + +void UCommonSessionSubsystem::NotifyUserRequestedSession(const FPlatformUserId& PlatformUserId, UCommonSession_SearchResult* RequestedSession, const FOnlineResultInformation& RequestedSessionResult) +{ + OnUserRequestedSessionEvent.Broadcast(PlatformUserId, RequestedSession, RequestedSessionResult); + K2_OnUserRequestedSessionEvent.Broadcast(PlatformUserId, RequestedSession, RequestedSessionResult); +} + +void UCommonSessionSubsystem::NotifyJoinSessionComplete(const FOnlineResultInformation& Result) +{ + OnJoinSessionCompleteEvent.Broadcast(Result); + K2_OnJoinSessionCompleteEvent.Broadcast(Result); +} + +void UCommonSessionSubsystem::NotifyCreateSessionComplete(const FOnlineResultInformation& Result) +{ + OnCreateSessionCompleteEvent.Broadcast(Result); + K2_OnCreateSessionCompleteEvent.Broadcast(Result); +} + +void UCommonSessionSubsystem::SetCreateSessionError(const FText& ErrorText) +{ + CreateSessionResult.bWasSuccessful = false; + CreateSessionResult.ErrorId = TEXT("InternalFailure"); + + // TODO May want to replace with a generic error text in shipping builds depending on how much data you want to give users + CreateSessionResult.ErrorText = ErrorText; +} + +#if COMMONUSER_OSSV1 +void UCommonSessionSubsystem::HandleSessionFailure(const FUniqueNetId& NetId, ESessionFailure::Type FailureType) +{ + UE_LOG(LogCommonSession, Warning, TEXT("UCommonSessionSubsystem::HandleSessionFailure(NetId: %s, FailureType: %s)"), *NetId.ToDebugString(), LexToString(FailureType)); + + //@TODO: Probably need to do a bit more... +} + +void UCommonSessionSubsystem::HandleSessionUserInviteAccepted(const bool bWasSuccessful, const int32 LocalUserIndex, FUniqueNetIdPtr AcceptingUserId, const FOnlineSessionSearchResult& SearchResult) +{ + FPlatformUserId PlatformUserId = IPlatformInputDeviceMapper::Get().GetPlatformUserForUserIndex(LocalUserIndex); + + UCommonSession_SearchResult* RequestedSession = nullptr; + FOnlineResultInformation ResultInfo; + if (bWasSuccessful) + { + RequestedSession = NewObject(this); + RequestedSession->Result = SearchResult; + } + else + { + // No FOnlineError to initialize from + ResultInfo.bWasSuccessful = false; + ResultInfo.ErrorId = TEXT("failed"); // This is not robust but there is no extended information available + ResultInfo.ErrorText = LOCTEXT("Error_SessionUserInviteAcceptedFailed", "Failed to handle the join request"); + } + NotifyUserRequestedSession(PlatformUserId, RequestedSession, ResultInfo); +} + +#endif // COMMONUSER_OSSV1 + +void UCommonSessionSubsystem::TravelLocalSessionFailure(UWorld* World, ETravelFailure::Type FailureType, const FString& ReasonString) +{ + // The delegate for this is global, but PIE can have more than one game instance, so make + // sure it's being raised for the same world this game instance subsystem is associated with + if (World != GetWorld()) + { + return; + } + + UE_LOG(LogCommonSession, Warning, TEXT("TravelLocalSessionFailure(World: %s, FailureType: %s, ReasonString: %s)"), + *GetPathNameSafe(World), + ETravelFailure::ToString(FailureType), + *ReasonString); + + // TODO: Broadcast this failure when we are also able to broadcast a success. Presently we broadcast a success before starting the travel, so a failure after a success is confusing. + //FOnlineResultInformation JoinSessionResult; + //JoinSessionResult.bWasSuccessful = false; + //JoinSessionResult.ErrorId = ReasonString; // TODO: Is this an adequate ErrorId? + //JoinSessionResult.ErrorText = FText::FromString(ReasonString); + //NotifyJoinSessionComplete(JoinSessionResult); +} + +void UCommonSessionSubsystem::HandlePostLoadMap(UWorld* World) +{ + // Ignore null worlds. + if (!World) + { + return; + } + + // Ignore any world that isn't part of this game instance, which can be the case in the editor. + if (World->GetGameInstance() != GetGameInstance()) + { + return; + } + + // We don't care about updating the session unless the world type is game/pie. + if (!(World->WorldType == EWorldType::Game || World->WorldType == EWorldType::PIE)) + { + return; + } + +#if COMMONUSER_OSSV1 + IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld()); + check(OnlineSub); + + const IOnlineSessionPtr SessionInterface = OnlineSub->GetSessionInterface(); + check(SessionInterface.IsValid()); + + // If we're hosting a session, update the advertised map name. + if (HostSettings.IsValid()) + { + // This needs to be the full package path to match the host GetMapName function, World->GetMapName is currently the short name + HostSettings->Set(SETTING_MAPNAME, UWorld::RemovePIEPrefix(World->GetOutermost()->GetName()), EOnlineDataAdvertisementType::ViaOnlineService); + + const FName SessionName(NAME_GameSession); + SessionInterface->UpdateSession(SessionName, *HostSettings, true); + } +#endif // COMMONUSER_OSSV1 +} + +#undef LOCTEXT_NAMESPACE + diff --git a/Plugins/CommonUser/Source/CommonUser/Private/CommonUserModule.cpp b/Plugins/CommonUser/Source/CommonUser/Private/CommonUserModule.cpp new file mode 100644 index 000000000..c738a9b79 --- /dev/null +++ b/Plugins/CommonUser/Source/CommonUser/Private/CommonUserModule.cpp @@ -0,0 +1,22 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "CommonUserModule.h" + +#include "Modules/ModuleManager.h" + +#define LOCTEXT_NAMESPACE "FCommonUserModule" + +void FCommonUserModule::StartupModule() +{ + // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module +} + +void FCommonUserModule::ShutdownModule() +{ + // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, + // we call this function before unloading the module. +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FCommonUserModule, CommonUser) \ No newline at end of file diff --git a/Plugins/CommonUser/Source/CommonUser/Private/CommonUserSubsystem.cpp b/Plugins/CommonUser/Source/CommonUser/Private/CommonUserSubsystem.cpp new file mode 100644 index 000000000..9f982a3f1 --- /dev/null +++ b/Plugins/CommonUser/Source/CommonUser/Private/CommonUserSubsystem.cpp @@ -0,0 +1,2640 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "CommonUserSubsystem.h" +#include "Engine/GameInstance.h" +#include "Engine/LocalPlayer.h" +#include "GameFramework/PlayerController.h" +#include "GameFramework/PlayerState.h" +#include "InputKeyEventArgs.h" +#include "NativeGameplayTags.h" +#include "TimerManager.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(CommonUserSubsystem) + +#if COMMONUSER_OSSV1 +#include "OnlineSubsystemNames.h" +#include "OnlineSubsystemUtils.h" +#else +#include "Online/Auth.h" +#include "Online/ExternalUI.h" +#include "Online/OnlineServices.h" +#include "Online/OnlineServicesEngineUtils.h" +#include "Online/Privileges.h" + +using namespace UE::Online; +#endif + +DECLARE_LOG_CATEGORY_EXTERN(LogCommonUser, Log, All); +DEFINE_LOG_CATEGORY(LogCommonUser); + +UE_DEFINE_GAMEPLAY_TAG(FCommonUserTags::SystemMessage_Error, "SystemMessage.Error"); +UE_DEFINE_GAMEPLAY_TAG(FCommonUserTags::SystemMessage_Warning, "SystemMessage.Warning"); +UE_DEFINE_GAMEPLAY_TAG(FCommonUserTags::SystemMessage_Display, "SystemMessage.Display"); +UE_DEFINE_GAMEPLAY_TAG(FCommonUserTags::SystemMessage_Error_InitializeLocalPlayerFailed, "SystemMessage.Error.InitializeLocalPlayerFailed"); + +UE_DEFINE_GAMEPLAY_TAG(FCommonUserTags::Platform_Trait_RequiresStrictControllerMapping, "Platform.Trait.RequiresStrictControllerMapping"); +UE_DEFINE_GAMEPLAY_TAG(FCommonUserTags::Platform_Trait_SingleOnlineUser, "Platform.Trait.SingleOnlineUser"); + + +////////////////////////////////////////////////////////////////////// +// UCommonUserInfo + +UCommonUserInfo::FCachedData* UCommonUserInfo::GetCachedData(ECommonUserOnlineContext Context) +{ + // Look up directly, game has a separate cache than default + FCachedData* FoundData = CachedDataMap.Find(Context); + if (FoundData) + { + return FoundData; + } + + // Now try system resolution + UCommonUserSubsystem* Subsystem = GetSubsystem(); + + ECommonUserOnlineContext ResolvedContext = Subsystem->ResolveOnlineContext(Context); + return CachedDataMap.Find(ResolvedContext); +} + +const UCommonUserInfo::FCachedData* UCommonUserInfo::GetCachedData(ECommonUserOnlineContext Context) const +{ + return const_cast(this)->GetCachedData(Context); +} + +void UCommonUserInfo::UpdateCachedPrivilegeResult(ECommonUserPrivilege Privilege, ECommonUserPrivilegeResult Result, ECommonUserOnlineContext Context) +{ + // This should only be called with a resolved and valid type + FCachedData* GameCache = GetCachedData(ECommonUserOnlineContext::Game); + FCachedData* ContextCache = GetCachedData(Context); + + if (!ensure(GameCache && ContextCache)) + { + // Should always be valid + return; + } + + // Update direct cache first + ContextCache->CachedPrivileges.Add(Privilege, Result); + + if (GameCache != ContextCache) + { + // Look for another context to merge into game + ECommonUserPrivilegeResult GameContextResult = Result; + ECommonUserPrivilegeResult OtherContextResult = ECommonUserPrivilegeResult::Available; + for (TPair& Pair : CachedDataMap) + { + if (&Pair.Value != ContextCache && &Pair.Value != GameCache) + { + ECommonUserPrivilegeResult* FoundResult = Pair.Value.CachedPrivileges.Find(Privilege); + if (FoundResult) + { + OtherContextResult = *FoundResult; + } + else + { + OtherContextResult = ECommonUserPrivilegeResult::Unknown; + } + break; + } + } + + if (GameContextResult == ECommonUserPrivilegeResult::Available && OtherContextResult != ECommonUserPrivilegeResult::Available) + { + // Other context is worse, use that + GameContextResult = OtherContextResult; + } + + GameCache->CachedPrivileges.Add(Privilege, GameContextResult); + } +} + +void UCommonUserInfo::UpdateCachedNetId(const FUniqueNetIdRepl& NewId, ECommonUserOnlineContext Context) +{ + FCachedData* ContextCache = GetCachedData(Context); + + if (ensure(ContextCache)) + { + ContextCache->CachedNetId = NewId; + } + + // We don't merge the ids because of how guests work +} + +class UCommonUserSubsystem* UCommonUserInfo::GetSubsystem() const +{ + return Cast(GetOuter()); +} + +bool UCommonUserInfo::IsLoggedIn() const +{ + return (InitializationState == ECommonUserInitializationState::LoggedInLocalOnly || InitializationState == ECommonUserInitializationState::LoggedInOnline); +} + +bool UCommonUserInfo::IsDoingLogin() const +{ + return (InitializationState == ECommonUserInitializationState::DoingInitialLogin || InitializationState == ECommonUserInitializationState::DoingNetworkLogin); +} + +ECommonUserPrivilegeResult UCommonUserInfo::GetCachedPrivilegeResult(ECommonUserPrivilege Privilege, ECommonUserOnlineContext Context) const +{ + const FCachedData* FoundCached = GetCachedData(Context); + + if (FoundCached) + { + const ECommonUserPrivilegeResult* FoundResult = FoundCached->CachedPrivileges.Find(Privilege); + if (FoundResult) + { + return *FoundResult; + } + } + return ECommonUserPrivilegeResult::Unknown; +} + +ECommonUserAvailability UCommonUserInfo::GetPrivilegeAvailability(ECommonUserPrivilege Privilege) const +{ + // Bad feature or user + if ((int32)Privilege < 0 || (int32)Privilege >= (int32)ECommonUserPrivilege::Invalid_Count || InitializationState == ECommonUserInitializationState::Invalid) + { + return ECommonUserAvailability::Invalid; + } + + ECommonUserPrivilegeResult CachedResult = GetCachedPrivilegeResult(Privilege, ECommonUserOnlineContext::Game); + + // First handle explicit failures + switch (CachedResult) + { + case ECommonUserPrivilegeResult::LicenseInvalid: + case ECommonUserPrivilegeResult::VersionOutdated: + case ECommonUserPrivilegeResult::AgeRestricted: + return ECommonUserAvailability::AlwaysUnavailable; + + case ECommonUserPrivilegeResult::NetworkConnectionUnavailable: + case ECommonUserPrivilegeResult::AccountTypeRestricted: + case ECommonUserPrivilegeResult::AccountUseRestricted: + case ECommonUserPrivilegeResult::PlatformFailure: + return ECommonUserAvailability::CurrentlyUnavailable; + + default: + break; + } + + if (bIsGuest) + { + // Guests can only play, cannot use online features + if (Privilege == ECommonUserPrivilege::CanPlay) + { + return ECommonUserAvailability::NowAvailable; + } + else + { + return ECommonUserAvailability::AlwaysUnavailable; + } + } + + // Check network status + if (Privilege == ECommonUserPrivilege::CanPlayOnline || + Privilege == ECommonUserPrivilege::CanUseCrossPlay || + Privilege == ECommonUserPrivilege::CanCommunicateViaTextOnline || + Privilege == ECommonUserPrivilege::CanCommunicateViaVoiceOnline) + { + UCommonUserSubsystem* Subsystem = GetSubsystem(); + if (ensure(Subsystem) && !Subsystem->HasOnlineConnection(ECommonUserOnlineContext::Game)) + { + return ECommonUserAvailability::CurrentlyUnavailable; + } + } + + if (InitializationState == ECommonUserInitializationState::FailedtoLogin) + { + // Failed a prior login attempt + return ECommonUserAvailability::CurrentlyUnavailable; + } + else if (InitializationState == ECommonUserInitializationState::Unknown || InitializationState == ECommonUserInitializationState::DoingInitialLogin) + { + // Haven't logged in yet + return ECommonUserAvailability::PossiblyAvailable; + } + else if (InitializationState == ECommonUserInitializationState::LoggedInLocalOnly || InitializationState == ECommonUserInitializationState::DoingNetworkLogin) + { + // Local login succeeded so play checks are valid + if (Privilege == ECommonUserPrivilege::CanPlay && CachedResult == ECommonUserPrivilegeResult::Available) + { + return ECommonUserAvailability::NowAvailable; + } + + // Haven't logged in online yet + return ECommonUserAvailability::PossiblyAvailable; + } + else if (InitializationState == ECommonUserInitializationState::LoggedInOnline) + { + // Fully logged in + if (CachedResult == ECommonUserPrivilegeResult::Available) + { + return ECommonUserAvailability::NowAvailable; + } + + // Failed for other reason + return ECommonUserAvailability::CurrentlyUnavailable; + } + + return ECommonUserAvailability::Unknown; +} + +FUniqueNetIdRepl UCommonUserInfo::GetNetId(ECommonUserOnlineContext Context) const +{ + const FCachedData* FoundCached = GetCachedData(Context); + + if (FoundCached) + { + return FoundCached->CachedNetId; + } + + return FUniqueNetIdRepl(); +} + +FString UCommonUserInfo::GetNickname() const +{ + if (bIsGuest) + { + return NSLOCTEXT("CommonUser", "GuestNickname", "Guest").ToString(); + } + + const UCommonUserSubsystem* Subsystem = GetSubsystem(); + + if (ensure(Subsystem)) + { +#if COMMONUSER_OSSV1 + IOnlineIdentity* Identity = Subsystem->GetOnlineIdentity(ECommonUserOnlineContext::Game); + if (ensure(Identity)) + { + return Identity->GetPlayerNickname(GetPlatformUserIndex()); + } +#else + if (IAuthPtr AuthService = Subsystem->GetOnlineAuth(ECommonUserOnlineContext::Game)) + { + if (TSharedPtr AccountInfo = Subsystem->GetOnlineServiceAccountInfo(AuthService, GetPlatformUserId())) + { + if (const FSchemaVariant* DisplayName = AccountInfo->Attributes.Find(AccountAttributeData::DisplayName)) + { + return DisplayName->GetString(); + } + } + } +#endif // COMMONUSER_OSSV1 + } + return FString(); +} + +FString UCommonUserInfo::GetDebugString() const +{ + FUniqueNetIdRepl NetId = GetNetId(); + return NetId.ToDebugString(); +} + +FPlatformUserId UCommonUserInfo::GetPlatformUserId() const +{ + return PlatformUser; +} + +int32 UCommonUserInfo::GetPlatformUserIndex() const +{ + // Convert our platform id to index + const UCommonUserSubsystem* Subsystem = GetSubsystem(); + + if (ensure(Subsystem)) + { + return Subsystem->GetPlatformUserIndexForId(PlatformUser); + } + + return INDEX_NONE; +} + + +////////////////////////////////////////////////////////////////////// +// UCommonUserSubsystem + +void UCommonUserSubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); + + // Create our OSS wrappers + CreateOnlineContexts(); + + BindOnlineDelegates(); + + IPlatformInputDeviceMapper& DeviceMapper = IPlatformInputDeviceMapper::Get(); + DeviceMapper.GetOnInputDeviceConnectionChange().AddUObject(this, &ThisClass::HandleInputDeviceConnectionChanged); + + // Matches the engine default + SetMaxLocalPlayers(4); + + ResetUserState(); + + UGameInstance* GameInstance = GetGameInstance(); + bIsDedicatedServer = GameInstance->IsDedicatedServerInstance(); +} + +void UCommonUserSubsystem::CreateOnlineContexts() +{ + // First initialize default + DefaultContextInternal = new FOnlineContextCache(); +#if COMMONUSER_OSSV1 + DefaultContextInternal->OnlineSubsystem = Online::GetSubsystem(GetWorld()); + check(DefaultContextInternal->OnlineSubsystem); + DefaultContextInternal->IdentityInterface = DefaultContextInternal->OnlineSubsystem->GetIdentityInterface(); + check(DefaultContextInternal->IdentityInterface.IsValid()); + + IOnlineSubsystem* PlatformSub = IOnlineSubsystem::GetByPlatform(); + + if (PlatformSub && DefaultContextInternal->OnlineSubsystem != PlatformSub) + { + // Set up the optional platform service if it exists + PlatformContextInternal = new FOnlineContextCache(); + PlatformContextInternal->OnlineSubsystem = PlatformSub; + PlatformContextInternal->IdentityInterface = PlatformSub->GetIdentityInterface(); + check(PlatformContextInternal->IdentityInterface.IsValid()); + } +#else + DefaultContextInternal->OnlineServices = GetServices(GetWorld(), EOnlineServices::Default); + check(DefaultContextInternal->OnlineServices); + DefaultContextInternal->AuthService = DefaultContextInternal->OnlineServices->GetAuthInterface(); + check(DefaultContextInternal->AuthService); + + UE::Online::IOnlineServicesPtr PlatformServices = GetServices(GetWorld(), EOnlineServices::Platform); + if (PlatformServices && DefaultContextInternal->OnlineServices != PlatformServices) + { + PlatformContextInternal = new FOnlineContextCache(); + PlatformContextInternal->OnlineServices = PlatformServices; + PlatformContextInternal->AuthService = PlatformContextInternal->OnlineServices->GetAuthInterface(); + check(PlatformContextInternal->AuthService); + } +#endif + + // Explicit external services can be set up after if needed +} + +void UCommonUserSubsystem::Deinitialize() +{ + DestroyOnlineContexts(); + + IPlatformInputDeviceMapper& DeviceMapper = IPlatformInputDeviceMapper::Get(); + DeviceMapper.GetOnInputDeviceConnectionChange().RemoveAll(this); + + LocalUserInfos.Reset(); + ActiveLoginRequests.Reset(); + + Super::Deinitialize(); +} + +void UCommonUserSubsystem::DestroyOnlineContexts() +{ + // All cached shared ptrs must be cleared here + if (ServiceContextInternal && ServiceContextInternal != DefaultContextInternal) + { + delete ServiceContextInternal; + } + if (PlatformContextInternal && PlatformContextInternal != DefaultContextInternal) + { + delete PlatformContextInternal; + } + if (DefaultContextInternal) + { + delete DefaultContextInternal; + } + + ServiceContextInternal = PlatformContextInternal = DefaultContextInternal = nullptr; +} + +UCommonUserInfo* UCommonUserSubsystem::CreateLocalUserInfo(int32 LocalPlayerIndex) +{ + UCommonUserInfo* NewUser = nullptr; + if (ensure(!LocalUserInfos.Contains(LocalPlayerIndex))) + { + NewUser = NewObject(this); + NewUser->LocalPlayerIndex = LocalPlayerIndex; + NewUser->InitializationState = ECommonUserInitializationState::Unknown; + + // Always create game and default cache + NewUser->CachedDataMap.Add(ECommonUserOnlineContext::Game, UCommonUserInfo::FCachedData()); + NewUser->CachedDataMap.Add(ECommonUserOnlineContext::Default, UCommonUserInfo::FCachedData()); + + // Add platform if needed + if (HasSeparatePlatformContext()) + { + NewUser->CachedDataMap.Add(ECommonUserOnlineContext::Platform, UCommonUserInfo::FCachedData()); + } + + LocalUserInfos.Add(LocalPlayerIndex, NewUser); + } + return NewUser; +} + +bool UCommonUserSubsystem::ShouldCreateSubsystem(UObject* Outer) const +{ + TArray ChildClasses; + GetDerivedClasses(GetClass(), ChildClasses, false); + + // Only create an instance if there is not a game-specific subclass + return ChildClasses.Num() == 0; +} + +void UCommonUserSubsystem::BindOnlineDelegates() +{ +#if COMMONUSER_OSSV1 + return BindOnlineDelegatesOSSv1(); +#else + return BindOnlineDelegatesOSSv2(); +#endif +} + +void UCommonUserSubsystem::LogOutLocalUser(FPlatformUserId PlatformUser) +{ + UCommonUserInfo* UserInfo = ModifyInfo(GetUserInfoForPlatformUser(PlatformUser)); + + // Don't need to do anything if the user has never logged in fully or is in the process of logging in + if (UserInfo && (UserInfo->InitializationState == ECommonUserInitializationState::LoggedInLocalOnly || UserInfo->InitializationState == ECommonUserInitializationState::LoggedInOnline)) + { + ECommonUserAvailability OldAvailablity = UserInfo->GetPrivilegeAvailability(ECommonUserPrivilege::CanPlay); + + UserInfo->InitializationState = ECommonUserInitializationState::FailedtoLogin; + + // This will broadcast the game delegate + HandleChangedAvailability(UserInfo, ECommonUserPrivilege::CanPlay, OldAvailablity); + } +} + +bool UCommonUserSubsystem::TransferPlatformAuth(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser) +{ +#if COMMONUSER_OSSV1 + // Not supported in V1 path + return false; +#else + return TransferPlatformAuthOSSv2(System, Request, PlatformUser); +#endif +} + +bool UCommonUserSubsystem::AutoLogin(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser) +{ + UE_LOG(LogCommonUser, Log, TEXT("Player AutoLogin requested - UserIdx:%d, Privilege:%d, Context:%d"), + PlatformUser.GetInternalId(), + (int32)Request->DesiredPrivilege, + (int32)Request->DesiredContext); + +#if COMMONUSER_OSSV1 + return AutoLoginOSSv1(System, Request, PlatformUser); +#else + return AutoLoginOSSv2(System, Request, PlatformUser); +#endif +} + +bool UCommonUserSubsystem::ShowLoginUI(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser) +{ + UE_LOG(LogCommonUser, Log, TEXT("Player LoginUI requested - UserIdx:%d, Privilege:%d, Context:%d"), + PlatformUser.GetInternalId(), + (int32)Request->DesiredPrivilege, + (int32)Request->DesiredContext); + +#if COMMONUSER_OSSV1 + return ShowLoginUIOSSv1(System, Request, PlatformUser); +#else + return ShowLoginUIOSSv2(System, Request, PlatformUser); +#endif +} + +bool UCommonUserSubsystem::QueryUserPrivilege(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser) +{ +#if COMMONUSER_OSSV1 + return QueryUserPrivilegeOSSv1(System, Request, PlatformUser); +#else + return QueryUserPrivilegeOSSv2(System, Request, PlatformUser); +#endif +} + + +#if COMMONUSER_OSSV1 +IOnlineSubsystem* UCommonUserSubsystem::GetOnlineSubsystem(ECommonUserOnlineContext Context) const +{ + const FOnlineContextCache* System = GetContextCache(Context); + + if (System) + { + return System->OnlineSubsystem; + } + + return nullptr; +} + +IOnlineIdentity* UCommonUserSubsystem::GetOnlineIdentity(ECommonUserOnlineContext Context) const +{ + const FOnlineContextCache* System = GetContextCache(Context); + if (System) + { + return System->IdentityInterface.Get(); + } + + return nullptr; +} + +FName UCommonUserSubsystem::GetOnlineSubsystemName(ECommonUserOnlineContext Context) const +{ + IOnlineSubsystem* SubSystem = GetOnlineSubsystem(Context); + if (SubSystem) + { + return SubSystem->GetSubsystemName(); + } + + return NAME_None; +} + +EOnlineServerConnectionStatus::Type UCommonUserSubsystem::GetConnectionStatus(ECommonUserOnlineContext Context) const +{ + const FOnlineContextCache* System = GetContextCache(Context); + if (System) + { + return System->CurrentConnectionStatus; + } + + return EOnlineServerConnectionStatus::ServiceUnavailable; +} + +void UCommonUserSubsystem::BindOnlineDelegatesOSSv1() +{ + ECommonUserOnlineContext ServiceType = ResolveOnlineContext(ECommonUserOnlineContext::ServiceOrDefault); + ECommonUserOnlineContext PlatformType = ResolveOnlineContext(ECommonUserOnlineContext::PlatformOrDefault); + FOnlineContextCache* ServiceContext = GetContextCache(ServiceType); + FOnlineContextCache* PlatformContext = GetContextCache(PlatformType); + check(ServiceContext && ServiceContext->OnlineSubsystem && PlatformContext && PlatformContext->OnlineSubsystem); + // Connection delegates need to listen for both systems + + ServiceContext->OnlineSubsystem->AddOnConnectionStatusChangedDelegate_Handle(FOnConnectionStatusChangedDelegate::CreateUObject(this, &ThisClass::HandleNetworkConnectionStatusChanged, ServiceType)); + ServiceContext->CurrentConnectionStatus = EOnlineServerConnectionStatus::Normal; + + for (int32 PlayerIdx = 0; PlayerIdx < MAX_LOCAL_PLAYERS; PlayerIdx++) + { + ServiceContext->IdentityInterface->AddOnLoginStatusChangedDelegate_Handle(PlayerIdx, FOnLoginStatusChangedDelegate::CreateUObject(this, &ThisClass::HandleIdentityLoginStatusChanged, ServiceType)); + ServiceContext->IdentityInterface->AddOnLoginCompleteDelegate_Handle(PlayerIdx, FOnLoginCompleteDelegate::CreateUObject(this, &ThisClass::HandleUserLoginCompleted, ServiceType)); + } + + if (ServiceType != PlatformType) + { + PlatformContext->OnlineSubsystem->AddOnConnectionStatusChangedDelegate_Handle(FOnConnectionStatusChangedDelegate::CreateUObject(this, &ThisClass::HandleNetworkConnectionStatusChanged, PlatformType)); + PlatformContext->CurrentConnectionStatus = EOnlineServerConnectionStatus::Normal; + + for (int32 PlayerIdx = 0; PlayerIdx < MAX_LOCAL_PLAYERS; PlayerIdx++) + { + PlatformContext->IdentityInterface->AddOnLoginStatusChangedDelegate_Handle(PlayerIdx, FOnLoginStatusChangedDelegate::CreateUObject(this, &ThisClass::HandleIdentityLoginStatusChanged, PlatformType)); + PlatformContext->IdentityInterface->AddOnLoginCompleteDelegate_Handle(PlayerIdx, FOnLoginCompleteDelegate::CreateUObject(this, &ThisClass::HandleUserLoginCompleted, PlatformType)); + } + } + + // Hardware change delegates only listen to platform + PlatformContext->IdentityInterface->AddOnControllerPairingChangedDelegate_Handle(FOnControllerPairingChangedDelegate::CreateUObject(this, &ThisClass::HandleControllerPairingChanged)); +} + +bool UCommonUserSubsystem::AutoLoginOSSv1(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser) +{ + return System->IdentityInterface->AutoLogin(GetPlatformUserIndexForId(PlatformUser)); +} + +bool UCommonUserSubsystem::ShowLoginUIOSSv1(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser) +{ + IOnlineExternalUIPtr ExternalUI = System->OnlineSubsystem->GetExternalUIInterface(); + if (ExternalUI.IsValid()) + { + // TODO Unclear which flags should be set + return ExternalUI->ShowLoginUI(GetPlatformUserIndexForId(PlatformUser), false, false, FOnLoginUIClosedDelegate::CreateUObject(this, &ThisClass::HandleOnLoginUIClosed, Request->CurrentContext)); + } + return false; +} + +bool UCommonUserSubsystem::QueryUserPrivilegeOSSv1(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser) +{ + // Start query on unknown or failure + EUserPrivileges::Type OSSPrivilege = ConvertOSSPrivilege(Request->DesiredPrivilege); + + FUniqueNetIdRepl CurrentId = GetLocalUserNetId(PlatformUser, Request->CurrentContext); + check(CurrentId.IsValid()); + IOnlineIdentity::FOnGetUserPrivilegeCompleteDelegate Delegate = IOnlineIdentity::FOnGetUserPrivilegeCompleteDelegate::CreateUObject(this, &UCommonUserSubsystem::HandleCheckPrivilegesComplete, Request->DesiredPrivilege, Request->UserInfo, Request->CurrentContext); + System->IdentityInterface->GetUserPrivilege(*CurrentId, OSSPrivilege, Delegate); + + // This may immediately succeed and reenter this function, so we have to return + return true; +} + +#else + +UE::Online::EOnlineServices UCommonUserSubsystem::GetOnlineServicesProvider(ECommonUserOnlineContext Context) const +{ + if (const FOnlineContextCache* System = GetContextCache(Context)) + { + return System->OnlineServices->GetServicesProvider(); + } + return UE::Online::EOnlineServices::None; +} + +UE::Online::IAuthPtr UCommonUserSubsystem::GetOnlineAuth(ECommonUserOnlineContext Context) const +{ + if (const FOnlineContextCache* System = GetContextCache(Context)) + { + return System->AuthService; + } + return nullptr; +} + +UE::Online::EOnlineServicesConnectionStatus UCommonUserSubsystem::GetConnectionStatus(ECommonUserOnlineContext Context) const +{ + if (const FOnlineContextCache* System = GetContextCache(Context)) + { + return System->CurrentConnectionStatus; + } + return UE::Online::EOnlineServicesConnectionStatus::NotConnected; +} + +void UCommonUserSubsystem::BindOnlineDelegatesOSSv2() +{ + ECommonUserOnlineContext ServiceType = ResolveOnlineContext(ECommonUserOnlineContext::ServiceOrDefault); + ECommonUserOnlineContext PlatformType = ResolveOnlineContext(ECommonUserOnlineContext::PlatformOrDefault); + FOnlineContextCache* ServiceContext = GetContextCache(ServiceType); + FOnlineContextCache* PlatformContext = GetContextCache(PlatformType); + check(ServiceContext && ServiceContext->OnlineServices && PlatformContext && PlatformContext->OnlineServices); + + ServiceContext->LoginStatusChangedHandle = ServiceContext->AuthService->OnLoginStatusChanged().Add(this, &ThisClass::HandleAuthLoginStatusChanged, ServiceType); + if (IConnectivityPtr ConnectivityInterface = ServiceContext->OnlineServices->GetConnectivityInterface()) + { + ServiceContext->ConnectionStatusChangedHandle = ConnectivityInterface->OnConnectionStatusChanged().Add(this, &ThisClass::HandleNetworkConnectionStatusChanged, ServiceType); + } + CacheConnectionStatus(ServiceType); + + if (ServiceType != PlatformType) + { + PlatformContext->LoginStatusChangedHandle = PlatformContext->AuthService->OnLoginStatusChanged().Add(this, &ThisClass::HandleAuthLoginStatusChanged, PlatformType); + if (IConnectivityPtr ConnectivityInterface = PlatformContext->OnlineServices->GetConnectivityInterface()) + { + PlatformContext->ConnectionStatusChangedHandle = ConnectivityInterface->OnConnectionStatusChanged().Add(this, &ThisClass::HandleNetworkConnectionStatusChanged, PlatformType); + } + CacheConnectionStatus(PlatformType); + } + // TODO: Controller Pairing Changed - move out of OSS and listen to CoreDelegate directly? +} + +void UCommonUserSubsystem::CacheConnectionStatus(ECommonUserOnlineContext Context) +{ + FOnlineContextCache* ContextCache = GetContextCache(Context); + check(ContextCache); + + EOnlineServicesConnectionStatus ConnectionStatus = EOnlineServicesConnectionStatus::NotConnected; + if (IConnectivityPtr ConnectivityInterface = ContextCache->OnlineServices->GetConnectivityInterface()) + { + const TOnlineResult Result = ConnectivityInterface->GetConnectionStatus(FGetConnectionStatus::Params()); + if (Result.IsOk()) + { + ConnectionStatus = Result.GetOkValue().Status; + } + } + else + { + ConnectionStatus = EOnlineServicesConnectionStatus::Connected; + } + + UE::Online::FConnectionStatusChanged EventParams; + EventParams.PreviousStatus = ContextCache->CurrentConnectionStatus; + EventParams.CurrentStatus = ConnectionStatus; + HandleNetworkConnectionStatusChanged(EventParams, Context); +} + +bool UCommonUserSubsystem::TransferPlatformAuthOSSv2(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser) +{ + IAuthPtr PlatformAuthInterface = GetOnlineAuth(ECommonUserOnlineContext::Platform); + if (Request->CurrentContext != ECommonUserOnlineContext::Platform + && PlatformAuthInterface) + { + FAuthQueryExternalAuthToken::Params Params; + Params.LocalAccountId = GetLocalUserNetId(PlatformUser, ECommonUserOnlineContext::Platform).GetV2(); + + PlatformAuthInterface->QueryExternalAuthToken(MoveTemp(Params)) + .OnComplete(this, [this, Request](const TOnlineResult& Result) + { + UCommonUserInfo* UserInfo = Request->UserInfo.Get(); + if (!UserInfo) + { + // User is gone, just delete this request + ActiveLoginRequests.Remove(Request); + return; + } + + if (Result.IsOk()) + { + const FAuthQueryExternalAuthToken::Result& GenerateAuthTokenResult = Result.GetOkValue(); + FAuthLogin::Params Params; + Params.PlatformUserId = UserInfo->GetPlatformUserId(); + Params.CredentialsType = LoginCredentialsType::ExternalAuth; + Params.CredentialsToken.Emplace(GenerateAuthTokenResult.ExternalAuthToken); + + IAuthPtr PrimaryAuthInterface = GetOnlineAuth(Request->CurrentContext); + PrimaryAuthInterface->Login(MoveTemp(Params)) + .OnComplete(this, [this, Request](const TOnlineResult& Result) + { + UCommonUserInfo* UserInfo = Request->UserInfo.Get(); + if (!UserInfo) + { + // User is gone, just delete this request + ActiveLoginRequests.Remove(Request); + return; + } + + if (Result.IsOk()) + { + Request->TransferPlatformAuthState = ECommonUserAsyncTaskState::Done; + Request->Error.Reset(); + } + else + { + Request->TransferPlatformAuthState = ECommonUserAsyncTaskState::Failed; + Request->Error = Result.GetErrorValue(); + } + ProcessLoginRequest(Request); + }); + } + else + { + Request->TransferPlatformAuthState = ECommonUserAsyncTaskState::Failed; + Request->Error = Result.GetErrorValue(); + ProcessLoginRequest(Request); + } + }); + return true; + } + return false; +} + +bool UCommonUserSubsystem::AutoLoginOSSv2(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser) +{ + FAuthLogin::Params LoginParameters; + LoginParameters.PlatformUserId = PlatformUser; + LoginParameters.CredentialsType = LoginCredentialsType::Auto; + // Leave other LoginParameters as default to allow the online service to determine how to try to automatically log in the user + TOnlineAsyncOpHandle LoginHandle = System->AuthService->Login(MoveTemp(LoginParameters)); + LoginHandle.OnComplete(this, &ThisClass::HandleUserLoginCompletedV2, PlatformUser, Request->CurrentContext); + return true; +} + +bool UCommonUserSubsystem::ShowLoginUIOSSv2(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser) +{ + IExternalUIPtr ExternalUI = System->OnlineServices->GetExternalUIInterface(); + if (ExternalUI.IsValid()) + { + FExternalUIShowLoginUI::Params ShowLoginUIParameters; + ShowLoginUIParameters.PlatformUserId = PlatformUser; + TOnlineAsyncOpHandle LoginHandle = ExternalUI->ShowLoginUI(MoveTemp(ShowLoginUIParameters)); + LoginHandle.OnComplete(this, &ThisClass::HandleOnLoginUIClosedV2, PlatformUser, Request->CurrentContext); + return true; + } + return false; +} + +bool UCommonUserSubsystem::QueryUserPrivilegeOSSv2(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser) +{ + UCommonUserInfo* UserInfo = Request->UserInfo.Get(); + + if (IPrivilegesPtr PrivilegesInterface = System->OnlineServices->GetPrivilegesInterface()) + { + const EUserPrivileges DesiredPrivilege = ConvertOnlineServicesPrivilege(Request->DesiredPrivilege); + + FQueryUserPrivilege::Params Params; + Params.LocalAccountId = GetLocalUserNetId(PlatformUser, Request->CurrentContext).GetV2(); + Params.Privilege = DesiredPrivilege; + TOnlineAsyncOpHandle QueryHandle = PrivilegesInterface->QueryUserPrivilege(MoveTemp(Params)); + QueryHandle.OnComplete(this, &ThisClass::HandleCheckPrivilegesComplete, Request->UserInfo, DesiredPrivilege, Request->CurrentContext); + return true; + } + else + { + UpdateUserPrivilegeResult(UserInfo, Request->DesiredPrivilege, ECommonUserPrivilegeResult::Available, Request->CurrentContext); + } + return false; +} + +TSharedPtr UCommonUserSubsystem::GetOnlineServiceAccountInfo(IAuthPtr AuthService, FPlatformUserId InUserId) const +{ + TSharedPtr AccountInfo; + FAuthGetLocalOnlineUserByPlatformUserId::Params GetAccountParams = { InUserId }; + TOnlineResult GetAccountResult = AuthService->GetLocalOnlineUserByPlatformUserId(MoveTemp(GetAccountParams)); + if (GetAccountResult.IsOk()) + { + AccountInfo = GetAccountResult.GetOkValue().AccountInfo; + } + return AccountInfo; +} + +#endif + +bool UCommonUserSubsystem::HasOnlineConnection(ECommonUserOnlineContext Context) const +{ +#if COMMONUSER_OSSV1 + EOnlineServerConnectionStatus::Type ConnectionType = GetConnectionStatus(Context); + + if (ConnectionType == EOnlineServerConnectionStatus::Normal || ConnectionType == EOnlineServerConnectionStatus::Connected) + { + return true; + } + + return false; +#else + return GetConnectionStatus(Context) == UE::Online::EOnlineServicesConnectionStatus::Connected; +#endif +} + +ELoginStatusType UCommonUserSubsystem::GetLocalUserLoginStatus(FPlatformUserId PlatformUser, ECommonUserOnlineContext Context) const +{ + if (!IsRealPlatformUser(PlatformUser)) + { + return ELoginStatusType::NotLoggedIn; + } + + const FOnlineContextCache* System = GetContextCache(Context); + if (System) + { +#if COMMONUSER_OSSV1 + return System->IdentityInterface->GetLoginStatus(GetPlatformUserIndexForId(PlatformUser)); +#else + if (TSharedPtr AccountInfo = GetOnlineServiceAccountInfo(System->AuthService, PlatformUser)) + { + return AccountInfo->LoginStatus; + } +#endif + } + return ELoginStatusType::NotLoggedIn; +} + +FUniqueNetIdRepl UCommonUserSubsystem::GetLocalUserNetId(FPlatformUserId PlatformUser, ECommonUserOnlineContext Context) const +{ + if (!IsRealPlatformUser(PlatformUser)) + { + return FUniqueNetIdRepl(); + } + + const FOnlineContextCache* System = GetContextCache(Context); + if (System) + { +#if COMMONUSER_OSSV1 + return FUniqueNetIdRepl(System->IdentityInterface->GetUniquePlayerId(GetPlatformUserIndexForId(PlatformUser))); +#else + // TODO: OSSv2 FUniqueNetIdRepl wrapping FAccountId is in progress + if (TSharedPtr AccountInfo = GetOnlineServiceAccountInfo(System->AuthService, PlatformUser)) + { + return FUniqueNetIdRepl(AccountInfo->AccountId); + } +#endif + } + + return FUniqueNetIdRepl(); +} + +void UCommonUserSubsystem::SendSystemMessage(FGameplayTag MessageType, FText TitleText, FText BodyText) +{ + OnHandleSystemMessage.Broadcast(MessageType, TitleText, BodyText); +} + +void UCommonUserSubsystem::SetMaxLocalPlayers(int32 InMaxLocalPlayers) +{ + if (ensure(InMaxLocalPlayers >= 1)) + { + // We can have more local players than MAX_LOCAL_PLAYERS, the rest are treated as guests + MaxNumberOfLocalPlayers = InMaxLocalPlayers; + + UGameInstance* GameInstance = GetGameInstance(); + UGameViewportClient* ViewportClient = GameInstance ? GameInstance->GetGameViewportClient() : nullptr; + + if (ViewportClient) + { + ViewportClient->MaxSplitscreenPlayers = MaxNumberOfLocalPlayers; + } + } +} + +int32 UCommonUserSubsystem::GetMaxLocalPlayers() const +{ + return MaxNumberOfLocalPlayers; +} + +int32 UCommonUserSubsystem::GetNumLocalPlayers() const +{ + UGameInstance* GameInstance = GetGameInstance(); + if (ensure(GameInstance)) + { + return GameInstance->GetNumLocalPlayers(); + } + return 1; +} + +ECommonUserInitializationState UCommonUserSubsystem::GetLocalPlayerInitializationState(int32 LocalPlayerIndex) const +{ + const UCommonUserInfo* UserInfo = GetUserInfoForLocalPlayerIndex(LocalPlayerIndex); + if (UserInfo) + { + return UserInfo->InitializationState; + } + + if (LocalPlayerIndex < 0 || LocalPlayerIndex >= GetMaxLocalPlayers()) + { + return ECommonUserInitializationState::Invalid; + } + + return ECommonUserInitializationState::Unknown; +} + +bool UCommonUserSubsystem::TryToInitializeForLocalPlay(int32 LocalPlayerIndex, FInputDeviceId PrimaryInputDevice, bool bCanUseGuestLogin) +{ + if (!PrimaryInputDevice.IsValid()) + { + // Set to default device + PrimaryInputDevice = IPlatformInputDeviceMapper::Get().GetDefaultInputDevice(); + } + + FCommonUserInitializeParams Params; + Params.LocalPlayerIndex = LocalPlayerIndex; + Params.PrimaryInputDevice = PrimaryInputDevice; + Params.bCanUseGuestLogin = bCanUseGuestLogin; + Params.bCanCreateNewLocalPlayer = true; + Params.RequestedPrivilege = ECommonUserPrivilege::CanPlay; + + return TryToInitializeUser(Params); +} + +bool UCommonUserSubsystem::TryToLoginForOnlinePlay(int32 LocalPlayerIndex) +{ + FCommonUserInitializeParams Params; + Params.LocalPlayerIndex = LocalPlayerIndex; + Params.bCanCreateNewLocalPlayer = false; + Params.RequestedPrivilege = ECommonUserPrivilege::CanPlayOnline; + + return TryToInitializeUser(Params); +} + +bool UCommonUserSubsystem::TryToInitializeUser(FCommonUserInitializeParams Params) +{ + if (Params.LocalPlayerIndex < 0 || (!Params.bCanCreateNewLocalPlayer && Params.LocalPlayerIndex >= GetNumLocalPlayers())) + { + if (!bIsDedicatedServer) + { + UE_LOG(LogCommonUser, Error, TEXT("TryToInitializeUser %d failed with current %d and max %d, invalid index"), + Params.LocalPlayerIndex, GetNumLocalPlayers(), GetMaxLocalPlayers()); + return false; + } + } + + if (Params.LocalPlayerIndex > GetNumLocalPlayers() || Params.LocalPlayerIndex >= GetMaxLocalPlayers()) + { + UE_LOG(LogCommonUser, Error, TEXT("TryToInitializeUser %d failed with current %d and max %d, can only create in order up to max players"), + Params.LocalPlayerIndex, GetNumLocalPlayers(), GetMaxLocalPlayers()); + return false; + } + + // Fill in platform user and input device if needed + if (Params.ControllerId != INDEX_NONE && (!Params.PrimaryInputDevice.IsValid() || !Params.PlatformUser.IsValid())) + { + IPlatformInputDeviceMapper::Get().RemapControllerIdToPlatformUserAndDevice(Params.ControllerId, Params.PlatformUser, Params.PrimaryInputDevice); + } + + if (Params.PrimaryInputDevice.IsValid() && !Params.PlatformUser.IsValid()) + { + Params.PlatformUser = GetPlatformUserIdForInputDevice(Params.PrimaryInputDevice); + } + else if (Params.PlatformUser.IsValid() && !Params.PrimaryInputDevice.IsValid()) + { + Params.PrimaryInputDevice = GetPrimaryInputDeviceForPlatformUser(Params.PlatformUser); + } + + UCommonUserInfo* LocalUserInfo = ModifyInfo(GetUserInfoForLocalPlayerIndex(Params.LocalPlayerIndex)); + UCommonUserInfo* LocalUserInfoForController = ModifyInfo(GetUserInfoForInputDevice(Params.PrimaryInputDevice)); + + if (LocalUserInfoForController && LocalUserInfo && LocalUserInfoForController != LocalUserInfo) + { + UE_LOG(LogCommonUser, Error, TEXT("TryToInitializeUser %d failed because controller %d is already assigned to player %d"), + Params.LocalPlayerIndex, Params.PrimaryInputDevice.GetId(), LocalUserInfoForController->LocalPlayerIndex); + return false; + } + + if (Params.LocalPlayerIndex == 0 && Params.bCanUseGuestLogin) + { + UE_LOG(LogCommonUser, Error, TEXT("TryToInitializeUser failed because player 0 cannot be a guest")); + return false; + } + + if (!LocalUserInfo) + { + LocalUserInfo = CreateLocalUserInfo(Params.LocalPlayerIndex); + } + else + { + // Copy from existing user info + if (!Params.PrimaryInputDevice.IsValid()) + { + Params.PrimaryInputDevice = LocalUserInfo->PrimaryInputDevice; + } + + if (!Params.PlatformUser.IsValid()) + { + Params.PlatformUser = LocalUserInfo->PlatformUser; + } + } + + if (LocalUserInfo->InitializationState != ECommonUserInitializationState::Unknown && LocalUserInfo->InitializationState != ECommonUserInitializationState::FailedtoLogin) + { + // Not allowed to change parameters during login + if (LocalUserInfo->PrimaryInputDevice != Params.PrimaryInputDevice || LocalUserInfo->PlatformUser != Params.PlatformUser || LocalUserInfo->bCanBeGuest != Params.bCanUseGuestLogin) + { + UE_LOG(LogCommonUser, Error, TEXT("TryToInitializeUser failed because player %d has already started the login process with diffrent settings!"), Params.LocalPlayerIndex); + return false; + } + } + + // Set desired index now so if it creates a player it knows what controller to use + LocalUserInfo->PrimaryInputDevice = Params.PrimaryInputDevice; + LocalUserInfo->PlatformUser = Params.PlatformUser; + LocalUserInfo->bCanBeGuest = Params.bCanUseGuestLogin; + RefreshLocalUserInfo(LocalUserInfo); + + // Either doing an initial or network login + if (LocalUserInfo->GetPrivilegeAvailability(ECommonUserPrivilege::CanPlay) == ECommonUserAvailability::NowAvailable && Params.RequestedPrivilege == ECommonUserPrivilege::CanPlayOnline) + { + LocalUserInfo->InitializationState = ECommonUserInitializationState::DoingNetworkLogin; + } + else + { + LocalUserInfo->InitializationState = ECommonUserInitializationState::DoingInitialLogin; + } + + LoginLocalUser(LocalUserInfo, Params.RequestedPrivilege, Params.OnlineContext, FOnLocalUserLoginCompleteDelegate::CreateUObject(this, &ThisClass::HandleLoginForUserInitialize, Params)); + + return true; +} + +void UCommonUserSubsystem::ListenForLoginKeyInput(TArray AnyUserKeys, TArray NewUserKeys, FCommonUserInitializeParams Params) +{ + UGameViewportClient* ViewportClient = GetGameInstance()->GetGameViewportClient(); + if (ensure(ViewportClient)) + { + const bool bIsMapped = LoginKeysForAnyUser.Num() > 0 || LoginKeysForNewUser.Num() > 0; + const bool bShouldBeMapped = AnyUserKeys.Num() > 0 || NewUserKeys.Num() > 0; + + if (bIsMapped && !bShouldBeMapped) + { + // Set it back to wrapped handler + ViewportClient->OnOverrideInputKey() = WrappedInputKeyHandler; + WrappedInputKeyHandler.Unbind(); + } + else if (!bIsMapped && bShouldBeMapped) + { + // Set up a wrapped handler + WrappedInputKeyHandler = ViewportClient->OnOverrideInputKey(); + ViewportClient->OnOverrideInputKey().BindUObject(this, &UCommonUserSubsystem::OverrideInputKeyForLogin); + } + + LoginKeysForAnyUser = AnyUserKeys; + LoginKeysForNewUser = NewUserKeys; + + if (bShouldBeMapped) + { + ParamsForLoginKey = Params; + } + else + { + ParamsForLoginKey = FCommonUserInitializeParams(); + } + } +} + +bool UCommonUserSubsystem::CancelUserInitialization(int32 LocalPlayerIndex) +{ + UCommonUserInfo* LocalUserInfo = ModifyInfo(GetUserInfoForLocalPlayerIndex(LocalPlayerIndex)); + if (!LocalUserInfo) + { + return false; + } + + if (!LocalUserInfo->IsDoingLogin()) + { + return false; + } + + // Remove from login queue + TArray> RequestsCopy = ActiveLoginRequests; + for (TSharedRef& Request : RequestsCopy) + { + if (Request->UserInfo.IsValid() && Request->UserInfo->LocalPlayerIndex == LocalPlayerIndex) + { + ActiveLoginRequests.Remove(Request); + } + } + + // Set state with best guess + if (LocalUserInfo->InitializationState == ECommonUserInitializationState::DoingNetworkLogin) + { + LocalUserInfo->InitializationState = ECommonUserInitializationState::LoggedInLocalOnly; + } + else + { + LocalUserInfo->InitializationState = ECommonUserInitializationState::FailedtoLogin; + } + + return true; +} + +bool UCommonUserSubsystem::TryToLogOutUser(int32 LocalPlayerIndex, bool bDestroyPlayer) +{ + UGameInstance* GameInstance = GetGameInstance(); + + if (!ensure(GameInstance)) + { + return false; + } + + if (LocalPlayerIndex == 0 && bDestroyPlayer) + { + UE_LOG(LogCommonUser, Error, TEXT("TryToLogOutUser cannot destroy player 0")); + return false; + } + + CancelUserInitialization(LocalPlayerIndex); + + UCommonUserInfo* LocalUserInfo = ModifyInfo(GetUserInfoForLocalPlayerIndex(LocalPlayerIndex)); + if (!LocalUserInfo) + { + UE_LOG(LogCommonUser, Warning, TEXT("TryToLogOutUser failed to log out user %i because they are not logged in"), LocalPlayerIndex); + return false; + } + + FPlatformUserId UserId = LocalUserInfo->PlatformUser; + if (IsRealPlatformUser(UserId)) + { + // Currently this does not do platform logout in case they want to log back in immediately after + UE_LOG(LogCommonUser, Log, TEXT("TryToLogOutUser succeeded for real platform user %d"), UserId.GetInternalId()); + + LogOutLocalUser(UserId); + } + else if (ensure(LocalUserInfo->bIsGuest)) + { + // For guest users just delete it + UE_LOG(LogCommonUser, Log, TEXT("TryToLogOutUser succeeded for guest player index %d"), LocalPlayerIndex); + + LocalUserInfos.Remove(LocalPlayerIndex); + } + + if (bDestroyPlayer) + { + ULocalPlayer* ExistingPlayer = GameInstance->FindLocalPlayerFromPlatformUserId(UserId); + + if (ExistingPlayer) + { + GameInstance->RemoveLocalPlayer(ExistingPlayer); + } + } + + return true; +} + +void UCommonUserSubsystem::ResetUserState() +{ + // Manually purge existing info objects + for (TPair Pair : LocalUserInfos) + { + if (Pair.Value) + { + Pair.Value->MarkAsGarbage(); + } + } + + LocalUserInfos.Reset(); + + // Cancel in-progress logins + ActiveLoginRequests.Reset(); + + // Create player info for id 0 + UCommonUserInfo* FirstUser = CreateLocalUserInfo(0); + + FirstUser->PlatformUser = IPlatformInputDeviceMapper::Get().GetPrimaryPlatformUser(); + FirstUser->PrimaryInputDevice = IPlatformInputDeviceMapper::Get().GetPrimaryInputDeviceForUser(FirstUser->PlatformUser); + + // TODO: Schedule a refresh of player 0 for next frame? + RefreshLocalUserInfo(FirstUser); +} + +bool UCommonUserSubsystem::OverrideInputKeyForLogin(FInputKeyEventArgs& EventArgs) +{ + int32 NextLocalPlayerIndex = INDEX_NONE; + + const UCommonUserInfo* MappedUser = GetUserInfoForInputDevice(EventArgs.InputDevice); + if (EventArgs.Event == IE_Pressed) + { + if (MappedUser == nullptr || !MappedUser->IsLoggedIn()) + { + if (MappedUser) + { + NextLocalPlayerIndex = MappedUser->LocalPlayerIndex; + } + else + { + // Find next player + for (int32 i = 0; i < MaxNumberOfLocalPlayers; i++) + { + if (GetLocalPlayerInitializationState(i) == ECommonUserInitializationState::Unknown) + { + NextLocalPlayerIndex = i; + break; + } + } + } + + if (NextLocalPlayerIndex != INDEX_NONE) + { + if (LoginKeysForAnyUser.Contains(EventArgs.Key)) + { + // If we're in the middle of logging in just return true to ignore platform-specific input + if (MappedUser && MappedUser->IsDoingLogin()) + { + return true; + } + + // Press start screen + FCommonUserInitializeParams NewParams = ParamsForLoginKey; + NewParams.LocalPlayerIndex = NextLocalPlayerIndex; + NewParams.PrimaryInputDevice = EventArgs.InputDevice; + + return TryToInitializeUser(NewParams); + } + + // See if this controller id is mapped + MappedUser = GetUserInfoForInputDevice(EventArgs.InputDevice); + + if (!MappedUser || MappedUser->LocalPlayerIndex == INDEX_NONE) + { + if (LoginKeysForNewUser.Contains(EventArgs.Key)) + { + // If we're in the middle of logging in just return true to ignore platform-specific input + if (MappedUser && MappedUser->IsDoingLogin()) + { + return true; + } + + // Local multiplayer + FCommonUserInitializeParams NewParams = ParamsForLoginKey; + NewParams.LocalPlayerIndex = NextLocalPlayerIndex; + NewParams.PrimaryInputDevice = EventArgs.InputDevice; + + return TryToInitializeUser(NewParams); + } + } + } + } + } + + if (WrappedInputKeyHandler.IsBound()) + { + return WrappedInputKeyHandler.Execute(EventArgs); + } + + return false; +} + +static inline FText GetErrorText(const FOnlineErrorType& InOnlineError) +{ +#if COMMONUSER_OSSV1 + return InOnlineError.GetErrorMessage(); +#else + return InOnlineError.GetText(); +#endif +} + +void UCommonUserSubsystem::HandleLoginForUserInitialize(const UCommonUserInfo* UserInfo, ELoginStatusType NewStatus, FUniqueNetIdRepl NetId, const TOptional& InError, ECommonUserOnlineContext Context, FCommonUserInitializeParams Params) +{ + UGameInstance* GameInstance = GetGameInstance(); + check(GameInstance); + FTimerManager& TimerManager = GameInstance->GetTimerManager(); + TOptional Error = InError; // Copy so we can reset on handled errors + + UCommonUserInfo* LocalUserInfo = ModifyInfo(UserInfo); + UCommonUserInfo* FirstUserInfo = ModifyInfo(GetUserInfoForLocalPlayerIndex(0)); + + if (!ensure(LocalUserInfo && FirstUserInfo)) + { + return; + } + + // Check the hard platform/service ids + RefreshLocalUserInfo(LocalUserInfo); + + FUniqueNetIdRepl FirstPlayerId = FirstUserInfo->GetNetId(ECommonUserOnlineContext::PlatformOrDefault); + + // Check to see if we should make a guest after a login failure. Some platforms return success but reuse the first player's id, count this as a failure + if (LocalUserInfo != FirstUserInfo && LocalUserInfo->bCanBeGuest && (NewStatus == ELoginStatusType::NotLoggedIn || NetId == FirstPlayerId)) + { +#if COMMONUSER_OSSV1 + NetId = (FUniqueNetIdRef)FUniqueNetIdString::Create(FString::Printf(TEXT("GuestPlayer%d"), LocalUserInfo->LocalPlayerIndex), NULL_SUBSYSTEM); +#else + // TODO: OSSv2 FUniqueNetIdRepl wrapping FAccountId is in progress + // TODO: OSSv2 - How to handle guest accounts? +#endif + LocalUserInfo->bIsGuest = true; + NewStatus = ELoginStatusType::UsingLocalProfile; + Error.Reset(); + UE_LOG(LogCommonUser, Log, TEXT("HandleLoginForUserInitialize created guest id %s for local player %d"), *NetId.ToString(), LocalUserInfo->LocalPlayerIndex); + } + else + { + LocalUserInfo->bIsGuest = false; + } + + ensure(LocalUserInfo->IsDoingLogin()); + + if (Error.IsSet()) + { + FText ErrorText = GetErrorText(Error.GetValue()); + TimerManager.SetTimerForNextTick(FTimerDelegate::CreateUObject(this, &UCommonUserSubsystem::HandleUserInitializeFailed, Params, ErrorText)); + return; + } + + if (Context == ECommonUserOnlineContext::Game) + { + LocalUserInfo->UpdateCachedNetId(NetId, ECommonUserOnlineContext::Game); + } + + ULocalPlayer* CurrentPlayer = GameInstance->GetLocalPlayerByIndex(LocalUserInfo->LocalPlayerIndex); + if (!CurrentPlayer && Params.bCanCreateNewLocalPlayer) + { + FString ErrorString; + CurrentPlayer = GameInstance->CreateLocalPlayer(LocalUserInfo->PlatformUser, ErrorString, true); + + if (!CurrentPlayer) + { + TimerManager.SetTimerForNextTick(FTimerDelegate::CreateUObject(this, &UCommonUserSubsystem::HandleUserInitializeFailed, Params, FText::AsCultureInvariant(ErrorString))); + return; + } + ensure(GameInstance->GetLocalPlayerByIndex(LocalUserInfo->LocalPlayerIndex) == CurrentPlayer); + } + + // Updates controller and net id if needed + SetLocalPlayerUserInfo(CurrentPlayer, LocalUserInfo); + + // Set a delayed callback + TimerManager.SetTimerForNextTick(FTimerDelegate::CreateUObject(this, &UCommonUserSubsystem::HandleUserInitializeSucceeded, Params)); +} + +void UCommonUserSubsystem::HandleUserInitializeFailed(FCommonUserInitializeParams Params, FText Error) +{ + UCommonUserInfo* LocalUserInfo = ModifyInfo(GetUserInfoForLocalPlayerIndex(Params.LocalPlayerIndex)); + + if (!LocalUserInfo) + { + // The user info was reset since this was scheduled + return; + } + + UE_LOG(LogCommonUser, Warning, TEXT("TryToInitializeUser %d failed with error %s"), LocalUserInfo->LocalPlayerIndex, *Error.ToString()); + + // If state is wrong, abort as we might have gotten canceled + if (!ensure(LocalUserInfo->IsDoingLogin())) + { + return; + } + + // If initial login failed or we ended up totally logged out, set to complete failure + ELoginStatusType NewStatus = GetLocalUserLoginStatus(Params.PlatformUser, Params.OnlineContext); + if (NewStatus == ELoginStatusType::NotLoggedIn || LocalUserInfo->InitializationState == ECommonUserInitializationState::DoingInitialLogin) + { + LocalUserInfo->InitializationState = ECommonUserInitializationState::FailedtoLogin; + } + else + { + LocalUserInfo->InitializationState = ECommonUserInitializationState::LoggedInLocalOnly; + } + + FText TitleText = NSLOCTEXT("CommonUser", "LoginFailedTitle", "Login Failure"); + + if (!Params.bSuppressLoginErrors) + { + SendSystemMessage(FCommonUserTags::SystemMessage_Error_InitializeLocalPlayerFailed, TitleText, Error); + } + + // Call callbacks + Params.OnUserInitializeComplete.ExecuteIfBound(LocalUserInfo, false, Error, Params.RequestedPrivilege, Params.OnlineContext); + OnUserInitializeComplete.Broadcast(LocalUserInfo, false, Error, Params.RequestedPrivilege, Params.OnlineContext); +} + +void UCommonUserSubsystem::HandleUserInitializeSucceeded(FCommonUserInitializeParams Params) +{ + UCommonUserInfo* LocalUserInfo = ModifyInfo(GetUserInfoForLocalPlayerIndex(Params.LocalPlayerIndex)); + + if (!LocalUserInfo) + { + // The user info was reset since this was scheduled + return; + } + + // If state is wrong, abort as we might have gotten cancelled + if (!ensure(LocalUserInfo->IsDoingLogin())) + { + return; + } + + // Fix up state + if (Params.RequestedPrivilege == ECommonUserPrivilege::CanPlayOnline) + { + LocalUserInfo->InitializationState = ECommonUserInitializationState::LoggedInOnline; + } + else + { + LocalUserInfo->InitializationState = ECommonUserInitializationState::LoggedInLocalOnly; + } + + ensure(LocalUserInfo->GetPrivilegeAvailability(Params.RequestedPrivilege) == ECommonUserAvailability::NowAvailable); + + // Call callbacks + Params.OnUserInitializeComplete.ExecuteIfBound(LocalUserInfo, true, FText(), Params.RequestedPrivilege, Params.OnlineContext); + OnUserInitializeComplete.Broadcast(LocalUserInfo, true, FText(), Params.RequestedPrivilege, Params.OnlineContext); +} + +bool UCommonUserSubsystem::LoginLocalUser(const UCommonUserInfo* UserInfo, ECommonUserPrivilege RequestedPrivilege, ECommonUserOnlineContext Context, FOnLocalUserLoginCompleteDelegate OnComplete) +{ + UCommonUserInfo* LocalUserInfo = ModifyInfo(UserInfo); + if (!ensure(UserInfo)) + { + return false; + } + + TSharedRef NewRequest = MakeShared(LocalUserInfo, RequestedPrivilege, Context, MoveTemp(OnComplete)); + ActiveLoginRequests.Add(NewRequest); + + // This will execute callback or start login process + ProcessLoginRequest(NewRequest); + + return true; +} + +void UCommonUserSubsystem::ProcessLoginRequest(TSharedRef Request) +{ + // First, see if we've fully logged in + UCommonUserInfo* UserInfo = Request->UserInfo.Get(); + + if (!UserInfo) + { + // User is gone, just delete this request + ActiveLoginRequests.Remove(Request); + + return; + } + + const FPlatformUserId PlatformUser = UserInfo->GetPlatformUserId(); + + // If the platform user id is invalid because this is a guest, skip right to failure + if (!IsRealPlatformUser(PlatformUser)) + { +#if COMMONUSER_OSSV1 + Request->Error = FOnlineError(NSLOCTEXT("CommonUser", "InvalidPlatformUser", "Invalid Platform User")); +#else + Request->Error = UE::Online::Errors::InvalidUser(); +#endif + // Remove from active array + ActiveLoginRequests.Remove(Request); + + // Execute delegate if bound + Request->Delegate.ExecuteIfBound(UserInfo, ELoginStatusType::NotLoggedIn, FUniqueNetIdRepl(), Request->Error, Request->DesiredContext); + + return; + } + + // Figure out what context to process first + if (Request->CurrentContext == ECommonUserOnlineContext::Invalid) + { + // First start with platform context if this is a game login + if (Request->DesiredContext == ECommonUserOnlineContext::Game) + { + Request->CurrentContext = ResolveOnlineContext(ECommonUserOnlineContext::PlatformOrDefault); + } + else + { + Request->CurrentContext = ResolveOnlineContext(Request->DesiredContext); + } + } + + ELoginStatusType CurrentStatus = GetLocalUserLoginStatus(PlatformUser, Request->CurrentContext); + FUniqueNetIdRepl CurrentId = GetLocalUserNetId(PlatformUser, Request->CurrentContext); + FOnlineContextCache* System = GetContextCache(Request->CurrentContext); + + if (!ensure(System)) + { + return; + } + + // Starting a new request + if (Request->OverallLoginState == ECommonUserAsyncTaskState::NotStarted) + { + Request->OverallLoginState = ECommonUserAsyncTaskState::InProgress; + } + + bool bHasRequiredStatus = (CurrentStatus == ELoginStatusType::LoggedIn); + if (Request->DesiredPrivilege == ECommonUserPrivilege::CanPlay) + { + // If this is not an online required login, allow local profile to count as fully logged in + bHasRequiredStatus |= (CurrentStatus == ELoginStatusType::UsingLocalProfile); + } + + // Check for overall success + if (CurrentStatus != ELoginStatusType::NotLoggedIn && CurrentId.IsValid()) + { + // Stall if we're waiting for the login UI to close + if (Request->LoginUIState == ECommonUserAsyncTaskState::InProgress) + { + return; + } + + Request->OverallLoginState = ECommonUserAsyncTaskState::Done; + } + else + { + // Try using platform auth to login + if (Request->TransferPlatformAuthState == ECommonUserAsyncTaskState::NotStarted) + { + Request->TransferPlatformAuthState = ECommonUserAsyncTaskState::InProgress; + + if (TransferPlatformAuth(System, Request, PlatformUser)) + { + return; + } + // We didn't start a login attempt, so set failure + Request->TransferPlatformAuthState = ECommonUserAsyncTaskState::Failed; + } + + // Next check AutoLogin + if (Request->AutoLoginState == ECommonUserAsyncTaskState::NotStarted) + { + if (Request->TransferPlatformAuthState == ECommonUserAsyncTaskState::Done || Request->TransferPlatformAuthState == ECommonUserAsyncTaskState::Failed) + { + Request->AutoLoginState = ECommonUserAsyncTaskState::InProgress; + + // Try an auto login with default credentials, this will work on many platforms + if (AutoLogin(System, Request, PlatformUser)) + { + return; + } + // We didn't start an autologin attempt, so set failure + Request->AutoLoginState = ECommonUserAsyncTaskState::Failed; + } + } + + // Next check login UI + if (Request->LoginUIState == ECommonUserAsyncTaskState::NotStarted) + { + if ((Request->TransferPlatformAuthState == ECommonUserAsyncTaskState::Done || Request->TransferPlatformAuthState == ECommonUserAsyncTaskState::Failed) + && (Request->AutoLoginState == ECommonUserAsyncTaskState::Done || Request->AutoLoginState == ECommonUserAsyncTaskState::Failed)) + { + Request->LoginUIState = ECommonUserAsyncTaskState::InProgress; + + if (ShowLoginUI(System, Request, PlatformUser)) + { + return; + } + // We didn't show a UI, so set failure + Request->LoginUIState = ECommonUserAsyncTaskState::Failed; + } + } + } + + // Check for overall failure + if (Request->LoginUIState == ECommonUserAsyncTaskState::Failed && + Request->AutoLoginState == ECommonUserAsyncTaskState::Failed && + Request->TransferPlatformAuthState == ECommonUserAsyncTaskState::Failed) + { + Request->OverallLoginState = ECommonUserAsyncTaskState::Failed; + } + + if (Request->OverallLoginState == ECommonUserAsyncTaskState::Done) + { + // Do the permissions check if needed + if (Request->PrivilegeCheckState == ECommonUserAsyncTaskState::NotStarted) + { + Request->PrivilegeCheckState = ECommonUserAsyncTaskState::InProgress; + + ECommonUserPrivilegeResult CachedResult = UserInfo->GetCachedPrivilegeResult(Request->DesiredPrivilege, Request->CurrentContext); + if (CachedResult == ECommonUserPrivilegeResult::Available) + { + // Use cached success value + Request->PrivilegeCheckState = ECommonUserAsyncTaskState::Done; + } + else + { + if (QueryUserPrivilege(System, Request, PlatformUser)) + { + return; + } + else + { +#if !COMMONUSER_OSSV1 + // Temp while OSSv2 gets privileges implemented + CachedResult = ECommonUserPrivilegeResult::Available; + Request->PrivilegeCheckState = ECommonUserAsyncTaskState::Done; +#endif + } + } + } + + if (Request->PrivilegeCheckState == ECommonUserAsyncTaskState::Failed) + { + // Count a privilege failure as a login failure + Request->OverallLoginState = ECommonUserAsyncTaskState::Failed; + } + else if (Request->PrivilegeCheckState == ECommonUserAsyncTaskState::Done) + { + // If platform context done but still need to do service context, do that next + ECommonUserOnlineContext ResolvedDesiredContext = ResolveOnlineContext(Request->DesiredContext); + + if (Request->OverallLoginState == ECommonUserAsyncTaskState::Done && Request->CurrentContext != ResolvedDesiredContext) + { + Request->CurrentContext = ResolvedDesiredContext; + Request->OverallLoginState = ECommonUserAsyncTaskState::NotStarted; + Request->PrivilegeCheckState = ECommonUserAsyncTaskState::NotStarted; + Request->TransferPlatformAuthState = ECommonUserAsyncTaskState::NotStarted; + + // Reprocess and immediately return + ProcessLoginRequest(Request); + return; + } + } + } + + if (Request->PrivilegeCheckState == ECommonUserAsyncTaskState::InProgress) + { + // Stall to wait for it to finish + return; + } + + // If done, remove and do callback + if (Request->OverallLoginState == ECommonUserAsyncTaskState::Done || Request->OverallLoginState == ECommonUserAsyncTaskState::Failed) + { + // Skip if this already happened in a nested function + if (ActiveLoginRequests.Contains(Request)) + { + // Add a generic error if none is set + if (Request->OverallLoginState == ECommonUserAsyncTaskState::Failed && !Request->Error.IsSet()) + { + #if COMMONUSER_OSSV1 + Request->Error = FOnlineError(NSLOCTEXT("CommonUser", "FailedToRequest", "Failed to Request Login")); + #else + Request->Error = UE::Online::Errors::RequestFailure(); + #endif + } + + // Remove from active array + ActiveLoginRequests.Remove(Request); + + // Execute delegate if bound + Request->Delegate.ExecuteIfBound(UserInfo, CurrentStatus, CurrentId, Request->Error, Request->DesiredContext); + } + } +} + +#if COMMONUSER_OSSV1 +void UCommonUserSubsystem::HandleUserLoginCompleted(int32 PlatformUserIndex, bool bWasSuccessful, const FUniqueNetId& NetId, const FString& ErrorString, ECommonUserOnlineContext Context) +{ + FPlatformUserId PlatformUser = GetPlatformUserIdForIndex(PlatformUserIndex); + ELoginStatusType NewStatus = GetLocalUserLoginStatus(PlatformUser, Context); + FUniqueNetIdRepl NewId = FUniqueNetIdRepl(NetId); + UE_LOG(LogCommonUser, Log, TEXT("Player login Completed - System:%s, UserIdx:%d, Successful:%d, NewStatus:%s, NewId:%s, ErrorIfAny:%s"), + *GetOnlineSubsystemName(Context).ToString(), + PlatformUserIndex, + (int32)bWasSuccessful, + ELoginStatus::ToString(NewStatus), + *NetId.ToString(), + *ErrorString); + + // Update any waiting login requests + TArray> RequestsCopy = ActiveLoginRequests; + for (TSharedRef& Request : RequestsCopy) + { + UCommonUserInfo* UserInfo = Request->UserInfo.Get(); + + if (!UserInfo) + { + // User is gone, just delete this request + ActiveLoginRequests.Remove(Request); + + continue; + } + + if (UserInfo->PlatformUser == PlatformUser && Request->CurrentContext == Context) + { + // On some platforms this gets called from the login UI with a failure + if (Request->AutoLoginState == ECommonUserAsyncTaskState::InProgress) + { + Request->AutoLoginState = bWasSuccessful ? ECommonUserAsyncTaskState::Done : ECommonUserAsyncTaskState::Failed; + } + + if (!bWasSuccessful) + { + Request->Error = FOnlineError(FText::FromString(ErrorString)); + } + + ProcessLoginRequest(Request); + } + } +} + +void UCommonUserSubsystem::HandleOnLoginUIClosed(TSharedPtr LoggedInNetId, const int PlatformUserIndex, const FOnlineError& Error, ECommonUserOnlineContext Context) +{ + FPlatformUserId PlatformUser = GetPlatformUserIdForIndex(PlatformUserIndex); + + // Update any waiting login requests + TArray> RequestsCopy = ActiveLoginRequests; + for (TSharedRef& Request : RequestsCopy) + { + UCommonUserInfo* UserInfo = Request->UserInfo.Get(); + + if (!UserInfo) + { + // User is gone, just delete this request + ActiveLoginRequests.Remove(Request); + + continue; + } + + // Look for first user trying to log in on this context + if (Request->CurrentContext == Context && Request->LoginUIState == ECommonUserAsyncTaskState::InProgress) + { + if (LoggedInNetId.IsValid() && LoggedInNetId->IsValid() && Error.WasSuccessful()) + { + // The platform user id that actually logged in may not be the same one who requested the UI, + // so swap it if the returned id is actually valid + if (UserInfo->PlatformUser != PlatformUser && PlatformUser != PLATFORMUSERID_NONE) + { + UserInfo->PlatformUser = PlatformUser; + } + + Request->LoginUIState = ECommonUserAsyncTaskState::Done; + Request->Error.Reset(); + } + else + { + Request->LoginUIState = ECommonUserAsyncTaskState::Failed; + Request->Error = Error; + } + + ProcessLoginRequest(Request); + } + } +} + +void UCommonUserSubsystem::HandleCheckPrivilegesComplete(const FUniqueNetId& UserId, EUserPrivileges::Type Privilege, uint32 PrivilegeResults, ECommonUserPrivilege UserPrivilege, TWeakObjectPtr CommonUserInfo, ECommonUserOnlineContext Context) +{ + // Only handle if user still exists + UCommonUserInfo* UserInfo = CommonUserInfo.Get(); + + if (!UserInfo) + { + return; + } + + ECommonUserPrivilegeResult UserResult = ConvertOSSPrivilegeResult(Privilege, PrivilegeResults); + + // Update the user cached value + UpdateUserPrivilegeResult(UserInfo, UserPrivilege, UserResult, Context); + + FOnlineContextCache* ContextCache = GetContextCache(Context); + check(ContextCache); + + // If this returns disconnected, update the connection status + if (UserResult == ECommonUserPrivilegeResult::NetworkConnectionUnavailable) + { + ContextCache->CurrentConnectionStatus = EOnlineServerConnectionStatus::NoNetworkConnection; + } + else if (UserResult == ECommonUserPrivilegeResult::Available && UserPrivilege == ECommonUserPrivilege::CanPlayOnline) + { + if (ContextCache->CurrentConnectionStatus == EOnlineServerConnectionStatus::NoNetworkConnection) + { + ContextCache->CurrentConnectionStatus = EOnlineServerConnectionStatus::Normal; + } + } + + // See if a login request is waiting on this + TArray> RequestsCopy = ActiveLoginRequests; + for (TSharedRef& Request : RequestsCopy) + { + if (Request->UserInfo.Get() == UserInfo && Request->CurrentContext == Context && Request->DesiredPrivilege == UserPrivilege && Request->PrivilegeCheckState == ECommonUserAsyncTaskState::InProgress) + { + if (UserResult == ECommonUserPrivilegeResult::Available) + { + Request->PrivilegeCheckState = ECommonUserAsyncTaskState::Done; + } + else + { + Request->PrivilegeCheckState = ECommonUserAsyncTaskState::Failed; + + // Forms strings in english like "(The user is not allowed) to (play the game)" + Request->Error = FOnlineError(FText::Format(NSLOCTEXT("CommonUser", "PrivilegeFailureFormat", "{0} to {1}"), GetPrivilegeResultDescription(UserResult), GetPrivilegeDescription(UserPrivilege))); + } + + ProcessLoginRequest(Request); + } + } +} +#else + +void UCommonUserSubsystem::HandleUserLoginCompletedV2(const UE::Online::TOnlineResult& Result, FPlatformUserId PlatformUser, ECommonUserOnlineContext Context) +{ + const bool bWasSuccessful = Result.IsOk(); + FAccountId NewId; + if (bWasSuccessful) + { + NewId = Result.GetOkValue().AccountInfo->AccountId; + } + + ELoginStatusType NewStatus = GetLocalUserLoginStatus(PlatformUser, Context); + UE_LOG(LogCommonUser, Log, TEXT("Player login Completed - System:%d, UserIdx:%d, Successful:%d, NewId:%s, ErrorIfAny:%s"), + (int32)Context, + PlatformUser.GetInternalId(), + (int32)Result.IsOk(), + *ToLogString(NewId), + Result.IsError() ? *Result.GetErrorValue().GetLogString() : TEXT("")); + + // Update any waiting login requests + TArray> RequestsCopy = ActiveLoginRequests; + for (TSharedRef& Request : RequestsCopy) + { + UCommonUserInfo* UserInfo = Request->UserInfo.Get(); + + if (!UserInfo) + { + // User is gone, just delete this request + ActiveLoginRequests.Remove(Request); + + continue; + } + + if (UserInfo->PlatformUser == PlatformUser && Request->CurrentContext == Context) + { + // On some platforms this gets called from the login UI with a failure + if (Request->AutoLoginState == ECommonUserAsyncTaskState::InProgress) + { + Request->AutoLoginState = bWasSuccessful ? ECommonUserAsyncTaskState::Done : ECommonUserAsyncTaskState::Failed; + } + + if (bWasSuccessful) + { + Request->Error.Reset(); + } + else + { + Request->Error = Result.GetErrorValue(); + } + + ProcessLoginRequest(Request); + } + } +} + +void UCommonUserSubsystem::HandleOnLoginUIClosedV2(const UE::Online::TOnlineResult& Result, FPlatformUserId PlatformUser, ECommonUserOnlineContext Context) +{ + // Update any waiting login requests + TArray> RequestsCopy = ActiveLoginRequests; + for (TSharedRef& Request : RequestsCopy) + { + UCommonUserInfo* UserInfo = Request->UserInfo.Get(); + + if (!UserInfo) + { + // User is gone, just delete this request + ActiveLoginRequests.Remove(Request); + + continue; + } + + // Look for first user trying to log in on this context + if (Request->CurrentContext == Context && Request->LoginUIState == ECommonUserAsyncTaskState::InProgress) + { + if (Result.IsOk()) + { + // The platform user id that actually logged in may not be the same one who requested the UI, + // so swap it if the returned id is actually valid + if (UserInfo->PlatformUser != PlatformUser && PlatformUser != PLATFORMUSERID_NONE) + { + UserInfo->PlatformUser = PlatformUser; + } + + Request->LoginUIState = ECommonUserAsyncTaskState::Done; + Request->Error.Reset(); + } + else + { + Request->LoginUIState = ECommonUserAsyncTaskState::Failed; + Request->Error = Result.GetErrorValue(); + } + + ProcessLoginRequest(Request); + } + } +} + +void UCommonUserSubsystem::HandleCheckPrivilegesComplete(const UE::Online::TOnlineResult& Result, TWeakObjectPtr CommonUserInfo, EUserPrivileges DesiredPrivilege, ECommonUserOnlineContext Context) +{ + // Only handle if user still exists + UCommonUserInfo* UserInfo = CommonUserInfo.Get(); + if (!UserInfo) + { + return; + } + + ECommonUserPrivilege UserPrivilege = ConvertOnlineServicesPrivilege(DesiredPrivilege); + ECommonUserPrivilegeResult UserResult = ECommonUserPrivilegeResult::PlatformFailure; + if (const FQueryUserPrivilege::Result* OkResult = Result.TryGetOkValue()) + { + UserResult = ConvertOnlineServicesPrivilegeResult(DesiredPrivilege, OkResult->PrivilegeResult); + } + else + { + UE_LOG(LogCommonUser, Warning, TEXT("QueryUserPrivilege failed: %s"), *Result.GetErrorValue().GetLogString()); + } + + // Update the user cached value + UserInfo->UpdateCachedPrivilegeResult(UserPrivilege, UserResult, Context); + + // See if a login request is waiting on this + TArray> RequestsCopy = ActiveLoginRequests; + for (TSharedRef& Request : RequestsCopy) + { + if (Request->UserInfo.Get() == UserInfo && Request->CurrentContext == Context && Request->DesiredPrivilege == UserPrivilege && Request->PrivilegeCheckState == ECommonUserAsyncTaskState::InProgress) + { + if (UserResult == ECommonUserPrivilegeResult::Available) + { + Request->PrivilegeCheckState = ECommonUserAsyncTaskState::Done; + } + else + { + Request->PrivilegeCheckState = ECommonUserAsyncTaskState::Failed; + Request->Error = Result.IsError() ? Result.GetErrorValue() : UE::Online::Errors::Unknown(); + } + + ProcessLoginRequest(Request); + } + } +} +#endif // COMMONUSER_OSSV1 + +void UCommonUserSubsystem::RefreshLocalUserInfo(UCommonUserInfo* UserInfo) +{ + if (ensure(UserInfo)) + { + // Always update default + UserInfo->UpdateCachedNetId(GetLocalUserNetId(UserInfo->PlatformUser, ECommonUserOnlineContext::Default), ECommonUserOnlineContext::Default); + + if (HasSeparatePlatformContext()) + { + // Also update platform + UserInfo->UpdateCachedNetId(GetLocalUserNetId(UserInfo->PlatformUser, ECommonUserOnlineContext::Platform), ECommonUserOnlineContext::Platform); + } + } +} + +void UCommonUserSubsystem::HandleChangedAvailability(UCommonUserInfo* UserInfo, ECommonUserPrivilege Privilege, ECommonUserAvailability OldAvailability) +{ + ECommonUserAvailability NewAvailability = UserInfo->GetPrivilegeAvailability(Privilege); + + if (OldAvailability != NewAvailability) + { + OnUserPrivilegeChanged.Broadcast(UserInfo, Privilege, OldAvailability, NewAvailability); + } +} + +void UCommonUserSubsystem::UpdateUserPrivilegeResult(UCommonUserInfo* UserInfo, ECommonUserPrivilege Privilege, ECommonUserPrivilegeResult Result, ECommonUserOnlineContext Context) +{ + check(UserInfo); + + ECommonUserAvailability OldAvailability = UserInfo->GetPrivilegeAvailability(Privilege); + + UserInfo->UpdateCachedPrivilegeResult(Privilege, Result, Context); + + HandleChangedAvailability(UserInfo, Privilege, OldAvailability); +} + +#if COMMONUSER_OSSV1 +ECommonUserPrivilege UCommonUserSubsystem::ConvertOSSPrivilege(EUserPrivileges::Type Privilege) const +{ + switch (Privilege) + { + case EUserPrivileges::CanPlay: + return ECommonUserPrivilege::CanPlay; + case EUserPrivileges::CanPlayOnline: + return ECommonUserPrivilege::CanPlayOnline; + case EUserPrivileges::CanCommunicateOnline: + return ECommonUserPrivilege::CanCommunicateViaTextOnline; // No good thing to do here, just mapping to text. + case EUserPrivileges::CanUseUserGeneratedContent: + return ECommonUserPrivilege::CanUseUserGeneratedContent; + case EUserPrivileges::CanUserCrossPlay: + return ECommonUserPrivilege::CanUseCrossPlay; + default: + return ECommonUserPrivilege::Invalid_Count; + } +} + +EUserPrivileges::Type UCommonUserSubsystem::ConvertOSSPrivilege(ECommonUserPrivilege Privilege) const +{ + switch (Privilege) + { + case ECommonUserPrivilege::CanPlay: + return EUserPrivileges::CanPlay; + case ECommonUserPrivilege::CanPlayOnline: + return EUserPrivileges::CanPlayOnline; + case ECommonUserPrivilege::CanCommunicateViaTextOnline: + case ECommonUserPrivilege::CanCommunicateViaVoiceOnline: + return EUserPrivileges::CanCommunicateOnline; + case ECommonUserPrivilege::CanUseUserGeneratedContent: + return EUserPrivileges::CanUseUserGeneratedContent; + case ECommonUserPrivilege::CanUseCrossPlay: + return EUserPrivileges::CanUserCrossPlay; + default: + // No failure type, return CanPlay + return EUserPrivileges::CanPlay; + } +} + +ECommonUserPrivilegeResult UCommonUserSubsystem::ConvertOSSPrivilegeResult(EUserPrivileges::Type Privilege, uint32 Results) const +{ + // The V1 results enum is a bitfield where each platform behaves a bit differently + if (Results == (uint32)IOnlineIdentity::EPrivilegeResults::NoFailures) + { + return ECommonUserPrivilegeResult::Available; + } + if ((Results & (uint32)IOnlineIdentity::EPrivilegeResults::UserNotFound) || (Results & (uint32)IOnlineIdentity::EPrivilegeResults::UserNotLoggedIn)) + { + return ECommonUserPrivilegeResult::UserNotLoggedIn; + } + if ((Results & (uint32)IOnlineIdentity::EPrivilegeResults::RequiredPatchAvailable) || (Results & (uint32)IOnlineIdentity::EPrivilegeResults::RequiredSystemUpdate)) + { + return ECommonUserPrivilegeResult::VersionOutdated; + } + if (Results & (uint32)IOnlineIdentity::EPrivilegeResults::AgeRestrictionFailure) + { + return ECommonUserPrivilegeResult::AgeRestricted; + } + if (Results & (uint32)IOnlineIdentity::EPrivilegeResults::AccountTypeFailure) + { + return ECommonUserPrivilegeResult::AccountTypeRestricted; + } + if (Results & (uint32)IOnlineIdentity::EPrivilegeResults::NetworkConnectionUnavailable) + { + return ECommonUserPrivilegeResult::NetworkConnectionUnavailable; + } + + // Bucket other account failures together + uint32 AccountUseFailures = (uint32)IOnlineIdentity::EPrivilegeResults::OnlinePlayRestricted + | (uint32)IOnlineIdentity::EPrivilegeResults::UGCRestriction + | (uint32)IOnlineIdentity::EPrivilegeResults::ChatRestriction; + + if (Results & AccountUseFailures) + { + return ECommonUserPrivilegeResult::AccountUseRestricted; + } + + // If you can't play at all, this is a license failure + if (Privilege == EUserPrivileges::CanPlay) + { + return ECommonUserPrivilegeResult::LicenseInvalid; + } + + // Unknown reason + return ECommonUserPrivilegeResult::PlatformFailure; +} +#else +ECommonUserPrivilege UCommonUserSubsystem::ConvertOnlineServicesPrivilege(EUserPrivileges Privilege) const +{ + switch (Privilege) + { + case EUserPrivileges::CanPlay: + return ECommonUserPrivilege::CanPlay; + case EUserPrivileges::CanPlayOnline: + return ECommonUserPrivilege::CanPlayOnline; + case EUserPrivileges::CanCommunicateViaTextOnline: + return ECommonUserPrivilege::CanCommunicateViaTextOnline; + case EUserPrivileges::CanCommunicateViaVoiceOnline: + return ECommonUserPrivilege::CanCommunicateViaVoiceOnline; + case EUserPrivileges::CanUseUserGeneratedContent: + return ECommonUserPrivilege::CanUseUserGeneratedContent; + case EUserPrivileges::CanCrossPlay: + return ECommonUserPrivilege::CanUseCrossPlay; + default: + return ECommonUserPrivilege::Invalid_Count; + } +} + +EUserPrivileges UCommonUserSubsystem::ConvertOnlineServicesPrivilege(ECommonUserPrivilege Privilege) const +{ + switch (Privilege) + { + case ECommonUserPrivilege::CanPlay: + return EUserPrivileges::CanPlay; + case ECommonUserPrivilege::CanPlayOnline: + return EUserPrivileges::CanPlayOnline; + case ECommonUserPrivilege::CanCommunicateViaTextOnline: + return EUserPrivileges::CanCommunicateViaTextOnline; + case ECommonUserPrivilege::CanCommunicateViaVoiceOnline: + return EUserPrivileges::CanCommunicateViaVoiceOnline; + case ECommonUserPrivilege::CanUseUserGeneratedContent: + return EUserPrivileges::CanUseUserGeneratedContent; + case ECommonUserPrivilege::CanUseCrossPlay: + return EUserPrivileges::CanCrossPlay; + default: + // No failure type, return CanPlay + return EUserPrivileges::CanPlay; + } +} + +ECommonUserPrivilegeResult UCommonUserSubsystem::ConvertOnlineServicesPrivilegeResult(EUserPrivileges Privilege, EPrivilegeResults Results) const +{ + // The V1 results enum is a bitfield where each platform behaves a bit differently + if (Results == EPrivilegeResults::NoFailures) + { + return ECommonUserPrivilegeResult::Available; + } + if (EnumHasAnyFlags(Results, EPrivilegeResults::UserNotFound | EPrivilegeResults::UserNotLoggedIn)) + { + return ECommonUserPrivilegeResult::UserNotLoggedIn; + } + if (EnumHasAnyFlags(Results, EPrivilegeResults::RequiredPatchAvailable | EPrivilegeResults::RequiredSystemUpdate)) + { + return ECommonUserPrivilegeResult::VersionOutdated; + } + if (EnumHasAnyFlags(Results, EPrivilegeResults::AgeRestrictionFailure)) + { + return ECommonUserPrivilegeResult::AgeRestricted; + } + if (EnumHasAnyFlags(Results, EPrivilegeResults::AccountTypeFailure)) + { + return ECommonUserPrivilegeResult::AccountTypeRestricted; + } + if (EnumHasAnyFlags(Results, EPrivilegeResults::NetworkConnectionUnavailable)) + { + return ECommonUserPrivilegeResult::NetworkConnectionUnavailable; + } + + // Bucket other account failures together + const EPrivilegeResults AccountUseFailures = EPrivilegeResults::OnlinePlayRestricted + | EPrivilegeResults::UGCRestriction + | EPrivilegeResults::ChatRestriction; + + if (EnumHasAnyFlags(Results, AccountUseFailures)) + { + return ECommonUserPrivilegeResult::AccountUseRestricted; + } + + // If you can't play at all, this is a license failure + if (Privilege == EUserPrivileges::CanPlay) + { + return ECommonUserPrivilegeResult::LicenseInvalid; + } + + // Unknown reason + return ECommonUserPrivilegeResult::PlatformFailure; +} +#endif // COMMONUSER_OSSV1 + +FString UCommonUserSubsystem::PlatformUserIdToString(FPlatformUserId UserId) +{ + if (UserId == PLATFORMUSERID_NONE) + { + return TEXT("None"); + } + else + { + return FString::Printf(TEXT("%d"), UserId.GetInternalId()); + } +} + +FString UCommonUserSubsystem::ECommonUserOnlineContextToString(ECommonUserOnlineContext Context) +{ + switch (Context) + { + case ECommonUserOnlineContext::Game: + return TEXT("Game"); + case ECommonUserOnlineContext::Default: + return TEXT("Default"); + case ECommonUserOnlineContext::Service: + return TEXT("Service"); + case ECommonUserOnlineContext::ServiceOrDefault: + return TEXT("Service/Default"); + case ECommonUserOnlineContext::Platform: + return TEXT("Platform"); + case ECommonUserOnlineContext::PlatformOrDefault: + return TEXT("Platform/Default"); + default: + return TEXT("Invalid"); + } +} + +FText UCommonUserSubsystem::GetPrivilegeDescription(ECommonUserPrivilege Privilege) const +{ + switch (Privilege) + { + case ECommonUserPrivilege::CanPlay: + return NSLOCTEXT("CommonUser", "PrivilegeCanPlay", "play the game"); + case ECommonUserPrivilege::CanPlayOnline: + return NSLOCTEXT("CommonUser", "PrivilegeCanPlayOnline", "play online"); + case ECommonUserPrivilege::CanCommunicateViaTextOnline: + return NSLOCTEXT("CommonUser", "PrivilegeCanCommunicateViaTextOnline", "communicate with text"); + case ECommonUserPrivilege::CanCommunicateViaVoiceOnline: + return NSLOCTEXT("CommonUser", "PrivilegeCanCommunicateViaVoiceOnline", "communicate with voice"); + case ECommonUserPrivilege::CanUseUserGeneratedContent: + return NSLOCTEXT("CommonUser", "PrivilegeCanUseUserGeneratedContent", "access user content"); + case ECommonUserPrivilege::CanUseCrossPlay: + return NSLOCTEXT("CommonUser", "PrivilegeCanUseCrossPlay", "play with other platforms"); + default: + return NSLOCTEXT("CommonUser", "PrivilegeInvalid", ""); + } +} + +FText UCommonUserSubsystem::GetPrivilegeResultDescription(ECommonUserPrivilegeResult Result) const +{ + // TODO these strings might have cert requirements we need to override per console + switch (Result) + { + case ECommonUserPrivilegeResult::Unknown: + return NSLOCTEXT("CommonUser", "ResultUnknown", "Unknown if the user is allowed"); + case ECommonUserPrivilegeResult::Available: + return NSLOCTEXT("CommonUser", "ResultAvailable", "The user is allowed"); + case ECommonUserPrivilegeResult::UserNotLoggedIn: + return NSLOCTEXT("CommonUser", "ResultUserNotLoggedIn", "The user must login"); + case ECommonUserPrivilegeResult::LicenseInvalid: + return NSLOCTEXT("CommonUser", "ResultLicenseInvalid", "A valid game license is required"); + case ECommonUserPrivilegeResult::VersionOutdated: + return NSLOCTEXT("CommonUser", "VersionOutdated", "The game or hardware needs to be updated"); + case ECommonUserPrivilegeResult::NetworkConnectionUnavailable: + return NSLOCTEXT("CommonUser", "ResultNetworkConnectionUnavailable", "A network connection is required"); + case ECommonUserPrivilegeResult::AgeRestricted: + return NSLOCTEXT("CommonUser", "ResultAgeRestricted", "This age restricted account is not allowed"); + case ECommonUserPrivilegeResult::AccountTypeRestricted: + return NSLOCTEXT("CommonUser", "ResultAccountTypeRestricted", "This account type does not have access"); + case ECommonUserPrivilegeResult::AccountUseRestricted: + return NSLOCTEXT("CommonUser", "ResultAccountUseRestricted", "This account is not allowed"); + case ECommonUserPrivilegeResult::PlatformFailure: + return NSLOCTEXT("CommonUser", "ResultPlatformFailure", "Not allowed"); + default: + return NSLOCTEXT("CommonUser", "ResultInvalid", ""); + + } +} + +const UCommonUserSubsystem::FOnlineContextCache* UCommonUserSubsystem::GetContextCache(ECommonUserOnlineContext Context) const +{ + return const_cast(this)->GetContextCache(Context); +} + +UCommonUserSubsystem::FOnlineContextCache* UCommonUserSubsystem::GetContextCache(ECommonUserOnlineContext Context) +{ + switch (Context) + { + case ECommonUserOnlineContext::Game: + case ECommonUserOnlineContext::Default: + return DefaultContextInternal; + + case ECommonUserOnlineContext::Service: + return ServiceContextInternal; + case ECommonUserOnlineContext::ServiceOrDefault: + return ServiceContextInternal ? ServiceContextInternal : DefaultContextInternal; + + case ECommonUserOnlineContext::Platform: + return PlatformContextInternal; + case ECommonUserOnlineContext::PlatformOrDefault: + return PlatformContextInternal ? PlatformContextInternal : DefaultContextInternal; + } + + return nullptr; +} + +ECommonUserOnlineContext UCommonUserSubsystem::ResolveOnlineContext(ECommonUserOnlineContext Context) const +{ + switch (Context) + { + case ECommonUserOnlineContext::Game: + case ECommonUserOnlineContext::Default: + return ECommonUserOnlineContext::Default; + + case ECommonUserOnlineContext::Service: + return ServiceContextInternal ? ECommonUserOnlineContext::Service : ECommonUserOnlineContext::Invalid; + case ECommonUserOnlineContext::ServiceOrDefault: + return ServiceContextInternal ? ECommonUserOnlineContext::Service : ECommonUserOnlineContext::Default; + + case ECommonUserOnlineContext::Platform: + return PlatformContextInternal ? ECommonUserOnlineContext::Platform : ECommonUserOnlineContext::Invalid; + case ECommonUserOnlineContext::PlatformOrDefault: + return PlatformContextInternal ? ECommonUserOnlineContext::Platform : ECommonUserOnlineContext::Default; + } + + return ECommonUserOnlineContext::Invalid; +} + +bool UCommonUserSubsystem::HasSeparatePlatformContext() const +{ + ECommonUserOnlineContext ServiceType = ResolveOnlineContext(ECommonUserOnlineContext::ServiceOrDefault); + ECommonUserOnlineContext PlatformType = ResolveOnlineContext(ECommonUserOnlineContext::PlatformOrDefault); + + if (ServiceType != PlatformType) + { + return true; + } + return false; +} + +void UCommonUserSubsystem::SetLocalPlayerUserInfo(ULocalPlayer* LocalPlayer, const UCommonUserInfo* UserInfo) +{ + if (!bIsDedicatedServer && ensure(LocalPlayer && UserInfo)) + { + LocalPlayer->SetPlatformUserId(UserInfo->GetPlatformUserId()); + + FUniqueNetIdRepl NetId = UserInfo->GetNetId(ECommonUserOnlineContext::Game); + LocalPlayer->SetCachedUniqueNetId(NetId); + + // Also update player state if possible + APlayerController* PlayerController = LocalPlayer->GetPlayerController(nullptr); + if (PlayerController && PlayerController->PlayerState) + { + PlayerController->PlayerState->SetUniqueId(NetId); + } + } +} + +const UCommonUserInfo* UCommonUserSubsystem::GetUserInfoForLocalPlayerIndex(int32 LocalPlayerIndex) const +{ + TObjectPtr const* Found = LocalUserInfos.Find(LocalPlayerIndex); + if (Found) + { + return *Found; + } + return nullptr; +} + +const UCommonUserInfo* UCommonUserSubsystem::GetUserInfoForPlatformUserIndex(int32 PlatformUserIndex) const +{ + FPlatformUserId PlatformUser = GetPlatformUserIdForIndex(PlatformUserIndex); + return GetUserInfoForPlatformUser(PlatformUser); +} + +const UCommonUserInfo* UCommonUserSubsystem::GetUserInfoForPlatformUser(FPlatformUserId PlatformUser) const +{ + if (!IsRealPlatformUser(PlatformUser)) + { + return nullptr; + } + + for (TPair Pair : LocalUserInfos) + { + // Don't include guest users in this check + if (ensure(Pair.Value) && Pair.Value->PlatformUser == PlatformUser && !Pair.Value->bIsGuest) + { + return Pair.Value; + } + } + + return nullptr; +} + +const UCommonUserInfo* UCommonUserSubsystem::GetUserInfoForUniqueNetId(const FUniqueNetIdRepl& NetId) const +{ + if (!NetId.IsValid()) + { + // TODO do we need to handle pre-login case on mobile platforms where netID is invalid? + return nullptr; + } + + for (TPair UserPair : LocalUserInfos) + { + if (ensure(UserPair.Value)) + { + for (const TPair& CachedPair : UserPair.Value->CachedDataMap) + { + if (NetId == CachedPair.Value.CachedNetId) + { + return UserPair.Value; + } + } + } + } + + return nullptr; +} + +const UCommonUserInfo* UCommonUserSubsystem::GetUserInfoForControllerId(int32 ControllerId) const +{ + FPlatformUserId PlatformUser; + FInputDeviceId IgnoreDevice; + + IPlatformInputDeviceMapper::Get().RemapControllerIdToPlatformUserAndDevice(ControllerId, PlatformUser, IgnoreDevice); + + return GetUserInfoForPlatformUser(PlatformUser); +} + +const UCommonUserInfo* UCommonUserSubsystem::GetUserInfoForInputDevice(FInputDeviceId InputDevice) const +{ + FPlatformUserId PlatformUser = GetPlatformUserIdForInputDevice(InputDevice); + return GetUserInfoForPlatformUser(PlatformUser); +} + +bool UCommonUserSubsystem::IsRealPlatformUserIndex(int32 PlatformUserIndex) const +{ + if (PlatformUserIndex < 0) + { + return false; + } + +#if COMMONUSER_OSSV1 + if (PlatformUserIndex >= MAX_LOCAL_PLAYERS) + { + // Check against OSS count + return false; + } +#else + // TODO: OSSv2 define MAX_LOCAL_PLAYERS? +#endif + + if (PlatformUserIndex > 0 && GetTraitTags().HasTag(FCommonUserTags::Platform_Trait_SingleOnlineUser)) + { + return false; + } + + return true; +} + +bool UCommonUserSubsystem::IsRealPlatformUser(FPlatformUserId PlatformUser) const +{ + // Validation is done at conversion/allocation time so trust the type + if (!PlatformUser.IsValid()) + { + return false; + } + + // TODO: Validate against OSS or input mapper somehow + + if (GetTraitTags().HasTag(FCommonUserTags::Platform_Trait_SingleOnlineUser)) + { + // Only the default user is supports online functionality + if (PlatformUser != IPlatformInputDeviceMapper::Get().GetPrimaryPlatformUser()) + { + return false; + } + } + + return true; +} + +FPlatformUserId UCommonUserSubsystem::GetPlatformUserIdForIndex(int32 PlatformUserIndex) const +{ + return IPlatformInputDeviceMapper::Get().GetPlatformUserForUserIndex(PlatformUserIndex); +} + +int32 UCommonUserSubsystem::GetPlatformUserIndexForId(FPlatformUserId PlatformUser) const +{ + return IPlatformInputDeviceMapper::Get().GetUserIndexForPlatformUser(PlatformUser); +} + +FPlatformUserId UCommonUserSubsystem::GetPlatformUserIdForInputDevice(FInputDeviceId InputDevice) const +{ + return IPlatformInputDeviceMapper::Get().GetUserForInputDevice(InputDevice); +} + +FInputDeviceId UCommonUserSubsystem::GetPrimaryInputDeviceForPlatformUser(FPlatformUserId PlatformUser) const +{ + return IPlatformInputDeviceMapper::Get().GetPrimaryInputDeviceForUser(PlatformUser); +} + +void UCommonUserSubsystem::SetTraitTags(const FGameplayTagContainer& InTags) +{ + CachedTraitTags = InTags; +} + +bool UCommonUserSubsystem::ShouldWaitForStartInput() const +{ + // By default, don't wait for input if this is a single user platform + return !HasTraitTag(FCommonUserTags::Platform_Trait_SingleOnlineUser.GetTag()); +} + +#if COMMONUSER_OSSV1 +void UCommonUserSubsystem::HandleIdentityLoginStatusChanged(int32 PlatformUserIndex, ELoginStatus::Type OldStatus, ELoginStatus::Type NewStatus, const FUniqueNetId& NewId, ECommonUserOnlineContext Context) +{ + UE_LOG(LogCommonUser, Log, TEXT("Player login status changed - System:%s, UserIdx:%d, OldStatus:%s, NewStatus:%s, NewId:%s"), + *GetOnlineSubsystemName(Context).ToString(), + PlatformUserIndex, + ELoginStatus::ToString(OldStatus), + ELoginStatus::ToString(NewStatus), + *NewId.ToString()); + + if (NewStatus == ELoginStatus::NotLoggedIn && OldStatus != ELoginStatus::NotLoggedIn) + { + FPlatformUserId PlatformUser = GetPlatformUserIdForIndex(PlatformUserIndex); + LogOutLocalUser(PlatformUser); + } +} + +void UCommonUserSubsystem::HandleControllerPairingChanged(int32 PlatformUserIndex, FControllerPairingChangedUserInfo PreviousUser, FControllerPairingChangedUserInfo NewUser) +{ + UE_LOG(LogCommonUser, Log, TEXT("Player controller pairing changed - UserIdx:%d, PreviousUser:%s, NewUser:%s"), + PlatformUserIndex, + *ToDebugString(PreviousUser), + *ToDebugString(NewUser)); + + UGameInstance* GameInstance = GetGameInstance(); + FPlatformUserId PlatformUser = GetPlatformUserIdForIndex(PlatformUserIndex); + ULocalPlayer* ControlledLocalPlayer = GameInstance->FindLocalPlayerFromPlatformUserId(PlatformUser); + ULocalPlayer* NewLocalPlayer = GameInstance->FindLocalPlayerFromUniqueNetId(NewUser.User); + const UCommonUserInfo* NewUserInfo = GetUserInfoForUniqueNetId(FUniqueNetIdRepl(NewUser.User)); + const UCommonUserInfo* PreviousUserInfo = GetUserInfoForUniqueNetId(FUniqueNetIdRepl(NewUser.User)); + + // See if we think this is already bound to an existing player + if (PreviousUser.ControllersRemaining == 0 && PreviousUserInfo && PreviousUserInfo != NewUserInfo) + { + // This means that the user deliberately logged out using a platform interface + if (IsRealPlatformUser(PlatformUser)) + { + LogOutLocalUser(PlatformUser); + } + } + + if (ControlledLocalPlayer && ControlledLocalPlayer != NewLocalPlayer) + { + // TODO Currently the platforms that call this delegate do not really handle swapping controller IDs + // SetLocalPlayerUserIndex(ControlledLocalPlayer, -1); + } +} + +void UCommonUserSubsystem::HandleNetworkConnectionStatusChanged(const FString& ServiceName, EOnlineServerConnectionStatus::Type LastConnectionStatus, EOnlineServerConnectionStatus::Type ConnectionStatus, ECommonUserOnlineContext Context) +{ + UE_LOG(LogCommonUser, Log, TEXT("HandleNetworkConnectionStatusChanged(ServiceName: %s, LastStatus: %s, ConnectionStatus: %s)"), + *ServiceName, + EOnlineServerConnectionStatus::ToString(LastConnectionStatus), + EOnlineServerConnectionStatus::ToString(ConnectionStatus)); + + // Cache old availablity for current users + TMap AvailabilityMap; + + for (TPair Pair : LocalUserInfos) + { + AvailabilityMap.Add(Pair.Value, Pair.Value->GetPrivilegeAvailability(ECommonUserPrivilege::CanPlayOnline)); + } + + FOnlineContextCache* System = GetContextCache(Context); + if (ensure(System)) + { + // Service name is normally the same as the OSS name, but not necessarily on all platforms + System->CurrentConnectionStatus = ConnectionStatus; + } + + for (TPair Pair : AvailabilityMap) + { + // Notify other systems when someone goes online/offline + HandleChangedAvailability(Pair.Key, ECommonUserPrivilege::CanPlayOnline, Pair.Value); + } + +} +#else +void UCommonUserSubsystem::HandleAuthLoginStatusChanged(const UE::Online::FAuthLoginStatusChanged& EventParameters, ECommonUserOnlineContext Context) +{ + UE_LOG(LogCommonUser, Log, TEXT("Player login status changed - System:%d, UserId:%s, NewStatus:%s"), + (int)Context, + *ToLogString(EventParameters.AccountInfo->AccountId), + LexToString(EventParameters.LoginStatus)); +} + +void UCommonUserSubsystem::HandleNetworkConnectionStatusChanged(const UE::Online::FConnectionStatusChanged& EventParameters, ECommonUserOnlineContext Context) +{ + UE_LOG(LogCommonUser, Log, TEXT("HandleNetworkConnectionStatusChanged(Context:%d, ServiceName:%s, OldStatus:%s, NewStatus:%s)"), + (int)Context, + *EventParameters.ServiceName, + LexToString(EventParameters.PreviousStatus), + LexToString(EventParameters.CurrentStatus)); + + // Cache old availablity for current users + TMap AvailabilityMap; + + for (TPair Pair : LocalUserInfos) + { + AvailabilityMap.Add(Pair.Value, Pair.Value->GetPrivilegeAvailability(ECommonUserPrivilege::CanPlayOnline)); + } + + FOnlineContextCache* System = GetContextCache(Context); + if (ensure(System)) + { + // Service name is normally the same as the OSS name, but not necessarily on all platforms + System->CurrentConnectionStatus = EventParameters.CurrentStatus; + } + + for (TPair Pair : AvailabilityMap) + { + // Notify other systems when someone goes online/offline + HandleChangedAvailability(Pair.Key, ECommonUserPrivilege::CanPlayOnline, Pair.Value); + } +} +#endif // COMMONUSER_OSSV1 + +void UCommonUserSubsystem::HandleInputDeviceConnectionChanged(EInputDeviceConnectionState NewConnectionState, FPlatformUserId PlatformUserId, FInputDeviceId InputDeviceId) +{ + FString InputDeviceIDString = FString::Printf(TEXT("%d"), InputDeviceId.GetId()); + const bool bIsConnected = NewConnectionState == EInputDeviceConnectionState::Connected; + UE_LOG(LogCommonUser, Log, TEXT("Controller connection changed - UserIdx:%d, UserID:%s, Connected:%d"), *InputDeviceIDString, *PlatformUserIdToString(PlatformUserId), bIsConnected ? 1 : 0); + + // TODO Implement for platforms that support this +} + diff --git a/Plugins/CommonUser/Source/CommonUser/Private/CommonUserTypes.cpp b/Plugins/CommonUser/Source/CommonUser/Private/CommonUserTypes.cpp new file mode 100644 index 000000000..4c9dc6d62 --- /dev/null +++ b/Plugins/CommonUser/Source/CommonUser/Private/CommonUserTypes.cpp @@ -0,0 +1,17 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "CommonUserTypes.h" +#include "OnlineError.h" + +void FOnlineResultInformation::FromOnlineError(const FOnlineErrorType& InOnlineError) +{ +#if COMMONUSER_OSSV1 + bWasSuccessful = InOnlineError.WasSuccessful(); + ErrorId = InOnlineError.GetErrorCode(); + ErrorText = InOnlineError.GetErrorMessage(); +#else + bWasSuccessful = InOnlineError != UE::Online::Errors::Success(); + ErrorId = InOnlineError.GetErrorId(); + ErrorText = InOnlineError.GetText(); +#endif +} diff --git a/Plugins/CommonUser/Source/CommonUser/Public/AsyncAction_CommonUserInitialize.h b/Plugins/CommonUser/Source/CommonUser/Public/AsyncAction_CommonUserInitialize.h new file mode 100644 index 000000000..aeb79c2aa --- /dev/null +++ b/Plugins/CommonUser/Source/CommonUser/Public/AsyncAction_CommonUserInitialize.h @@ -0,0 +1,64 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CommonUserSubsystem.h" +#include "Engine/CancellableAsyncAction.h" + +#include "AsyncAction_CommonUserInitialize.generated.h" + +enum class ECommonUserOnlineContext : uint8; +enum class ECommonUserPrivilege : uint8; +struct FInputDeviceId; + +class FText; +class UObject; +struct FFrame; + +/** + * Async action to handle different functions for initializing users + */ +UCLASS() +class COMMONUSER_API UAsyncAction_CommonUserInitialize : public UCancellableAsyncAction +{ + GENERATED_BODY() + +public: + /** + * Initializes a local player with the common user system, which includes doing platform-specific login and privilege checks. + * When the process has succeeded or failed, it will broadcast the OnInitializationComplete delegate. + * + * @param LocalPlayerIndex Desired index of ULocalPlayer in Game Instance, 0 will be primary player and 1+ for local multiplayer + * @param PrimaryInputDevice Primary input device for the user, if invalid will use the system default + * @param bCanUseGuestLogin If true, this player can be a guest without a real system net id + */ + UFUNCTION(BlueprintCallable, Category = CommonUser, meta = (BlueprintInternalUseOnly = "true")) + static UAsyncAction_CommonUserInitialize* InitializeForLocalPlay(UCommonUserSubsystem* Target, int32 LocalPlayerIndex, FInputDeviceId PrimaryInputDevice, bool bCanUseGuestLogin); + + /** + * Attempts to log an existing user into the platform-specific online backend to enable full online play + * When the process has succeeded or failed, it will broadcast the OnInitializationComplete delegate. + * + * @param LocalPlayerIndex Index of existing LocalPlayer in Game Instance + */ + UFUNCTION(BlueprintCallable, Category = CommonUser, meta = (BlueprintInternalUseOnly = "true")) + static UAsyncAction_CommonUserInitialize* LoginForOnlinePlay(UCommonUserSubsystem* Target, int32 LocalPlayerIndex); + + /** Call when initialization succeeds or fails */ + UPROPERTY(BlueprintAssignable) + FCommonUserOnInitializeCompleteMulticast OnInitializationComplete; + + /** Fail and send callbacks if needed */ + void HandleFailure(); + + /** Wrapper delegate, will pass on to OnInitializationComplete if appropriate */ + UFUNCTION() + virtual void HandleInitializationComplete(const UCommonUserInfo* UserInfo, bool bSuccess, FText Error, ECommonUserPrivilege RequestedPrivilege, ECommonUserOnlineContext OnlineContext); + +protected: + /** Actually start the initialization */ + virtual void Activate() override; + + TWeakObjectPtr Subsystem; + FCommonUserInitializeParams Params; +}; diff --git a/Plugins/CommonUser/Source/CommonUser/Public/CommonSessionSubsystem.h b/Plugins/CommonUser/Source/CommonUser/Public/CommonSessionSubsystem.h new file mode 100644 index 000000000..289463818 --- /dev/null +++ b/Plugins/CommonUser/Source/CommonUser/Public/CommonSessionSubsystem.h @@ -0,0 +1,382 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CommonUserTypes.h" +#include "Subsystems/GameInstanceSubsystem.h" +#include "UObject/ObjectPtr.h" +#include "UObject/StrongObjectPtr.h" +#include "UObject/PrimaryAssetId.h" +#include "UObject/WeakObjectPtr.h" + +class APlayerController; +class ULocalPlayer; +namespace ETravelFailure { enum Type : int; } +struct FOnlineResultInformation; + +#if COMMONUSER_OSSV1 +#include "Interfaces/OnlineSessionInterface.h" +#include "Public/OnlineSessionSettings.h" +#else +#include "Online/Lobbies.h" +#endif // COMMONUSER_OSSV1 + +#include "CommonSessionSubsystem.generated.h" + +class UWorld; +class FCommonSession_OnlineSessionSettings; + +#if COMMONUSER_OSSV1 +class FCommonOnlineSearchSettingsOSSv1; +using FCommonOnlineSearchSettings = FCommonOnlineSearchSettingsOSSv1; +#else +class FCommonOnlineSearchSettingsOSSv2; +using FCommonOnlineSearchSettings = FCommonOnlineSearchSettingsOSSv2; +#endif // COMMONUSER_OSSV1 + + +////////////////////////////////////////////////////////////////////// +// UCommonSession_HostSessionRequest + +/** Specifies the online features and connectivity that should be used for a game session */ +UENUM(BlueprintType) +enum class ECommonSessionOnlineMode : uint8 +{ + Offline, + LAN, + Online +}; + +/** A request object that stores the parameters used when hosting a gameplay session */ +UCLASS(BlueprintType) +class COMMONUSER_API UCommonSession_HostSessionRequest : public UObject +{ + GENERATED_BODY() + +public: + /** Indicates if the session is a full online session or a different type */ + UPROPERTY(BlueprintReadWrite, Category=Session) + ECommonSessionOnlineMode OnlineMode; + + /** True if this request should create a player-hosted lobbies if available */ + UPROPERTY(BlueprintReadWrite, Category = Session) + bool bUseLobbies; + + /** String used during matchmaking to specify what type of game mode this is */ + UPROPERTY(BlueprintReadWrite, Category=Session) + FString ModeNameForAdvertisement; + + /** The map that will be loaded at the start of gameplay, this needs to be a valid Primary Asset top-level map */ + UPROPERTY(BlueprintReadWrite, Category=Session, meta=(AllowedTypes="World")) + FPrimaryAssetId MapID; + + /** Extra arguments passed as URL options to the game */ + UPROPERTY(BlueprintReadWrite, Category=Session) + TMap ExtraArgs; + + /** Maximum players allowed per gameplay session */ + UPROPERTY(BlueprintReadWrite, Category=Session) + int32 MaxPlayerCount = 16; + +public: + /** Returns the maximum players that should actually be used, could be overridden in child classes */ + virtual int32 GetMaxPlayers() const; + + /** Returns the full map name that will be used during gameplay */ + virtual FString GetMapName() const; + + /** Constructs the full URL that will be passed to ServerTravel */ + virtual FString ConstructTravelURL() const; + + /** Returns true if this request is valid, returns false and logs errors if it is not */ + virtual bool ValidateAndLogErrors(FText& OutError) const; +}; + + +////////////////////////////////////////////////////////////////////// +// UCommonSession_SearchResult + +/** A result object returned from the online system that describes a joinable game session */ +UCLASS(BlueprintType) +class COMMONUSER_API UCommonSession_SearchResult : public UObject +{ + GENERATED_BODY() + +public: + /** Returns an internal description of the session, not meant to be human readable */ + UFUNCTION(BlueprintCallable, Category=Session) + FString GetDescription() const; + + /** Gets an arbitrary string setting, bFoundValue will be false if the setting does not exist */ + UFUNCTION(BlueprintPure, Category=Sessions) + void GetStringSetting(FName Key, FString& Value, bool& bFoundValue) const; + + /** Gets an arbitrary integer setting, bFoundValue will be false if the setting does not exist */ + UFUNCTION(BlueprintPure, Category = Sessions) + void GetIntSetting(FName Key, int32& Value, bool& bFoundValue) const; + + /** The number of private connections that are available */ + UFUNCTION(BlueprintPure, Category=Sessions) + int32 GetNumOpenPrivateConnections() const; + + /** The number of publicly available connections that are available */ + UFUNCTION(BlueprintPure, Category=Sessions) + int32 GetNumOpenPublicConnections() const; + + /** The maximum number of publicly available connections that could be available, including already filled connections */ + UFUNCTION(BlueprintPure, Category = Sessions) + int32 GetMaxPublicConnections() const; + + /** Ping to the search result, MAX_QUERY_PING is unreachable */ + UFUNCTION(BlueprintPure, Category=Sessions) + int32 GetPingInMs() const; + +public: + /** Pointer to the platform-specific implementation */ +#if COMMONUSER_OSSV1 + FOnlineSessionSearchResult Result; +#else + TSharedPtr Lobby; +#endif // COMMONUSER_OSSV1 + +}; + + +////////////////////////////////////////////////////////////////////// +// UCommonSession_SearchSessionRequest + +/** Delegates called when a session search completes */ +DECLARE_MULTICAST_DELEGATE_TwoParams(FCommonSession_FindSessionsFinished, bool bSucceeded, const FText& ErrorMessage); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FCommonSession_FindSessionsFinishedDynamic, bool, bSucceeded, FText, ErrorMessage); + +/** Request object describing a session search, this object will be updated once the search has completed */ +UCLASS(BlueprintType) +class COMMONUSER_API UCommonSession_SearchSessionRequest : public UObject +{ + GENERATED_BODY() + +public: + /** Indicates if the this is looking for full online games or a different type like LAN */ + UPROPERTY(BlueprintReadWrite, Category = Session) + ECommonSessionOnlineMode OnlineMode; + + /** True if this request should look for player-hosted lobbies if they are available, false will only search for registered server sessions */ + UPROPERTY(BlueprintReadWrite, Category = Session) + bool bUseLobbies; + + /** List of all found sessions, will be valid when OnSearchFinished is called */ + UPROPERTY(BlueprintReadOnly, Category=Session) + TArray> Results; + + /** Native Delegate called when a session search completes */ + FCommonSession_FindSessionsFinished OnSearchFinished; + + /** Called by subsystem to execute finished delegates */ + void NotifySearchFinished(bool bSucceeded, const FText& ErrorMessage); + +private: + /** Delegate called when a session search completes */ + UPROPERTY(BlueprintAssignable, Category = "Events", meta = (DisplayName = "On Search Finished", AllowPrivateAccess = true)) + FCommonSession_FindSessionsFinishedDynamic K2_OnSearchFinished; +}; + + +////////////////////////////////////////////////////////////////////// +// CommonSessionSubsystem Events + +/** + * Event triggered when the local user has requested to join a session from an external source, for example from a platform overlay. + * Generally, the game should transition the player into the session. + * @param LocalPlatformUserId the local user id that accepted the invitation. This is a platform user id because the user might not be signed in yet. + * @param RequestedSession the requested session. Can be null if there was an error processing the request. + * @param RequestedSessionResult result of the requested session processing + */ +DECLARE_MULTICAST_DELEGATE_ThreeParams(FCommonSessionOnUserRequestedSession, const FPlatformUserId& /*LocalPlatformUserId*/, UCommonSession_SearchResult* /*RequestedSession*/, const FOnlineResultInformation& /*RequestedSessionResult*/); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FCommonSessionOnUserRequestedSession_Dynamic, const FPlatformUserId&, LocalPlatformUserId, UCommonSession_SearchResult*, RequestedSession, const FOnlineResultInformation&, RequestedSessionResult); + +/** + * Event triggered when a session join has completed, after joining the underlying session and before traveling to the server if it was successful. + * The event parameters indicate if this was successful, or if there was an error that will stop it from traveling. + * @param Result result of the session join + */ +DECLARE_MULTICAST_DELEGATE_OneParam(FCommonSessionOnJoinSessionComplete, const FOnlineResultInformation& /*Result*/); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FCommonSessionOnJoinSessionComplete_Dynamic, const FOnlineResultInformation&, Result); + +/** + * Event triggered when a session creation for hosting has completed, right before it travels to the map. + * The event parameters indicate if this was successful, or if there was an error that will stop it from traveling. + * @param Result result of the session join + */ +DECLARE_MULTICAST_DELEGATE_OneParam(FCommonSessionOnCreateSessionComplete, const FOnlineResultInformation& /*Result*/); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FCommonSessionOnCreateSessionComplete_Dynamic, const FOnlineResultInformation&, Result); + +/** + * Event triggered when a session join has completed, after resolving the connect string and prior to the client traveling. + * @param URL resolved connection string for the session with any additional arguments + */ +DECLARE_MULTICAST_DELEGATE_OneParam(FCommonSessionOnPreClientTravel, FString& /*URL*/); + +////////////////////////////////////////////////////////////////////// +// UCommonSessionSubsystem + +/** + * Game subsystem that handles requests for hosting and joining online games. + * One subsystem is created for each game instance and can be accessed from blueprints or C++ code. + * If a game-specific subclass exists, this base subsystem will not be created. + */ +UCLASS() +class COMMONUSER_API UCommonSessionSubsystem : public UGameInstanceSubsystem +{ + GENERATED_BODY() + +public: + UCommonSessionSubsystem() { } + + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + virtual bool ShouldCreateSubsystem(UObject* Outer) const override; + + /** Creates a host session request with default options for online games, this can be modified after creation */ + UFUNCTION(BlueprintCallable, Category = Session) + virtual UCommonSession_HostSessionRequest* CreateOnlineHostSessionRequest(); + + /** Creates a session search object with default options to look for default online games, this can be modified after creation */ + UFUNCTION(BlueprintCallable, Category = Session) + virtual UCommonSession_SearchSessionRequest* CreateOnlineSearchSessionRequest(); + + /** Creates a new online game using the session request information, if successful this will start a hard map transfer */ + UFUNCTION(BlueprintCallable, Category=Session) + virtual void HostSession(APlayerController* HostingPlayer, UCommonSession_HostSessionRequest* Request); + + /** Starts a process to look for existing sessions or create a new one if no viable sessions are found */ + UFUNCTION(BlueprintCallable, Category=Session) + virtual void QuickPlaySession(APlayerController* JoiningOrHostingPlayer, UCommonSession_HostSessionRequest* Request); + + /** Starts process to join an existing session, if successful this will connect to the specified server */ + UFUNCTION(BlueprintCallable, Category=Session) + virtual void JoinSession(APlayerController* JoiningPlayer, UCommonSession_SearchResult* Request); + + /** Queries online system for the list of joinable sessions matching the search request */ + UFUNCTION(BlueprintCallable, Category=Session) + virtual void FindSessions(APlayerController* SearchingPlayer, UCommonSession_SearchSessionRequest* Request); + + /** Clean up any active sessions, called from cases like returning to the main menu */ + UFUNCTION(BlueprintCallable, Category=Session) + virtual void CleanUpSessions(); + + ////////////////////////////////////////////////////////////////////// + // Events + + /** Native Delegate when a local user has accepted an invite */ + FCommonSessionOnUserRequestedSession OnUserRequestedSessionEvent; + /** Event broadcast when a local user has accepted an invite */ + UPROPERTY(BlueprintAssignable, Category = "Events", meta = (DisplayName = "On User Requested Session")) + FCommonSessionOnUserRequestedSession_Dynamic K2_OnUserRequestedSessionEvent; + + /** Native Delegate when a JoinSession call has completed */ + FCommonSessionOnJoinSessionComplete OnJoinSessionCompleteEvent; + /** Event broadcast when a JoinSession call has completed */ + UPROPERTY(BlueprintAssignable, Category = "Events", meta = (DisplayName = "On Join Session Complete")) + FCommonSessionOnJoinSessionComplete_Dynamic K2_OnJoinSessionCompleteEvent; + + /** Native Delegate when a CreateSession call has completed */ + FCommonSessionOnCreateSessionComplete OnCreateSessionCompleteEvent; + /** Event broadcast when a CreateSession call has completed */ + UPROPERTY(BlueprintAssignable, Category = "Events", meta = (DisplayName = "On Create Session Complete")) + FCommonSessionOnCreateSessionComplete_Dynamic K2_OnCreateSessionCompleteEvent; + + /** Native Delegate for modifying the connect URL prior to a client travel */ + FCommonSessionOnPreClientTravel OnPreClientTravelEvent; + +protected: + // Functions called during the process of creating or joining a session, these can be overidden for game-specific behavior + + /** Called to fill in a session request from quick play host settings, can be overridden for game-specific behavior */ + virtual TSharedRef CreateQuickPlaySearchSettings(UCommonSession_HostSessionRequest* Request, UCommonSession_SearchSessionRequest* QuickPlayRequest); + + /** Called when a quick play search finishes, can be overridden for game-specific behavior */ + virtual void HandleQuickPlaySearchFinished(bool bSucceeded, const FText& ErrorMessage, TWeakObjectPtr JoiningOrHostingPlayer, TStrongObjectPtr HostRequest); + + /** Called when traveling to a session fails */ + virtual void TravelLocalSessionFailure(UWorld* World, ETravelFailure::Type FailureType, const FString& ReasonString); + + /** Called when a new session is either created or fails to be created */ + virtual void OnCreateSessionComplete(FName SessionName, bool bWasSuccessful); + + /** Called to finalize session creation */ + virtual void FinishSessionCreation(bool bWasSuccessful); + + /** Called after traveling to the new hosted session map */ + virtual void HandlePostLoadMap(UWorld* World); + +protected: + // Internal functions for initializing and handling results from the online systems + + void BindOnlineDelegates(); + void CreateOnlineSessionInternal(ULocalPlayer* LocalPlayer, UCommonSession_HostSessionRequest* Request); + void FindSessionsInternal(APlayerController* SearchingPlayer, const TSharedRef& InSearchSettings); + void JoinSessionInternal(ULocalPlayer* LocalPlayer, UCommonSession_SearchResult* Request); + void InternalTravelToSession(const FName SessionName); + void NotifyUserRequestedSession(const FPlatformUserId& PlatformUserId, UCommonSession_SearchResult* RequestedSession, const FOnlineResultInformation& RequestedSessionResult); + void NotifyJoinSessionComplete(const FOnlineResultInformation& Result); + void NotifyCreateSessionComplete(const FOnlineResultInformation& Result); + void SetCreateSessionError(const FText& ErrorText); + +#if COMMONUSER_OSSV1 + void BindOnlineDelegatesOSSv1(); + void CreateOnlineSessionInternalOSSv1(ULocalPlayer* LocalPlayer, UCommonSession_HostSessionRequest* Request); + void FindSessionsInternalOSSv1(ULocalPlayer* LocalPlayer); + void JoinSessionInternalOSSv1(ULocalPlayer* LocalPlayer, UCommonSession_SearchResult* Request); + TSharedRef CreateQuickPlaySearchSettingsOSSv1(UCommonSession_HostSessionRequest* Request, UCommonSession_SearchSessionRequest* QuickPlayRequest); + void CleanUpSessionsOSSv1(); + + void HandleSessionFailure(const FUniqueNetId& NetId, ESessionFailure::Type FailureType); + void HandleSessionUserInviteAccepted(const bool bWasSuccessful, const int32 LocalUserIndex, FUniqueNetIdPtr AcceptingUserId, const FOnlineSessionSearchResult& SearchResult); + void OnStartSessionComplete(FName SessionName, bool bWasSuccessful); + void OnRegisterLocalPlayerComplete_CreateSession(const FUniqueNetId& PlayerId, EOnJoinSessionCompleteResult::Type Result); + void OnUpdateSessionComplete(FName SessionName, bool bWasSuccessful); + void OnEndSessionComplete(FName SessionName, bool bWasSuccessful); + void OnDestroySessionComplete(FName SessionName, bool bWasSuccessful); + void OnFindSessionsComplete(bool bWasSuccessful); + void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result); + void OnRegisterJoiningLocalPlayerComplete(const FUniqueNetId& PlayerId, EOnJoinSessionCompleteResult::Type Result); + void FinishJoinSession(EOnJoinSessionCompleteResult::Type Result); + +#else + void BindOnlineDelegatesOSSv2(); + void CreateOnlineSessionInternalOSSv2(ULocalPlayer* LocalPlayer, UCommonSession_HostSessionRequest* Request); + void FindSessionsInternalOSSv2(ULocalPlayer* LocalPlayer); + void JoinSessionInternalOSSv2(ULocalPlayer* LocalPlayer, UCommonSession_SearchResult* Request); + TSharedRef CreateQuickPlaySearchSettingsOSSv2(UCommonSession_HostSessionRequest* HostRequest, UCommonSession_SearchSessionRequest* SearchRequest); + void CleanUpSessionsOSSv2(); + + /** Process a join request originating from the online service */ + void OnSessionJoinRequested(const UE::Online::FUILobbyJoinRequested& EventParams); + + /** Get the local user id for a given controller */ + UE::Online::FAccountId GetAccountId(APlayerController* PlayerController) const; + /** Get the lobby id for a given session name */ + UE::Online::FLobbyId GetLobbyId(const FName SessionName) const; + /** Event handle for UI lobby join requested */ + UE::Online::FOnlineEventDelegateHandle LobbyJoinRequestedHandle; +#endif // COMMONUSER_OSSV1 + +protected: + /** The travel URL that will be used after session operations are complete */ + FString PendingTravelURL; + + /** Most recent result information for a session creation attempt, stored here to allow storing error codes for later */ + FOnlineResultInformation CreateSessionResult; + + /** True if we want to cancel the session after it is created */ + bool bWantToDestroyPendingSession = false; + + /** True if this is a dedicated server, which doesn't require a LocalPlayer to create a session */ + bool bIsDedicatedServer = false; + + /** Settings for the current search */ + TSharedPtr SearchSettings; + + /** Settings for the current host request */ + TSharedPtr HostSettings; +}; diff --git a/Plugins/CommonUser/Source/CommonUser/Public/CommonUserModule.h b/Plugins/CommonUser/Source/CommonUser/Public/CommonUserModule.h new file mode 100644 index 000000000..da3fb20f3 --- /dev/null +++ b/Plugins/CommonUser/Source/CommonUser/Public/CommonUserModule.h @@ -0,0 +1,14 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "Modules/ModuleInterface.h" + +class FCommonUserModule : public IModuleInterface +{ +public: + + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; +}; diff --git a/Plugins/CommonUser/Source/CommonUser/Public/CommonUserSubsystem.h b/Plugins/CommonUser/Source/CommonUser/Public/CommonUserSubsystem.h new file mode 100644 index 000000000..53e936750 --- /dev/null +++ b/Plugins/CommonUser/Source/CommonUser/Public/CommonUserSubsystem.h @@ -0,0 +1,635 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CommonUserTypes.h" +#include "Engine/GameViewportClient.h" +#include "GameFramework/OnlineReplStructs.h" +#include "Subsystems/GameInstanceSubsystem.h" + +#include "GameplayTagContainer.h" +#include "Interfaces/OnlineIdentityInterface.h" +#include "OnlineError.h" +#include "UObject/WeakObjectPtr.h" +#include "CommonUserSubsystem.generated.h" + +class FNativeGameplayTag; +class IOnlineSubsystem; + +/** List of tags used by the common user subsystem */ +struct COMMONUSER_API FCommonUserTags +{ + // General severity levels and specific system messages + + static FNativeGameplayTag SystemMessage_Error; // SystemMessage.Error + static FNativeGameplayTag SystemMessage_Warning; // SystemMessage.Warning + static FNativeGameplayTag SystemMessage_Display; // SystemMessage.Display + + /** All attempts to initialize a player failed, user has to do something before trying again */ + static FNativeGameplayTag SystemMessage_Error_InitializeLocalPlayerFailed; // SystemMessage.Error.InitializeLocalPlayerFailed + + + // Platform trait tags, it is expected that the game instance or other system calls SetTraitTags with these tags for the appropriate platform + + /** This tag means it is a console platform that directly maps controller IDs to different system users. If false, the same user can have multiple controllers */ + static FNativeGameplayTag Platform_Trait_RequiresStrictControllerMapping; // Platform.Trait.RequiresStrictControllerMapping + + /** This tag means the platform has a single online user and all players use index 0 */ + static FNativeGameplayTag Platform_Trait_SingleOnlineUser; // Platform.Trait.SingleOnlineUser +}; + +/** Logical representation of an individual user, one of these will exist for all initialized local players */ +UCLASS(BlueprintType) +class COMMONUSER_API UCommonUserInfo : public UObject +{ + GENERATED_BODY() + +public: + /** Primary controller input device for this user, they could also have additional secondary devices */ + UPROPERTY(BlueprintReadOnly, Category = UserInfo) + FInputDeviceId PrimaryInputDevice; + + /** Specifies the logical user on the local platform, guest users will point to the primary user */ + UPROPERTY(BlueprintReadOnly, Category = UserInfo) + FPlatformUserId PlatformUser; + + /** If this user is assigned a LocalPlayer, this will match the index in the GameInstance localplayers array once it is fully created */ + UPROPERTY(BlueprintReadOnly, Category = UserInfo) + int32 LocalPlayerIndex = -1; + + /** If true, this user is allowed to be a guest */ + UPROPERTY(BlueprintReadOnly, Category = UserInfo) + bool bCanBeGuest = false; + + /** If true, this is a guest user attached to primary user 0 */ + UPROPERTY(BlueprintReadOnly, Category = UserInfo) + bool bIsGuest = false; + + /** Overall state of the user's initialization process */ + UPROPERTY(BlueprintReadOnly, Category = UserInfo) + ECommonUserInitializationState InitializationState = ECommonUserInitializationState::Invalid; + + /** Returns true if this user has successfully logged in */ + UFUNCTION(BlueprintCallable, Category = UserInfo) + bool IsLoggedIn() const; + + /** Returns true if this user is in the middle of logging in */ + UFUNCTION(BlueprintCallable, Category = UserInfo) + bool IsDoingLogin() const; + + /** Returns the most recently queries result for a specific privilege, will return unknown if never queried */ + UFUNCTION(BlueprintCallable, Category = UserInfo) + ECommonUserPrivilegeResult GetCachedPrivilegeResult(ECommonUserPrivilege Privilege, ECommonUserOnlineContext Context = ECommonUserOnlineContext::Game) const; + + /** Ask about the general availability of a feature, this combines cached results with state */ + UFUNCTION(BlueprintCallable, Category = UserInfo) + ECommonUserAvailability GetPrivilegeAvailability(ECommonUserPrivilege Privilege) const; + + /** Returns the net id for the given context */ + UFUNCTION(BlueprintCallable, Category = UserInfo) + FUniqueNetIdRepl GetNetId(ECommonUserOnlineContext Context = ECommonUserOnlineContext::Game) const; + + /** Returns the user's human readable nickname */ + UFUNCTION(BlueprintCallable, Category = UserInfo) + FString GetNickname() const; + + /** Returns an internal debug string for this player */ + UFUNCTION(BlueprintCallable, Category = UserInfo) + FString GetDebugString() const; + + /** Accessor for platform user id */ + FPlatformUserId GetPlatformUserId() const; + + /** Gets the platform user index for older functions expecting an integer */ + int32 GetPlatformUserIndex() const; + + // Internal data, only intended to be accessed by online subsystems + + /** Cached data for each online system */ + struct FCachedData + { + /** Cached net id per system */ + FUniqueNetIdRepl CachedNetId; + + /** Cached values of various user privileges */ + TMap CachedPrivileges; + }; + + /** Per context cache, game will always exist but others may not */ + TMap CachedDataMap; + + /** Looks up cached data using resolution rules */ + FCachedData* GetCachedData(ECommonUserOnlineContext Context); + const FCachedData* GetCachedData(ECommonUserOnlineContext Context) const; + + /** Updates cached privilege results, will propagate to game if needed */ + void UpdateCachedPrivilegeResult(ECommonUserPrivilege Privilege, ECommonUserPrivilegeResult Result, ECommonUserOnlineContext Context); + + /** Updates cached privilege results, will propagate to game if needed */ + void UpdateCachedNetId(const FUniqueNetIdRepl& NewId, ECommonUserOnlineContext Context); + + /** Return the subsystem this is owned by */ + class UCommonUserSubsystem* GetSubsystem() const; +}; + + +/** Delegates when initialization processes succeed or fail */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_FiveParams(FCommonUserOnInitializeCompleteMulticast, const UCommonUserInfo*, UserInfo, bool, bSuccess, FText, Error, ECommonUserPrivilege, RequestedPrivilege, ECommonUserOnlineContext, OnlineContext); +DECLARE_DYNAMIC_DELEGATE_FiveParams(FCommonUserOnInitializeComplete, const UCommonUserInfo*, UserInfo, bool, bSuccess, FText, Error, ECommonUserPrivilege, RequestedPrivilege, ECommonUserOnlineContext, OnlineContext); + +/** Delegate when a system error message is sent, the game can choose to display it to the user using the type tag */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FCommonUserHandleSystemMessageDelegate, FGameplayTag, MessageType, FText, TitleText, FText, BodyText); + +/** Delegate when a privilege changes, this can be bound to see if online status/etc changes during gameplay */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams(FCommonUserAvailabilityChangedDelegate, const UCommonUserInfo*, UserInfo, ECommonUserPrivilege, Privilege, ECommonUserAvailability, OldAvailability, ECommonUserAvailability, NewAvailability); + + +/** Parameter struct for initialize functions, this would normally be filled in by wrapper functions like async nodes */ +USTRUCT(BlueprintType) +struct COMMONUSER_API FCommonUserInitializeParams +{ + GENERATED_BODY() + + /** What local player index to use, can specify one above current if can create player is enabled */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Default) + int32 LocalPlayerIndex = 0; + + /** Deprecated method of selecting platform user and input device */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Default) + int32 ControllerId = -1; + + /** Primary controller input device for this user, they could also have additional secondary devices */ + UPROPERTY(BlueprintReadOnly, Category = UserInfo) + FInputDeviceId PrimaryInputDevice; + + /** Specifies the logical user on the local platform */ + UPROPERTY(BlueprintReadOnly, Category = UserInfo) + FPlatformUserId PlatformUser; + + /** Generally either CanPlay or CanPlayOnline, specifies what level of privilege is required */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Default) + ECommonUserPrivilege RequestedPrivilege = ECommonUserPrivilege::CanPlay; + + /** What specific online context to log in to, game means to login to all relevant ones */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Default) + ECommonUserOnlineContext OnlineContext = ECommonUserOnlineContext::Game; + + /** True if this is allowed to create a new local player for initial login */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Default) + bool bCanCreateNewLocalPlayer = false; + + /** True if this player can be a guest user without an actual online presence */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Default) + bool bCanUseGuestLogin = false; + + /** True if we should not show login errors, the game will be responsible for displaying them */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Default) + bool bSuppressLoginErrors = false; + + /** If bound, call this dynamic delegate at completion of login */ + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Default) + FCommonUserOnInitializeComplete OnUserInitializeComplete; +}; + +/** + * Game subsystem that handles queries and changes to user identity and login status. + * One subsystem is created for each game instance and can be accessed from blueprints or C++ code. + * If a game-specific subclass exists, this base subsystem will not be created. + */ +UCLASS(BlueprintType, Config=Game) +class COMMONUSER_API UCommonUserSubsystem : public UGameInstanceSubsystem +{ + GENERATED_BODY() + +public: + UCommonUserSubsystem() { } + + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + virtual bool ShouldCreateSubsystem(UObject* Outer) const override; + + + /** BP delegate called when any requested initialization request completes */ + UPROPERTY(BlueprintAssignable, Category = CommonUser) + FCommonUserOnInitializeCompleteMulticast OnUserInitializeComplete; + + /** BP delegate called when the system sends an error/warning message */ + UPROPERTY(BlueprintAssignable, Category = CommonUser) + FCommonUserHandleSystemMessageDelegate OnHandleSystemMessage; + + /** BP delegate called when privilege availability changes for a user */ + UPROPERTY(BlueprintAssignable, Category = CommonUser) + FCommonUserAvailabilityChangedDelegate OnUserPrivilegeChanged; + + /** Send a system message via OnHandleSystemMessage */ + UFUNCTION(BlueprintCallable, Category = CommonUser) + virtual void SendSystemMessage(FGameplayTag MessageType, FText TitleText, FText BodyText); + + /** Sets the maximum number of local players, will not destroy existing ones */ + UFUNCTION(BlueprintCallable, Category = CommonUser) + virtual void SetMaxLocalPlayers(int32 InMaxLocalPLayers); + + /** Gets the maximum number of local players */ + UFUNCTION(BlueprintPure, Category = CommonUser) + int32 GetMaxLocalPlayers() const; + + /** Gets the current number of local players, will always be at least 1 */ + UFUNCTION(BlueprintPure, Category = CommonUser) + int32 GetNumLocalPlayers() const; + + /** Returns the state of initializing the specified local player */ + UFUNCTION(BlueprintPure, Category = CommonUser) + ECommonUserInitializationState GetLocalPlayerInitializationState(int32 LocalPlayerIndex) const; + + /** Returns the user info for a given local player index in game instance, 0 is always valid in a running game */ + UFUNCTION(BlueprintCallable, BlueprintPure = False, Category = CommonUser) + const UCommonUserInfo* GetUserInfoForLocalPlayerIndex(int32 LocalPlayerIndex) const; + + /** Deprecated, use PlatformUserId when available */ + UFUNCTION(BlueprintCallable, BlueprintPure = False, Category = CommonUser) + const UCommonUserInfo* GetUserInfoForPlatformUserIndex(int32 PlatformUserIndex) const; + + /** Returns the primary user info for a given platform user index. Can return null */ + UFUNCTION(BlueprintCallable, BlueprintPure = False, Category = CommonUser) + const UCommonUserInfo* GetUserInfoForPlatformUser(FPlatformUserId PlatformUser) const; + + /** Returns the user info for a unique net id. Can return null */ + UFUNCTION(BlueprintCallable, BlueprintPure = False, Category = CommonUser) + const UCommonUserInfo* GetUserInfoForUniqueNetId(const FUniqueNetIdRepl& NetId) const; + + /** Deprecated, use InputDeviceId when available */ + UFUNCTION(BlueprintCallable, BlueprintPure = False, Category = CommonUser) + const UCommonUserInfo* GetUserInfoForControllerId(int32 ControllerId) const; + + /** Returns the user info for a given input device. Can return null */ + UFUNCTION(BlueprintCallable, BlueprintPure = False, Category = CommonUser) + const UCommonUserInfo* GetUserInfoForInputDevice(FInputDeviceId InputDevice) const; + + /** + * Tries to start the process of creating or updating a local player, including logging in and creating a player controller. + * When the process has succeeded or failed, it will broadcast the OnUserInitializeComplete delegate. + * + * @param LocalPlayerIndex Desired index of LocalPlayer in Game Instance, 0 will be primary player and 1+ for local multiplayer + * @param PrimaryInputDevice The physical controller that should be mapped to this user, will use the default device if invalid + * @param bCanUseGuestLogin If true, this player can be a guest without a real Unique Net Id + * + * @returns true if the process was started, false if it failed before properly starting + */ + UFUNCTION(BlueprintCallable, Category = CommonUser) + virtual bool TryToInitializeForLocalPlay(int32 LocalPlayerIndex, FInputDeviceId PrimaryInputDevice, bool bCanUseGuestLogin); + + /** + * Starts the process of taking a locally logged in user and doing a full online login including account permission checks. + * When the process has succeeded or failed, it will broadcast the OnUserInitializeComplete delegate. + * + * @param LocalPlayerIndex Index of existing LocalPlayer in Game Instance + * + * @returns true if the process was started, false if it failed before properly starting + */ + UFUNCTION(BlueprintCallable, Category = CommonUser) + virtual bool TryToLoginForOnlinePlay(int32 LocalPlayerIndex); + + /** + * Starts a general user login and initialization process, using the params structure to determine what to log in to. + * When the process has succeeded or failed, it will broadcast the OnUserInitializeComplete delegate. + * AsyncAction_CommonUserInitialize provides several wrapper functions for using this in an Event graph. + * + * @returns true if the process was started, false if it failed before properly starting + */ + UFUNCTION(BlueprintCallable, Category = CommonUser) + virtual bool TryToInitializeUser(FCommonUserInitializeParams Params); + + /** + * Starts the process of listening for user input for new and existing controllers and logging them. + * This will insert a key input handler on the active GameViewportClient and is turned off by calling again with empty key arrays. + * + * @param AnyUserKeys Listen for these keys for any user, even the default user. Set this for an initial press start screen or empty to disable + * @param NewUserKeys Listen for these keys for a new user without a player controller. Set this for splitscreen/local multiplayer or empty to disable + * @param Params Params passed to TryToInitializeUser after detecting key input + */ + UFUNCTION(BlueprintCallable, Category = CommonUser) + virtual void ListenForLoginKeyInput(TArray AnyUserKeys, TArray NewUserKeys, FCommonUserInitializeParams Params); + + /** Attempts to cancel an in-progress initialization attempt, this may not work on all platforms but will disable callbacks */ + UFUNCTION(BlueprintCallable, Category = CommonUser) + virtual bool CancelUserInitialization(int32 LocalPlayerIndex); + + /** Logs a player out of any online systems, and optionally destroys the player entirely if it's not the first one */ + UFUNCTION(BlueprintCallable, Category = CommonUser) + virtual bool TryToLogOutUser(int32 LocalPlayerIndex, bool bDestroyPlayer = false); + + /** Resets the login and initialization state when returning to the main menu after an error */ + UFUNCTION(BlueprintCallable, Category = CommonUser) + virtual void ResetUserState(); + + /** Returns true if this this could be a real platform user with a valid identity (even if not currently logged in) */ + virtual bool IsRealPlatformUserIndex(int32 PlatformUserIndex) const; + + /** Returns true if this this could be a real platform user with a valid identity (even if not currently logged in) */ + virtual bool IsRealPlatformUser(FPlatformUserId PlatformUser) const; + + /** Converts index to id */ + virtual FPlatformUserId GetPlatformUserIdForIndex(int32 PlatformUserIndex) const; + + /** Converts id to index */ + virtual int32 GetPlatformUserIndexForId(FPlatformUserId PlatformUser) const; + + /** Gets the user for an input device */ + virtual FPlatformUserId GetPlatformUserIdForInputDevice(FInputDeviceId InputDevice) const; + + /** Gets a user's primary input device id */ + virtual FInputDeviceId GetPrimaryInputDeviceForPlatformUser(FPlatformUserId PlatformUser) const; + + /** Call from game code to set the cached trait tags when platform state or options changes */ + virtual void SetTraitTags(const FGameplayTagContainer& InTags); + + /** Gets the current tags that affect feature avialability */ + const FGameplayTagContainer& GetTraitTags() const { return CachedTraitTags; } + + /** Checks if a specific platform/feature tag is enabled */ + UFUNCTION(BlueprintPure, Category=CommonUser) + bool HasTraitTag(const FGameplayTag TraitTag) const { return CachedTraitTags.HasTag(TraitTag); } + + /** Checks to see if we should display a press start/input confirmation screen at startup. Games can call this or check the trait tags directly */ + UFUNCTION(BlueprintPure, BlueprintPure, Category=CommonUser) + virtual bool ShouldWaitForStartInput() const; + + + // Functions for accessing low-level online system information + +#if COMMONUSER_OSSV1 + /** Returns OSS interface of specific type, will return null if there is no type */ + IOnlineSubsystem* GetOnlineSubsystem(ECommonUserOnlineContext Context = ECommonUserOnlineContext::Game) const; + + /** Returns identity interface of specific type, will return null if there is no type */ + IOnlineIdentity* GetOnlineIdentity(ECommonUserOnlineContext Context = ECommonUserOnlineContext::Game) const; + + /** Returns human readable name of OSS system */ + FName GetOnlineSubsystemName(ECommonUserOnlineContext Context = ECommonUserOnlineContext::Game) const; + + /** Returns the current online connection status */ + EOnlineServerConnectionStatus::Type GetConnectionStatus(ECommonUserOnlineContext Context = ECommonUserOnlineContext::Game) const; +#else + /** Get the services provider type, or None if there isn't one. */ + UE::Online::EOnlineServices GetOnlineServicesProvider(ECommonUserOnlineContext Context = ECommonUserOnlineContext::Game) const; + + /** Returns auth interface of specific type, will return null if there is no type */ + UE::Online::IAuthPtr GetOnlineAuth(ECommonUserOnlineContext Context = ECommonUserOnlineContext::Game) const; + + /** Returns the current online connection status */ + UE::Online::EOnlineServicesConnectionStatus GetConnectionStatus(ECommonUserOnlineContext Context = ECommonUserOnlineContext::Game) const; +#endif + + /** Returns true if we are currently connected to backend servers */ + bool HasOnlineConnection(ECommonUserOnlineContext Context = ECommonUserOnlineContext::Game) const; + + /** Returns the current login status for a player on the specified online system, only works for real platform users */ + ELoginStatusType GetLocalUserLoginStatus(FPlatformUserId PlatformUser, ECommonUserOnlineContext Context = ECommonUserOnlineContext::Game) const; + + /** Returns the unique net id for a local platform user */ + FUniqueNetIdRepl GetLocalUserNetId(FPlatformUserId PlatformUser, ECommonUserOnlineContext Context = ECommonUserOnlineContext::Game) const; + + /** Convert a user id to a debug string */ + FString PlatformUserIdToString(FPlatformUserId UserId); + + /** Convert a context to a debug string */ + FString ECommonUserOnlineContextToString(ECommonUserOnlineContext Context); + + /** Returns human readable string for privilege checks */ + virtual FText GetPrivilegeDescription(ECommonUserPrivilege Privilege) const; + virtual FText GetPrivilegeResultDescription(ECommonUserPrivilegeResult Result) const; + + /** + * Starts the process of login for an existing local user, will return false if callback was not scheduled + * This activates the low level state machine and does not modify the initialization state on user info + */ + DECLARE_DELEGATE_FiveParams(FOnLocalUserLoginCompleteDelegate, const UCommonUserInfo* /*UserInfo*/, ELoginStatusType /*NewStatus*/, FUniqueNetIdRepl /*NetId*/, const TOptional& /*Error*/, ECommonUserOnlineContext /*Type*/); + virtual bool LoginLocalUser(const UCommonUserInfo* UserInfo, ECommonUserPrivilege RequestedPrivilege, ECommonUserOnlineContext Context, FOnLocalUserLoginCompleteDelegate OnComplete); + + /** Assign a local player to a specific local user and call callbacks as needed */ + virtual void SetLocalPlayerUserInfo(ULocalPlayer* LocalPlayer, const UCommonUserInfo* UserInfo); + + /** Resolves a context that has default behavior into a specific context */ + ECommonUserOnlineContext ResolveOnlineContext(ECommonUserOnlineContext Context) const; + + /** True if there is a separate platform and service interface */ + bool HasSeparatePlatformContext() const; + +protected: + /** Internal structure that caches status and pointers for each online context */ + struct FOnlineContextCache + { +#if COMMONUSER_OSSV1 + /** Pointer to base subsystem, will stay valid as long as game instance does */ + IOnlineSubsystem* OnlineSubsystem = nullptr; + + /** Cached identity system, this will always be valid */ + IOnlineIdentityPtr IdentityInterface; + + /** Last connection status that was passed into the HandleNetworkConnectionStatusChanged hander */ + EOnlineServerConnectionStatus::Type CurrentConnectionStatus = EOnlineServerConnectionStatus::Normal; +#else + /** Online services, accessor to specific services */ + UE::Online::IOnlineServicesPtr OnlineServices; + /** Cached auth service */ + UE::Online::IAuthPtr AuthService; + /** Login status changed event handle */ + UE::Online::FOnlineEventDelegateHandle LoginStatusChangedHandle; + /** Connection status changed event handle */ + UE::Online::FOnlineEventDelegateHandle ConnectionStatusChangedHandle; + /** Last connection status that was passed into the HandleNetworkConnectionStatusChanged hander */ + UE::Online::EOnlineServicesConnectionStatus CurrentConnectionStatus = UE::Online::EOnlineServicesConnectionStatus::NotConnected; +#endif + + /** Resets state, important to clear all shared ptrs */ + void Reset() + { +#if COMMONUSER_OSSV1 + OnlineSubsystem = nullptr; + IdentityInterface.Reset(); + CurrentConnectionStatus = EOnlineServerConnectionStatus::Normal; +#else + OnlineServices.Reset(); + AuthService.Reset(); + CurrentConnectionStatus = UE::Online::EOnlineServicesConnectionStatus::NotConnected; +#endif + } + }; + + /** Internal structure to represent an in-progress login request */ + struct FUserLoginRequest : public TSharedFromThis + { + FUserLoginRequest(UCommonUserInfo* InUserInfo, ECommonUserPrivilege InPrivilege, ECommonUserOnlineContext InContext, FOnLocalUserLoginCompleteDelegate&& InDelegate) + : UserInfo(TWeakObjectPtr(InUserInfo)) + , DesiredPrivilege(InPrivilege) + , DesiredContext(InContext) + , Delegate(MoveTemp(InDelegate)) + {} + + /** Which local user is trying to log on */ + TWeakObjectPtr UserInfo; + + /** Overall state of login request, could come from many sources */ + ECommonUserAsyncTaskState OverallLoginState = ECommonUserAsyncTaskState::NotStarted; + + /** State of attempt to use platform auth. When started, this immediately transitions to Failed for OSSv1, as we do not support platform auth there. */ + ECommonUserAsyncTaskState TransferPlatformAuthState = ECommonUserAsyncTaskState::NotStarted; + + /** State of attempt to use AutoLogin */ + ECommonUserAsyncTaskState AutoLoginState = ECommonUserAsyncTaskState::NotStarted; + + /** State of attempt to use external login UI */ + ECommonUserAsyncTaskState LoginUIState = ECommonUserAsyncTaskState::NotStarted; + + /** Final privilege to that is requested */ + ECommonUserPrivilege DesiredPrivilege = ECommonUserPrivilege::Invalid_Count; + + /** State of attempt to request the relevant privilege */ + ECommonUserAsyncTaskState PrivilegeCheckState = ECommonUserAsyncTaskState::NotStarted; + + /** The final context to log into */ + ECommonUserOnlineContext DesiredContext = ECommonUserOnlineContext::Invalid; + + /** What online system we are currently logging into */ + ECommonUserOnlineContext CurrentContext = ECommonUserOnlineContext::Invalid; + + /** User callback for completion */ + FOnLocalUserLoginCompleteDelegate Delegate; + + /** Most recent/relevant error to display to user */ + TOptional Error; + }; + + + /** Create a new user info object */ + virtual UCommonUserInfo* CreateLocalUserInfo(int32 LocalPlayerIndex); + + /** Deconst wrapper for const getters */ + FORCEINLINE UCommonUserInfo* ModifyInfo(const UCommonUserInfo* Info) { return const_cast(Info); } + + /** Refresh user info from OSS */ + virtual void RefreshLocalUserInfo(UCommonUserInfo* UserInfo); + + /** Possibly send privilege availability notification, compares current value to cached old value */ + virtual void HandleChangedAvailability(UCommonUserInfo* UserInfo, ECommonUserPrivilege Privilege, ECommonUserAvailability OldAvailability); + + /** Updates the cached privilege on a user and notifies delegate */ + virtual void UpdateUserPrivilegeResult(UCommonUserInfo* UserInfo, ECommonUserPrivilege Privilege, ECommonUserPrivilegeResult Result, ECommonUserOnlineContext Context); + + /** Gets internal data for a type of online system, can return null for service */ + const FOnlineContextCache* GetContextCache(ECommonUserOnlineContext Context = ECommonUserOnlineContext::Game) const; + FOnlineContextCache* GetContextCache(ECommonUserOnlineContext Context = ECommonUserOnlineContext::Game); + + /** Create and set up system objects before delegates are bound */ + virtual void CreateOnlineContexts(); + virtual void DestroyOnlineContexts(); + + /** Bind online delegates */ + virtual void BindOnlineDelegates(); + + /** Forcibly logs out and deinitializes a single user */ + virtual void LogOutLocalUser(FPlatformUserId PlatformUser); + + /** Performs the next step of a login request, which could include completing it. Returns true if it's done */ + virtual void ProcessLoginRequest(TSharedRef Request); + + /** Call login on OSS, with platform auth from the platform OSS. Return true if AutoLogin started */ + virtual bool TransferPlatformAuth(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser); + + /** Call AutoLogin on OSS. Return true if AutoLogin started. */ + virtual bool AutoLogin(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser); + + /** Call ShowLoginUI on OSS. Return true if ShowLoginUI started. */ + virtual bool ShowLoginUI(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser); + + /** Call QueryUserPrivilege on OSS. Return true if QueryUserPrivilege started. */ + virtual bool QueryUserPrivilege(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser); + + /** OSS-specific functions */ +#if COMMONUSER_OSSV1 + virtual ECommonUserPrivilege ConvertOSSPrivilege(EUserPrivileges::Type Privilege) const; + virtual EUserPrivileges::Type ConvertOSSPrivilege(ECommonUserPrivilege Privilege) const; + virtual ECommonUserPrivilegeResult ConvertOSSPrivilegeResult(EUserPrivileges::Type Privilege, uint32 Results) const; + + void BindOnlineDelegatesOSSv1(); + bool AutoLoginOSSv1(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser); + bool ShowLoginUIOSSv1(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser); + bool QueryUserPrivilegeOSSv1(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser); +#else + virtual ECommonUserPrivilege ConvertOnlineServicesPrivilege(UE::Online::EUserPrivileges Privilege) const; + virtual UE::Online::EUserPrivileges ConvertOnlineServicesPrivilege(ECommonUserPrivilege Privilege) const; + virtual ECommonUserPrivilegeResult ConvertOnlineServicesPrivilegeResult(UE::Online::EUserPrivileges Privilege, UE::Online::EPrivilegeResults Results) const; + + void BindOnlineDelegatesOSSv2(); + void CacheConnectionStatus(ECommonUserOnlineContext Context); + bool TransferPlatformAuthOSSv2(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser); + bool AutoLoginOSSv2(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser); + bool ShowLoginUIOSSv2(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser); + bool QueryUserPrivilegeOSSv2(FOnlineContextCache* System, TSharedRef Request, FPlatformUserId PlatformUser); + TSharedPtr GetOnlineServiceAccountInfo(UE::Online::IAuthPtr AuthService, FPlatformUserId InUserId) const; +#endif + + /** Callbacks for OSS functions */ +#if COMMONUSER_OSSV1 + virtual void HandleIdentityLoginStatusChanged(int32 PlatformUserIndex, ELoginStatus::Type OldStatus, ELoginStatus::Type NewStatus, const FUniqueNetId& NewId, ECommonUserOnlineContext Context); + virtual void HandleUserLoginCompleted(int32 PlatformUserIndex, bool bWasSuccessful, const FUniqueNetId& NetId, const FString& Error, ECommonUserOnlineContext Context); + virtual void HandleControllerPairingChanged(int32 PlatformUserIndex, FControllerPairingChangedUserInfo PreviousUser, FControllerPairingChangedUserInfo NewUser); + virtual void HandleNetworkConnectionStatusChanged(const FString& ServiceName, EOnlineServerConnectionStatus::Type LastConnectionStatus, EOnlineServerConnectionStatus::Type ConnectionStatus, ECommonUserOnlineContext Context); + virtual void HandleOnLoginUIClosed(TSharedPtr LoggedInNetId, const int PlatformUserIndex, const FOnlineError& Error, ECommonUserOnlineContext Context); + virtual void HandleCheckPrivilegesComplete(const FUniqueNetId& UserId, EUserPrivileges::Type Privilege, uint32 PrivilegeResults, ECommonUserPrivilege RequestedPrivilege, TWeakObjectPtr CommonUserInfo, ECommonUserOnlineContext Context); +#else + virtual void HandleAuthLoginStatusChanged(const UE::Online::FAuthLoginStatusChanged& EventParameters, ECommonUserOnlineContext Context); + virtual void HandleUserLoginCompletedV2(const UE::Online::TOnlineResult& Result, FPlatformUserId PlatformUser, ECommonUserOnlineContext Context); + virtual void HandleOnLoginUIClosedV2(const UE::Online::TOnlineResult& Result, FPlatformUserId PlatformUser, ECommonUserOnlineContext Context); + virtual void HandleNetworkConnectionStatusChanged(const UE::Online::FConnectionStatusChanged& EventParameters, ECommonUserOnlineContext Context); + virtual void HandleCheckPrivilegesComplete(const UE::Online::TOnlineResult& Result, TWeakObjectPtr CommonUserInfo, UE::Online::EUserPrivileges DesiredPrivilege, ECommonUserOnlineContext Context); +#endif + + /** + * Callback for when an input device (i.e. a gamepad) has been connected or disconnected. + */ + virtual void HandleInputDeviceConnectionChanged(EInputDeviceConnectionState NewConnectionState, FPlatformUserId PlatformUserId, FInputDeviceId InputDeviceId); + + virtual void HandleLoginForUserInitialize(const UCommonUserInfo* UserInfo, ELoginStatusType NewStatus, FUniqueNetIdRepl NetId, const TOptional& Error, ECommonUserOnlineContext Context, FCommonUserInitializeParams Params); + virtual void HandleUserInitializeFailed(FCommonUserInitializeParams Params, FText Error); + virtual void HandleUserInitializeSucceeded(FCommonUserInitializeParams Params); + + /** Callback for handling press start/login logic */ + virtual bool OverrideInputKeyForLogin(FInputKeyEventArgs& EventArgs); + + + /** Previous override handler, will restore on cancel */ + FOverrideInputKeyHandler WrappedInputKeyHandler; + + /** List of keys to listen for from any user */ + TArray LoginKeysForAnyUser; + + /** List of keys to listen for a new unmapped user */ + TArray LoginKeysForNewUser; + + /** Params to use for a key-triggered login */ + FCommonUserInitializeParams ParamsForLoginKey; + + /** Maximum number of local players */ + int32 MaxNumberOfLocalPlayers = 0; + + /** True if this is a dedicated server, which doesn't require a LocalPlayer */ + bool bIsDedicatedServer = false; + + /** List of current in progress login requests */ + TArray> ActiveLoginRequests; + + /** Information about each local user, from local player index to user */ + UPROPERTY() + TMap> LocalUserInfos; + + /** Cached platform/mode trait tags */ + FGameplayTagContainer CachedTraitTags; + + /** Do not access this outside of initialization */ + FOnlineContextCache* DefaultContextInternal = nullptr; + FOnlineContextCache* ServiceContextInternal = nullptr; + FOnlineContextCache* PlatformContextInternal = nullptr; + + friend UCommonUserInfo; +}; diff --git a/Plugins/CommonUser/Source/CommonUser/Public/CommonUserTypes.h b/Plugins/CommonUser/Source/CommonUser/Public/CommonUserTypes.h new file mode 100644 index 000000000..8120a64b2 --- /dev/null +++ b/Plugins/CommonUser/Source/CommonUser/Public/CommonUserTypes.h @@ -0,0 +1,218 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + + +#if COMMONUSER_OSSV1 + +// Online Subsystem (OSS v1) includes and forward declares +#include "OnlineSubsystemTypes.h" +class IOnlineSubsystem; +struct FOnlineError; +using FOnlineErrorType = FOnlineError; +using ELoginStatusType = ELoginStatus::Type; + +#else + +// Online Services (OSS v2) includes and forward declares +#include "Online/Connectivity.h" +#include "Online/OnlineError.h" +namespace UE::Online +{ + enum class ELoginStatus : uint8; + enum class EPrivilegeResults : uint32; + enum class EUserPrivileges : uint8; + using IAuthPtr = TSharedPtr; + using IOnlineServicesPtr = TSharedPtr; + template + class TOnlineResult; + struct FAuthLogin; + struct FConnectionStatusChanged; + struct FExternalUIShowLoginUI; + struct FAuthLoginStatusChanged; + struct FQueryUserPrivilege; + struct FAccountInfo; +} +using FOnlineErrorType = UE::Online::FOnlineError; +using ELoginStatusType = UE::Online::ELoginStatus; + +#endif + +#include "CommonUserTypes.generated.h" + + +/** Enum specifying where and how to run online queries */ +UENUM(BlueprintType) +enum class ECommonUserOnlineContext : uint8 +{ + /** Called from game code, this uses the default system but with special handling that could merge results from multiple contexts */ + Game, + + /** The default engine online system, this will always exist and will be the same as either Service or Platform */ + Default, + + /** Explicitly ask for the external service, which may not exist */ + Service, + + /** Looks for external service first, then falls back to default */ + ServiceOrDefault, + + /** Explicitly ask for the platform system, which may not exist */ + Platform, + + /** Looks for platform system first, then falls back to default */ + PlatformOrDefault, + + /** Invalid system */ + Invalid +}; + +/** Enum describing the state of initialization for a specific user */ +UENUM(BlueprintType) +enum class ECommonUserInitializationState : uint8 +{ + /** User has not started login process */ + Unknown, + + /** Player is in the process of acquiring a user id with local login */ + DoingInitialLogin, + + /** Player is performing the network login, they have already logged in locally */ + DoingNetworkLogin, + + /** Player failed to log in at all */ + FailedtoLogin, + + + /** Player is logged in and has access to online functionality */ + LoggedInOnline, + + /** Player is logged in locally (either guest or real user), but cannot perform online actions */ + LoggedInLocalOnly, + + + /** Invalid state or user */ + Invalid, +}; + +/** Enum specifying different privileges and capabilities available to a user */ +UENUM(BlueprintType) +enum class ECommonUserPrivilege : uint8 +{ + /** Whether the user can play at all, online or offline */ + CanPlay, + + /** Whether the user can play in online modes */ + CanPlayOnline, + + /** Whether the user can use text chat */ + CanCommunicateViaTextOnline, + + /** Whether the user can use voice chat */ + CanCommunicateViaVoiceOnline, + + /** Whether the user can access content generated by other users */ + CanUseUserGeneratedContent, + + /** Whether the user can ever participate in cross-play */ + CanUseCrossPlay, + + /** Invalid privilege (also the count of valid ones) */ + Invalid_Count UMETA(Hidden) +}; + +/** Enum specifying the general availability of a feature or privilege, this combines information from multiple sources */ +UENUM(BlueprintType) +enum class ECommonUserAvailability : uint8 +{ + /** State is completely unknown and needs to be queried */ + Unknown, + + /** This feature is fully available for use right now */ + NowAvailable, + + /** This might be available after the completion of normal login procedures */ + PossiblyAvailable, + + /** This feature is not available now because of something like network connectivity but may be available in the future */ + CurrentlyUnavailable, + + /** This feature will never be available for the rest of this session due to hard account or platform restrictions */ + AlwaysUnavailable, + + /** Invalid feature */ + Invalid, +}; + +/** Enum giving specific reasons why a user may or may not use a certain privilege */ +UENUM(BlueprintType) +enum class ECommonUserPrivilegeResult : uint8 +{ + /** State is unknown and needs to be queried */ + Unknown, + + /** This privilege is fully available for use */ + Available, + + /** User has not fully logged in */ + UserNotLoggedIn, + + /** User does not own the game or content */ + LicenseInvalid, + + /** The game needs to be updated or patched before this will be available */ + VersionOutdated, + + /** No network connection, this may be resolved by reconnecting */ + NetworkConnectionUnavailable, + + /** Parental control failure */ + AgeRestricted, + + /** Account does not have a required subscription or account type */ + AccountTypeRestricted, + + /** Another account/user restriction such as being banned by the service */ + AccountUseRestricted, + + /** Other platform-specific failure */ + PlatformFailure, +}; + +/** Used to track the progress of different asynchronous operations */ +enum class ECommonUserAsyncTaskState : uint8 +{ + /** The task has not been started */ + NotStarted, + /** The task is currently being processed */ + InProgress, + /** The task has completed successfully */ + Done, + /** The task failed to complete */ + Failed +}; + +/** Detailed information about the online error. Effectively a wrapper for FOnlineError. */ +USTRUCT(BlueprintType) +struct FOnlineResultInformation +{ + GENERATED_BODY() + + /** Whether the operation was successful or not. If it was successful, the error fields of this struct will not contain extra information. */ + UPROPERTY(BlueprintReadOnly) + bool bWasSuccessful = true; + + /** The unique error id. Can be used to compare against specific handled errors. */ + UPROPERTY(BlueprintReadOnly) + FString ErrorId; + + /** Error text to display to the user. */ + UPROPERTY(BlueprintReadOnly) + FText ErrorText; + + /** + * Initialize this from an FOnlineErrorType + * @param InOnlineError the online error to initialize from + */ + void COMMONUSER_API FromOnlineError(const FOnlineErrorType& InOnlineError); +}; diff --git a/Plugins/ModularGameplayActors/ModularGameplayActors.uplugin b/Plugins/ModularGameplayActors/ModularGameplayActors.uplugin new file mode 100644 index 000000000..b5828344a --- /dev/null +++ b/Plugins/ModularGameplayActors/ModularGameplayActors.uplugin @@ -0,0 +1,30 @@ +{ + "FileVersion": 3, + "Version": 1, + "VersionName": "1.0", + "FriendlyName": "Modular Gameplay Actors", + "Description": "Base classes designed to be used with the Modular Gameplay plugin.", + "Category": "Gameplay", + "CreatedBy": "Epic Games, Inc.", + "CreatedByURL": "http://epicgames.com", + "DocsURL": "", + "MarketplaceURL": "", + "SupportURL": "", + "EnabledByDefault": true, + "CanContainContent": false, + "IsBetaVersion": false, + "Installed": false, + "Modules": [ + { + "Name": "ModularGameplayActors", + "Type": "Runtime", + "LoadingPhase": "Default" + } + ], + "Plugins": [ + { + "Name": "ModularGameplay", + "Enabled": true + } + ] +} diff --git a/Plugins/ModularGameplayActors/Source/ModularGameplayActors/ModularGameplayActors.Build.cs b/Plugins/ModularGameplayActors/Source/ModularGameplayActors/ModularGameplayActors.Build.cs new file mode 100644 index 000000000..7966f7c73 --- /dev/null +++ b/Plugins/ModularGameplayActors/Source/ModularGameplayActors/ModularGameplayActors.Build.cs @@ -0,0 +1,54 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; +using System.IO; // for Path + +public class ModularGameplayActors : ModuleRules +{ + public ModularGameplayActors(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicIncludePaths.AddRange( + new string[] { + // ... add public include paths required here ... + } + ); + + + PrivateIncludePaths.AddRange( + new string[] { + // ... add other private include paths required here ... + } + ); + + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "CoreUObject", + "Engine", + "ModularGameplay", + "AIModule", + // ... add other public dependencies that you statically link with here ... + } + ); + + + PrivateDependencyModuleNames.AddRange( + new string[] + { + // ... add private dependencies that you statically link with here ... + } + ); + + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + // ... add any modules that your module loads dynamically here ... + } + ); + } +} diff --git a/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Private/ModularAIController.cpp b/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Private/ModularAIController.cpp new file mode 100644 index 000000000..7b2175109 --- /dev/null +++ b/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Private/ModularAIController.cpp @@ -0,0 +1,27 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ModularAIController.h" +#include "Components/GameFrameworkComponentManager.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(ModularAIController) + +void AModularAIController::PreInitializeComponents() +{ + Super::PreInitializeComponents(); + + UGameFrameworkComponentManager::AddGameFrameworkComponentReceiver(this); +} + +void AModularAIController::BeginPlay() +{ + UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(this, UGameFrameworkComponentManager::NAME_GameActorReady); + + Super::BeginPlay(); +} + +void AModularAIController::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + UGameFrameworkComponentManager::RemoveGameFrameworkComponentReceiver(this); + + Super::EndPlay(EndPlayReason); +} diff --git a/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Private/ModularCharacter.cpp b/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Private/ModularCharacter.cpp new file mode 100644 index 000000000..ae5aa4f83 --- /dev/null +++ b/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Private/ModularCharacter.cpp @@ -0,0 +1,28 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ModularCharacter.h" +#include "Components/GameFrameworkComponentManager.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(ModularCharacter) + +void AModularCharacter::PreInitializeComponents() +{ + Super::PreInitializeComponents(); + + UGameFrameworkComponentManager::AddGameFrameworkComponentReceiver(this); +} + +void AModularCharacter::BeginPlay() +{ + UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(this, UGameFrameworkComponentManager::NAME_GameActorReady); + + Super::BeginPlay(); +} + +void AModularCharacter::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + UGameFrameworkComponentManager::RemoveGameFrameworkComponentReceiver(this); + + Super::EndPlay(EndPlayReason); +} + diff --git a/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Private/ModularGameMode.cpp b/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Private/ModularGameMode.cpp new file mode 100644 index 000000000..b812c865a --- /dev/null +++ b/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Private/ModularGameMode.cpp @@ -0,0 +1,29 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ModularGameMode.h" + +#include "ModularGameState.h" +#include "ModularPawn.h" +#include "ModularPlayerController.h" +#include "ModularPlayerState.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(ModularGameMode) + +AModularGameModeBase::AModularGameModeBase(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + GameStateClass = AModularGameStateBase::StaticClass(); + PlayerControllerClass = AModularPlayerController::StaticClass(); + PlayerStateClass = AModularPlayerState::StaticClass(); + DefaultPawnClass = AModularPawn::StaticClass(); +} + +AModularGameMode::AModularGameMode(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + GameStateClass = AModularGameState::StaticClass(); + PlayerControllerClass = AModularPlayerController::StaticClass(); + PlayerStateClass = AModularPlayerState::StaticClass(); + DefaultPawnClass = AModularPawn::StaticClass(); +} + diff --git a/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Private/ModularGameState.cpp b/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Private/ModularGameState.cpp new file mode 100644 index 000000000..5f34e1a2f --- /dev/null +++ b/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Private/ModularGameState.cpp @@ -0,0 +1,64 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ModularGameState.h" + +#include "Components/GameFrameworkComponentManager.h" +#include "Components/GameStateComponent.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(ModularGameState) + +void AModularGameStateBase::PreInitializeComponents() +{ + Super::PreInitializeComponents(); + + UGameFrameworkComponentManager::AddGameFrameworkComponentReceiver(this); +} + +void AModularGameStateBase::BeginPlay() +{ + UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(this, UGameFrameworkComponentManager::NAME_GameActorReady); + + Super::BeginPlay(); +} + +void AModularGameStateBase::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + UGameFrameworkComponentManager::RemoveGameFrameworkComponentReceiver(this); + + Super::EndPlay(EndPlayReason); +} + + +void AModularGameState::PreInitializeComponents() +{ + Super::PreInitializeComponents(); + + UGameFrameworkComponentManager::AddGameFrameworkComponentReceiver(this); +} + +void AModularGameState::BeginPlay() +{ + UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(this, UGameFrameworkComponentManager::NAME_GameActorReady); + + Super::BeginPlay(); +} + +void AModularGameState::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + UGameFrameworkComponentManager::RemoveGameFrameworkComponentReceiver(this); + + Super::EndPlay(EndPlayReason); +} + +void AModularGameState::HandleMatchHasStarted() +{ + Super::HandleMatchHasStarted(); + + TArray ModularComponents; + GetComponents(ModularComponents); + for (UGameStateComponent* Component : ModularComponents) + { + Component->HandleMatchHasStarted(); + } +} + diff --git a/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Private/ModularGameplayActorsModule.cpp b/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Private/ModularGameplayActorsModule.cpp new file mode 100644 index 000000000..afa04208a --- /dev/null +++ b/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Private/ModularGameplayActorsModule.cpp @@ -0,0 +1,5 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "Modules/ModuleManager.h" + +IMPLEMENT_MODULE(FDefaultModuleImpl, ModularGameplayActors); diff --git a/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Private/ModularPawn.cpp b/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Private/ModularPawn.cpp new file mode 100644 index 000000000..1a84f1b62 --- /dev/null +++ b/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Private/ModularPawn.cpp @@ -0,0 +1,27 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ModularPawn.h" +#include "Components/GameFrameworkComponentManager.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(ModularPawn) + +void AModularPawn::PreInitializeComponents() +{ + Super::PreInitializeComponents(); + + UGameFrameworkComponentManager::AddGameFrameworkComponentReceiver(this); +} + +void AModularPawn::BeginPlay() +{ + UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(this, UGameFrameworkComponentManager::NAME_GameActorReady); + + Super::BeginPlay(); +} + +void AModularPawn::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + UGameFrameworkComponentManager::RemoveGameFrameworkComponentReceiver(this); + + Super::EndPlay(EndPlayReason); +} diff --git a/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Private/ModularPlayerController.cpp b/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Private/ModularPlayerController.cpp new file mode 100644 index 000000000..8e03b0a4a --- /dev/null +++ b/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Private/ModularPlayerController.cpp @@ -0,0 +1,49 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ModularPlayerController.h" + +#include "Components/ControllerComponent.h" +#include "Components/GameFrameworkComponentManager.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(ModularPlayerController) + +void AModularPlayerController::PreInitializeComponents() +{ + Super::PreInitializeComponents(); + + UGameFrameworkComponentManager::AddGameFrameworkComponentReceiver(this); +} + +void AModularPlayerController::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + UGameFrameworkComponentManager::RemoveGameFrameworkComponentReceiver(this); + + Super::EndPlay(EndPlayReason); +} + +void AModularPlayerController::ReceivedPlayer() +{ + // Player controllers always get assigned a player and can't do much until then + UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(this, UGameFrameworkComponentManager::NAME_GameActorReady); + + Super::ReceivedPlayer(); + + TArray ModularComponents; + GetComponents(ModularComponents); + for (UControllerComponent* Component : ModularComponents) + { + Component->ReceivedPlayer(); + } +} + +void AModularPlayerController::PlayerTick(float DeltaTime) +{ + Super::PlayerTick(DeltaTime); + + TArray ModularComponents; + GetComponents(ModularComponents); + for (UControllerComponent* Component : ModularComponents) + { + Component->PlayerTick(DeltaTime); + } +} diff --git a/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Private/ModularPlayerState.cpp b/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Private/ModularPlayerState.cpp new file mode 100644 index 000000000..e7a0c6d94 --- /dev/null +++ b/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Private/ModularPlayerState.cpp @@ -0,0 +1,56 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "ModularPlayerState.h" + +#include "Components/GameFrameworkComponentManager.h" +#include "Components/PlayerStateComponent.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(ModularPlayerState) + +void AModularPlayerState::PreInitializeComponents() +{ + Super::PreInitializeComponents(); + + UGameFrameworkComponentManager::AddGameFrameworkComponentReceiver(this); +} + +void AModularPlayerState::BeginPlay() +{ + UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(this, UGameFrameworkComponentManager::NAME_GameActorReady); + + Super::BeginPlay(); +} + +void AModularPlayerState::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + UGameFrameworkComponentManager::RemoveGameFrameworkComponentReceiver(this); + + Super::EndPlay(EndPlayReason); +} + +void AModularPlayerState::Reset() +{ + Super::Reset(); + + TArray ModularComponents; + GetComponents(ModularComponents); + for (UPlayerStateComponent* Component : ModularComponents) + { + Component->Reset(); + } +} + +void AModularPlayerState::CopyProperties(APlayerState* PlayerState) +{ + Super::CopyProperties(PlayerState); + + TInlineComponentArray PlayerStateComponents; + GetComponents(PlayerStateComponents); + for (UPlayerStateComponent* SourcePSComp : PlayerStateComponents) + { + if (UPlayerStateComponent* TargetComp = Cast(static_cast(FindObjectWithOuter(PlayerState, SourcePSComp->GetClass(), SourcePSComp->GetFName())))) + { + SourcePSComp->CopyProperties(TargetComp); + } + } +} diff --git a/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Public/ModularAIController.h b/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Public/ModularAIController.h new file mode 100644 index 000000000..e3c1ed120 --- /dev/null +++ b/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Public/ModularAIController.h @@ -0,0 +1,23 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "AIController.h" + +#include "ModularAIController.generated.h" + +class UObject; + +/** Minimal class that supports extension by game feature plugins */ +UCLASS(Blueprintable) +class MODULARGAMEPLAYACTORS_API AModularAIController : public AAIController +{ + GENERATED_BODY() + +public: + //~ Begin AActor Interface + virtual void PreInitializeComponents() override; + virtual void BeginPlay() override; + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + //~ End AActor Interface +}; diff --git a/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Public/ModularCharacter.h b/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Public/ModularCharacter.h new file mode 100644 index 000000000..d19b80919 --- /dev/null +++ b/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Public/ModularCharacter.h @@ -0,0 +1,23 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "GameFramework/Character.h" + +#include "ModularCharacter.generated.h" + +class UObject; + +/** Minimal class that supports extension by game feature plugins */ +UCLASS(Blueprintable) +class MODULARGAMEPLAYACTORS_API AModularCharacter : public ACharacter +{ + GENERATED_BODY() + +public: + //~ Begin AActor Interface + virtual void PreInitializeComponents() override; + virtual void BeginPlay() override; + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + //~ End AActor Interface +}; diff --git a/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Public/ModularGameMode.h b/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Public/ModularGameMode.h new file mode 100644 index 000000000..280d01c80 --- /dev/null +++ b/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Public/ModularGameMode.h @@ -0,0 +1,29 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "GameFramework/GameMode.h" + +#include "ModularGameMode.generated.h" + +class UObject; + +/** Pair this with a ModularGameStateBase */ +UCLASS(Blueprintable) +class MODULARGAMEPLAYACTORS_API AModularGameModeBase : public AGameModeBase +{ + GENERATED_BODY() + +public: + AModularGameModeBase(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); +}; + +/** Pair this with a ModularGameState */ +UCLASS(Blueprintable) +class MODULARGAMEPLAYACTORS_API AModularGameMode : public AGameMode +{ + GENERATED_BODY() + +public: + AModularGameMode(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); +}; diff --git a/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Public/ModularGameState.h b/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Public/ModularGameState.h new file mode 100644 index 000000000..939801a4e --- /dev/null +++ b/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Public/ModularGameState.h @@ -0,0 +1,43 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "GameFramework/GameState.h" + +#include "ModularGameState.generated.h" + +class UObject; + +/** Pair this with a ModularGameModeBase */ +UCLASS(Blueprintable) +class MODULARGAMEPLAYACTORS_API AModularGameStateBase : public AGameStateBase +{ + GENERATED_BODY() + +public: + //~ Begin AActor interface + virtual void PreInitializeComponents() override; + virtual void BeginPlay() override; + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + //~ End AActor interface +}; + + +/** Pair this with a ModularGameState */ +UCLASS(Blueprintable) +class MODULARGAMEPLAYACTORS_API AModularGameState : public AGameState +{ + GENERATED_BODY() + +public: + //~ Begin AActor interface + virtual void PreInitializeComponents() override; + virtual void BeginPlay() override; + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + //~ End AActor interface + +protected: + //~ Begin AGameState interface + virtual void HandleMatchHasStarted() override; + //~ Begin AGameState interface +}; diff --git a/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Public/ModularPawn.h b/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Public/ModularPawn.h new file mode 100644 index 000000000..64161ca0d --- /dev/null +++ b/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Public/ModularPawn.h @@ -0,0 +1,24 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "GameFramework/Pawn.h" + +#include "ModularPawn.generated.h" + +class UObject; + +/** Minimal class that supports extension by game feature plugins */ +UCLASS(Blueprintable) +class MODULARGAMEPLAYACTORS_API AModularPawn : public APawn +{ + GENERATED_BODY() + +public: + //~ Begin AActor interface + virtual void PreInitializeComponents() override; + virtual void BeginPlay() override; + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + //~ End AActor interface + +}; diff --git a/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Public/ModularPlayerController.h b/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Public/ModularPlayerController.h new file mode 100644 index 000000000..c7bb498cc --- /dev/null +++ b/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Public/ModularPlayerController.h @@ -0,0 +1,27 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "GameFramework/PlayerController.h" + +#include "ModularPlayerController.generated.h" + +class UObject; + +/** Minimal class that supports extension by game feature plugins */ +UCLASS(Blueprintable) +class MODULARGAMEPLAYACTORS_API AModularPlayerController : public APlayerController +{ + GENERATED_BODY() + +public: + //~ Begin AActor interface + virtual void PreInitializeComponents() override; + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + //~ End AActor interface + + //~ Begin APlayerController interface + virtual void ReceivedPlayer() override; + virtual void PlayerTick(float DeltaTime) override; + //~ End APlayerController interface +}; diff --git a/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Public/ModularPlayerState.h b/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Public/ModularPlayerState.h new file mode 100644 index 000000000..58e0e1380 --- /dev/null +++ b/Plugins/ModularGameplayActors/Source/ModularGameplayActors/Public/ModularPlayerState.h @@ -0,0 +1,31 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "GameFramework/PlayerState.h" + +#include "ModularPlayerState.generated.h" + +namespace EEndPlayReason { enum Type : int; } + +class UObject; + +/** Minimal class that supports extension by game feature plugins */ +UCLASS(Blueprintable) +class MODULARGAMEPLAYACTORS_API AModularPlayerState : public APlayerState +{ + GENERATED_BODY() + +public: + //~ Begin AActor interface + virtual void PreInitializeComponents() override; + virtual void BeginPlay() override; + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + virtual void Reset() override; + //~ End AActor interface + +protected: + //~ Begin APlayerState interface + virtual void CopyProperties(APlayerState* PlayerState); + //~ End APlayerState interface +}; diff --git a/Source/TG_ARPG/Private/TGCommonActivatableWidget.cpp b/Source/TG_ARPG/Private/TGCommonActivatableWidget.cpp new file mode 100644 index 000000000..839396de8 --- /dev/null +++ b/Source/TG_ARPG/Private/TGCommonActivatableWidget.cpp @@ -0,0 +1,52 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "TGCommonActivatableWidget.h" +#include "Editor/WidgetCompilerLog.h" +#include UE_INLINE_GENERATED_CPP_BY_NAME(TGCommonActivatableWidget) + +#define LOCTEXT_NAMESPACE "Lyra" + +UTGCommonActivatableWidget::UTGCommonActivatableWidget(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} +TOptional UTGCommonActivatableWidget::GetDesiredInputConfig() const +{ + switch (InputConfig) + { + case ELyraWidgetInputMode::GameAndMenu: + return FUIInputConfig(ECommonInputMode::All, GameMouseCaptureMode); + case ELyraWidgetInputMode::Game: + return FUIInputConfig(ECommonInputMode::Game, GameMouseCaptureMode); + case ELyraWidgetInputMode::Menu: + return FUIInputConfig(ECommonInputMode::Menu, EMouseCaptureMode::NoCapture); + case ELyraWidgetInputMode::Default: + default: + return TOptional(); + } +} + +#if WITH_EDITOR + +void UTGCommonActivatableWidget::ValidateCompiledWidgetTree(const UWidgetTree& BlueprintWidgetTree, class IWidgetCompilerLog& CompileLog) const +{ + Super::ValidateCompiledWidgetTree(BlueprintWidgetTree, CompileLog); + + if (!GetClass()->IsFunctionImplementedInScript(GET_FUNCTION_NAME_CHECKED(UTGCommonActivatableWidget, BP_GetDesiredFocusTarget))) + { + if (GetParentNativeClass(GetClass()) == UTGCommonActivatableWidget::StaticClass()) + { + CompileLog.Warning(LOCTEXT("ValidateGetDesiredFocusTarget_Warning", "GetDesiredFocusTarget wasn't implemented, you're going to have trouble using gamepads on this screen.")); + } + else + { + //TODO - Note for now, because we can't guarantee it isn't implemented in a native subclass of this one. + CompileLog.Note(LOCTEXT("ValidateGetDesiredFocusTarget_Note", "GetDesiredFocusTarget wasn't implemented, you're going to have trouble using gamepads on this screen. If it was implemented in the native base class you can ignore this message.")); + } + } +} + +#endif + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/TG_ARPG/Private/TGHUDLayout.cpp b/Source/TG_ARPG/Private/TGHUDLayout.cpp new file mode 100644 index 000000000..40edfed09 --- /dev/null +++ b/Source/TG_ARPG/Private/TGHUDLayout.cpp @@ -0,0 +1,36 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "TGHUDLayout.h" + +#include "Input/CommonUIInputTypes.h" +#include "CommonUIExtensions.h" +#include "NativeGameplayTags.h" +#include "TGCommonActivatableWidget.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(TGHUDLayout) + +UE_DEFINE_GAMEPLAY_TAG_STATIC(TAG_UI_LAYER_MENU, "UI.Layer.Menu"); +UE_DEFINE_GAMEPLAY_TAG_STATIC(TAG_UI_ACTION_ESCAPE, "UI.Action.Escape"); + + +UTGHUDLayout::UTGHUDLayout(const FObjectInitializer& ObjectInitializer) +: Super(ObjectInitializer) +{ +} + +void UTGHUDLayout::NativeOnInitialized() +{ + Super::NativeOnInitialized(); + RegisterUIActionBinding(FBindUIActionArgs(FUIActionTag::ConvertChecked(TAG_UI_ACTION_ESCAPE), false, FSimpleDelegate::CreateUObject(this, &ThisClass::HandleEscapeAction))); + + +} + +void UTGHUDLayout::HandleEscapeAction() +{ + if (ensure(!EscapeMenuClass.IsNull())) + { + UCommonUIExtensions::PushStreamedContentToLayer_ForPlayer(GetOwningLocalPlayer(), TAG_UI_LAYER_MENU, EscapeMenuClass); + } +} diff --git a/Source/TG_ARPG/Public/TGCommonActivatableWidget.h b/Source/TG_ARPG/Public/TGCommonActivatableWidget.h new file mode 100644 index 000000000..99af79d79 --- /dev/null +++ b/Source/TG_ARPG/Public/TGCommonActivatableWidget.h @@ -0,0 +1,50 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "CommonActivatableWidget.h" +#include "TGCommonActivatableWidget.generated.h" + +/** + * + */ +struct FUIInputConfig; + +UENUM(BlueprintType) +enum class ELyraWidgetInputMode : uint8 +{ + Default, + GameAndMenu, + Game, + Menu +}; + + +UCLASS(Abstract, Blueprintable) +class TG_ARPG_API UTGCommonActivatableWidget : public UCommonActivatableWidget +{ + GENERATED_BODY() +public: + UTGCommonActivatableWidget(const FObjectInitializer& ObjectInitializer); + +public: + + //~UCommonActivatableWidget interface + virtual TOptional GetDesiredInputConfig() const override; + //~End of UCommonActivatableWidget interface + + #if WITH_EDITOR + virtual void ValidateCompiledWidgetTree(const UWidgetTree& BlueprintWidgetTree, class IWidgetCompilerLog& CompileLog) const override; +#endif + +protected: + /** The desired input mode to use while this UI is activated, for example do you want key presses to still reach the game/player controller? */ + UPROPERTY(EditDefaultsOnly, Category = Input) + ELyraWidgetInputMode InputConfig = ELyraWidgetInputMode::Default; + + /** The desired mouse behavior when the game gets input. */ + UPROPERTY(EditDefaultsOnly, Category = Input) + EMouseCaptureMode GameMouseCaptureMode = EMouseCaptureMode::CapturePermanently; +}; + diff --git a/Source/TG_ARPG/Public/TGHUDLayout.h b/Source/TG_ARPG/Public/TGHUDLayout.h new file mode 100644 index 000000000..76a066dfa --- /dev/null +++ b/Source/TG_ARPG/Public/TGHUDLayout.h @@ -0,0 +1,29 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "TGCommonActivatableWidget.h" +#include "TGHUDLayout.generated.h" + + +class UCommonActivatableWidget; +class UObject; +/** + * + */ +UCLASS(Abstract, BlueprintType, Blueprintable, Meta = (DisplayName = "TenGen HUD Layout", Category = "TenGen|HUD")) +class TG_ARPG_API UTGHUDLayout : public UTGCommonActivatableWidget +{ + GENERATED_BODY() +public: + UTGHUDLayout(const FObjectInitializer& ObjectInitializer); + + void NativeOnInitialized() override; + +protected: + void HandleEscapeAction(); + + UPROPERTY(EditDefaultsOnly) + TSoftClassPtr EscapeMenuClass; +}; diff --git a/Source/TG_ARPG/TG_ARPG.Build.cs b/Source/TG_ARPG/TG_ARPG.Build.cs index 2ca7337ac..b8d23201a 100644 --- a/Source/TG_ARPG/TG_ARPG.Build.cs +++ b/Source/TG_ARPG/TG_ARPG.Build.cs @@ -8,7 +8,7 @@ public class TG_ARPG : ModuleRules { PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; - PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay", "EnhancedInput","GameplayAbilities", "GameplayTags", "GameplayTasks" }); + PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay", "EnhancedInput","GameplayAbilities", "GameplayTags", "GameplayTasks", "CommonUI" ,"UMG","CommonGame"}); PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" }); } diff --git a/TG_ARPG.uproject b/TG_ARPG.uproject index 7d9bb268d..a63f51ecf 100644 --- a/TG_ARPG.uproject +++ b/TG_ARPG.uproject @@ -10,7 +10,9 @@ "LoadingPhase": "Default", "AdditionalDependencies": [ "Engine", - "GameplayAbilities" + "GameplayAbilities", + "CommonUI", + "UMG" ] } ],