diff --git a/Directory.Packages.props b/Directory.Packages.props index 973f82f0b..803e5974c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -38,6 +38,7 @@ + diff --git a/samples/ChatApp/ChatApp.Unity/Assets/Scenes/ChatScene.unity b/samples/ChatApp/ChatApp.Unity/Assets/Scenes/ChatScene.unity index 46292a812..72b1bc3c0 100644 --- a/samples/ChatApp/ChatApp.Unity/Assets/Scenes/ChatScene.unity +++ b/samples/ChatApp/ChatApp.Unity/Assets/Scenes/ChatScene.unity @@ -43,7 +43,7 @@ RenderSettings: --- !u!157 &3 LightmapSettings: m_ObjectHideFlags: 0 - serializedVersion: 11 + serializedVersion: 12 m_GIWorkflowMode: 0 m_GISettings: serializedVersion: 2 @@ -54,7 +54,7 @@ LightmapSettings: m_EnableBakedLightmaps: 1 m_EnableRealtimeLightmaps: 1 m_LightmapEditorSettings: - serializedVersion: 10 + serializedVersion: 12 m_Resolution: 2 m_BakeResolution: 40 m_AtlasSize: 1024 @@ -62,6 +62,7 @@ LightmapSettings: m_AOMaxDistance: 1 m_CompAOExponent: 1 m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 m_Padding: 2 m_LightmapParameters: {fileID: 0} m_LightmapsBakeMode: 1 @@ -76,10 +77,16 @@ LightmapSettings: m_PVRDirectSampleCount: 32 m_PVRSampleCount: 500 m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 500 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 2 + m_PVRDenoiserTypeDirect: 0 + m_PVRDenoiserTypeIndirect: 0 + m_PVRDenoiserTypeAO: 0 m_PVRFilterTypeDirect: 0 m_PVRFilterTypeIndirect: 0 m_PVRFilterTypeAO: 0 - m_PVRFilteringMode: 2 + m_PVREnvironmentMIS: 0 m_PVRCulling: 1 m_PVRFilteringGaussRadiusDirect: 1 m_PVRFilteringGaussRadiusIndirect: 5 @@ -87,9 +94,11 @@ LightmapSettings: m_PVRFilteringAtrousPositionSigmaDirect: 0.5 m_PVRFilteringAtrousPositionSigmaIndirect: 2 m_PVRFilteringAtrousPositionSigmaAO: 1 - m_ShowResolutionOverlay: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 m_LightingDataAsset: {fileID: 0} - m_UseShadowmask: 1 + m_LightingSettings: {fileID: 988913869} --- !u!196 &4 NavMeshSettings: serializedVersion: 2 @@ -109,6 +118,8 @@ NavMeshSettings: manualTileSize: 0 tileSize: 256 accuratePlacement: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 debug: m_Flags: 0 m_NavMeshData: {fileID: 0} @@ -140,6 +151,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1634534318} m_RootOrder: 0 @@ -158,17 +170,17 @@ MonoBehaviour: m_GameObject: {fileID: 164639239} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 0.38679248, g: 0.38679248, b: 0.38679248, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, - Version=1.0.0.0, Culture=neutral, PublicKeyToken=null m_Sprite: {fileID: 0} m_Type: 1 m_PreserveAspect: 0 @@ -178,6 +190,7 @@ MonoBehaviour: m_FillClockwise: 1 m_FillOrigin: 0 m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 --- !u!222 &164639242 CanvasRenderer: m_ObjectHideFlags: 0 @@ -215,6 +228,7 @@ RectTransform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 525475477} m_Father: {fileID: 1309622324} @@ -234,11 +248,12 @@ MonoBehaviour: m_GameObject: {fileID: 190778106} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 1392445389, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} m_Name: m_EditorClassIdentifier: m_Navigation: m_Mode: 3 + m_WrapAround: 0 m_SelectOnUp: {fileID: 0} m_SelectOnDown: {fileID: 0} m_SelectOnLeft: {fileID: 0} @@ -248,17 +263,20 @@ MonoBehaviour: m_NormalColor: {r: 1, g: 1, b: 1, a: 1} m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} m_DisabledColor: {r: 0.11764706, g: 0.11764706, b: 0.11764706, a: 0.5019608} m_ColorMultiplier: 1 m_FadeDuration: 0.1 m_SpriteState: m_HighlightedSprite: {fileID: 0} m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} m_DisabledSprite: {fileID: 0} m_AnimationTriggers: m_NormalTrigger: Normal m_HighlightedTrigger: Highlighted m_PressedTrigger: Pressed + m_SelectedTrigger: Highlighted m_DisabledTrigger: Disabled m_Interactable: 1 m_TargetGraphic: {fileID: 190778109} @@ -266,6 +284,7 @@ MonoBehaviour: m_PersistentCalls: m_Calls: - m_Target: {fileID: 1634534319} + m_TargetAssemblyTypeName: m_MethodName: SendReport m_Mode: 1 m_Arguments: @@ -276,8 +295,6 @@ MonoBehaviour: m_StringArgument: m_BoolArgument: 0 m_CallState: 2 - m_TypeName: UnityEngine.UI.Button+ButtonClickedEvent, UnityEngine.UI, Version=1.0.0.0, - Culture=neutral, PublicKeyToken=null --- !u!114 &190778109 MonoBehaviour: m_ObjectHideFlags: 0 @@ -287,17 +304,17 @@ MonoBehaviour: m_GameObject: {fileID: 190778106} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 0.5943396, g: 0.5747152, b: 0.5747152, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, - Version=1.0.0.0, Culture=neutral, PublicKeyToken=null m_Sprite: {fileID: 0} m_Type: 1 m_PreserveAspect: 0 @@ -307,6 +324,7 @@ MonoBehaviour: m_FillClockwise: 1 m_FillOrigin: 0 m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 --- !u!222 &190778110 CanvasRenderer: m_ObjectHideFlags: 0 @@ -353,9 +371,10 @@ Camera: m_ClearFlags: 1 m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 m_SensorSize: {x: 36, y: 24} m_LensShift: {x: 0, y: 0} - m_GateFitMode: 2 m_FocalLength: 50 m_NormalizedViewPortRect: serializedVersion: 2 @@ -393,6 +412,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 1, z: -10} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 2 @@ -425,6 +445,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1634534318} m_RootOrder: 5 @@ -443,17 +464,17 @@ MonoBehaviour: m_GameObject: {fileID: 247055287} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 0, g: 0, b: 0, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, - Version=1.0.0.0, Culture=neutral, PublicKeyToken=null m_Sprite: {fileID: 0} m_Type: 0 m_PreserveAspect: 0 @@ -463,6 +484,7 @@ MonoBehaviour: m_FillClockwise: 1 m_FillOrigin: 0 m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 --- !u!222 &247055290 CanvasRenderer: m_ObjectHideFlags: 0 @@ -500,6 +522,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1337382646} - {fileID: 1329709669} @@ -520,11 +543,12 @@ MonoBehaviour: m_GameObject: {fileID: 330057072} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 575553740, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: d199490a83bb2b844b9695cbf13b01ef, type: 3} m_Name: m_EditorClassIdentifier: m_Navigation: m_Mode: 3 + m_WrapAround: 0 m_SelectOnUp: {fileID: 0} m_SelectOnDown: {fileID: 0} m_SelectOnLeft: {fileID: 0} @@ -534,17 +558,20 @@ MonoBehaviour: m_NormalColor: {r: 1, g: 1, b: 1, a: 1} m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} m_ColorMultiplier: 1 m_FadeDuration: 0.1 m_SpriteState: m_HighlightedSprite: {fileID: 0} m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} m_DisabledSprite: {fileID: 0} m_AnimationTriggers: m_NormalTrigger: Normal m_HighlightedTrigger: Highlighted m_PressedTrigger: Pressed + m_SelectedTrigger: Highlighted m_DisabledTrigger: Disabled m_Interactable: 1 m_TargetGraphic: {fileID: 330057075} @@ -558,16 +585,15 @@ MonoBehaviour: m_HideMobileInput: 0 m_CharacterValidation: 0 m_CharacterLimit: 0 - m_OnEndEdit: + m_OnSubmit: + m_PersistentCalls: + m_Calls: [] + m_OnDidEndEdit: m_PersistentCalls: m_Calls: [] - m_TypeName: UnityEngine.UI.InputField+SubmitEvent, UnityEngine.UI, Version=1.0.0.0, - Culture=neutral, PublicKeyToken=null m_OnValueChanged: m_PersistentCalls: m_Calls: [] - m_TypeName: UnityEngine.UI.InputField+OnChangeEvent, UnityEngine.UI, Version=1.0.0.0, - Culture=neutral, PublicKeyToken=null m_CaretColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} m_CustomCaretColor: 0 m_SelectionColor: {r: 0.65882355, g: 0.80784315, b: 1, a: 0.7529412} @@ -575,6 +601,7 @@ MonoBehaviour: m_CaretBlinkRate: 0.85 m_CaretWidth: 1 m_ReadOnly: 0 + m_ShouldActivateOnSelect: 1 --- !u!114 &330057075 MonoBehaviour: m_ObjectHideFlags: 0 @@ -584,17 +611,17 @@ MonoBehaviour: m_GameObject: {fileID: 330057072} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, - Version=1.0.0.0, Culture=neutral, PublicKeyToken=null m_Sprite: {fileID: 10911, guid: 0000000000000000f000000000000000, type: 0} m_Type: 1 m_PreserveAspect: 0 @@ -604,6 +631,7 @@ MonoBehaviour: m_FillClockwise: 1 m_FillOrigin: 0 m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 --- !u!222 &330057076 CanvasRenderer: m_ObjectHideFlags: 0 @@ -641,6 +669,7 @@ RectTransform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 610550729} m_Father: {fileID: 632675412} @@ -660,11 +689,12 @@ MonoBehaviour: m_GameObject: {fileID: 390611515} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 1392445389, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} m_Name: m_EditorClassIdentifier: m_Navigation: m_Mode: 3 + m_WrapAround: 0 m_SelectOnUp: {fileID: 0} m_SelectOnDown: {fileID: 0} m_SelectOnLeft: {fileID: 0} @@ -674,17 +704,20 @@ MonoBehaviour: m_NormalColor: {r: 1, g: 1, b: 1, a: 1} m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} m_DisabledColor: {r: 0.11764706, g: 0.11764706, b: 0.11764706, a: 0.5019608} m_ColorMultiplier: 1 m_FadeDuration: 0.1 m_SpriteState: m_HighlightedSprite: {fileID: 0} m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} m_DisabledSprite: {fileID: 0} m_AnimationTriggers: m_NormalTrigger: Normal m_HighlightedTrigger: Highlighted m_PressedTrigger: Pressed + m_SelectedTrigger: Highlighted m_DisabledTrigger: Disabled m_Interactable: 1 m_TargetGraphic: {fileID: 390611518} @@ -692,6 +725,7 @@ MonoBehaviour: m_PersistentCalls: m_Calls: - m_Target: {fileID: 1634534319} + m_TargetAssemblyTypeName: m_MethodName: ReconnectInitializedServer m_Mode: 1 m_Arguments: @@ -702,8 +736,6 @@ MonoBehaviour: m_StringArgument: m_BoolArgument: 0 m_CallState: 2 - m_TypeName: UnityEngine.UI.Button+ButtonClickedEvent, UnityEngine.UI, Version=1.0.0.0, - Culture=neutral, PublicKeyToken=null --- !u!114 &390611518 MonoBehaviour: m_ObjectHideFlags: 0 @@ -713,17 +745,17 @@ MonoBehaviour: m_GameObject: {fileID: 390611515} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 0.69753474, g: 0.70003784, b: 0.7075472, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, - Version=1.0.0.0, Culture=neutral, PublicKeyToken=null m_Sprite: {fileID: 0} m_Type: 1 m_PreserveAspect: 0 @@ -733,6 +765,7 @@ MonoBehaviour: m_FillClockwise: 1 m_FillOrigin: 0 m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 --- !u!222 &390611519 CanvasRenderer: m_ObjectHideFlags: 0 @@ -769,6 +802,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 190778107} m_RootOrder: 0 @@ -787,17 +821,17 @@ MonoBehaviour: m_GameObject: {fileID: 525475476} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 0, g: 0, b: 0, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, - Version=1.0.0.0, Culture=neutral, PublicKeyToken=null m_FontData: m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} m_FontSize: 14 @@ -848,6 +882,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1487425764} - {fileID: 1383832575} @@ -868,7 +903,7 @@ MonoBehaviour: m_GameObject: {fileID: 557907330} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: -405508275, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3} m_Name: m_EditorClassIdentifier: m_Padding: @@ -882,6 +917,9 @@ MonoBehaviour: m_ChildForceExpandHeight: 1 m_ChildControlWidth: 1 m_ChildControlHeight: 1 + m_ChildScaleWidth: 0 + m_ChildScaleHeight: 0 + m_ReverseArrangement: 0 --- !u!222 &557907334 CanvasRenderer: m_ObjectHideFlags: 0 @@ -918,6 +956,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 10} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1096149280} m_RootOrder: 0 @@ -936,17 +975,17 @@ MonoBehaviour: m_GameObject: {fileID: 574419691} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 0, g: 0, b: 0, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, - Version=1.0.0.0, Culture=neutral, PublicKeyToken=null m_FontData: m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} m_FontSize: 14 @@ -997,6 +1036,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1671399962} m_RootOrder: 0 @@ -1015,17 +1055,17 @@ MonoBehaviour: m_GameObject: {fileID: 609138041} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 0.5} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, - Version=1.0.0.0, Culture=neutral, PublicKeyToken=null m_FontData: m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} m_FontSize: 30 @@ -1076,6 +1116,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 10} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 390611516} m_RootOrder: 0 @@ -1094,17 +1135,17 @@ MonoBehaviour: m_GameObject: {fileID: 610550728} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 0, g: 0, b: 0, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, - Version=1.0.0.0, Culture=neutral, PublicKeyToken=null m_FontData: m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} m_FontSize: 14 @@ -1155,6 +1196,7 @@ RectTransform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 390611516} - {fileID: 1096149280} @@ -1185,7 +1227,7 @@ MonoBehaviour: m_GameObject: {fileID: 632675411} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: -405508275, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3} m_Name: m_EditorClassIdentifier: m_Padding: @@ -1199,6 +1241,9 @@ MonoBehaviour: m_ChildForceExpandHeight: 1 m_ChildControlWidth: 1 m_ChildControlHeight: 1 + m_ChildScaleWidth: 0 + m_ChildScaleHeight: 0 + m_ReverseArrangement: 0 --- !u!1 &771375552 GameObject: m_ObjectHideFlags: 0 @@ -1227,6 +1272,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 10} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1487425764} m_RootOrder: 0 @@ -1245,17 +1291,17 @@ MonoBehaviour: m_GameObject: {fileID: 771375552} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 0, g: 0, b: 0, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, - Version=1.0.0.0, Culture=neutral, PublicKeyToken=null m_FontData: m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} m_FontSize: 14 @@ -1306,6 +1352,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 963732563} m_RootOrder: 0 @@ -1324,17 +1371,17 @@ MonoBehaviour: m_GameObject: {fileID: 817501237} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, - Version=1.0.0.0, Culture=neutral, PublicKeyToken=null m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} m_Type: 1 m_PreserveAspect: 0 @@ -1344,6 +1391,7 @@ MonoBehaviour: m_FillClockwise: 1 m_FillOrigin: 0 m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 --- !u!222 &817501240 CanvasRenderer: m_ObjectHideFlags: 0 @@ -1381,6 +1429,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1978894556} - {fileID: 1799772182} @@ -1401,17 +1450,17 @@ MonoBehaviour: m_GameObject: {fileID: 927062024} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, - Version=1.0.0.0, Culture=neutral, PublicKeyToken=null m_Sprite: {fileID: 0} m_Type: 0 m_PreserveAspect: 0 @@ -1421,6 +1470,7 @@ MonoBehaviour: m_FillClockwise: 1 m_FillOrigin: 0 m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 --- !u!222 &927062027 CanvasRenderer: m_ObjectHideFlags: 0 @@ -1438,7 +1488,7 @@ MonoBehaviour: m_GameObject: {fileID: 927062024} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 1367256648, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 1aa08ab6e0800fa44ae55d278d1423e3, type: 3} m_Name: m_EditorClassIdentifier: m_Content: {fileID: 1522127878} @@ -1459,8 +1509,86 @@ MonoBehaviour: m_OnValueChanged: m_PersistentCalls: m_Calls: [] - m_TypeName: UnityEngine.UI.ScrollRect+ScrollRectEvent, UnityEngine.UI, Version=1.0.0.0, - Culture=neutral, PublicKeyToken=null +--- !u!1 &934640857 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 934640860} + - component: {fileID: 934640859} + - component: {fileID: 934640858} + m_Layer: 5 + m_Name: LabelRtt + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &934640858 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 934640857} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.8, g: 0.8, b: 0.8, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 'RTT: -ms' +--- !u!222 &934640859 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 934640857} + m_CullTransparentMesh: 1 +--- !u!224 &934640860 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 934640857} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1634534318} + m_RootOrder: 7 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 1, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: -10, y: -10} + m_SizeDelta: {x: 79.29999, y: 30} + m_Pivot: {x: 1, y: 1} --- !u!1 &963732562 GameObject: m_ObjectHideFlags: 0 @@ -1487,6 +1615,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 817501238} m_Father: {fileID: 1799772182} @@ -1497,6 +1626,68 @@ RectTransform: m_AnchoredPosition: {x: 0, y: 0} m_SizeDelta: {x: -20, y: -20} m_Pivot: {x: 0.5, y: 0.5} +--- !u!850595691 &988913869 +LightingSettings: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Settings.lighting + serializedVersion: 4 + m_GIWorkflowMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 1 + m_RealtimeEnvironmentLighting: 1 + m_BounceScale: 1 + m_AlbedoBoost: 1 + m_IndirectOutputScale: 1 + m_UsingShadowmask: 1 + m_BakeBackend: 1 + m_LightmapMaxSize: 1024 + m_BakeResolution: 40 + m_Padding: 2 + m_LightmapCompression: 3 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAO: 0 + m_MixedBakeMode: 2 + m_LightmapsBakeMode: 1 + m_FilterMode: 1 + m_LightmapParameters: {fileID: 15204, guid: 0000000000000000f000000000000000, type: 0} + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_RealtimeResolution: 2 + m_ForceWhiteAlbedo: 0 + m_ForceUpdates: 0 + m_FinalGather: 0 + m_FinalGatherRayCount: 256 + m_FinalGatherFiltering: 1 + m_PVRCulling: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 500 + m_PVREnvironmentSampleCount: 500 + m_PVREnvironmentReferencePointCount: 2048 + m_LightProbeSampleCountMultiplier: 4 + m_PVRBounces: 2 + m_PVRMinBounces: 2 + m_PVREnvironmentMIS: 0 + m_PVRFilteringMode: 2 + m_PVRDenoiserTypeDirect: 0 + m_PVRDenoiserTypeIndirect: 0 + m_PVRDenoiserTypeAO: 0 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_PVRTiledBaking: 0 --- !u!1 &1040922706 GameObject: m_ObjectHideFlags: 0 @@ -1525,6 +1716,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1868729446} m_RootOrder: 0 @@ -1543,17 +1735,17 @@ MonoBehaviour: m_GameObject: {fileID: 1040922706} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 0, g: 0, b: 0, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, - Version=1.0.0.0, Culture=neutral, PublicKeyToken=null m_FontData: m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} m_FontSize: 14 @@ -1605,6 +1797,7 @@ RectTransform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 574419692} m_Father: {fileID: 632675412} @@ -1624,11 +1817,12 @@ MonoBehaviour: m_GameObject: {fileID: 1096149279} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 1392445389, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} m_Name: m_EditorClassIdentifier: m_Navigation: m_Mode: 3 + m_WrapAround: 0 m_SelectOnUp: {fileID: 0} m_SelectOnDown: {fileID: 0} m_SelectOnLeft: {fileID: 0} @@ -1638,17 +1832,20 @@ MonoBehaviour: m_NormalColor: {r: 1, g: 1, b: 1, a: 1} m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} m_DisabledColor: {r: 0.11764706, g: 0.11764706, b: 0.11764706, a: 0.5019608} m_ColorMultiplier: 1 m_FadeDuration: 0.1 m_SpriteState: m_HighlightedSprite: {fileID: 0} m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} m_DisabledSprite: {fileID: 0} m_AnimationTriggers: m_NormalTrigger: Normal m_HighlightedTrigger: Highlighted m_PressedTrigger: Pressed + m_SelectedTrigger: Highlighted m_DisabledTrigger: Disabled m_Interactable: 1 m_TargetGraphic: {fileID: 1096149282} @@ -1656,6 +1853,7 @@ MonoBehaviour: m_PersistentCalls: m_Calls: - m_Target: {fileID: 1634534319} + m_TargetAssemblyTypeName: m_MethodName: DisconnectServer m_Mode: 1 m_Arguments: @@ -1666,8 +1864,6 @@ MonoBehaviour: m_StringArgument: m_BoolArgument: 0 m_CallState: 2 - m_TypeName: UnityEngine.UI.Button+ButtonClickedEvent, UnityEngine.UI, Version=1.0.0.0, - Culture=neutral, PublicKeyToken=null --- !u!114 &1096149282 MonoBehaviour: m_ObjectHideFlags: 0 @@ -1677,17 +1873,17 @@ MonoBehaviour: m_GameObject: {fileID: 1096149279} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 0.69753474, g: 0.70003784, b: 0.7075472, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, - Version=1.0.0.0, Culture=neutral, PublicKeyToken=null m_Sprite: {fileID: 0} m_Type: 1 m_PreserveAspect: 0 @@ -1697,6 +1893,7 @@ MonoBehaviour: m_FillClockwise: 1 m_FillOrigin: 0 m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 --- !u!222 &1096149283 CanvasRenderer: m_ObjectHideFlags: 0 @@ -1733,6 +1930,7 @@ RectTransform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1671399962} - {fileID: 190778107} @@ -1761,7 +1959,7 @@ MonoBehaviour: m_GameObject: {fileID: 1309622323} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: -405508275, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3} m_Name: m_EditorClassIdentifier: m_Padding: @@ -1775,6 +1973,9 @@ MonoBehaviour: m_ChildForceExpandHeight: 1 m_ChildControlWidth: 1 m_ChildControlHeight: 1 + m_ChildScaleWidth: 0 + m_ChildScaleHeight: 0 + m_ReverseArrangement: 0 --- !u!1 &1329709668 GameObject: m_ObjectHideFlags: 0 @@ -1803,6 +2004,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 330057073} m_RootOrder: 1 @@ -1821,17 +2023,17 @@ MonoBehaviour: m_GameObject: {fileID: 1329709668} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, - Version=1.0.0.0, Culture=neutral, PublicKeyToken=null m_FontData: m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} m_FontSize: 30 @@ -1882,6 +2084,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 330057073} m_RootOrder: 0 @@ -1900,17 +2103,17 @@ MonoBehaviour: m_GameObject: {fileID: 1337382645} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 0.5} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, - Version=1.0.0.0, Culture=neutral, PublicKeyToken=null m_FontData: m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} m_FontSize: 30 @@ -1962,6 +2165,7 @@ RectTransform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1420340486} m_Father: {fileID: 557907331} @@ -1981,11 +2185,12 @@ MonoBehaviour: m_GameObject: {fileID: 1383832574} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 1392445389, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} m_Name: m_EditorClassIdentifier: m_Navigation: m_Mode: 3 + m_WrapAround: 0 m_SelectOnUp: {fileID: 0} m_SelectOnDown: {fileID: 0} m_SelectOnLeft: {fileID: 0} @@ -1995,17 +2200,20 @@ MonoBehaviour: m_NormalColor: {r: 1, g: 1, b: 1, a: 1} m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} m_DisabledColor: {r: 0.11764706, g: 0.11764706, b: 0.11764706, a: 0.5019608} m_ColorMultiplier: 1 m_FadeDuration: 0.1 m_SpriteState: m_HighlightedSprite: {fileID: 0} m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} m_DisabledSprite: {fileID: 0} m_AnimationTriggers: m_NormalTrigger: Normal m_HighlightedTrigger: Highlighted m_PressedTrigger: Pressed + m_SelectedTrigger: Highlighted m_DisabledTrigger: Disabled m_Interactable: 1 m_TargetGraphic: {fileID: 1383832577} @@ -2013,6 +2221,7 @@ MonoBehaviour: m_PersistentCalls: m_Calls: - m_Target: {fileID: 1634534319} + m_TargetAssemblyTypeName: m_MethodName: SendMessage m_Mode: 1 m_Arguments: @@ -2023,8 +2232,6 @@ MonoBehaviour: m_StringArgument: m_BoolArgument: 0 m_CallState: 2 - m_TypeName: UnityEngine.UI.Button+ButtonClickedEvent, UnityEngine.UI, Version=1.0.0.0, - Culture=neutral, PublicKeyToken=null --- !u!114 &1383832577 MonoBehaviour: m_ObjectHideFlags: 0 @@ -2034,17 +2241,17 @@ MonoBehaviour: m_GameObject: {fileID: 1383832574} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 0.33521715, g: 0.764151, b: 0.5044038, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, - Version=1.0.0.0, Culture=neutral, PublicKeyToken=null m_Sprite: {fileID: 0} m_Type: 1 m_PreserveAspect: 0 @@ -2054,6 +2261,7 @@ MonoBehaviour: m_FillClockwise: 1 m_FillOrigin: 0 m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 --- !u!222 &1383832578 CanvasRenderer: m_ObjectHideFlags: 0 @@ -2089,9 +2297,10 @@ MonoBehaviour: m_GameObject: {fileID: 1409526054} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 1077351063, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3} m_Name: m_EditorClassIdentifier: + m_SendPointerHoverToParent: 1 m_HorizontalAxis: Horizontal m_VerticalAxis: Vertical m_SubmitButton: Submit @@ -2108,7 +2317,7 @@ MonoBehaviour: m_GameObject: {fileID: 1409526054} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: -619905303, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3} m_Name: m_EditorClassIdentifier: m_FirstSelected: {fileID: 0} @@ -2124,6 +2333,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 1 @@ -2156,6 +2366,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1383832575} m_RootOrder: 0 @@ -2174,17 +2385,17 @@ MonoBehaviour: m_GameObject: {fileID: 1420340485} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 0, g: 0, b: 0, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, - Version=1.0.0.0, Culture=neutral, PublicKeyToken=null m_FontData: m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} m_FontSize: 14 @@ -2235,6 +2446,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 2120537442} m_RootOrder: 0 @@ -2253,17 +2465,17 @@ MonoBehaviour: m_GameObject: {fileID: 1434588224} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 0, g: 0, b: 0, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, - Version=1.0.0.0, Culture=neutral, PublicKeyToken=null m_FontData: m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} m_FontSize: 14 @@ -2315,6 +2527,7 @@ RectTransform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 771375553} m_Father: {fileID: 557907331} @@ -2334,11 +2547,12 @@ MonoBehaviour: m_GameObject: {fileID: 1487425763} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 1392445389, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} m_Name: m_EditorClassIdentifier: m_Navigation: m_Mode: 3 + m_WrapAround: 0 m_SelectOnUp: {fileID: 0} m_SelectOnDown: {fileID: 0} m_SelectOnLeft: {fileID: 0} @@ -2348,17 +2562,20 @@ MonoBehaviour: m_NormalColor: {r: 1, g: 1, b: 1, a: 1} m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} m_DisabledColor: {r: 0.11764706, g: 0.11764706, b: 0.11764706, a: 0.5019608} m_ColorMultiplier: 1 m_FadeDuration: 0.1 m_SpriteState: m_HighlightedSprite: {fileID: 0} m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} m_DisabledSprite: {fileID: 0} m_AnimationTriggers: m_NormalTrigger: Normal m_HighlightedTrigger: Highlighted m_PressedTrigger: Pressed + m_SelectedTrigger: Highlighted m_DisabledTrigger: Disabled m_Interactable: 1 m_TargetGraphic: {fileID: 1487425766} @@ -2366,6 +2583,7 @@ MonoBehaviour: m_PersistentCalls: m_Calls: - m_Target: {fileID: 1634534319} + m_TargetAssemblyTypeName: m_MethodName: JoinOrLeave m_Mode: 1 m_Arguments: @@ -2376,8 +2594,6 @@ MonoBehaviour: m_StringArgument: m_BoolArgument: 0 m_CallState: 2 - m_TypeName: UnityEngine.UI.Button+ButtonClickedEvent, UnityEngine.UI, Version=1.0.0.0, - Culture=neutral, PublicKeyToken=null --- !u!114 &1487425766 MonoBehaviour: m_ObjectHideFlags: 0 @@ -2387,17 +2603,17 @@ MonoBehaviour: m_GameObject: {fileID: 1487425763} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 0.3137255, g: 0.5686275, b: 0.627451, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, - Version=1.0.0.0, Culture=neutral, PublicKeyToken=null m_Sprite: {fileID: 0} m_Type: 1 m_PreserveAspect: 0 @@ -2407,6 +2623,7 @@ MonoBehaviour: m_FillClockwise: 1 m_FillOrigin: 0 m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 --- !u!222 &1487425767 CanvasRenderer: m_ObjectHideFlags: 0 @@ -2444,13 +2661,14 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1978894556} m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0.02, y: 0.02} m_AnchorMax: {x: 0.98, y: 0.98} - m_AnchoredPosition: {x: 0, y: 7.8887787} + m_AnchoredPosition: {x: 0, y: 6.5812073} m_SizeDelta: {x: 0, y: 0} m_Pivot: {x: 0.5, y: 1} --- !u!114 &1522127879 @@ -2462,7 +2680,7 @@ MonoBehaviour: m_GameObject: {fileID: 1522127877} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 1741964061, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3} m_Name: m_EditorClassIdentifier: m_HorizontalFit: 0 @@ -2476,17 +2694,17 @@ MonoBehaviour: m_GameObject: {fileID: 1522127877} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, - Version=1.0.0.0, Culture=neutral, PublicKeyToken=null m_FontData: m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} m_FontSize: 14 @@ -2538,7 +2756,7 @@ MonoBehaviour: m_GameObject: {fileID: 1634534314} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 1301386320, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3} m_Name: m_EditorClassIdentifier: m_IgnoreReversedGraphics: 1 @@ -2555,7 +2773,7 @@ MonoBehaviour: m_GameObject: {fileID: 1634534314} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 1980459831, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3} m_Name: m_EditorClassIdentifier: m_UiScaleMode: 1 @@ -2568,6 +2786,7 @@ MonoBehaviour: m_FallbackScreenDPI: 96 m_DefaultSpriteDPI: 96 m_DynamicPixelsPerUnit: 1 + m_PresetInfoIsWorld: 0 --- !u!223 &1634534317 Canvas: m_ObjectHideFlags: 0 @@ -2599,6 +2818,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 0, y: 0, z: 0} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 164639240} - {fileID: 927062025} @@ -2607,6 +2827,7 @@ RectTransform: - {fileID: 632675412} - {fileID: 247055288} - {fileID: 1309622324} + - {fileID: 934640860} m_Father: {fileID: 0} m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} @@ -2637,6 +2858,7 @@ MonoBehaviour: DisconnectButon: {fileID: 1096149281} ExceptionButton: {fileID: 1868729447} UnaryExceptionButton: {fileID: 2120537443} + LabelRtt: {fileID: 934640858} --- !u!1 &1671399961 GameObject: m_ObjectHideFlags: 0 @@ -2666,6 +2888,7 @@ RectTransform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 609138042} - {fileID: 1901126666} @@ -2686,11 +2909,12 @@ MonoBehaviour: m_GameObject: {fileID: 1671399961} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 575553740, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: d199490a83bb2b844b9695cbf13b01ef, type: 3} m_Name: m_EditorClassIdentifier: m_Navigation: m_Mode: 3 + m_WrapAround: 0 m_SelectOnUp: {fileID: 0} m_SelectOnDown: {fileID: 0} m_SelectOnLeft: {fileID: 0} @@ -2700,17 +2924,20 @@ MonoBehaviour: m_NormalColor: {r: 1, g: 1, b: 1, a: 1} m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} m_ColorMultiplier: 1 m_FadeDuration: 0.1 m_SpriteState: m_HighlightedSprite: {fileID: 0} m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} m_DisabledSprite: {fileID: 0} m_AnimationTriggers: m_NormalTrigger: Normal m_HighlightedTrigger: Highlighted m_PressedTrigger: Pressed + m_SelectedTrigger: Highlighted m_DisabledTrigger: Disabled m_Interactable: 1 m_TargetGraphic: {fileID: 1671399964} @@ -2724,16 +2951,15 @@ MonoBehaviour: m_HideMobileInput: 0 m_CharacterValidation: 0 m_CharacterLimit: 0 - m_OnEndEdit: + m_OnSubmit: + m_PersistentCalls: + m_Calls: [] + m_OnDidEndEdit: m_PersistentCalls: m_Calls: [] - m_TypeName: UnityEngine.UI.InputField+SubmitEvent, UnityEngine.UI, Version=1.0.0.0, - Culture=neutral, PublicKeyToken=null m_OnValueChanged: m_PersistentCalls: m_Calls: [] - m_TypeName: UnityEngine.UI.InputField+OnChangeEvent, UnityEngine.UI, Version=1.0.0.0, - Culture=neutral, PublicKeyToken=null m_CaretColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} m_CustomCaretColor: 0 m_SelectionColor: {r: 0.65882355, g: 0.80784315, b: 1, a: 0.7529412} @@ -2741,6 +2967,7 @@ MonoBehaviour: m_CaretBlinkRate: 0.85 m_CaretWidth: 1 m_ReadOnly: 0 + m_ShouldActivateOnSelect: 1 --- !u!114 &1671399964 MonoBehaviour: m_ObjectHideFlags: 0 @@ -2750,17 +2977,17 @@ MonoBehaviour: m_GameObject: {fileID: 1671399961} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, - Version=1.0.0.0, Culture=neutral, PublicKeyToken=null m_Sprite: {fileID: 10911, guid: 0000000000000000f000000000000000, type: 0} m_Type: 1 m_PreserveAspect: 0 @@ -2770,6 +2997,7 @@ MonoBehaviour: m_FillClockwise: 1 m_FillOrigin: 0 m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 --- !u!222 &1671399965 CanvasRenderer: m_ObjectHideFlags: 0 @@ -2807,6 +3035,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 963732563} m_Father: {fileID: 927062025} @@ -2826,11 +3055,12 @@ MonoBehaviour: m_GameObject: {fileID: 1799772181} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: -2061169968, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 2a4db7a114972834c8e4117be1d82ba3, type: 3} m_Name: m_EditorClassIdentifier: m_Navigation: m_Mode: 3 + m_WrapAround: 0 m_SelectOnUp: {fileID: 0} m_SelectOnDown: {fileID: 0} m_SelectOnLeft: {fileID: 0} @@ -2840,30 +3070,31 @@ MonoBehaviour: m_NormalColor: {r: 1, g: 1, b: 1, a: 1} m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} m_ColorMultiplier: 1 m_FadeDuration: 0.1 m_SpriteState: m_HighlightedSprite: {fileID: 0} m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} m_DisabledSprite: {fileID: 0} m_AnimationTriggers: m_NormalTrigger: Normal m_HighlightedTrigger: Highlighted m_PressedTrigger: Pressed + m_SelectedTrigger: Highlighted m_DisabledTrigger: Disabled m_Interactable: 1 m_TargetGraphic: {fileID: 817501239} m_HandleRect: {fileID: 817501238} m_Direction: 3 m_Value: 0 - m_Size: 0.99999994 + m_Size: 1 m_NumberOfSteps: 0 m_OnValueChanged: m_PersistentCalls: m_Calls: [] - m_TypeName: UnityEngine.UI.Scrollbar+ScrollEvent, UnityEngine.UI, Version=1.0.0.0, - Culture=neutral, PublicKeyToken=null --- !u!114 &1799772184 MonoBehaviour: m_ObjectHideFlags: 0 @@ -2873,17 +3104,17 @@ MonoBehaviour: m_GameObject: {fileID: 1799772181} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, - Version=1.0.0.0, Culture=neutral, PublicKeyToken=null m_Sprite: {fileID: 10907, guid: 0000000000000000f000000000000000, type: 0} m_Type: 1 m_PreserveAspect: 0 @@ -2893,6 +3124,7 @@ MonoBehaviour: m_FillClockwise: 1 m_FillOrigin: 0 m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 --- !u!222 &1799772185 CanvasRenderer: m_ObjectHideFlags: 0 @@ -2930,6 +3162,7 @@ RectTransform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1040922707} m_Father: {fileID: 632675412} @@ -2949,11 +3182,12 @@ MonoBehaviour: m_GameObject: {fileID: 1868729445} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 1392445389, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} m_Name: m_EditorClassIdentifier: m_Navigation: m_Mode: 3 + m_WrapAround: 0 m_SelectOnUp: {fileID: 0} m_SelectOnDown: {fileID: 0} m_SelectOnLeft: {fileID: 0} @@ -2963,17 +3197,20 @@ MonoBehaviour: m_NormalColor: {r: 1, g: 1, b: 1, a: 1} m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} m_DisabledColor: {r: 0.11764706, g: 0.11764706, b: 0.11764706, a: 0.5019608} m_ColorMultiplier: 1 m_FadeDuration: 0.1 m_SpriteState: m_HighlightedSprite: {fileID: 0} m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} m_DisabledSprite: {fileID: 0} m_AnimationTriggers: m_NormalTrigger: Normal m_HighlightedTrigger: Highlighted m_PressedTrigger: Pressed + m_SelectedTrigger: Highlighted m_DisabledTrigger: Disabled m_Interactable: 1 m_TargetGraphic: {fileID: 1868729448} @@ -2981,6 +3218,7 @@ MonoBehaviour: m_PersistentCalls: m_Calls: - m_Target: {fileID: 1634534319} + m_TargetAssemblyTypeName: m_MethodName: GenerateException m_Mode: 1 m_Arguments: @@ -2991,8 +3229,6 @@ MonoBehaviour: m_StringArgument: m_BoolArgument: 0 m_CallState: 2 - m_TypeName: UnityEngine.UI.Button+ButtonClickedEvent, UnityEngine.UI, Version=1.0.0.0, - Culture=neutral, PublicKeyToken=null --- !u!114 &1868729448 MonoBehaviour: m_ObjectHideFlags: 0 @@ -3002,17 +3238,17 @@ MonoBehaviour: m_GameObject: {fileID: 1868729445} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 0.7647059, g: 0.33333337, b: 0.3651321, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, - Version=1.0.0.0, Culture=neutral, PublicKeyToken=null m_Sprite: {fileID: 0} m_Type: 1 m_PreserveAspect: 0 @@ -3022,6 +3258,7 @@ MonoBehaviour: m_FillClockwise: 1 m_FillOrigin: 0 m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 --- !u!222 &1868729449 CanvasRenderer: m_ObjectHideFlags: 0 @@ -3058,6 +3295,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 1671399962} m_RootOrder: 1 @@ -3076,17 +3314,17 @@ MonoBehaviour: m_GameObject: {fileID: 1901126665} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, - Version=1.0.0.0, Culture=neutral, PublicKeyToken=null m_FontData: m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} m_FontSize: 30 @@ -3137,6 +3375,7 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1522127878} m_Father: {fileID: 927062025} @@ -3164,9 +3403,11 @@ MonoBehaviour: m_GameObject: {fileID: 1978894555} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: -146154839, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 3312d7739989d2b4e91e6319e9a96d76, type: 3} m_Name: m_EditorClassIdentifier: + m_Padding: {x: 0, y: 0, z: 0, w: 0} + m_Softness: {x: 0, y: 0} --- !u!1 &2120537441 GameObject: m_ObjectHideFlags: 0 @@ -3196,6 +3437,7 @@ RectTransform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1434588225} m_Father: {fileID: 632675412} @@ -3215,11 +3457,12 @@ MonoBehaviour: m_GameObject: {fileID: 2120537441} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 1392445389, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} m_Name: m_EditorClassIdentifier: m_Navigation: m_Mode: 3 + m_WrapAround: 0 m_SelectOnUp: {fileID: 0} m_SelectOnDown: {fileID: 0} m_SelectOnLeft: {fileID: 0} @@ -3229,17 +3472,20 @@ MonoBehaviour: m_NormalColor: {r: 1, g: 1, b: 1, a: 1} m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} m_DisabledColor: {r: 0.11764706, g: 0.11764706, b: 0.11764706, a: 0.5019608} m_ColorMultiplier: 1 m_FadeDuration: 0.1 m_SpriteState: m_HighlightedSprite: {fileID: 0} m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} m_DisabledSprite: {fileID: 0} m_AnimationTriggers: m_NormalTrigger: Normal m_HighlightedTrigger: Highlighted m_PressedTrigger: Pressed + m_SelectedTrigger: Highlighted m_DisabledTrigger: Disabled m_Interactable: 1 m_TargetGraphic: {fileID: 2120537444} @@ -3247,6 +3493,7 @@ MonoBehaviour: m_PersistentCalls: m_Calls: - m_Target: {fileID: 1634534319} + m_TargetAssemblyTypeName: m_MethodName: UnaryGenerateException m_Mode: 1 m_Arguments: @@ -3257,8 +3504,6 @@ MonoBehaviour: m_StringArgument: m_BoolArgument: 0 m_CallState: 2 - m_TypeName: UnityEngine.UI.Button+ButtonClickedEvent, UnityEngine.UI, Version=1.0.0.0, - Culture=neutral, PublicKeyToken=null --- !u!114 &2120537444 MonoBehaviour: m_ObjectHideFlags: 0 @@ -3268,17 +3513,17 @@ MonoBehaviour: m_GameObject: {fileID: 2120537441} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 0.7647059, g: 0.33333337, b: 0.3651321, a: 1} m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, - Version=1.0.0.0, Culture=neutral, PublicKeyToken=null m_Sprite: {fileID: 0} m_Type: 1 m_PreserveAspect: 0 @@ -3288,6 +3533,7 @@ MonoBehaviour: m_FillClockwise: 1 m_FillOrigin: 0 m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 --- !u!222 &2120537445 CanvasRenderer: m_ObjectHideFlags: 0 diff --git a/samples/ChatApp/ChatApp.Unity/Assets/Scripts/ChatComponent.cs b/samples/ChatApp/ChatApp.Unity/Assets/Scripts/ChatComponent.cs index 0839f01b1..8b0264bba 100644 --- a/samples/ChatApp/ChatApp.Unity/Assets/Scripts/ChatComponent.cs +++ b/samples/ChatApp/ChatApp.Unity/Assets/Scripts/ChatComponent.cs @@ -42,6 +42,7 @@ public class ChatComponent : MonoBehaviour, IChatHubReceiver public Button ExceptionButton; public Button UnaryExceptionButton; + public Text LabelRtt; async void Start() { @@ -71,7 +72,11 @@ private async Task InitializeClientAsync() try { Debug.Log($"Connecting to the server..."); - this.streamingClient = await StreamingHubClient.ConnectAsync(this.channel, this, cancellationToken: shutdownCancellation.Token); + var options = StreamingHubClientOptions.CreateWithDefault() + .WithClientHeartbeatResponseReceived(x => LabelRtt.text = $"RTT: {x.RoundTripTime.TotalMilliseconds:#,0}ms") + .WithClientHeartbeatInterval(TimeSpan.FromSeconds(10)) + .WithClientHeartbeatTimeout(TimeSpan.FromSeconds(5)); + this.streamingClient = await StreamingHubClient.ConnectAsync(this.channel, this, options, cancellationToken: shutdownCancellation.Token); this.RegisterDisconnectEvent(streamingClient); Debug.Log($"Connection is established."); break; diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubClientMessageReader.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubClientMessageReader.cs index 247caec1f..5d4b59474 100644 --- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubClientMessageReader.cs +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubClientMessageReader.cs @@ -24,8 +24,9 @@ public StreamingHubMessageType ReadMessageType() 4 => StreamingHubMessageType.ResponseWithError, 5 => reader.ReadByte() switch { - 0x00 /* 0:ClientResultRequest */ => StreamingHubMessageType.ClientResultRequest, - 0x7f /* 127:Heartbeat */ => StreamingHubMessageType.Heartbeat, + 0x00 /* 0:ClientResultRequest */ => StreamingHubMessageType.ClientResultRequest, + 0x7e /* 126:ClientHeartbeatResponse */ => StreamingHubMessageType.ClientHeartbeatResponse, + 0x7f /* 127:ServerHeartbeat */ => StreamingHubMessageType.ServerHeartbeat, var x => throw new InvalidOperationException($"Unknown Type: {x}"), }, _ => throw new InvalidOperationException($"Unknown message format: ArrayLength = {arrayLength}"), @@ -73,7 +74,7 @@ public StreamingHubMessageType ReadMessageType() return (clientRequestMessageId, methodId, data.Slice(offset)); } - public ReadOnlyMemory ReadHeartbeat() + public ReadOnlyMemory ReadServerHeartbeat() { //var type = reader.ReadByte(); // Type is already read by ReadMessageType reader.Skip(); // Dummy (1) @@ -82,5 +83,20 @@ public ReadOnlyMemory ReadHeartbeat() return data.Slice((int)reader.Consumed); } + + public long ReadClientHeartbeatResponse() + { + //var type = reader.ReadByte(); // Type is already read by ReadMessageType + reader.Skip(); // Dummy (1) + reader.Skip(); // Dummy (2) + reader.Skip(); // Dummy (3) + + // Extra: [SentAt(long)] + var arrayLen = reader.ReadArrayHeader(); + if (arrayLen == 0) throw new InvalidOperationException("Invalid client heartbeat response. An extra data is empty."); + var sentAt = reader.ReadInt64(); + + return sentAt; + } } } diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubMessageWriter.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubMessageWriter.cs index 93832281e..9be226ff4 100644 --- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubMessageWriter.cs +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubMessageWriter.cs @@ -26,9 +26,13 @@ namespace MagicOnion.Internal /// Array(5): [Type=0x00, Nil, ClientResultMessageId(Guid), MethodId(int), SerializedArguments] /// /// - /// Heartbeat: + /// ServerHeartbeat/Request: /// Array(5): [Type=0x7f, Nil, Nil, Nil, Extras] /// + /// + /// ClientHeartbeat/Response: + /// Array(5): [Type=0x7e, Nil, Nil, Nil, [ClientTime(long)]] + /// /// /// StreamingHub message formats (from Client to Server): /// @@ -49,9 +53,13 @@ namespace MagicOnion.Internal /// Array(4): [Type=0x01, ClientResultMessageId(Guid), MethodId(int), [StatusCode(int), Detail(string), Message(string)]] /// /// - /// Heartbeat/Response: + /// ServerHeartbeat/Response: /// Array(4): [Type=0x7f, Nil, Nil, Nil] /// + /// + /// ClientHeartbeat/Request: + /// Array(4): [Type=0x7e, Nil, Nil, [ClientTime(long)]] + /// /// /// internal static class StreamingHubMessageWriter @@ -193,58 +201,138 @@ public static void WriteClientResultResponseMessageForError(IBufferWriter // Array(5)[127, Nil, Nil, Nil, ] - static ReadOnlySpan HeartbeatMessageForServerToClientHeader => new byte[] { 0x95, 0x7f, 0xc0, 0xc0, 0xc0 }; + static ReadOnlySpan ServerHeartbeatMessageForServerToClientHeader => new byte[] { 0x95, 0x7f, 0xc0, 0xc0, 0xc0 }; /// - /// Writes a heartbeat message for sending from the server. + /// Writes a server heartbeat message for sending from the server. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteHeartbeatMessageForServerToClientHeader(IBufferWriter bufferWriter) + public static void WriteServerHeartbeatMessageHeader(IBufferWriter bufferWriter) { - bufferWriter.Write(HeartbeatMessageForServerToClientHeader); + bufferWriter.Write(ServerHeartbeatMessageForServerToClientHeader); //var writer = new MessagePackWriter(bufferWriter); //writer.WriteArrayHeader(5); //writer.Write(0x7f); // Type = 0x7f / 127 (Heartbeat) + //writer.WriteNil(); // Dummy + //writer.WriteNil(); // Dummy + //writer.WriteNil(); // Dummy + //writer.Flush(); + // // + } + + // Array(4)[127, Nil, Nil, Nil] + static ReadOnlySpan ServerHeartbeatMessageForClientToServer => new byte[] { 0x94, 0x7f, 0xc0, 0xc0, 0xc0 }; + + /// + /// Writes a server heartbeat message for sending response from the client. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteServerHeartbeatMessageResponse(IBufferWriter bufferWriter) + { + bufferWriter.Write(ServerHeartbeatMessageForClientToServer); + //var writer = new MessagePackWriter(bufferWriter); + //writer.WriteArrayHeader(4); + //writer.Write(0x7f); // Type = 0x7f / 127 (Heartbeat) //writer.WriteNil(); // Dummy //writer.WriteNil(); // Dummy //writer.WriteNil(); // Dummy //writer.Flush(); } - // Array(4)[127, Nil, Nil, Nil] - static ReadOnlySpan HeartbeatMessageForClientToServer => new byte[] { 0x94, 0x7f, 0xc0, 0xc0, 0xc0 }; + // Array(4)[0x7e(126), Nil, Nil, ] + static ReadOnlySpan ClientHeartbeatMessageHeader => new byte[] { 0x94, 0x7e, 0xc0, 0xc0 }; /// - /// Writes a heartbeat message for sending from the client. + /// Writes a client heartbeat message for sending from the client. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteHeartbeatMessageForClientToServer(IBufferWriter bufferWriter) + public static void WriteClientHeartbeatMessageHeader(IBufferWriter bufferWriter) { - bufferWriter.Write(HeartbeatMessageForClientToServer); + bufferWriter.Write(ClientHeartbeatMessageHeader); //var writer = new MessagePackWriter(bufferWriter); //writer.WriteArrayHeader(4); - //writer.Write(0x7f); // Type = 0x7f / 127 (Heartbeat) + //writer.Write(0x7f); // Type = 0x7e / 126 (ClientHeartbeat) + //writer.WriteNil(); // Dummy + //writer.WriteNil(); // Dummy + //writer.Flush(); + // // + } + + // Array(5)[0x7e(126), Nil, Nil, Nil, ] + static ReadOnlySpan ClientHeartbeatMessageResponseHeader => new byte[] { 0x95, 0x7e, 0xc0, 0xc0, 0xc0 }; + + /// + /// Writes a client heartbeat message for sending response from the server. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteClientHeartbeatMessageResponseHeader(IBufferWriter bufferWriter) + { + bufferWriter.Write(ClientHeartbeatMessageResponseHeader); + //var writer = new MessagePackWriter(bufferWriter); + //writer.WriteArrayHeader(5); + //writer.Write(0x7f); // Type = 0x7e / 126 (Heartbeat) //writer.WriteNil(); // Dummy //writer.WriteNil(); // Dummy //writer.WriteNil(); // Dummy //writer.Flush(); + // // } } internal enum StreamingHubMessageType { - // Client to Server + /// + /// Request: Client -> Server + /// Request, + /// + /// Request: Client -> Server / Fire-and-Forget + /// RequestFireAndForget, + /// + /// Request: Client -> Server -> Client + /// Response, + /// + /// Request: Client -> Server -(Error)-> Client + /// ResponseWithError, - HeartbeatResponse, - // Server to Client + /// + /// Broadcast: Server -> Client + /// Broadcast, + + /// + /// ClientResult: Server -> Client + /// ClientResultRequest, + /// + /// ClientResult: Server -> Client -> Server + /// ClientResultResponse, + /// + /// ClientResult: Server -> Client -(Error)-> Server + /// ClientResultResponseWithError, - Heartbeat, + + + /// + /// Heartbeat: Server -> Client -> Server + /// + ServerHeartbeatResponse, + /// + /// Heartbeat: Server -> Client + /// + ServerHeartbeat, + + /// + /// Heartbeat: Client -> Server + /// + ClientHeartbeat, + /// + /// Heartbeat: Client -> Server -> Client + /// + ClientHeartbeatResponse, } } diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayload.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayload.cs index d3387100f..d4c5b1ee0 100644 --- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayload.cs +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayload.cs @@ -104,12 +104,21 @@ public void Initialize(ReadOnlySequence data) memory = buffer.AsMemory(0, (int)data.Length); } - public void Initialize(ReadOnlyMemory data) + public void Initialize(ReadOnlyMemory data, bool holdReference) { ThrowIfUsing(); - buffer = null; - memory = data; + if (holdReference) + { + buffer = null; + memory = data; + } + else + { + buffer = ArrayPool.Shared.Rent((int)data.Length); + data.CopyTo(buffer); + memory = buffer.AsMemory(0, (int)data.Length); + } } public void Uninitialize() diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayloadPool.BuiltIn.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayloadPool.BuiltIn.cs index 8a8334abb..2245d3e88 100644 --- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayloadPool.BuiltIn.cs +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayloadPool.BuiltIn.cs @@ -105,10 +105,10 @@ public StreamingHubPayload RentOrCreate(ReadOnlySpan data) #endif } - public StreamingHubPayload RentOrCreate(ReadOnlyMemory data) + public StreamingHubPayload RentOrCreate(ReadOnlyMemory data, bool holdReference) { var payload = pool.RentOrCreateCore(); - payload.Initialize(data); + payload.Initialize(data, holdReference); #if DEBUG return new StreamingHubPayload(payload); #else diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayloadPool.ObjectPool.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayloadPool.ObjectPool.cs index 62c85b124..1a0948825 100644 --- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayloadPool.ObjectPool.cs +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayloadPool.ObjectPool.cs @@ -34,10 +34,10 @@ public StreamingHubPayload RentOrCreate(ReadOnlySpan data) #endif } - public StreamingHubPayload RentOrCreate(ReadOnlyMemory data) + public StreamingHubPayload RentOrCreate(ReadOnlyMemory data, bool holdReference) { var payload = pool.Get(); - payload.Initialize(data); + payload.Initialize(data, holdReference); #if DEBUG return new StreamingHubPayload(payload); #else diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubServerMessageReader.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubServerMessageReader.cs index b1d8326cf..ae03323b2 100644 --- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubServerMessageReader.cs +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubServerMessageReader.cs @@ -25,7 +25,8 @@ public StreamingHubMessageType ReadMessageType() { 0x00 => StreamingHubMessageType.ClientResultResponse, 0x01 => StreamingHubMessageType.ClientResultResponseWithError, - 0x7f => StreamingHubMessageType.HeartbeatResponse, + 0x7e => StreamingHubMessageType.ClientHeartbeat, + 0x7f => StreamingHubMessageType.ServerHeartbeatResponse, var subType => throw new InvalidOperationException($"Unknown client response message: {subType}"), }, _ => throw new InvalidOperationException($"Unknown message format: ArrayLength = {arrayLength}"), @@ -75,5 +76,14 @@ public StreamingHubMessageType ReadMessageType() return (clientResultMessageId, clientMethodId, statusCode, detail, message); } + + public ReadOnlyMemory ReadClientHeartbeat() + { + // [Nil, Nil, [SentAt(long)]] + reader.Skip(); // Dummy + reader.Skip(); // Dummy + + return data.Slice((int)reader.Consumed); + } } } diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClientBase.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClientBase.cs index 51905894f..613909c7a 100644 --- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClientBase.cs +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClientBase.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using System.Buffers; +using System.Diagnostics; using System.Linq; using System.Threading.Channels; using Grpc.Core; @@ -19,51 +20,139 @@ public class StreamingHubClientOptions public IMagicOnionSerializerProvider SerializerProvider { get; } public IMagicOnionClientLogger Logger { get; } - public TimeSpan? HeartbeatInterval { get; } - public Action>? HeartbeatReceivedFromServer { get; } + public TimeSpan? ClientHeartbeatInterval { get; } + public TimeSpan? ClientHeartbeatTimeout { get; } + public Action>? OnServerHeartbeatReceived { get; } + public Action? OnClientHeartbeatResponseReceived { get; } +#if NET8_0_OR_GREATER + public TimeProvider? TimeProvider { get; } +#endif public StreamingHubClientOptions(string? host, CallOptions callOptions, IMagicOnionSerializerProvider serializerProvider, IMagicOnionClientLogger logger) - : this(host, callOptions, serializerProvider, logger, default, default) +#if NET8_0_OR_GREATER + : this(host, callOptions, serializerProvider, logger, default, default, default, default, default) +#else + : this(host, callOptions, serializerProvider, logger, default, default, default, default) +#endif { } - public StreamingHubClientOptions(string? host, CallOptions callOptions, IMagicOnionSerializerProvider serializerProvider, IMagicOnionClientLogger logger, TimeSpan? heartbeatInterval, Action>? heartbeatReceivedFromServer) +#if NET8_0_OR_GREATER + public StreamingHubClientOptions(string? host, CallOptions callOptions, IMagicOnionSerializerProvider serializerProvider, IMagicOnionClientLogger logger, TimeSpan? clientHeartbeatInterval, TimeSpan? clientHeartbeatTimeout, Action>? onServerHeartbeatReceived, Action? onClientHeartbeatResponseReceived,TimeProvider? timeProvider) +#else + public StreamingHubClientOptions(string? host, CallOptions callOptions, IMagicOnionSerializerProvider serializerProvider, IMagicOnionClientLogger logger, TimeSpan? clientHeartbeatInterval, TimeSpan? clientHeartbeatTimeout, Action>? onServerHeartbeatReceived, Action? onClientHeartbeatResponseReceived) +#endif { Host = host; CallOptions = callOptions; SerializerProvider = serializerProvider ?? throw new ArgumentNullException(nameof(serializerProvider)); Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - HeartbeatInterval = heartbeatInterval; - HeartbeatReceivedFromServer = heartbeatReceivedFromServer; + ClientHeartbeatInterval = clientHeartbeatInterval; + ClientHeartbeatTimeout = clientHeartbeatTimeout; + OnServerHeartbeatReceived = onServerHeartbeatReceived; + OnClientHeartbeatResponseReceived = onClientHeartbeatResponseReceived; +#if NET8_0_OR_GREATER + TimeProvider = timeProvider; +#endif } public static StreamingHubClientOptions CreateWithDefault(string? host = default, CallOptions callOptions = default, IMagicOnionSerializerProvider? serializerProvider = default, IMagicOnionClientLogger? logger = default) => new(host, callOptions, serializerProvider ?? MagicOnionSerializerProvider.Default, logger ?? NullMagicOnionClientLogger.Instance); public StreamingHubClientOptions WithHost(string? host) - => new(host, CallOptions, SerializerProvider, Logger, HeartbeatInterval, HeartbeatReceivedFromServer); + => new(host, CallOptions, SerializerProvider, Logger + , ClientHeartbeatInterval, ClientHeartbeatTimeout, OnServerHeartbeatReceived, OnClientHeartbeatResponseReceived +#if NET8_0_OR_GREATER + , TimeProvider +#endif + ); public StreamingHubClientOptions WithCallOptions(CallOptions callOptions) - => new(Host, callOptions, SerializerProvider, Logger, HeartbeatInterval, HeartbeatReceivedFromServer); + => new(Host, callOptions, SerializerProvider, Logger + , ClientHeartbeatInterval, ClientHeartbeatTimeout, OnServerHeartbeatReceived, OnClientHeartbeatResponseReceived +#if NET8_0_OR_GREATER + , TimeProvider +#endif + ); public StreamingHubClientOptions WithSerializerProvider(IMagicOnionSerializerProvider serializerProvider) - => new(Host, CallOptions, serializerProvider, Logger, HeartbeatInterval, HeartbeatReceivedFromServer); + => new( + Host, CallOptions, serializerProvider, Logger + , ClientHeartbeatInterval, ClientHeartbeatTimeout, OnServerHeartbeatReceived, OnClientHeartbeatResponseReceived +#if NET8_0_OR_GREATER + , TimeProvider +#endif + ); public StreamingHubClientOptions WithLogger(IMagicOnionClientLogger logger) - => new(Host, CallOptions, SerializerProvider, logger, HeartbeatInterval, HeartbeatReceivedFromServer); + => new(Host, CallOptions, SerializerProvider, logger + , ClientHeartbeatInterval, ClientHeartbeatTimeout, OnServerHeartbeatReceived, OnClientHeartbeatResponseReceived +#if NET8_0_OR_GREATER + , TimeProvider +#endif + ); /// /// Sets a heartbeat interval. If a value is , the heartbeat from the client is disabled. /// /// /// - public StreamingHubClientOptions WithHeartbeatInterval(TimeSpan? interval) - => new(Host, CallOptions, SerializerProvider, Logger, interval, HeartbeatReceivedFromServer); + public StreamingHubClientOptions WithClientHeartbeatInterval(TimeSpan? interval) + => new(Host, CallOptions, SerializerProvider, Logger + , interval, ClientHeartbeatTimeout, OnServerHeartbeatReceived, OnClientHeartbeatResponseReceived +#if NET8_0_OR_GREATER + , TimeProvider +#endif + ); + + /// + /// Sets a heartbeat timeout period. If a value is , the client does not time out. + /// + /// + /// + public StreamingHubClientOptions WithClientHeartbeatTimeout(TimeSpan? timeout) + => new(Host, CallOptions, SerializerProvider, Logger + , ClientHeartbeatInterval, timeout, OnServerHeartbeatReceived, OnClientHeartbeatResponseReceived +#if NET8_0_OR_GREATER + , TimeProvider +#endif + ); /// /// Sets a heartbeat callback. If additional metadata is provided by the server in the heartbeat message, this metadata is provided as an argument. /// - /// + /// /// - public StreamingHubClientOptions WithHeartbeatReceived(Action>? onHeartbeatReceived) - => new(Host, CallOptions, SerializerProvider, Logger, HeartbeatInterval, onHeartbeatReceived); + public StreamingHubClientOptions WithServerHeartbeatReceived(Action>? onServerHeartbeatReceived) + => new(Host, CallOptions, SerializerProvider, Logger + , ClientHeartbeatInterval, ClientHeartbeatTimeout, onServerHeartbeatReceived, OnClientHeartbeatResponseReceived +#if NET8_0_OR_GREATER + , TimeProvider +#endif + ); + + /// + /// Sets a client heartbeat response callback. + /// + /// + /// + public StreamingHubClientOptions WithClientHeartbeatResponseReceived(Action? onClientHeartbeatResponseReceived) + => new(Host, CallOptions, SerializerProvider, Logger + , ClientHeartbeatInterval, ClientHeartbeatTimeout, OnServerHeartbeatReceived, onClientHeartbeatResponseReceived +#if NET8_0_OR_GREATER + , TimeProvider +#endif + ); + +#if NET8_0_OR_GREATER + /// + /// Sets a + /// + /// + /// + public StreamingHubClientOptions WithTimeProvider(TimeProvider timeProvider) + => new(Host, CallOptions, SerializerProvider, Logger + , ClientHeartbeatInterval, ClientHeartbeatTimeout, OnServerHeartbeatReceived, OnClientHeartbeatResponseReceived + , timeProvider + ); +#endif } public abstract class StreamingHubClientBase @@ -83,21 +172,18 @@ public abstract class StreamingHubClientBase readonly Dictionary responseFutures = new(); readonly TaskCompletionSource waitForDisconnect = new(); readonly CancellationTokenSource cancellationTokenSource = new(); - readonly Dictionary postCallbackCache = new(); - SendOrPostCallback? heartbeatCallbackCache; + int messageIdSequence = 0; bool disposed; - Task? heartbeatTask; - DateTimeOffset lastHeartbeatSentAt; - readonly Channel writerQueue = Channel.CreateUnbounded(new UnboundedChannelOptions() { SingleReader = true, SingleWriter = false, AllowSynchronousContinuations = false }); Task? writerTask; IClientStreamWriter writer = default!; IAsyncStreamReader reader = default!; + StreamingHubClientHeartbeatManager heartbeatManager = default!; Task subscription = default!; protected readonly TReceiver receiver; @@ -183,12 +269,29 @@ static Method CreateConnectMethod(stri async Task StartSubscribe(SynchronizationContext? syncContext, Task firstMoveNext) { - if (options.HeartbeatInterval is { } heartbeatInterval) + var cancellationToken = cancellationTokenSource.Token; + + heartbeatManager = new StreamingHubClientHeartbeatManager( + writerQueue.Writer, + options.ClientHeartbeatInterval ?? TimeSpan.Zero /* Disable */, + options.ClientHeartbeatTimeout ?? Timeout.InfiniteTimeSpan, + options.OnServerHeartbeatReceived, + options.OnClientHeartbeatResponseReceived, + syncContext, + cancellationTokenSource.Token +#if NET8_0_OR_GREATER + , options.TimeProvider ?? TimeProvider.System +#endif + ); + + // Activate the Heartbeat Manager if enabled in the options. + if (options.ClientHeartbeatInterval is {} heartbeatInterval && heartbeatInterval > TimeSpan.Zero) { - heartbeatTask = RunHeartbeatLoopAsync(heartbeatInterval, cancellationTokenSource.Token); + heartbeatManager.StartClientHeartbeatLoop(); + cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(heartbeatManager.TimeoutToken, cancellationTokenSource.Token).Token; } - writerTask = RunWriterLoopAsync(cancellationTokenSource.Token); + writerTask = RunWriterLoopAsync(cancellationToken); var reader = this.reader; try @@ -214,7 +317,7 @@ async Task StartSubscribe(SynchronizationContext? syncContext, Task firstM } } - moveNext = reader.MoveNext(cancellationTokenSource.Token); + moveNext = reader.MoveNext(cancellationToken); } } catch (Exception ex) @@ -236,6 +339,8 @@ async Task StartSubscribe(SynchronizationContext? syncContext, Task firstM } finally { + heartbeatManager.Dispose(); + try { #if !UNITY_WEBGL @@ -255,10 +360,6 @@ async Task StartSubscribe(SynchronizationContext? syncContext, Task firstM } } - // MessageFormat: - // broadcast: [methodId, [argument]] - // response: [messageId, methodId, response] - // error-response: [messageId, statusCode, detail, StringMessage] void ConsumeData(SynchronizationContext? syncContext, StreamingHubPayload payload) { var messageReader = new StreamingHubClientMessageReader(payload.Memory); @@ -276,8 +377,11 @@ void ConsumeData(SynchronizationContext? syncContext, StreamingHubPayload payloa case StreamingHubMessageType.ClientResultRequest: ProcessClientResultRequest(syncContext, payload, ref messageReader); break; - case StreamingHubMessageType.Heartbeat: - ProcessHeartbeat(syncContext, payload, ref messageReader); + case StreamingHubMessageType.ServerHeartbeat: + heartbeatManager.ProcessServerHeartbeat(payload); + break; + case StreamingHubMessageType.ClientHeartbeatResponse: + heartbeatManager.ProcessClientHeartbeatResponse(payload); break; } } @@ -386,46 +490,6 @@ void ProcessClientResultRequest(SynchronizationContext? syncContext, StreamingHu } } - void ProcessHeartbeat(SynchronizationContext? syncContext, StreamingHubPayload payload, ref StreamingHubClientMessageReader messageReader) - { - var metadata = messageReader.ReadHeartbeat(); - if (this.options.HeartbeatReceivedFromServer is { } heartbeatReceived) - { - if (syncContext is null) - { - heartbeatReceived(metadata); - StreamingHubPayloadPool.Shared.Return(payload); - } - else - { - heartbeatCallbackCache ??= CreateHeartbeatCallback(heartbeatReceived); - syncContext.Post(heartbeatCallbackCache, payload); - } - } - WriteHeartbeat(); - } - - SendOrPostCallback CreateHeartbeatCallback(Action> heartbeatReceivedAction) => (state) => - { - var p = (StreamingHubPayload)state!; - heartbeatReceivedAction(p.Memory.Slice(5)); - StreamingHubPayloadPool.Shared.Return(p); - }; - - async Task RunHeartbeatLoopAsync(TimeSpan heartbeatInterval, CancellationToken cancellationToken) - { - while (!cancellationToken.IsCancellationRequested) - { - await Task.Delay(heartbeatInterval, cancellationToken).ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); - - if ((DateTimeOffset.UtcNow - lastHeartbeatSentAt) > heartbeatInterval) - { - WriteHeartbeat(); - } - } - } - async Task RunWriterLoopAsync(CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) @@ -440,15 +504,6 @@ async Task RunWriterLoopAsync(CancellationToken cancellationToken) } } - void WriteHeartbeat() - { - if (disposed) return; - var v = BuildHeartbeatMessage(); - _ = writerQueue.Writer.TryWrite(v); - - lastHeartbeatSentAt = DateTimeOffset.UtcNow; - } - protected Task WriteMessageFireAndForgetTaskAsync(int methodId, TRequest message) => WriteMessageFireAndForgetValueTaskOfTAsync(methodId, message).AsTask(); @@ -674,12 +729,5 @@ StreamingHubPayload BuildClientResultResponseMessageForError(int methodId, Guid StreamingHubMessageWriter.WriteClientResultResponseMessageForError(buffer, methodId, messageId, statusCode, detail, ex, messageSerializer); return StreamingHubPayloadPool.Shared.RentOrCreate(buffer.WrittenSpan); } - - StreamingHubPayload BuildHeartbeatMessage() - { - using var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter(); - StreamingHubMessageWriter.WriteHeartbeatMessageForClientToServer(buffer); - return StreamingHubPayloadPool.Shared.RentOrCreate(buffer.WrittenSpan); - } } } diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClientHeartbeatManager.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClientHeartbeatManager.cs new file mode 100644 index 000000000..22506f613 --- /dev/null +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClientHeartbeatManager.cs @@ -0,0 +1,204 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using MagicOnion.Internal; +using MagicOnion.Internal.Buffers; +using MessagePack; + +namespace MagicOnion.Client +{ + public readonly struct ClientHeartbeatEvent + { + /// + /// Gets the round trip time (RTT) between client and server. + /// + public TimeSpan RoundTripTime { get; } + + public ClientHeartbeatEvent(long roundTripTimeMs) + { + RoundTripTime = TimeSpan.FromMilliseconds(roundTripTimeMs); + } + } + + internal class StreamingHubClientHeartbeatManager : IDisposable + { + readonly CancellationTokenSource timeoutTokenSource; + readonly CancellationTokenSource shutdownTokenSource; + readonly TimeSpan heartbeatInterval; + readonly TimeSpan timeoutPeriod; + readonly Action>? onServerHeartbeatReceived; + readonly Action? onClientHeartbeatResponseReceived; + readonly SynchronizationContext? synchronizationContext; + readonly ChannelWriter writer; +#if NET8_0_OR_GREATER + readonly TimeProvider timeProvider; +#endif + + SendOrPostCallback? serverHeartbeatCallbackCache; + SendOrPostCallback? clientHeartbeatResponseCallbackCache; + Task? heartbeatLoopTask; + + public CancellationToken TimeoutToken => timeoutTokenSource.Token; + + public StreamingHubClientHeartbeatManager( + ChannelWriter writer, + TimeSpan heartbeatInterval, + TimeSpan timeoutPeriod, + Action>? onServerHeartbeatReceived, + Action? onClientHeartbeatResponseReceived, + SynchronizationContext? synchronizationContext, + CancellationToken shutdownToken +#if NET8_0_OR_GREATER + , TimeProvider timeProvider +#endif + ) + { + this.timeoutTokenSource = new( +#if NET8_0_OR_GREATER + Timeout.InfiniteTimeSpan, timeProvider +#endif + ); + this.writer = writer; + this.heartbeatInterval = heartbeatInterval; + this.timeoutPeriod = timeoutPeriod; + this.onServerHeartbeatReceived = onServerHeartbeatReceived; + this.onClientHeartbeatResponseReceived = onClientHeartbeatResponseReceived; + this.synchronizationContext = synchronizationContext; + this.shutdownTokenSource = CancellationTokenSource.CreateLinkedTokenSource(shutdownToken, timeoutTokenSource.Token); +#if NET8_0_OR_GREATER + this.timeProvider = timeProvider; +#endif + } + + public void StartClientHeartbeatLoop() + { + heartbeatLoopTask = RunClientHeartbeatLoopAsync(); + } + + async Task RunClientHeartbeatLoopAsync() + { + while (!shutdownTokenSource.IsCancellationRequested) + { + await Task.Delay(heartbeatInterval +#if NET8_0_OR_GREATER + , timeProvider +#endif + , shutdownTokenSource.Token).ConfigureAwait(false); + + shutdownTokenSource.Token.ThrowIfCancellationRequested(); + + // Writes a ClientHeartbeat to the writer queue. + _ = writer.TryWrite(BuildClientHeartbeatMessage()); + + // Start/Restart the timeout cancellation timer. + timeoutTokenSource.CancelAfter(timeoutPeriod); + } + } + + public void ProcessClientHeartbeatResponse(StreamingHubPayload payload) + { + if (shutdownTokenSource.IsCancellationRequested) return; + + // Cancel the running timeout cancellation timer. + timeoutTokenSource.CancelAfter(Timeout.InfiniteTimeSpan); + + if (onClientHeartbeatResponseReceived is { } heartbeatReceived) + { + clientHeartbeatResponseCallbackCache ??= CreateClientHeartbeatResponseCallback(heartbeatReceived); + + if (synchronizationContext is null) + { + clientHeartbeatResponseCallbackCache(payload); + } + else + { + synchronizationContext.Post(clientHeartbeatResponseCallbackCache, payload); + } + } + } + + public void ProcessServerHeartbeat(StreamingHubPayload payload) + { + if (shutdownTokenSource.IsCancellationRequested) return; + + if (onServerHeartbeatReceived is { } heartbeatReceived) + { + serverHeartbeatCallbackCache ??= CreateServerHeartbeatCallback(heartbeatReceived); + + if (synchronizationContext is null) + { + serverHeartbeatCallbackCache(payload); + } + else + { + synchronizationContext.Post(serverHeartbeatCallbackCache, payload); + } + } + + // Writes a ServerHeartbeatResponse to the writer queue. + _ = writer.TryWrite(BuildServerHeartbeatMessage()); + } + + SendOrPostCallback CreateClientHeartbeatResponseCallback(Action heartbeatReceivedAction) => (state) => + { + var p = (StreamingHubPayload)state!; + + var reader = new StreamingHubClientMessageReader(p.Memory); + _ = reader.ReadMessageType(); + +#if NET8_0_OR_GREATER + var now = timeProvider.GetUtcNow(); +#else + var now = DateTimeOffset.UtcNow; +#endif + var sentAt = reader.ReadClientHeartbeatResponse(); + var elapsed = now.ToUnixTimeMilliseconds() - sentAt; + + heartbeatReceivedAction(new ClientHeartbeatEvent(elapsed)); + StreamingHubPayloadPool.Shared.Return(p); + }; + + SendOrPostCallback CreateServerHeartbeatCallback(Action> heartbeatReceivedAction) => (state) => + { + var p = (StreamingHubPayload)state!; + var remain = p.Memory.Slice(5); // header + heartbeatReceivedAction(remain); + StreamingHubPayloadPool.Shared.Return(p); + }; + + StreamingHubPayload BuildServerHeartbeatMessage() + { + using var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter(); + StreamingHubMessageWriter.WriteServerHeartbeatMessageResponse(buffer); + return StreamingHubPayloadPool.Shared.RentOrCreate(buffer.WrittenSpan); + } + + StreamingHubPayload BuildClientHeartbeatMessage() + { + using var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter(); + StreamingHubMessageWriter.WriteClientHeartbeatMessageHeader(buffer); + +#if NET8_0_OR_GREATER + var now = timeProvider.GetUtcNow(); +#else + var now = DateTimeOffset.UtcNow; +#endif + + // Extra: [SentAt(long)] + var writer = new MessagePackWriter(buffer); + writer.WriteArrayHeader(1); + writer.Write(now.ToUnixTimeMilliseconds()); + writer.Flush(); + return StreamingHubPayloadPool.Shared.RentOrCreate(buffer.WrittenSpan); + } + + public void Dispose() + { + shutdownTokenSource.Cancel(); + shutdownTokenSource.Dispose(); + timeoutTokenSource.Dispose(); + } + } +} diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClientHeartbeatManager.cs.meta b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClientHeartbeatManager.cs.meta new file mode 100644 index 000000000..e26f1a2aa --- /dev/null +++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClientHeartbeatManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f997866831803b34aa603952177c34e2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/MagicOnion.Client/StreamingHubClientBase.cs b/src/MagicOnion.Client/StreamingHubClientBase.cs index 51905894f..613909c7a 100644 --- a/src/MagicOnion.Client/StreamingHubClientBase.cs +++ b/src/MagicOnion.Client/StreamingHubClientBase.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using System.Buffers; +using System.Diagnostics; using System.Linq; using System.Threading.Channels; using Grpc.Core; @@ -19,51 +20,139 @@ public class StreamingHubClientOptions public IMagicOnionSerializerProvider SerializerProvider { get; } public IMagicOnionClientLogger Logger { get; } - public TimeSpan? HeartbeatInterval { get; } - public Action>? HeartbeatReceivedFromServer { get; } + public TimeSpan? ClientHeartbeatInterval { get; } + public TimeSpan? ClientHeartbeatTimeout { get; } + public Action>? OnServerHeartbeatReceived { get; } + public Action? OnClientHeartbeatResponseReceived { get; } +#if NET8_0_OR_GREATER + public TimeProvider? TimeProvider { get; } +#endif public StreamingHubClientOptions(string? host, CallOptions callOptions, IMagicOnionSerializerProvider serializerProvider, IMagicOnionClientLogger logger) - : this(host, callOptions, serializerProvider, logger, default, default) +#if NET8_0_OR_GREATER + : this(host, callOptions, serializerProvider, logger, default, default, default, default, default) +#else + : this(host, callOptions, serializerProvider, logger, default, default, default, default) +#endif { } - public StreamingHubClientOptions(string? host, CallOptions callOptions, IMagicOnionSerializerProvider serializerProvider, IMagicOnionClientLogger logger, TimeSpan? heartbeatInterval, Action>? heartbeatReceivedFromServer) +#if NET8_0_OR_GREATER + public StreamingHubClientOptions(string? host, CallOptions callOptions, IMagicOnionSerializerProvider serializerProvider, IMagicOnionClientLogger logger, TimeSpan? clientHeartbeatInterval, TimeSpan? clientHeartbeatTimeout, Action>? onServerHeartbeatReceived, Action? onClientHeartbeatResponseReceived,TimeProvider? timeProvider) +#else + public StreamingHubClientOptions(string? host, CallOptions callOptions, IMagicOnionSerializerProvider serializerProvider, IMagicOnionClientLogger logger, TimeSpan? clientHeartbeatInterval, TimeSpan? clientHeartbeatTimeout, Action>? onServerHeartbeatReceived, Action? onClientHeartbeatResponseReceived) +#endif { Host = host; CallOptions = callOptions; SerializerProvider = serializerProvider ?? throw new ArgumentNullException(nameof(serializerProvider)); Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - HeartbeatInterval = heartbeatInterval; - HeartbeatReceivedFromServer = heartbeatReceivedFromServer; + ClientHeartbeatInterval = clientHeartbeatInterval; + ClientHeartbeatTimeout = clientHeartbeatTimeout; + OnServerHeartbeatReceived = onServerHeartbeatReceived; + OnClientHeartbeatResponseReceived = onClientHeartbeatResponseReceived; +#if NET8_0_OR_GREATER + TimeProvider = timeProvider; +#endif } public static StreamingHubClientOptions CreateWithDefault(string? host = default, CallOptions callOptions = default, IMagicOnionSerializerProvider? serializerProvider = default, IMagicOnionClientLogger? logger = default) => new(host, callOptions, serializerProvider ?? MagicOnionSerializerProvider.Default, logger ?? NullMagicOnionClientLogger.Instance); public StreamingHubClientOptions WithHost(string? host) - => new(host, CallOptions, SerializerProvider, Logger, HeartbeatInterval, HeartbeatReceivedFromServer); + => new(host, CallOptions, SerializerProvider, Logger + , ClientHeartbeatInterval, ClientHeartbeatTimeout, OnServerHeartbeatReceived, OnClientHeartbeatResponseReceived +#if NET8_0_OR_GREATER + , TimeProvider +#endif + ); public StreamingHubClientOptions WithCallOptions(CallOptions callOptions) - => new(Host, callOptions, SerializerProvider, Logger, HeartbeatInterval, HeartbeatReceivedFromServer); + => new(Host, callOptions, SerializerProvider, Logger + , ClientHeartbeatInterval, ClientHeartbeatTimeout, OnServerHeartbeatReceived, OnClientHeartbeatResponseReceived +#if NET8_0_OR_GREATER + , TimeProvider +#endif + ); public StreamingHubClientOptions WithSerializerProvider(IMagicOnionSerializerProvider serializerProvider) - => new(Host, CallOptions, serializerProvider, Logger, HeartbeatInterval, HeartbeatReceivedFromServer); + => new( + Host, CallOptions, serializerProvider, Logger + , ClientHeartbeatInterval, ClientHeartbeatTimeout, OnServerHeartbeatReceived, OnClientHeartbeatResponseReceived +#if NET8_0_OR_GREATER + , TimeProvider +#endif + ); public StreamingHubClientOptions WithLogger(IMagicOnionClientLogger logger) - => new(Host, CallOptions, SerializerProvider, logger, HeartbeatInterval, HeartbeatReceivedFromServer); + => new(Host, CallOptions, SerializerProvider, logger + , ClientHeartbeatInterval, ClientHeartbeatTimeout, OnServerHeartbeatReceived, OnClientHeartbeatResponseReceived +#if NET8_0_OR_GREATER + , TimeProvider +#endif + ); /// /// Sets a heartbeat interval. If a value is , the heartbeat from the client is disabled. /// /// /// - public StreamingHubClientOptions WithHeartbeatInterval(TimeSpan? interval) - => new(Host, CallOptions, SerializerProvider, Logger, interval, HeartbeatReceivedFromServer); + public StreamingHubClientOptions WithClientHeartbeatInterval(TimeSpan? interval) + => new(Host, CallOptions, SerializerProvider, Logger + , interval, ClientHeartbeatTimeout, OnServerHeartbeatReceived, OnClientHeartbeatResponseReceived +#if NET8_0_OR_GREATER + , TimeProvider +#endif + ); + + /// + /// Sets a heartbeat timeout period. If a value is , the client does not time out. + /// + /// + /// + public StreamingHubClientOptions WithClientHeartbeatTimeout(TimeSpan? timeout) + => new(Host, CallOptions, SerializerProvider, Logger + , ClientHeartbeatInterval, timeout, OnServerHeartbeatReceived, OnClientHeartbeatResponseReceived +#if NET8_0_OR_GREATER + , TimeProvider +#endif + ); /// /// Sets a heartbeat callback. If additional metadata is provided by the server in the heartbeat message, this metadata is provided as an argument. /// - /// + /// /// - public StreamingHubClientOptions WithHeartbeatReceived(Action>? onHeartbeatReceived) - => new(Host, CallOptions, SerializerProvider, Logger, HeartbeatInterval, onHeartbeatReceived); + public StreamingHubClientOptions WithServerHeartbeatReceived(Action>? onServerHeartbeatReceived) + => new(Host, CallOptions, SerializerProvider, Logger + , ClientHeartbeatInterval, ClientHeartbeatTimeout, onServerHeartbeatReceived, OnClientHeartbeatResponseReceived +#if NET8_0_OR_GREATER + , TimeProvider +#endif + ); + + /// + /// Sets a client heartbeat response callback. + /// + /// + /// + public StreamingHubClientOptions WithClientHeartbeatResponseReceived(Action? onClientHeartbeatResponseReceived) + => new(Host, CallOptions, SerializerProvider, Logger + , ClientHeartbeatInterval, ClientHeartbeatTimeout, OnServerHeartbeatReceived, onClientHeartbeatResponseReceived +#if NET8_0_OR_GREATER + , TimeProvider +#endif + ); + +#if NET8_0_OR_GREATER + /// + /// Sets a + /// + /// + /// + public StreamingHubClientOptions WithTimeProvider(TimeProvider timeProvider) + => new(Host, CallOptions, SerializerProvider, Logger + , ClientHeartbeatInterval, ClientHeartbeatTimeout, OnServerHeartbeatReceived, OnClientHeartbeatResponseReceived + , timeProvider + ); +#endif } public abstract class StreamingHubClientBase @@ -83,21 +172,18 @@ public abstract class StreamingHubClientBase readonly Dictionary responseFutures = new(); readonly TaskCompletionSource waitForDisconnect = new(); readonly CancellationTokenSource cancellationTokenSource = new(); - readonly Dictionary postCallbackCache = new(); - SendOrPostCallback? heartbeatCallbackCache; + int messageIdSequence = 0; bool disposed; - Task? heartbeatTask; - DateTimeOffset lastHeartbeatSentAt; - readonly Channel writerQueue = Channel.CreateUnbounded(new UnboundedChannelOptions() { SingleReader = true, SingleWriter = false, AllowSynchronousContinuations = false }); Task? writerTask; IClientStreamWriter writer = default!; IAsyncStreamReader reader = default!; + StreamingHubClientHeartbeatManager heartbeatManager = default!; Task subscription = default!; protected readonly TReceiver receiver; @@ -183,12 +269,29 @@ static Method CreateConnectMethod(stri async Task StartSubscribe(SynchronizationContext? syncContext, Task firstMoveNext) { - if (options.HeartbeatInterval is { } heartbeatInterval) + var cancellationToken = cancellationTokenSource.Token; + + heartbeatManager = new StreamingHubClientHeartbeatManager( + writerQueue.Writer, + options.ClientHeartbeatInterval ?? TimeSpan.Zero /* Disable */, + options.ClientHeartbeatTimeout ?? Timeout.InfiniteTimeSpan, + options.OnServerHeartbeatReceived, + options.OnClientHeartbeatResponseReceived, + syncContext, + cancellationTokenSource.Token +#if NET8_0_OR_GREATER + , options.TimeProvider ?? TimeProvider.System +#endif + ); + + // Activate the Heartbeat Manager if enabled in the options. + if (options.ClientHeartbeatInterval is {} heartbeatInterval && heartbeatInterval > TimeSpan.Zero) { - heartbeatTask = RunHeartbeatLoopAsync(heartbeatInterval, cancellationTokenSource.Token); + heartbeatManager.StartClientHeartbeatLoop(); + cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(heartbeatManager.TimeoutToken, cancellationTokenSource.Token).Token; } - writerTask = RunWriterLoopAsync(cancellationTokenSource.Token); + writerTask = RunWriterLoopAsync(cancellationToken); var reader = this.reader; try @@ -214,7 +317,7 @@ async Task StartSubscribe(SynchronizationContext? syncContext, Task firstM } } - moveNext = reader.MoveNext(cancellationTokenSource.Token); + moveNext = reader.MoveNext(cancellationToken); } } catch (Exception ex) @@ -236,6 +339,8 @@ async Task StartSubscribe(SynchronizationContext? syncContext, Task firstM } finally { + heartbeatManager.Dispose(); + try { #if !UNITY_WEBGL @@ -255,10 +360,6 @@ async Task StartSubscribe(SynchronizationContext? syncContext, Task firstM } } - // MessageFormat: - // broadcast: [methodId, [argument]] - // response: [messageId, methodId, response] - // error-response: [messageId, statusCode, detail, StringMessage] void ConsumeData(SynchronizationContext? syncContext, StreamingHubPayload payload) { var messageReader = new StreamingHubClientMessageReader(payload.Memory); @@ -276,8 +377,11 @@ void ConsumeData(SynchronizationContext? syncContext, StreamingHubPayload payloa case StreamingHubMessageType.ClientResultRequest: ProcessClientResultRequest(syncContext, payload, ref messageReader); break; - case StreamingHubMessageType.Heartbeat: - ProcessHeartbeat(syncContext, payload, ref messageReader); + case StreamingHubMessageType.ServerHeartbeat: + heartbeatManager.ProcessServerHeartbeat(payload); + break; + case StreamingHubMessageType.ClientHeartbeatResponse: + heartbeatManager.ProcessClientHeartbeatResponse(payload); break; } } @@ -386,46 +490,6 @@ void ProcessClientResultRequest(SynchronizationContext? syncContext, StreamingHu } } - void ProcessHeartbeat(SynchronizationContext? syncContext, StreamingHubPayload payload, ref StreamingHubClientMessageReader messageReader) - { - var metadata = messageReader.ReadHeartbeat(); - if (this.options.HeartbeatReceivedFromServer is { } heartbeatReceived) - { - if (syncContext is null) - { - heartbeatReceived(metadata); - StreamingHubPayloadPool.Shared.Return(payload); - } - else - { - heartbeatCallbackCache ??= CreateHeartbeatCallback(heartbeatReceived); - syncContext.Post(heartbeatCallbackCache, payload); - } - } - WriteHeartbeat(); - } - - SendOrPostCallback CreateHeartbeatCallback(Action> heartbeatReceivedAction) => (state) => - { - var p = (StreamingHubPayload)state!; - heartbeatReceivedAction(p.Memory.Slice(5)); - StreamingHubPayloadPool.Shared.Return(p); - }; - - async Task RunHeartbeatLoopAsync(TimeSpan heartbeatInterval, CancellationToken cancellationToken) - { - while (!cancellationToken.IsCancellationRequested) - { - await Task.Delay(heartbeatInterval, cancellationToken).ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); - - if ((DateTimeOffset.UtcNow - lastHeartbeatSentAt) > heartbeatInterval) - { - WriteHeartbeat(); - } - } - } - async Task RunWriterLoopAsync(CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) @@ -440,15 +504,6 @@ async Task RunWriterLoopAsync(CancellationToken cancellationToken) } } - void WriteHeartbeat() - { - if (disposed) return; - var v = BuildHeartbeatMessage(); - _ = writerQueue.Writer.TryWrite(v); - - lastHeartbeatSentAt = DateTimeOffset.UtcNow; - } - protected Task WriteMessageFireAndForgetTaskAsync(int methodId, TRequest message) => WriteMessageFireAndForgetValueTaskOfTAsync(methodId, message).AsTask(); @@ -674,12 +729,5 @@ StreamingHubPayload BuildClientResultResponseMessageForError(int methodId, Guid StreamingHubMessageWriter.WriteClientResultResponseMessageForError(buffer, methodId, messageId, statusCode, detail, ex, messageSerializer); return StreamingHubPayloadPool.Shared.RentOrCreate(buffer.WrittenSpan); } - - StreamingHubPayload BuildHeartbeatMessage() - { - using var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter(); - StreamingHubMessageWriter.WriteHeartbeatMessageForClientToServer(buffer); - return StreamingHubPayloadPool.Shared.RentOrCreate(buffer.WrittenSpan); - } } } diff --git a/src/MagicOnion.Client/StreamingHubClientHeartbeatManager.cs b/src/MagicOnion.Client/StreamingHubClientHeartbeatManager.cs new file mode 100644 index 000000000..22506f613 --- /dev/null +++ b/src/MagicOnion.Client/StreamingHubClientHeartbeatManager.cs @@ -0,0 +1,204 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using MagicOnion.Internal; +using MagicOnion.Internal.Buffers; +using MessagePack; + +namespace MagicOnion.Client +{ + public readonly struct ClientHeartbeatEvent + { + /// + /// Gets the round trip time (RTT) between client and server. + /// + public TimeSpan RoundTripTime { get; } + + public ClientHeartbeatEvent(long roundTripTimeMs) + { + RoundTripTime = TimeSpan.FromMilliseconds(roundTripTimeMs); + } + } + + internal class StreamingHubClientHeartbeatManager : IDisposable + { + readonly CancellationTokenSource timeoutTokenSource; + readonly CancellationTokenSource shutdownTokenSource; + readonly TimeSpan heartbeatInterval; + readonly TimeSpan timeoutPeriod; + readonly Action>? onServerHeartbeatReceived; + readonly Action? onClientHeartbeatResponseReceived; + readonly SynchronizationContext? synchronizationContext; + readonly ChannelWriter writer; +#if NET8_0_OR_GREATER + readonly TimeProvider timeProvider; +#endif + + SendOrPostCallback? serverHeartbeatCallbackCache; + SendOrPostCallback? clientHeartbeatResponseCallbackCache; + Task? heartbeatLoopTask; + + public CancellationToken TimeoutToken => timeoutTokenSource.Token; + + public StreamingHubClientHeartbeatManager( + ChannelWriter writer, + TimeSpan heartbeatInterval, + TimeSpan timeoutPeriod, + Action>? onServerHeartbeatReceived, + Action? onClientHeartbeatResponseReceived, + SynchronizationContext? synchronizationContext, + CancellationToken shutdownToken +#if NET8_0_OR_GREATER + , TimeProvider timeProvider +#endif + ) + { + this.timeoutTokenSource = new( +#if NET8_0_OR_GREATER + Timeout.InfiniteTimeSpan, timeProvider +#endif + ); + this.writer = writer; + this.heartbeatInterval = heartbeatInterval; + this.timeoutPeriod = timeoutPeriod; + this.onServerHeartbeatReceived = onServerHeartbeatReceived; + this.onClientHeartbeatResponseReceived = onClientHeartbeatResponseReceived; + this.synchronizationContext = synchronizationContext; + this.shutdownTokenSource = CancellationTokenSource.CreateLinkedTokenSource(shutdownToken, timeoutTokenSource.Token); +#if NET8_0_OR_GREATER + this.timeProvider = timeProvider; +#endif + } + + public void StartClientHeartbeatLoop() + { + heartbeatLoopTask = RunClientHeartbeatLoopAsync(); + } + + async Task RunClientHeartbeatLoopAsync() + { + while (!shutdownTokenSource.IsCancellationRequested) + { + await Task.Delay(heartbeatInterval +#if NET8_0_OR_GREATER + , timeProvider +#endif + , shutdownTokenSource.Token).ConfigureAwait(false); + + shutdownTokenSource.Token.ThrowIfCancellationRequested(); + + // Writes a ClientHeartbeat to the writer queue. + _ = writer.TryWrite(BuildClientHeartbeatMessage()); + + // Start/Restart the timeout cancellation timer. + timeoutTokenSource.CancelAfter(timeoutPeriod); + } + } + + public void ProcessClientHeartbeatResponse(StreamingHubPayload payload) + { + if (shutdownTokenSource.IsCancellationRequested) return; + + // Cancel the running timeout cancellation timer. + timeoutTokenSource.CancelAfter(Timeout.InfiniteTimeSpan); + + if (onClientHeartbeatResponseReceived is { } heartbeatReceived) + { + clientHeartbeatResponseCallbackCache ??= CreateClientHeartbeatResponseCallback(heartbeatReceived); + + if (synchronizationContext is null) + { + clientHeartbeatResponseCallbackCache(payload); + } + else + { + synchronizationContext.Post(clientHeartbeatResponseCallbackCache, payload); + } + } + } + + public void ProcessServerHeartbeat(StreamingHubPayload payload) + { + if (shutdownTokenSource.IsCancellationRequested) return; + + if (onServerHeartbeatReceived is { } heartbeatReceived) + { + serverHeartbeatCallbackCache ??= CreateServerHeartbeatCallback(heartbeatReceived); + + if (synchronizationContext is null) + { + serverHeartbeatCallbackCache(payload); + } + else + { + synchronizationContext.Post(serverHeartbeatCallbackCache, payload); + } + } + + // Writes a ServerHeartbeatResponse to the writer queue. + _ = writer.TryWrite(BuildServerHeartbeatMessage()); + } + + SendOrPostCallback CreateClientHeartbeatResponseCallback(Action heartbeatReceivedAction) => (state) => + { + var p = (StreamingHubPayload)state!; + + var reader = new StreamingHubClientMessageReader(p.Memory); + _ = reader.ReadMessageType(); + +#if NET8_0_OR_GREATER + var now = timeProvider.GetUtcNow(); +#else + var now = DateTimeOffset.UtcNow; +#endif + var sentAt = reader.ReadClientHeartbeatResponse(); + var elapsed = now.ToUnixTimeMilliseconds() - sentAt; + + heartbeatReceivedAction(new ClientHeartbeatEvent(elapsed)); + StreamingHubPayloadPool.Shared.Return(p); + }; + + SendOrPostCallback CreateServerHeartbeatCallback(Action> heartbeatReceivedAction) => (state) => + { + var p = (StreamingHubPayload)state!; + var remain = p.Memory.Slice(5); // header + heartbeatReceivedAction(remain); + StreamingHubPayloadPool.Shared.Return(p); + }; + + StreamingHubPayload BuildServerHeartbeatMessage() + { + using var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter(); + StreamingHubMessageWriter.WriteServerHeartbeatMessageResponse(buffer); + return StreamingHubPayloadPool.Shared.RentOrCreate(buffer.WrittenSpan); + } + + StreamingHubPayload BuildClientHeartbeatMessage() + { + using var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter(); + StreamingHubMessageWriter.WriteClientHeartbeatMessageHeader(buffer); + +#if NET8_0_OR_GREATER + var now = timeProvider.GetUtcNow(); +#else + var now = DateTimeOffset.UtcNow; +#endif + + // Extra: [SentAt(long)] + var writer = new MessagePackWriter(buffer); + writer.WriteArrayHeader(1); + writer.Write(now.ToUnixTimeMilliseconds()); + writer.Flush(); + return StreamingHubPayloadPool.Shared.RentOrCreate(buffer.WrittenSpan); + } + + public void Dispose() + { + shutdownTokenSource.Cancel(); + shutdownTokenSource.Dispose(); + timeoutTokenSource.Dispose(); + } + } +} diff --git a/src/MagicOnion.Internal/StreamingHubClientMessageReader.cs b/src/MagicOnion.Internal/StreamingHubClientMessageReader.cs index 247caec1f..5d4b59474 100644 --- a/src/MagicOnion.Internal/StreamingHubClientMessageReader.cs +++ b/src/MagicOnion.Internal/StreamingHubClientMessageReader.cs @@ -24,8 +24,9 @@ public StreamingHubMessageType ReadMessageType() 4 => StreamingHubMessageType.ResponseWithError, 5 => reader.ReadByte() switch { - 0x00 /* 0:ClientResultRequest */ => StreamingHubMessageType.ClientResultRequest, - 0x7f /* 127:Heartbeat */ => StreamingHubMessageType.Heartbeat, + 0x00 /* 0:ClientResultRequest */ => StreamingHubMessageType.ClientResultRequest, + 0x7e /* 126:ClientHeartbeatResponse */ => StreamingHubMessageType.ClientHeartbeatResponse, + 0x7f /* 127:ServerHeartbeat */ => StreamingHubMessageType.ServerHeartbeat, var x => throw new InvalidOperationException($"Unknown Type: {x}"), }, _ => throw new InvalidOperationException($"Unknown message format: ArrayLength = {arrayLength}"), @@ -73,7 +74,7 @@ public StreamingHubMessageType ReadMessageType() return (clientRequestMessageId, methodId, data.Slice(offset)); } - public ReadOnlyMemory ReadHeartbeat() + public ReadOnlyMemory ReadServerHeartbeat() { //var type = reader.ReadByte(); // Type is already read by ReadMessageType reader.Skip(); // Dummy (1) @@ -82,5 +83,20 @@ public ReadOnlyMemory ReadHeartbeat() return data.Slice((int)reader.Consumed); } + + public long ReadClientHeartbeatResponse() + { + //var type = reader.ReadByte(); // Type is already read by ReadMessageType + reader.Skip(); // Dummy (1) + reader.Skip(); // Dummy (2) + reader.Skip(); // Dummy (3) + + // Extra: [SentAt(long)] + var arrayLen = reader.ReadArrayHeader(); + if (arrayLen == 0) throw new InvalidOperationException("Invalid client heartbeat response. An extra data is empty."); + var sentAt = reader.ReadInt64(); + + return sentAt; + } } } diff --git a/src/MagicOnion.Internal/StreamingHubMessageWriter.cs b/src/MagicOnion.Internal/StreamingHubMessageWriter.cs index 93832281e..9be226ff4 100644 --- a/src/MagicOnion.Internal/StreamingHubMessageWriter.cs +++ b/src/MagicOnion.Internal/StreamingHubMessageWriter.cs @@ -26,9 +26,13 @@ namespace MagicOnion.Internal /// Array(5): [Type=0x00, Nil, ClientResultMessageId(Guid), MethodId(int), SerializedArguments] /// /// - /// Heartbeat: + /// ServerHeartbeat/Request: /// Array(5): [Type=0x7f, Nil, Nil, Nil, Extras] /// + /// + /// ClientHeartbeat/Response: + /// Array(5): [Type=0x7e, Nil, Nil, Nil, [ClientTime(long)]] + /// /// /// StreamingHub message formats (from Client to Server): /// @@ -49,9 +53,13 @@ namespace MagicOnion.Internal /// Array(4): [Type=0x01, ClientResultMessageId(Guid), MethodId(int), [StatusCode(int), Detail(string), Message(string)]] /// /// - /// Heartbeat/Response: + /// ServerHeartbeat/Response: /// Array(4): [Type=0x7f, Nil, Nil, Nil] /// + /// + /// ClientHeartbeat/Request: + /// Array(4): [Type=0x7e, Nil, Nil, [ClientTime(long)]] + /// /// /// internal static class StreamingHubMessageWriter @@ -193,58 +201,138 @@ public static void WriteClientResultResponseMessageForError(IBufferWriter // Array(5)[127, Nil, Nil, Nil, ] - static ReadOnlySpan HeartbeatMessageForServerToClientHeader => new byte[] { 0x95, 0x7f, 0xc0, 0xc0, 0xc0 }; + static ReadOnlySpan ServerHeartbeatMessageForServerToClientHeader => new byte[] { 0x95, 0x7f, 0xc0, 0xc0, 0xc0 }; /// - /// Writes a heartbeat message for sending from the server. + /// Writes a server heartbeat message for sending from the server. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteHeartbeatMessageForServerToClientHeader(IBufferWriter bufferWriter) + public static void WriteServerHeartbeatMessageHeader(IBufferWriter bufferWriter) { - bufferWriter.Write(HeartbeatMessageForServerToClientHeader); + bufferWriter.Write(ServerHeartbeatMessageForServerToClientHeader); //var writer = new MessagePackWriter(bufferWriter); //writer.WriteArrayHeader(5); //writer.Write(0x7f); // Type = 0x7f / 127 (Heartbeat) + //writer.WriteNil(); // Dummy + //writer.WriteNil(); // Dummy + //writer.WriteNil(); // Dummy + //writer.Flush(); + // // + } + + // Array(4)[127, Nil, Nil, Nil] + static ReadOnlySpan ServerHeartbeatMessageForClientToServer => new byte[] { 0x94, 0x7f, 0xc0, 0xc0, 0xc0 }; + + /// + /// Writes a server heartbeat message for sending response from the client. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteServerHeartbeatMessageResponse(IBufferWriter bufferWriter) + { + bufferWriter.Write(ServerHeartbeatMessageForClientToServer); + //var writer = new MessagePackWriter(bufferWriter); + //writer.WriteArrayHeader(4); + //writer.Write(0x7f); // Type = 0x7f / 127 (Heartbeat) //writer.WriteNil(); // Dummy //writer.WriteNil(); // Dummy //writer.WriteNil(); // Dummy //writer.Flush(); } - // Array(4)[127, Nil, Nil, Nil] - static ReadOnlySpan HeartbeatMessageForClientToServer => new byte[] { 0x94, 0x7f, 0xc0, 0xc0, 0xc0 }; + // Array(4)[0x7e(126), Nil, Nil, ] + static ReadOnlySpan ClientHeartbeatMessageHeader => new byte[] { 0x94, 0x7e, 0xc0, 0xc0 }; /// - /// Writes a heartbeat message for sending from the client. + /// Writes a client heartbeat message for sending from the client. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteHeartbeatMessageForClientToServer(IBufferWriter bufferWriter) + public static void WriteClientHeartbeatMessageHeader(IBufferWriter bufferWriter) { - bufferWriter.Write(HeartbeatMessageForClientToServer); + bufferWriter.Write(ClientHeartbeatMessageHeader); //var writer = new MessagePackWriter(bufferWriter); //writer.WriteArrayHeader(4); - //writer.Write(0x7f); // Type = 0x7f / 127 (Heartbeat) + //writer.Write(0x7f); // Type = 0x7e / 126 (ClientHeartbeat) + //writer.WriteNil(); // Dummy + //writer.WriteNil(); // Dummy + //writer.Flush(); + // // + } + + // Array(5)[0x7e(126), Nil, Nil, Nil, ] + static ReadOnlySpan ClientHeartbeatMessageResponseHeader => new byte[] { 0x95, 0x7e, 0xc0, 0xc0, 0xc0 }; + + /// + /// Writes a client heartbeat message for sending response from the server. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteClientHeartbeatMessageResponseHeader(IBufferWriter bufferWriter) + { + bufferWriter.Write(ClientHeartbeatMessageResponseHeader); + //var writer = new MessagePackWriter(bufferWriter); + //writer.WriteArrayHeader(5); + //writer.Write(0x7f); // Type = 0x7e / 126 (Heartbeat) //writer.WriteNil(); // Dummy //writer.WriteNil(); // Dummy //writer.WriteNil(); // Dummy //writer.Flush(); + // // } } internal enum StreamingHubMessageType { - // Client to Server + /// + /// Request: Client -> Server + /// Request, + /// + /// Request: Client -> Server / Fire-and-Forget + /// RequestFireAndForget, + /// + /// Request: Client -> Server -> Client + /// Response, + /// + /// Request: Client -> Server -(Error)-> Client + /// ResponseWithError, - HeartbeatResponse, - // Server to Client + /// + /// Broadcast: Server -> Client + /// Broadcast, + + /// + /// ClientResult: Server -> Client + /// ClientResultRequest, + /// + /// ClientResult: Server -> Client -> Server + /// ClientResultResponse, + /// + /// ClientResult: Server -> Client -(Error)-> Server + /// ClientResultResponseWithError, - Heartbeat, + + + /// + /// Heartbeat: Server -> Client -> Server + /// + ServerHeartbeatResponse, + /// + /// Heartbeat: Server -> Client + /// + ServerHeartbeat, + + /// + /// Heartbeat: Client -> Server + /// + ClientHeartbeat, + /// + /// Heartbeat: Client -> Server -> Client + /// + ClientHeartbeatResponse, } } diff --git a/src/MagicOnion.Internal/StreamingHubPayload.cs b/src/MagicOnion.Internal/StreamingHubPayload.cs index d3387100f..d4c5b1ee0 100644 --- a/src/MagicOnion.Internal/StreamingHubPayload.cs +++ b/src/MagicOnion.Internal/StreamingHubPayload.cs @@ -104,12 +104,21 @@ public void Initialize(ReadOnlySequence data) memory = buffer.AsMemory(0, (int)data.Length); } - public void Initialize(ReadOnlyMemory data) + public void Initialize(ReadOnlyMemory data, bool holdReference) { ThrowIfUsing(); - buffer = null; - memory = data; + if (holdReference) + { + buffer = null; + memory = data; + } + else + { + buffer = ArrayPool.Shared.Rent((int)data.Length); + data.CopyTo(buffer); + memory = buffer.AsMemory(0, (int)data.Length); + } } public void Uninitialize() diff --git a/src/MagicOnion.Internal/StreamingHubPayloadPool.BuiltIn.cs b/src/MagicOnion.Internal/StreamingHubPayloadPool.BuiltIn.cs index 8a8334abb..2245d3e88 100644 --- a/src/MagicOnion.Internal/StreamingHubPayloadPool.BuiltIn.cs +++ b/src/MagicOnion.Internal/StreamingHubPayloadPool.BuiltIn.cs @@ -105,10 +105,10 @@ public StreamingHubPayload RentOrCreate(ReadOnlySpan data) #endif } - public StreamingHubPayload RentOrCreate(ReadOnlyMemory data) + public StreamingHubPayload RentOrCreate(ReadOnlyMemory data, bool holdReference) { var payload = pool.RentOrCreateCore(); - payload.Initialize(data); + payload.Initialize(data, holdReference); #if DEBUG return new StreamingHubPayload(payload); #else diff --git a/src/MagicOnion.Internal/StreamingHubPayloadPool.ObjectPool.cs b/src/MagicOnion.Internal/StreamingHubPayloadPool.ObjectPool.cs index 62c85b124..1a0948825 100644 --- a/src/MagicOnion.Internal/StreamingHubPayloadPool.ObjectPool.cs +++ b/src/MagicOnion.Internal/StreamingHubPayloadPool.ObjectPool.cs @@ -34,10 +34,10 @@ public StreamingHubPayload RentOrCreate(ReadOnlySpan data) #endif } - public StreamingHubPayload RentOrCreate(ReadOnlyMemory data) + public StreamingHubPayload RentOrCreate(ReadOnlyMemory data, bool holdReference) { var payload = pool.Get(); - payload.Initialize(data); + payload.Initialize(data, holdReference); #if DEBUG return new StreamingHubPayload(payload); #else diff --git a/src/MagicOnion.Internal/StreamingHubServerMessageReader.cs b/src/MagicOnion.Internal/StreamingHubServerMessageReader.cs index b1d8326cf..ae03323b2 100644 --- a/src/MagicOnion.Internal/StreamingHubServerMessageReader.cs +++ b/src/MagicOnion.Internal/StreamingHubServerMessageReader.cs @@ -25,7 +25,8 @@ public StreamingHubMessageType ReadMessageType() { 0x00 => StreamingHubMessageType.ClientResultResponse, 0x01 => StreamingHubMessageType.ClientResultResponseWithError, - 0x7f => StreamingHubMessageType.HeartbeatResponse, + 0x7e => StreamingHubMessageType.ClientHeartbeat, + 0x7f => StreamingHubMessageType.ServerHeartbeatResponse, var subType => throw new InvalidOperationException($"Unknown client response message: {subType}"), }, _ => throw new InvalidOperationException($"Unknown message format: ArrayLength = {arrayLength}"), @@ -75,5 +76,14 @@ public StreamingHubMessageType ReadMessageType() return (clientResultMessageId, clientMethodId, statusCode, detail, message); } + + public ReadOnlyMemory ReadClientHeartbeat() + { + // [Nil, Nil, [SentAt(long)]] + reader.Skip(); // Dummy + reader.Skip(); // Dummy + + return data.Slice((int)reader.Consumed); + } } } diff --git a/src/MagicOnion.Server/Hubs/StreamingHub.cs b/src/MagicOnion.Server/Hubs/StreamingHub.cs index 0b6da9f9c..f4a463cb9 100644 --- a/src/MagicOnion.Server/Hubs/StreamingHub.cs +++ b/src/MagicOnion.Server/Hubs/StreamingHub.cs @@ -1,3 +1,4 @@ +using System.Buffers; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading.Channels; @@ -5,6 +6,7 @@ using Cysharp.Runtime.Multicast.Remoting; using Grpc.Core; using MagicOnion.Internal; +using MagicOnion.Internal.Buffers; using MagicOnion.Server.Diagnostics; using MagicOnion.Server.Internal; using MessagePack; @@ -224,11 +226,22 @@ ValueTask ProcessMessageAsync(StreamingHubPayload payload, UniqueHashDictionary< } return default; } - case StreamingHubMessageType.HeartbeatResponse: + case StreamingHubMessageType.ServerHeartbeatResponse: { heartbeatHandle.Ack(); return default; } + case StreamingHubMessageType.ClientHeartbeat: + { + var heartbeatBody = reader.ReadClientHeartbeat(); + + using var bufferWriter = ArrayPoolBufferWriter.RentThreadStaticWriter(); + StreamingHubMessageWriter.WriteClientHeartbeatMessageResponseHeader(bufferWriter); + bufferWriter.Write(heartbeatBody.Span); + + StreamingServiceContext.QueueResponseStreamWrite(StreamingHubPayloadPool.Shared.RentOrCreate(bufferWriter.WrittenSpan)); + return default; + } default: throw new InvalidOperationException($"Unknown MessageType: {messageType}"); } diff --git a/src/MagicOnion.Server/Hubs/StreamingHubHeartbeatManager.cs b/src/MagicOnion.Server/Hubs/StreamingHubHeartbeatManager.cs index d42853f6d..00ad58931 100644 --- a/src/MagicOnion.Server/Hubs/StreamingHubHeartbeatManager.cs +++ b/src/MagicOnion.Server/Hubs/StreamingHubHeartbeatManager.cs @@ -23,12 +23,16 @@ internal class StreamingHubHeartbeatHandle : IDisposable public IStreamingServiceContext ServiceContext { get; } public CancellationToken TimeoutToken => timeoutToken.Token; - public StreamingHubHeartbeatHandle(IStreamingHubHeartbeatManager manager, IStreamingServiceContext serviceContext, TimeSpan timeoutDuration) + public StreamingHubHeartbeatHandle(IStreamingHubHeartbeatManager manager, IStreamingServiceContext serviceContext, TimeSpan timeoutDuration, TimeProvider timeProvider) { this.manager = manager; this.ServiceContext = serviceContext; this.timeoutDuration = timeoutDuration; - this.timeoutToken = new CancellationTokenSource(); + this.timeoutToken = new CancellationTokenSource(Timeout.InfiniteTimeSpan +#if NET8_0_OR_GREATER + , timeProvider +#endif + ); } public void RestartTimeoutTimer() @@ -59,7 +63,7 @@ internal class NopStreamingHubHeartbeatManager : IStreamingHubHeartbeatManager NopStreamingHubHeartbeatManager() {} public StreamingHubHeartbeatHandle Register(IStreamingServiceContext serviceContext) - => new(this, serviceContext, Timeout.InfiniteTimeSpan); + => new(this, serviceContext, Timeout.InfiniteTimeSpan, TimeProvider.System); public void Unregister(IStreamingServiceContext serviceContext) { } public void Dispose() { } } @@ -72,23 +76,25 @@ internal class StreamingHubHeartbeatManager : IStreamingHubHeartbeatManager readonly IStreamingHubHeartbeatMetadataProvider? heartbeatMetadataProvider; readonly TimeSpan heartbeatInterval; readonly TimeSpan timeoutDuration; + readonly TimeProvider timeProvider; readonly ILogger logger; PeriodicTimer? timer; int registeredCount; ConcurrentDictionary contexts = new(); - public StreamingHubHeartbeatManager(TimeSpan heartbeatInterval, TimeSpan timeoutDuration, IStreamingHubHeartbeatMetadataProvider? heartbeatMetadataProvider, ILogger logger) + public StreamingHubHeartbeatManager(TimeSpan heartbeatInterval, TimeSpan timeoutDuration, IStreamingHubHeartbeatMetadataProvider? heartbeatMetadataProvider, TimeProvider timeProvider, ILogger logger) { this.heartbeatInterval = heartbeatInterval; this.timeoutDuration = timeoutDuration; this.heartbeatMetadataProvider = heartbeatMetadataProvider; + this.timeProvider = timeProvider; this.logger = logger; } public StreamingHubHeartbeatHandle Register(IStreamingServiceContext serviceContext) { - var handle = new StreamingHubHeartbeatHandle(this, serviceContext, timeoutDuration); + var handle = new StreamingHubHeartbeatHandle(this, serviceContext, timeoutDuration, timeProvider); if (contexts.TryAdd(serviceContext.ContextId, handle)) { if (Interlocked.Increment(ref registeredCount) == 1) @@ -97,7 +103,11 @@ public StreamingHubHeartbeatHandle Register(IStreamingServiceContext(); while (await runningTimer.WaitForNextTickAsync()) { - StreamingHubMessageWriter.WriteHeartbeatMessageForServerToClientHeader(writer); + StreamingHubMessageWriter.WriteServerHeartbeatMessageHeader(writer); if (!(heartbeatMetadataProvider?.TryWriteMetadata(writer) ?? false)) { writer.Write(Nil); diff --git a/src/MagicOnion.Server/MagicOnionEngine.cs b/src/MagicOnion.Server/MagicOnionEngine.cs index 871ad4e0f..f168a7e8a 100644 --- a/src/MagicOnion.Server/MagicOnionEngine.cs +++ b/src/MagicOnion.Server/MagicOnionEngine.cs @@ -254,6 +254,7 @@ public static MagicOnionServiceDefinition BuildServerServiceDefinition(IServiceP heartbeatInterval.Value, heartbeatTimeout ?? Timeout.InfiniteTimeSpan, heartbeatMetadataProvider ?? serviceProvider.GetService(), + TimeProvider.System, serviceProvider.GetRequiredService>() ); } diff --git a/tests/MagicOnion.Client.Tests/MagicOnion.Client.Tests.csproj b/tests/MagicOnion.Client.Tests/MagicOnion.Client.Tests.csproj index 13484171c..957b2448f 100644 --- a/tests/MagicOnion.Client.Tests/MagicOnion.Client.Tests.csproj +++ b/tests/MagicOnion.Client.Tests/MagicOnion.Client.Tests.csproj @@ -13,6 +13,7 @@ + diff --git a/tests/MagicOnion.Client.Tests/StreamingHubClientHeartbeatManagerTest.cs b/tests/MagicOnion.Client.Tests/StreamingHubClientHeartbeatManagerTest.cs new file mode 100644 index 000000000..0f88a74c7 --- /dev/null +++ b/tests/MagicOnion.Client.Tests/StreamingHubClientHeartbeatManagerTest.cs @@ -0,0 +1,208 @@ +using System.Buffers; +using System.Diagnostics; +using System.Threading.Channels; +using MagicOnion.Internal; +using Microsoft.Extensions.Time.Testing; + +namespace MagicOnion.Client.Tests; + +public class StreamingHubClientHeartbeatManagerTest +{ + [Fact] + public async Task Interval_TimeoutDisabled() + { + // Arrange + var channel = Channel.CreateUnbounded(); + var interval = TimeSpan.FromSeconds(1); + var timeout = Timeout.InfiniteTimeSpan; + var serverHeartbeatReceived = new List>(); + var clientHeartbeatResponseReceived = new List(); + var origin = new DateTimeOffset(2024, 7, 1, 0, 0, 0, TimeSpan.Zero); + var timeProvider = new FakeTimeProvider(origin); + using var manager = new StreamingHubClientHeartbeatManager( + channel.Writer, + interval, + timeout, + onServerHeartbeatReceived: x => serverHeartbeatReceived.Add(x), + onClientHeartbeatResponseReceived: x => clientHeartbeatResponseReceived.Add(x), + synchronizationContext: null, + shutdownToken: CancellationToken.None, + timeProvider + ); + + // Act + manager.StartClientHeartbeatLoop(); + timeProvider.Advance(TimeSpan.FromSeconds(1)); + await Task.Delay(50); + timeProvider.Advance(TimeSpan.FromSeconds(1)); + await Task.Delay(50); + timeProvider.Advance(TimeSpan.FromSeconds(1)); + await Task.Delay(50); + + // Assert + Assert.True(channel.Reader.TryRead(out var heartbeat1)); + Assert.Equal((byte[])[0x94 /* Array(4) */, 0x7e /* 0x7e(127) */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0x91 /* Array(1) */, .. ToMessagePackBytes(origin.AddSeconds(1))], heartbeat1.Memory.ToArray()); + Assert.True(channel.Reader.TryRead(out var heartbeat2)); + Assert.Equal((byte[])[0x94 /* Array(4) */, 0x7e /* 0x7e(127) */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0x91 /* Array(1) */, .. ToMessagePackBytes(origin.AddSeconds(2))], heartbeat2.Memory.ToArray()); + Assert.True(channel.Reader.TryRead(out var heartbeat3)); + Assert.Equal((byte[])[0x94 /* Array(4) */, 0x7e /* 0x7e(127) */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0x91 /* Array(1) */, .. ToMessagePackBytes(origin.AddSeconds(3))], heartbeat3.Memory.ToArray()); + + Assert.False(manager.TimeoutToken.IsCancellationRequested); + } + + [Fact] + public async Task Elapsed_RoundTripTime() + { + // Arrange + var channel = Channel.CreateUnbounded(); + var interval = TimeSpan.FromSeconds(1); + var timeout = Timeout.InfiniteTimeSpan; + var serverHeartbeatReceived = new List>(); + var clientHeartbeatResponseReceived = new List(); + var origin = new DateTimeOffset(2024, 7, 1, 0, 0, 0, TimeSpan.Zero); + var timeProvider = new FakeTimeProvider(origin); + using var manager = new StreamingHubClientHeartbeatManager( + channel.Writer, + interval, + timeout, + onServerHeartbeatReceived: x => serverHeartbeatReceived.Add(x), + onClientHeartbeatResponseReceived: x => clientHeartbeatResponseReceived.Add(x), + synchronizationContext: null, + shutdownToken: CancellationToken.None, + timeProvider + ); + + // Act + manager.StartClientHeartbeatLoop(); + timeProvider.Advance(TimeSpan.FromMilliseconds(1000)); // Send + await Task.Delay(50); + timeProvider.Advance(TimeSpan.FromMilliseconds(100)); + await Task.Delay(50); + manager.ProcessClientHeartbeatResponse(StreamingHubPayloadPool.Shared.RentOrCreate([0x95 /* Array(5) */, 0x7e /* 0x7e(127) */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0x91 /* Array(1) */, .. ToMessagePackBytes(timeProvider.GetUtcNow().AddMilliseconds(-100))])); + + timeProvider.Advance(TimeSpan.FromMilliseconds(900)); // Send + await Task.Delay(50); + timeProvider.Advance(TimeSpan.FromMilliseconds(100)); + await Task.Delay(50); + manager.ProcessClientHeartbeatResponse(StreamingHubPayloadPool.Shared.RentOrCreate([0x95 /* Array(5) */, 0x7e /* 0x7e(127) */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0x91 /* Array(1) */, .. ToMessagePackBytes(timeProvider.GetUtcNow().AddMilliseconds(-100))])); + + timeProvider.Advance(TimeSpan.FromMilliseconds(900)); // Send + await Task.Delay(50); + timeProvider.Advance(TimeSpan.FromMilliseconds(100)); + await Task.Delay(50); + manager.ProcessClientHeartbeatResponse(StreamingHubPayloadPool.Shared.RentOrCreate([0x95 /* Array(5) */, 0x7e /* 0x7e(127) */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0x91 /* Array(1) */, .. ToMessagePackBytes(timeProvider.GetUtcNow().AddMilliseconds(-100))])); + + await Task.Delay(50); + + // Assert + Assert.Equal(3, clientHeartbeatResponseReceived.Count); + Assert.True(channel.Reader.TryRead(out var heartbeat1)); + Assert.Equal((byte[])[0x94 /* Array(4) */, 0x7e /* 0x7e(127) */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0x91 /* Array(1) */, .. ToMessagePackBytes(origin.AddSeconds(1))], heartbeat1.Memory.ToArray()); + Assert.Equal(TimeSpan.FromMilliseconds(100), clientHeartbeatResponseReceived[0].RoundTripTime); + Assert.True(channel.Reader.TryRead(out var heartbeat2)); + Assert.Equal((byte[])[0x94 /* Array(4) */, 0x7e /* 0x7e(127) */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0x91 /* Array(1) */, .. ToMessagePackBytes(origin.AddSeconds(2))], heartbeat2.Memory.ToArray()); + Assert.Equal(TimeSpan.FromMilliseconds(100), clientHeartbeatResponseReceived[1].RoundTripTime); + Assert.True(channel.Reader.TryRead(out var heartbeat3)); + Assert.Equal((byte[])[0x94 /* Array(4) */, 0x7e /* 0x7e(127) */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0x91 /* Array(1) */, .. ToMessagePackBytes(origin.AddSeconds(3))], heartbeat3.Memory.ToArray()); + Assert.Equal(TimeSpan.FromMilliseconds(100), clientHeartbeatResponseReceived[2].RoundTripTime); + + + Assert.False(manager.TimeoutToken.IsCancellationRequested); + } + + [Fact] + public async Task Timeout_Not_Responding() + { + // Arrange + var channel = Channel.CreateUnbounded(); + var interval = TimeSpan.FromSeconds(1); + var timeout = TimeSpan.FromMilliseconds(500); + var serverHeartbeatReceived = new List>(); + var clientHeartbeatResponseReceived = new List(); + var origin = new DateTimeOffset(2024, 7, 1, 0, 0, 0, TimeSpan.Zero); + var timeProvider = new FakeTimeProvider(origin); + using var manager = new StreamingHubClientHeartbeatManager( + channel.Writer, + interval, + timeout, + onServerHeartbeatReceived: x => serverHeartbeatReceived.Add(x), + onClientHeartbeatResponseReceived: x => clientHeartbeatResponseReceived.Add(x), + synchronizationContext: null, + shutdownToken: CancellationToken.None, + timeProvider + ); + + // Act + manager.StartClientHeartbeatLoop(); + timeProvider.Advance(TimeSpan.FromSeconds(1)); + await Task.Delay(50); + timeProvider.Advance(TimeSpan.FromSeconds(1)); + await Task.Delay(50); + + // Assert + Assert.True(channel.Reader.TryRead(out var heartbeat1)); + Assert.Equal((byte[])[0x94 /* Array(4) */, 0x7e /* 0x7e(127) */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0x91 /* Array(1) */, .. (ToMessagePackBytes(origin.AddSeconds(1)))], heartbeat1.Memory.ToArray()); + Assert.False(channel.Reader.TryRead(out var heartbeat2)); + + Assert.True(manager.TimeoutToken.IsCancellationRequested); + } + + [Fact] + public async Task Timeout_Keep() + { + // Arrange + var channel = Channel.CreateUnbounded(); + var interval = TimeSpan.FromSeconds(1); + var timeout = TimeSpan.FromMilliseconds(500); + var serverHeartbeatReceived = new List>(); + var clientHeartbeatResponseReceived = new List(); + var origin = new DateTimeOffset(2024, 7, 1, 0, 0, 0, TimeSpan.Zero); + var timeProvider = new FakeTimeProvider(origin); + using var manager = new StreamingHubClientHeartbeatManager( + channel.Writer, + interval, + timeout, + onServerHeartbeatReceived: x => serverHeartbeatReceived.Add(x), + onClientHeartbeatResponseReceived: x => clientHeartbeatResponseReceived.Add(x), + synchronizationContext: null, + shutdownToken: CancellationToken.None, + timeProvider + ); + + // Act && Assert + manager.StartClientHeartbeatLoop(); + + timeProvider.Advance(TimeSpan.FromSeconds(1)); // Send a client heartbeat message + await Task.Delay(50); + + Assert.True(channel.Reader.TryRead(out var heartbeat1)); + + timeProvider.Advance(TimeSpan.FromMilliseconds(250)); + await Task.Delay(50); + Assert.False(manager.TimeoutToken.IsCancellationRequested); + + // Received a response message from the server. + manager.ProcessClientHeartbeatResponse(StreamingHubPayloadPool.Shared.RentOrCreate([0x95 /* Array(5) */, 0x7e /* 0x7e(127) */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0x91 /* Array(1) */, .. ToMessagePackBytes(origin.AddSeconds(1))])); + + timeProvider.Advance(TimeSpan.FromMilliseconds(250)); + await Task.Delay(50); + Assert.False(manager.TimeoutToken.IsCancellationRequested); + + timeProvider.Advance(TimeSpan.FromMilliseconds(500)); + await Task.Delay(50); + Assert.False(manager.TimeoutToken.IsCancellationRequested); + + Assert.True(channel.Reader.TryRead(out var heartbeat2)); + } + + static byte[] ToMessagePackBytes(DateTimeOffset dt) + { + var ms = dt.ToUnixTimeMilliseconds(); + + var arrayBufferWriter = new ArrayBufferWriter(); + var writer = new MessagePackWriter(arrayBufferWriter); + writer.Write(ms); + writer.Flush(); + return arrayBufferWriter.WrittenMemory.ToArray(); + } +} diff --git a/tests/MagicOnion.Client.Tests/StreamingHubTest.cs b/tests/MagicOnion.Client.Tests/StreamingHubTest.cs index 38aa490bb..03e1c8f0a 100644 --- a/tests/MagicOnion.Client.Tests/StreamingHubTest.cs +++ b/tests/MagicOnion.Client.Tests/StreamingHubTest.cs @@ -1,4 +1,6 @@ using MagicOnion.Client.DynamicClient; +using Microsoft.Extensions.Time.Testing; +using System.Buffers; namespace MagicOnion.Client.Tests; @@ -436,14 +438,22 @@ public async Task Void_Parameter_Many() public async Task Heartbeat_Interval() { // Arrange + var origin = new DateTimeOffset(2024, 7, 1, 0, 0, 0, TimeSpan.Zero); + var timeProvider = new FakeTimeProvider(origin); var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); var helper = new StreamingHubClientTestHelper(factoryProvider: DynamicStreamingHubClientFactoryProvider.Instance); - var options = StreamingHubClientOptions.CreateWithDefault().WithHeartbeatInterval(TimeSpan.FromMilliseconds(100)); + var options = StreamingHubClientOptions.CreateWithDefault().WithClientHeartbeatInterval(TimeSpan.FromMilliseconds(100)).WithTimeProvider(timeProvider); var client = await helper.ConnectAsync(options, timeout.Token); + byte[] sentAt = [0xcf, 0x00, 0x00, 0x01, 0x90, 0x6b, 0x97, 0x5c, 0x00]; // MessagePackWriter.Write(timeProvider.GetUtcNow().ToUnixTimeMilliseconds()); // Act var t = client.Parameter_One(1234); - await Task.Delay(300); + timeProvider.Advance(TimeSpan.FromMilliseconds(100)); + await Task.Delay(100); // Wait for processing queue. + timeProvider.Advance(TimeSpan.FromMilliseconds(100)); + await Task.Delay(100); // Wait for processing queue. + timeProvider.Advance(TimeSpan.FromMilliseconds(100)); + await Task.Delay(100); // Wait for processing queue. // Assert var (messageId, methodId, requestBody) = await helper.ReadRequestAsync(); @@ -452,9 +462,20 @@ public async Task Heartbeat_Interval() var request1 = await helper.ReadRequestRawAsync(); var request2 = await helper.ReadRequestRawAsync(); var request3 = await helper.ReadRequestRawAsync(); - Assert.Equal([0x94, 0x7f, 0xc0, 0xc0, 0xc0], request1.ToArray()); - Assert.Equal([0x94, 0x7f, 0xc0, 0xc0, 0xc0], request2.ToArray()); - Assert.Equal([0x94, 0x7f, 0xc0, 0xc0, 0xc0], request3.ToArray()); + Assert.Equal((byte[])[0x94 /* Array(4) */, 0x7e /* 0x7e(127) */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0x91 /* Array(1) */, .. (ToMessagePackBytes(origin.AddMilliseconds(100)))], request1.ToArray()); + Assert.Equal((byte[])[0x94 /* Array(4) */, 0x7e /* 0x7e(127) */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0x91 /* Array(1) */, .. (ToMessagePackBytes(origin.AddMilliseconds(200)))], request2.ToArray()); + Assert.Equal((byte[])[0x94 /* Array(4) */, 0x7e /* 0x7e(127) */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0x91 /* Array(1) */, .. (ToMessagePackBytes(origin.AddMilliseconds(300)))], request3.ToArray()); + + static byte[] ToMessagePackBytes(DateTimeOffset dt) + { + var ms = dt.ToUnixTimeMilliseconds(); + + var arrayBufferWriter = new ArrayBufferWriter(); + var writer = new MessagePackWriter(arrayBufferWriter); + writer.Write(ms); + writer.Flush(); + return arrayBufferWriter.WrittenMemory.ToArray(); + } } [Fact] @@ -463,7 +484,7 @@ public async Task Heartbeat_Respond() // Arrange var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); var helper = new StreamingHubClientTestHelper(factoryProvider: DynamicStreamingHubClientFactoryProvider.Instance); - var options = StreamingHubClientOptions.CreateWithDefault().WithHeartbeatInterval(Timeout.InfiniteTimeSpan); // Disable Heartbeat timer. + var options = StreamingHubClientOptions.CreateWithDefault().WithClientHeartbeatInterval(Timeout.InfiniteTimeSpan); // Disable Heartbeat timer. var client = await helper.ConnectAsync(options, timeout.Token); // Act @@ -487,8 +508,8 @@ public async Task Heartbeat_Extra() var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); var helper = new StreamingHubClientTestHelper(factoryProvider: DynamicStreamingHubClientFactoryProvider.Instance); var options = StreamingHubClientOptions.CreateWithDefault() - .WithHeartbeatReceived(x => received = x.ToArray()) - .WithHeartbeatInterval(Timeout.InfiniteTimeSpan); // Disable Heartbeat timer. + .WithServerHeartbeatReceived(x => received = x.ToArray()) + .WithClientHeartbeatInterval(Timeout.InfiniteTimeSpan); // Disable Heartbeat timer. var client = await helper.ConnectAsync(options, timeout.Token); // Act diff --git a/tests/MagicOnion.Server.Tests/MagicOnion.Server.Tests.csproj b/tests/MagicOnion.Server.Tests/MagicOnion.Server.Tests.csproj index 5bdca88a7..f67f8e07b 100644 --- a/tests/MagicOnion.Server.Tests/MagicOnion.Server.Tests.csproj +++ b/tests/MagicOnion.Server.Tests/MagicOnion.Server.Tests.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -17,6 +17,7 @@ + diff --git a/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubClientHeartbeatResponseTest.cs b/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubClientHeartbeatResponseTest.cs new file mode 100644 index 000000000..8b860b051 --- /dev/null +++ b/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubClientHeartbeatResponseTest.cs @@ -0,0 +1,48 @@ +using MagicOnion.Client; +using MagicOnion.Server.Hubs; +using NSubstitute; +using static MagicOnion.Server.Tests.StreamingHubHeartbeat.StreamingHubClientHeartbeatResponseTest; + +namespace MagicOnion.Server.Tests.StreamingHubHeartbeat; + +public class StreamingHubClientHeartbeatResponseTest(StreamingHubClientHeartbeatResponseTestServerFixture fixture) : IClassFixture +{ + protected ServerFixture Fixture { get; } = fixture; + + public class StreamingHubClientHeartbeatResponseTestServerFixture : ServerFixture< + StreamingHubClientHeartbeatResponseTestHub + > + { + protected override void ConfigureMagicOnion(MagicOnionOptions options) + { + options.EnableStreamingHubHeartbeat = false; // Server Heartbeat is disabled on this test. + } + } + + [Fact] + public async Task Interval() + { + // Arrange + var receiver = Substitute.For(); + var receivedHeartbeatEvents = new List(); + var options = StreamingHubClientOptions.CreateWithDefault() + .WithClientHeartbeatInterval(TimeSpan.FromMilliseconds(100)) + .WithClientHeartbeatResponseReceived(x => receivedHeartbeatEvents.Add(x)); + + // Act + var client = await Fixture.CreateStreamingHubClientAsync(receiver, options); + await Task.Delay(250); + await client.DisposeAsync(); + + // Assert + Assert.Equal(2, receivedHeartbeatEvents.Count); + } + +} + +public interface IStreamingHubClientHeartbeatResponseTestHub : IStreamingHub; +public interface IStreamingHubClientHeartbeatResponseTestHubReceiver; + +public class StreamingHubClientHeartbeatResponseTestHub : StreamingHubBase, IStreamingHubClientHeartbeatResponseTestHub +{ +} diff --git a/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubHeartbeatManagerTest.cs b/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubHeartbeatManagerTest.cs index 100abd5bd..70cf43429 100644 --- a/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubHeartbeatManagerTest.cs +++ b/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubHeartbeatManagerTest.cs @@ -1,3 +1,4 @@ +using System; using System.Buffers; using Grpc.Core; using MagicOnion.Internal; @@ -5,6 +6,7 @@ using MagicOnion.Server.Hubs; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Testing; +using Microsoft.Extensions.Time.Testing; namespace MagicOnion.Server.Tests.StreamingHubHeartbeat; @@ -18,7 +20,8 @@ public void Register() { // Arrange var logger = new FakeLogger(); - var manager = new StreamingHubHeartbeatManager(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan, null, logger); + var timeProvider = new FakeTimeProvider(); + var manager = new StreamingHubHeartbeatManager(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan, null, timeProvider, logger); var context = CreateFakeStreamingServiceContext(); // Act @@ -35,7 +38,8 @@ public void Handle_Dispose() { // Arrange var logger = new FakeLogger(); - var manager = new StreamingHubHeartbeatManager(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan, null, logger); + var timeProvider = new FakeTimeProvider(); + var manager = new StreamingHubHeartbeatManager(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan, null, timeProvider, logger); var context = CreateFakeStreamingServiceContext(); var handle = manager.Register(context); @@ -48,7 +52,8 @@ public async Task Interval_Disable_Timeout() { // Arrange var logger = new FakeLogger(); - var manager = new StreamingHubHeartbeatManager(TimeSpan.FromMilliseconds(100), Timeout.InfiniteTimeSpan, null, logger); + var timeProvider = new FakeTimeProvider(); + var manager = new StreamingHubHeartbeatManager(TimeSpan.FromMilliseconds(100), Timeout.InfiniteTimeSpan, null, timeProvider, logger); var context1 = CreateFakeStreamingServiceContext(); var context2 = CreateFakeStreamingServiceContext(); var context3 = CreateFakeStreamingServiceContext(); @@ -57,7 +62,12 @@ public async Task Interval_Disable_Timeout() using var handle1 = manager.Register(context1); using var handle2 = manager.Register(context2); using var handle3 = manager.Register(context3); - await Task.Delay(310); + timeProvider.Advance(TimeSpan.FromMilliseconds(100)); + await Task.Delay(50); + timeProvider.Advance(TimeSpan.FromMilliseconds(100)); + await Task.Delay(50); + timeProvider.Advance(TimeSpan.FromMilliseconds(100)); + await Task.Delay(50); // Assert Assert.Equal(3, context1.Responses.Count); @@ -80,7 +90,8 @@ public async Task Interval_Keep() // Arrange var collector = FakeLogCollector.Create(new FakeLogCollectorOptions()); var logger = new FakeLogger(collector); - var manager = new StreamingHubHeartbeatManager(TimeSpan.FromMilliseconds(300), TimeSpan.FromMilliseconds(200), null, logger); + var timeProvider = new FakeTimeProvider(); + var manager = new StreamingHubHeartbeatManager(TimeSpan.FromMilliseconds(300), TimeSpan.FromMilliseconds(200), null, timeProvider, logger); var context1 = CreateFakeStreamingServiceContext(); var context2 = CreateFakeStreamingServiceContext(); var context3 = CreateFakeStreamingServiceContext(); @@ -89,7 +100,8 @@ public async Task Interval_Keep() using var handle1 = manager.Register(context1); using var handle2 = manager.Register(context2); using var handle3 = manager.Register(context3); - await Task.Delay(350); + timeProvider.Advance(TimeSpan.FromMilliseconds(350)); + await Task.Delay(50); var isCanceled1 = handle1.TimeoutToken.IsCancellationRequested; var isCanceled2 = handle2.TimeoutToken.IsCancellationRequested; var isCanceled3 = handle3.TimeoutToken.IsCancellationRequested; @@ -97,7 +109,8 @@ public async Task Interval_Keep() handle1.Ack(); handle2.Ack(); handle3.Ack(); - await Task.Delay(250); + timeProvider.Advance(TimeSpan.FromMilliseconds(250)); + await Task.Delay(50); // Assert Assert.False(isCanceled1); @@ -114,7 +127,8 @@ public async Task Interval_With_Timeout() // Arrange var collector = FakeLogCollector.Create(new FakeLogCollectorOptions()); var logger = new FakeLogger(collector); - var manager = new StreamingHubHeartbeatManager(TimeSpan.FromMilliseconds(300), TimeSpan.FromMilliseconds(200), null, logger); + var timeProvider = new FakeTimeProvider(); + var manager = new StreamingHubHeartbeatManager(TimeSpan.FromMilliseconds(300), TimeSpan.FromMilliseconds(200), null, timeProvider, logger); var context1 = CreateFakeStreamingServiceContext(); var context2 = CreateFakeStreamingServiceContext(); var context3 = CreateFakeStreamingServiceContext(); @@ -123,11 +137,13 @@ public async Task Interval_With_Timeout() using var handle1 = manager.Register(context1); using var handle2 = manager.Register(context2); using var handle3 = manager.Register(context3); - await Task.Delay(350); + timeProvider.Advance(TimeSpan.FromMilliseconds(350)); + await Task.Delay(50); var isCanceled1 = handle1.TimeoutToken.IsCancellationRequested; var isCanceled2 = handle2.TimeoutToken.IsCancellationRequested; var isCanceled3 = handle3.TimeoutToken.IsCancellationRequested; - await Task.Delay(250); // No responses from clients and timeouts are reached. + timeProvider.Advance(TimeSpan.FromMilliseconds(250)); // No responses from clients and timeouts are reached. + await Task.Delay(50); // Assert Assert.False(isCanceled1); @@ -143,7 +159,8 @@ public async Task Interval_Stop_After_HandleDisposed() { // Arrange var logger = new FakeLogger(); - var manager = new StreamingHubHeartbeatManager(TimeSpan.FromMilliseconds(100), Timeout.InfiniteTimeSpan, null, logger); + var timeProvider = new FakeTimeProvider(); + var manager = new StreamingHubHeartbeatManager(TimeSpan.FromMilliseconds(100), Timeout.InfiniteTimeSpan, null, timeProvider, logger); var context1 = CreateFakeStreamingServiceContext(); var context2 = CreateFakeStreamingServiceContext(); var context3 = CreateFakeStreamingServiceContext(); @@ -152,11 +169,21 @@ public async Task Interval_Stop_After_HandleDisposed() var handle1 = manager.Register(context1); var handle2 = manager.Register(context2); var handle3 = manager.Register(context3); - await Task.Delay(310); + timeProvider.Advance(TimeSpan.FromMilliseconds(100)); + await Task.Delay(50); + timeProvider.Advance(TimeSpan.FromMilliseconds(100)); + await Task.Delay(50); + timeProvider.Advance(TimeSpan.FromMilliseconds(100)); + await Task.Delay(50); handle1.Dispose(); handle2.Dispose(); handle3.Dispose(); - await Task.Delay(300); + timeProvider.Advance(TimeSpan.FromMilliseconds(100)); + await Task.Delay(50); + timeProvider.Advance(TimeSpan.FromMilliseconds(100)); + await Task.Delay(50); + timeProvider.Advance(TimeSpan.FromMilliseconds(100)); + await Task.Delay(50); // Assert Assert.Equal(3, context1.Responses.Count); @@ -178,7 +205,8 @@ public async Task CustomMetadataProvider() { // Arrange var logger = new FakeLogger(); - var manager = new StreamingHubHeartbeatManager(TimeSpan.FromMilliseconds(100), Timeout.InfiniteTimeSpan, new CustomHeartbeatMetadataProvider(), logger); + var timeProvider = new FakeTimeProvider(); + var manager = new StreamingHubHeartbeatManager(TimeSpan.FromMilliseconds(100), Timeout.InfiniteTimeSpan, new CustomHeartbeatMetadataProvider(), timeProvider, logger); var context1 = CreateFakeStreamingServiceContext(); var context2 = CreateFakeStreamingServiceContext(); var context3 = CreateFakeStreamingServiceContext(); @@ -188,7 +216,12 @@ public async Task CustomMetadataProvider() using var handle1 = manager.Register(context1); using var handle2 = manager.Register(context2); using var handle3 = manager.Register(context3); - await Task.Delay(310); + timeProvider.Advance(TimeSpan.FromMilliseconds(100)); + await Task.Delay(50); + timeProvider.Advance(TimeSpan.FromMilliseconds(100)); + await Task.Delay(50); + timeProvider.Advance(TimeSpan.FromMilliseconds(100)); + await Task.Delay(50); // Assert Assert.Equal(3, context1.Responses.Count); diff --git a/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubHeartbeatTest.cs b/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubHeartbeatTest.cs deleted file mode 100644 index 4525fe28a..000000000 --- a/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubHeartbeatTest.cs +++ /dev/null @@ -1,218 +0,0 @@ -using System.Collections.Concurrent; -using MagicOnion.Client; -using MagicOnion.Server.Hubs; -using Microsoft.Extensions.DependencyInjection; -using NSubstitute; - -namespace MagicOnion.Server.Tests.StreamingHubHeartbeat; - -public abstract class StreamingHubHeartbeatTestBase -{ - protected ServerFixture Fixture { get; } - - public StreamingHubHeartbeatTestBase(ServerFixture fixture) - { - this.Fixture = fixture; - } - - [Fact] - public async Task EnableByAttribute() - { - // Arrange - var receiver = Substitute.For(); - var receivedHeartbeatMetadata = new List(); - var options = StreamingHubClientOptions.CreateWithDefault().WithHeartbeatReceived(x => receivedHeartbeatMetadata.Add(x.ToArray())); - - // Act - var client = await Fixture.CreateStreamingHubClientAsync(receiver, options); - await Task.Delay(650); - await client.DisposeAsync(); - - // Assert - Assert.Equal(2, receivedHeartbeatMetadata.Count); // The client must receive a heartbeat every 300ms from the server. - } - - [Fact] - public async Task DisableByAttribute() - { - // Arrange - var receiver = Substitute.For(); - var receivedHeartbeatMetadata = new List(); - var options = StreamingHubClientOptions.CreateWithDefault().WithHeartbeatReceived(x => receivedHeartbeatMetadata.Add(x.ToArray())); - - // Act - var client = await Fixture.CreateStreamingHubClientAsync(receiver, options); - await Task.Delay(650); - await client.DisposeAsync(); - - // Assert - Assert.Empty(receivedHeartbeatMetadata); - } - - [Fact] - public async Task Override_Interval() - { - // Arrange - var receiver = Substitute.For(); - var receivedHeartbeatMetadata = new List(); - var options = StreamingHubClientOptions.CreateWithDefault().WithHeartbeatReceived(x => receivedHeartbeatMetadata.Add(x.ToArray())); - - // Act - var client = await Fixture.CreateStreamingHubClientAsync(receiver, options); - await Task.Delay(650); - await client.DisposeAsync(); - - // Assert - Assert.Single(receivedHeartbeatMetadata); // The client must receive a heartbeat every 500ms from the server. - } - - [Fact] - public async Task Timeout() - { - // Arrange - var heartbeatReceived = new TaskCompletionSource(); - var receiver = Substitute.For(); - var options = StreamingHubClientOptions.CreateWithDefault().WithHeartbeatReceived(x => - { - heartbeatReceived.SetResult(); - Thread.Sleep(200); // Block consuming message loop. - }); - - // We need to consume message inline. Avoid post continuations to the synchronization context. - SynchronizationContext.SetSynchronizationContext(null); - - // Act - var client = await Fixture.CreateStreamingHubClientAsync(receiver, options); - - // Wait for receiving a heartbeat from the server. - // The client must receive a heartbeat every 200ms from the server. - await heartbeatReceived.Task.WaitAsync(TimeSpan.FromSeconds(1)); - - // Timeout at 100 ms after receiving a heartbeat. - await Task.Delay(500); - - // Assert - Assert.True((bool)Fixture.Items.GetValueOrDefault("Disconnected")); - Assert.True(client.WaitForDisconnect().IsCompleted); - } -} - - -public class StreamingHubHeartbeatTest_DisabledByDefault : StreamingHubHeartbeatTestBase, IClassFixture -{ - public StreamingHubHeartbeatTest_DisabledByDefault(StreamingHubHeartbeatTestServerFixture fixture) - : base(fixture) - { - } - - public class StreamingHubHeartbeatTestServerFixture : ServerFixture< - StreamingHubHeartbeatTestHub, - StreamingHubHeartbeatTestHub_EnableByAttribute, - StreamingHubHeartbeatTestHub_DisableByAttribute, - StreamingHubHeartbeatTestHub_CustomIntervalAndTimeout, - StreamingHubHeartbeatTestHub_TimeoutBehavior - > - { - protected override void ConfigureMagicOnion(MagicOnionOptions options) - { - options.StreamingHubHeartbeatInterval = TimeSpan.FromMilliseconds(300); - options.StreamingHubHeartbeatTimeout = TimeSpan.FromMilliseconds(200); - options.EnableStreamingHubHeartbeat = false; // Disabled by default. - } - } - - [Fact] - public async Task Default_Disable() - { - // Arrange - var receiver = Substitute.For(); - var receivedHeartbeatMetadata = new List(); - var options = StreamingHubClientOptions.CreateWithDefault().WithHeartbeatReceived(x => receivedHeartbeatMetadata.Add(x.ToArray())); - - // Act - var client = await Fixture.CreateStreamingHubClientAsync(receiver, options); - await Task.Delay(650); - await client.DisposeAsync(); - - // Assert - Assert.Empty(receivedHeartbeatMetadata); - } -} - - -public class StreamingHubHeartbeatTest_EnabledByDefault : StreamingHubHeartbeatTestBase, IClassFixture -{ - public StreamingHubHeartbeatTest_EnabledByDefault(StreamingHubHeartbeatTestServerFixture fixture) - : base(fixture) - { - } - - public class StreamingHubHeartbeatTestServerFixture : ServerFixture< - StreamingHubHeartbeatTestHub, - StreamingHubHeartbeatTestHub_EnableByAttribute, - StreamingHubHeartbeatTestHub_DisableByAttribute, - StreamingHubHeartbeatTestHub_CustomIntervalAndTimeout, - StreamingHubHeartbeatTestHub_TimeoutBehavior - > - { - protected override void ConfigureMagicOnion(MagicOnionOptions options) - { - options.StreamingHubHeartbeatInterval = TimeSpan.FromMilliseconds(300); - options.StreamingHubHeartbeatTimeout = TimeSpan.FromMilliseconds(200); - options.EnableStreamingHubHeartbeat = true; // Enabled by default. - } - } - - [Fact] - public async Task Default_Enable() - { - // Arrange - var receiver = Substitute.For(); - var receivedHeartbeatMetadata = new List(); - var options = StreamingHubClientOptions.CreateWithDefault().WithHeartbeatReceived(x => receivedHeartbeatMetadata.Add(x.ToArray())); - - // Act - var client = await Fixture.CreateStreamingHubClientAsync(receiver, options); - await Task.Delay(650); - await client.DisposeAsync(); - - // Assert - Assert.Equal(2, receivedHeartbeatMetadata.Count); - } -} - -public interface IStreamingHubHeartbeatTestHub : IStreamingHub; -public interface IStreamingHubHeartbeatTestHub_EnableByAttribute : IStreamingHub; -public interface IStreamingHubHeartbeatTestHub_DisableByAttribute : IStreamingHub; -public interface IStreamingHubHeartbeatTestHub_CustomIntervalAndTimeout : IStreamingHub; -public interface IStreamingHubHeartbeatTestHub_TimeoutBehavior : IStreamingHub; -public interface IStreamingHubHeartbeatTestHubReceiver; - -// Implementations - -// This streaming hub has no `Heartbeat` attribute. -public class StreamingHubHeartbeatTestHub() - : StreamingHubBase, IStreamingHubHeartbeatTestHub; - -[Heartbeat] -public class StreamingHubHeartbeatTestHub_EnableByAttribute() - : StreamingHubBase, IStreamingHubHeartbeatTestHub_EnableByAttribute; - -[Heartbeat(Enable = false)] -public class StreamingHubHeartbeatTestHub_DisableByAttribute() - : StreamingHubBase, IStreamingHubHeartbeatTestHub_DisableByAttribute; - -[Heartbeat(Interval = 500, Timeout = 100)] -public class StreamingHubHeartbeatTestHub_CustomIntervalAndTimeout() - : StreamingHubBase, IStreamingHubHeartbeatTestHub_CustomIntervalAndTimeout; - -[Heartbeat(Enable = true, Interval = 200, Timeout = 100)] -public class StreamingHubHeartbeatTestHub_TimeoutBehavior([FromKeyedServices(ServerFixture.ItemsServiceKey)] ConcurrentDictionary items) - : StreamingHubBase, IStreamingHubHeartbeatTestHub_TimeoutBehavior -{ - protected override ValueTask OnDisconnected() - { - items["Disconnected"] = true; - return base.OnDisconnected(); - } -} diff --git a/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubServerHeartbeatTest.cs b/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubServerHeartbeatTest.cs new file mode 100644 index 000000000..bdbd32f38 --- /dev/null +++ b/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubServerHeartbeatTest.cs @@ -0,0 +1,218 @@ +using System.Collections.Concurrent; +using MagicOnion.Client; +using MagicOnion.Server.Hubs; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; + +namespace MagicOnion.Server.Tests.StreamingHubHeartbeat; + +public abstract class StreamingHubServerHeartbeatTestBase +{ + protected ServerFixture Fixture { get; } + + public StreamingHubServerHeartbeatTestBase(ServerFixture fixture) + { + this.Fixture = fixture; + } + + [Fact] + public async Task EnableByAttribute() + { + // Arrange + var receiver = Substitute.For(); + var receivedHeartbeatMetadata = new List(); + var options = StreamingHubClientOptions.CreateWithDefault().WithServerHeartbeatReceived(x => receivedHeartbeatMetadata.Add(x.ToArray())); + + // Act + var client = await Fixture.CreateStreamingHubClientAsync(receiver, options); + await Task.Delay(650); + await client.DisposeAsync(); + + // Assert + Assert.Equal(2, receivedHeartbeatMetadata.Count); // The client must receive a heartbeat every 300ms from the server. + } + + [Fact] + public async Task DisableByAttribute() + { + // Arrange + var receiver = Substitute.For(); + var receivedHeartbeatMetadata = new List(); + var options = StreamingHubClientOptions.CreateWithDefault().WithServerHeartbeatReceived(x => receivedHeartbeatMetadata.Add(x.ToArray())); + + // Act + var client = await Fixture.CreateStreamingHubClientAsync(receiver, options); + await Task.Delay(650); + await client.DisposeAsync(); + + // Assert + Assert.Empty(receivedHeartbeatMetadata); + } + + [Fact] + public async Task Override_Interval() + { + // Arrange + var receiver = Substitute.For(); + var receivedHeartbeatMetadata = new List(); + var options = StreamingHubClientOptions.CreateWithDefault().WithServerHeartbeatReceived(x => receivedHeartbeatMetadata.Add(x.ToArray())); + + // Act + var client = await Fixture.CreateStreamingHubClientAsync(receiver, options); + await Task.Delay(650); + await client.DisposeAsync(); + + // Assert + Assert.Single(receivedHeartbeatMetadata); // The client must receive a heartbeat every 500ms from the server. + } + + [Fact] + public async Task Timeout() + { + // Arrange + var heartbeatReceived = new TaskCompletionSource(); + var receiver = Substitute.For(); + var options = StreamingHubClientOptions.CreateWithDefault().WithServerHeartbeatReceived(x => + { + heartbeatReceived.SetResult(); + Thread.Sleep(200); // Block consuming message loop. + }); + + // We need to consume message inline. Avoid post continuations to the synchronization context. + SynchronizationContext.SetSynchronizationContext(null); + + // Act + var client = await Fixture.CreateStreamingHubClientAsync(receiver, options); + + // Wait for receiving a heartbeat from the server. + // The client must receive a heartbeat every 200ms from the server. + await heartbeatReceived.Task.WaitAsync(TimeSpan.FromSeconds(1)); + + // Timeout at 100 ms after receiving a heartbeat. + await Task.Delay(500); + + // Assert + Assert.True((bool)Fixture.Items.GetValueOrDefault("Disconnected")); + Assert.True(client.WaitForDisconnect().IsCompleted); + } +} + + +public class StreamingHubServerHeartbeatTest_DisabledByDefault : StreamingHubServerHeartbeatTestBase, IClassFixture +{ + public StreamingHubServerHeartbeatTest_DisabledByDefault(StreamingHubHeartbeatTestServerFixture fixture) + : base(fixture) + { + } + + public class StreamingHubHeartbeatTestServerFixture : ServerFixture< + StreamingHubServerHeartbeatTestHub, + StreamingHubServerHeartbeatTestHub_EnableByAttribute, + StreamingHubServerHeartbeatTestHub_DisableByAttribute, + StreamingHubServerHeartbeatTestHub_CustomIntervalAndTimeout, + StreamingHubServerHeartbeatTestHub_TimeoutBehavior + > + { + protected override void ConfigureMagicOnion(MagicOnionOptions options) + { + options.StreamingHubHeartbeatInterval = TimeSpan.FromMilliseconds(300); + options.StreamingHubHeartbeatTimeout = TimeSpan.FromMilliseconds(200); + options.EnableStreamingHubHeartbeat = false; // Disabled by default. + } + } + + [Fact] + public async Task Default_Disable() + { + // Arrange + var receiver = Substitute.For(); + var receivedHeartbeatMetadata = new List(); + var options = StreamingHubClientOptions.CreateWithDefault().WithServerHeartbeatReceived(x => receivedHeartbeatMetadata.Add(x.ToArray())); + + // Act + var client = await Fixture.CreateStreamingHubClientAsync(receiver, options); + await Task.Delay(650); + await client.DisposeAsync(); + + // Assert + Assert.Empty(receivedHeartbeatMetadata); + } +} + + +public class StreamingHubServerHeartbeatTest_EnabledByDefault : StreamingHubServerHeartbeatTestBase, IClassFixture +{ + public StreamingHubServerHeartbeatTest_EnabledByDefault(StreamingHubHeartbeatTestServerFixture fixture) + : base(fixture) + { + } + + public class StreamingHubHeartbeatTestServerFixture : ServerFixture< + StreamingHubServerHeartbeatTestHub, + StreamingHubServerHeartbeatTestHub_EnableByAttribute, + StreamingHubServerHeartbeatTestHub_DisableByAttribute, + StreamingHubServerHeartbeatTestHub_CustomIntervalAndTimeout, + StreamingHubServerHeartbeatTestHub_TimeoutBehavior + > + { + protected override void ConfigureMagicOnion(MagicOnionOptions options) + { + options.StreamingHubHeartbeatInterval = TimeSpan.FromMilliseconds(300); + options.StreamingHubHeartbeatTimeout = TimeSpan.FromMilliseconds(200); + options.EnableStreamingHubHeartbeat = true; // Enabled by default. + } + } + + [Fact] + public async Task Default_Enable() + { + // Arrange + var receiver = Substitute.For(); + var receivedHeartbeatMetadata = new List(); + var options = StreamingHubClientOptions.CreateWithDefault().WithServerHeartbeatReceived(x => receivedHeartbeatMetadata.Add(x.ToArray())); + + // Act + var client = await Fixture.CreateStreamingHubClientAsync(receiver, options); + await Task.Delay(650); + await client.DisposeAsync(); + + // Assert + Assert.Equal(2, receivedHeartbeatMetadata.Count); + } +} + +public interface IStreamingHubServerHeartbeatTestHub : IStreamingHub; +public interface IStreamingHubServerHeartbeatTestHub_EnableByAttribute : IStreamingHub; +public interface IStreamingHubServerHeartbeatTestHub_DisableByAttribute : IStreamingHub; +public interface IStreamingHubServerHeartbeatTestHub_CustomIntervalAndTimeout : IStreamingHub; +public interface IStreamingHubServerHeartbeatTestHub_TimeoutBehavior : IStreamingHub; +public interface IStreamingHubServerHeartbeatTestHubReceiver; + +// Implementations + +// This streaming hub has no `Heartbeat` attribute. +public class StreamingHubServerHeartbeatTestHub() + : StreamingHubBase, IStreamingHubServerHeartbeatTestHub; + +[Heartbeat] +public class StreamingHubServerHeartbeatTestHub_EnableByAttribute() + : StreamingHubBase, IStreamingHubServerHeartbeatTestHub_EnableByAttribute; + +[Heartbeat(Enable = false)] +public class StreamingHubServerHeartbeatTestHub_DisableByAttribute() + : StreamingHubBase, IStreamingHubServerHeartbeatTestHub_DisableByAttribute; + +[Heartbeat(Interval = 500, Timeout = 100)] +public class StreamingHubServerHeartbeatTestHub_CustomIntervalAndTimeout() + : StreamingHubBase, IStreamingHubServerHeartbeatTestHub_CustomIntervalAndTimeout; + +[Heartbeat(Enable = true, Interval = 200, Timeout = 100)] +public class StreamingHubServerHeartbeatTestHub_TimeoutBehavior([FromKeyedServices(ServerFixture.ItemsServiceKey)] ConcurrentDictionary items) + : StreamingHubBase, IStreamingHubServerHeartbeatTestHub_TimeoutBehavior +{ + protected override ValueTask OnDisconnected() + { + items["Disconnected"] = true; + return base.OnDisconnected(); + } +}