diff --git a/Assets/Mirror/Examples/Tanks/Scenes/MirrorTanks-unmanaged.unity b/Assets/Mirror/Examples/Tanks/Scenes/MirrorTanks-unmanaged.unity new file mode 100644 index 0000000000..7d94802580 --- /dev/null +++ b/Assets/Mirror/Examples/Tanks/Scenes/MirrorTanks-unmanaged.unity @@ -0,0 +1,790 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 3 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_IndirectSpecularColor: {r: 0, g: 0, b: 0, a: 1} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 12 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 0 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 0 + m_PVRSampling: 1 + 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_PVREnvironmentMIS: 0 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 1064449595} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 2 + agentTypeID: 0 + agentRadius: 2 + agentHeight: 3.5 + agentSlope: 45 + agentClimb: 2 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.6666667 + manualTileSize: 0 + tileSize: 256 + accuratePlacement: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 23800000, guid: c772aa575956c59478e2d55eb019e17a, type: 2} +--- !u!1 &88936773 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 88936777} + - component: {fileID: 88936776} + - component: {fileID: 88936778} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!20 &88936776 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 88936773} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0, g: 0, b: 0, a: 0} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_FocalLength: 50 + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 0 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &88936777 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 88936773} + m_LocalRotation: {x: 0.3420201, y: 0, z: 0, w: 0.9396927} + m_LocalPosition: {x: 0, y: 20, z: -30} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 40, y: 0, z: 0} +--- !u!114 &88936778 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 88936773} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9021b6cc314944290986ab6feb48db79, type: 3} + m_Name: + m_EditorClassIdentifier: + height: 150 + offsetY: 40 + maxLogCount: 50 + showInEditor: 0 + hotKey: 293 +--- !u!1 &251893064 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 251893065} + - component: {fileID: 251893066} + m_Layer: 0 + m_Name: Spawn + m_TagString: Untagged + m_Icon: {fileID: -964228994112308473, guid: 0000000000000000d000000000000000, type: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &251893065 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 251893064} + m_LocalRotation: {x: 0, y: -0.92387956, z: 0, w: 0.38268343} + m_LocalPosition: {x: 14, y: 0, z: 14} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 4 + m_LocalEulerAnglesHint: {x: 0, y: -135, z: 0} +--- !u!114 &251893066 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 251893064} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 41f84591ce72545258ea98cb7518d8b9, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!1 &535739935 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 535739936} + - component: {fileID: 535739937} + m_Layer: 0 + m_Name: Spawn + m_TagString: Untagged + m_Icon: {fileID: -964228994112308473, guid: 0000000000000000d000000000000000, type: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &535739936 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 535739935} + m_LocalRotation: {x: 0, y: -0.38268343, z: 0, w: 0.92387956} + m_LocalPosition: {x: 14, y: 0, z: -14} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 5 + m_LocalEulerAnglesHint: {x: 0, y: -45, z: 0} +--- !u!114 &535739937 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 535739935} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 41f84591ce72545258ea98cb7518d8b9, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!850595691 &1064449595 +LightingSettings: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Settings.lighting + serializedVersion: 4 + m_GIWorkflowMode: 1 + m_EnableBakedLightmaps: 0 + m_EnableRealtimeLightmaps: 0 + m_RealtimeEnvironmentLighting: 1 + m_BounceScale: 1 + m_AlbedoBoost: 1 + m_IndirectOutputScale: 1 + m_UsingShadowmask: 1 + m_BakeBackend: 0 + 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: 512 + m_PVREnvironmentSampleCount: 512 + m_PVREnvironmentReferencePointCount: 2048 + m_LightProbeSampleCountMultiplier: 4 + m_PVRBounces: 2 + m_PVRMinBounces: 2 + m_PVREnvironmentMIS: 1 + 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 &1107091652 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1107091656} + - component: {fileID: 1107091655} + - component: {fileID: 1107091654} + - component: {fileID: 1107091653} + m_Layer: 0 + m_Name: Ground + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 4294967295 + m_IsActive: 1 +--- !u!23 &1107091653 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1107091652} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RenderingLayerMask: 4294967295 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: 29b49c27a74f145918356859bd7af511, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 1 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} +--- !u!64 &1107091654 +MeshCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1107091652} + m_Material: {fileID: 0} + m_IsTrigger: 0 + m_Enabled: 1 + serializedVersion: 4 + m_Convex: 0 + m_CookingOptions: 30 + m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0} +--- !u!33 &1107091655 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1107091652} + m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0} +--- !u!4 &1107091656 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1107091652} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 4, y: 1, z: 4} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1282001517 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1282001518} + - component: {fileID: 1282001520} + - component: {fileID: 1282001519} + - component: {fileID: 1282001522} + - component: {fileID: 1282001523} + m_Layer: 0 + m_Name: NetworkManager + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1282001518 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1282001517} + 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: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1282001519 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1282001517} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6442dc8070ceb41f094e44de0bf87274, type: 3} + m_Name: + m_EditorClassIdentifier: + offsetX: 0 + offsetY: 0 +--- !u!114 &1282001520 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1282001517} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 8aab4c8111b7c411b9b92cf3dbc5bd4e, type: 3} + m_Name: + m_EditorClassIdentifier: + dontDestroyOnLoad: 0 + runInBackground: 1 + headlessStartMode: 1 + editorAutoStart: 0 + sendRate: 120 + autoStartServerBuild: 0 + autoConnectClientBuild: 0 + offlineScene: + onlineScene: + transport: {fileID: 1282001523} + networkAddress: localhost + maxConnections: 100 + disconnectInactiveConnections: 0 + disconnectInactiveTimeout: 60 + authenticator: {fileID: 0} + playerPrefab: {fileID: 1916082411674582, guid: 6f43bf5488a7443d19ab2a83c6b91f35, + type: 3} + autoCreatePlayer: 1 + playerSpawnMethod: 1 + spawnPrefabs: + - {fileID: 5890560936853567077, guid: b7dd46dbf38c643f09e206f9fa4be008, type: 3} + exceptionsDisconnect: 1 + snapshotSettings: + bufferTimeMultiplier: 2 + bufferLimit: 32 + catchupNegativeThreshold: -1 + catchupPositiveThreshold: 1 + catchupSpeed: 0.019999999552965164 + slowdownSpeed: 0.03999999910593033 + driftEmaDuration: 1 + dynamicAdjustment: 1 + dynamicAdjustmentTolerance: 1 + deliveryTimeEmaDuration: 2 + evaluationMethod: 0 + evaluationInterval: 3 + timeInterpolationGui: 0 +--- !u!114 &1282001522 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1282001517} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: bc654f29862fc2643b948f772ebb9e68, type: 3} + m_Name: + m_EditorClassIdentifier: + color: {r: 1, g: 1, b: 1, a: 1} + padding: 2 + width: 180 + height: 25 +--- !u!114 &1282001523 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1282001517} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9b4b7c4aecdaf824cb300c034c53e9cd, type: 3} + m_Name: + m_EditorClassIdentifier: + port: 7777 + DualMode: 1 + NoDelay: 1 + Interval: 10 + Timeout: 10000 + RecvBufferSize: 7361536 + SendBufferSize: 7361536 + FastResend: 2 + ReceiveWindowSize: 4096 + SendWindowSize: 4096 + MaxRetransmit: 40 + MaximizeSocketBuffers: 1 + ReliableMaxMessageSize: 297433 + UnreliableMaxMessageSize: 1194 + debugLog: 0 + statisticsGUI: 0 + statisticsLog: 0 +--- !u!1 &1458789072 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1458789073} + - component: {fileID: 1458789074} + m_Layer: 0 + m_Name: Spawn + m_TagString: Untagged + m_Icon: {fileID: -964228994112308473, guid: 0000000000000000d000000000000000, type: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1458789073 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1458789072} + m_LocalRotation: {x: 0, y: 0.92387956, z: 0, w: 0.38268343} + m_LocalPosition: {x: -14, y: 0, z: 14} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 6 + m_LocalEulerAnglesHint: {x: 0, y: 135, z: 0} +--- !u!114 &1458789074 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1458789072} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 41f84591ce72545258ea98cb7518d8b9, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!1 &1501912662 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1501912663} + - component: {fileID: 1501912664} + m_Layer: 0 + m_Name: Spawn + m_TagString: Untagged + m_Icon: {fileID: -964228994112308473, guid: 0000000000000000d000000000000000, type: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1501912663 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1501912662} + m_LocalRotation: {x: 0, y: 0.38268343, z: 0, w: 0.92387956} + m_LocalPosition: {x: -14, y: 0, z: -14} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 7 + m_LocalEulerAnglesHint: {x: 0, y: 45, z: 0} +--- !u!114 &1501912664 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1501912662} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 41f84591ce72545258ea98cb7518d8b9, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!1 &2054208274 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2054208276} + - component: {fileID: 2054208275} + m_Layer: 0 + m_Name: Directional light + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!108 &2054208275 +Light: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2054208274} + m_Enabled: 1 + serializedVersion: 10 + m_Type: 1 + m_Shape: 0 + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_Intensity: 1 + m_Range: 10 + m_SpotAngle: 30 + m_InnerSpotAngle: 21.802082 + m_CookieSize: 10 + m_Shadows: + m_Type: 2 + m_Resolution: -1 + m_CustomResolution: -1 + m_Strength: 1 + m_Bias: 0.05 + m_NormalBias: 0.4 + m_NearPlane: 0.2 + m_CullingMatrixOverride: + e00: 1 + e01: 0 + e02: 0 + e03: 0 + e10: 0 + e11: 1 + e12: 0 + e13: 0 + e20: 0 + e21: 0 + e22: 1 + e23: 0 + e30: 0 + e31: 0 + e32: 0 + e33: 1 + m_UseCullingMatrixOverride: 0 + m_Cookie: {fileID: 0} + m_DrawHalo: 0 + m_Flare: {fileID: 0} + m_RenderMode: 0 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingLayerMask: 1 + m_Lightmapping: 4 + m_LightShadowCasterMode: 0 + m_AreaSize: {x: 1, y: 1} + m_BounceIntensity: 1 + m_ColorTemperature: 6570 + m_UseColorTemperature: 0 + m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} + m_UseBoundingSphereOverride: 0 + m_UseViewFrustumForShadowCasterCull: 1 + m_ShadowRadius: 0 + m_ShadowAngle: 0 +--- !u!4 &2054208276 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2054208274} + m_LocalRotation: {x: 0.10938167, y: 0.8754261, z: -0.40821788, w: 0.23456976} + m_LocalPosition: {x: 0, y: 10, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 50, y: 150, z: 0} diff --git a/Assets/Mirror/Examples/Tanks/Scenes/MirrorTanks-unmanaged.unity.meta b/Assets/Mirror/Examples/Tanks/Scenes/MirrorTanks-unmanaged.unity.meta new file mode 100644 index 0000000000..50cc06360c --- /dev/null +++ b/Assets/Mirror/Examples/Tanks/Scenes/MirrorTanks-unmanaged.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 992df67f970686844b2196bc2d47ec7a +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/KCP-unmanaged.meta b/Assets/Mirror/Transports/KCP-unmanaged.meta new file mode 100644 index 0000000000..b0f556276e --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 803ac9118db92254fbeebdb192a01b77 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/KCP-unmanaged/KcpTransport.cs b/Assets/Mirror/Transports/KCP-unmanaged/KcpTransport.cs new file mode 100644 index 0000000000..b6d0501e17 --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/KcpTransport.cs @@ -0,0 +1,365 @@ +//#if MIRROR <- commented out because MIRROR isn't defined on first import yet +using System; +using System.Linq; +using System.Net; +using Mirror; +using UnityEngine; +using UnityEngine.Serialization; + +namespace kcp2k.unmanaged +{ + [HelpURL("https://mirror-networking.gitbook.io/docs/transports/kcp-transport")] + [DisallowMultipleComponent] + public class KcpTransport : Transport, PortTransport + { + // scheme used by this transport + public const string Scheme = "kcp"; + + // common + [Header("Transport Configuration")] + [FormerlySerializedAs("Port")] + public ushort port = 7777; + public ushort Port { get => port; set => port=value; } + [Tooltip("DualMode listens to IPv6 and IPv4 simultaneously. Disable if the platform only supports IPv4.")] + public bool DualMode = true; + [Tooltip("NoDelay is recommended to reduce latency. This also scales better without buffers getting full.")] + public bool NoDelay = true; + [Tooltip("KCP internal update interval. 100ms is KCP default, but a lower interval is recommended to minimize latency and to scale to more networked entities.")] + public uint Interval = 10; + [Tooltip("KCP timeout in milliseconds. Note that KCP sends a ping automatically.")] + public int Timeout = 10000; + [Tooltip("Socket receive buffer size. Large buffer helps support more connections. Increase operating system socket buffer size limits if needed.")] + public int RecvBufferSize = 1024 * 1027 * 7; + [Tooltip("Socket send buffer size. Large buffer helps support more connections. Increase operating system socket buffer size limits if needed.")] + public int SendBufferSize = 1024 * 1027 * 7; + + [Header("Advanced")] + [Tooltip("KCP fastresend parameter. Faster resend for the cost of higher bandwidth. 0 in normal mode, 2 in turbo mode.")] + public int FastResend = 2; + [Tooltip("KCP congestion window. Restricts window size to reduce congestion. Results in only 2-3 MTU messages per Flush even on loopback. Best to keept his disabled.")] + /*public*/ bool CongestionWindow = false; // KCP 'NoCongestionWindow' is false by default. here we negate it for ease of use. + [Tooltip("KCP window size can be modified to support higher loads. This also increases max message size.")] + public uint ReceiveWindowSize = 4096; //Kcp.WND_RCV; 128 by default. Mirror sends a lot, so we need a lot more. + [Tooltip("KCP window size can be modified to support higher loads.")] + public uint SendWindowSize = 4096; //Kcp.WND_SND; 32 by default. Mirror sends a lot, so we need a lot more. + [Tooltip("KCP will try to retransmit lost messages up to MaxRetransmit (aka dead_link) before disconnecting.")] + public uint MaxRetransmit = Kcp.DEADLINK * 2; // default prematurely disconnects a lot of people (#3022). use 2x. + [Tooltip("Enable to automatically set client & server send/recv buffers to OS limit. Avoids issues with too small buffers under heavy load, potentially dropping connections. Increase the OS limit if this is still too small.")] + [FormerlySerializedAs("MaximizeSendReceiveBuffersToOSLimit")] + public bool MaximizeSocketBuffers = true; + + [Header("Allowed Max Message Sizes\nBased on Receive Window Size")] + [Tooltip("KCP reliable max message size shown for convenience. Can be changed via ReceiveWindowSize.")] + [ReadOnly] public int ReliableMaxMessageSize = 0; // readonly, displayed from OnValidate + [Tooltip("KCP unreliable channel max message size for convenience. Not changeable.")] + [ReadOnly] public int UnreliableMaxMessageSize = 0; // readonly, displayed from OnValidate + + // config is created from the serialized properties above. + // we can expose the config directly in the future. + // for now, let's not break people's old settings. + protected KcpConfig config; + + // use default MTU for this transport. + const int MTU = Kcp.MTU_DEF; + + // server & client + protected KcpServer server; + protected KcpClient client; + + // debugging + [Header("Debug")] + public bool debugLog; + // show statistics in OnGUI + public bool statisticsGUI; + // log statistics for headless servers that can't show them in GUI + public bool statisticsLog; + + // translate Kcp <-> Mirror channels + public static int FromKcpChannel(KcpChannel channel) => + channel == KcpChannel.Reliable ? Channels.Reliable : Channels.Unreliable; + + public static KcpChannel ToKcpChannel(int channel) => + channel == Channels.Reliable ? KcpChannel.Reliable : KcpChannel.Unreliable; + + public static TransportError ToTransportError(ErrorCode error) + { + switch(error) + { + case ErrorCode.DnsResolve: return TransportError.DnsResolve; + case ErrorCode.Timeout: return TransportError.Timeout; + case ErrorCode.Congestion: return TransportError.Congestion; + case ErrorCode.InvalidReceive: return TransportError.InvalidReceive; + case ErrorCode.InvalidSend: return TransportError.InvalidSend; + case ErrorCode.ConnectionClosed: return TransportError.ConnectionClosed; + case ErrorCode.Unexpected: return TransportError.Unexpected; + default: throw new InvalidCastException($"KCP: missing error translation for {error}"); + } + } + + protected virtual void Awake() + { + // logging + // Log.Info should use Debug.Log if enabled, or nothing otherwise + // (don't want to spam the console on headless servers) + if (debugLog) + Log.Info = Debug.Log; + else + Log.Info = _ => {}; + Log.Warning = Debug.LogWarning; + Log.Error = Debug.LogError; + + // create config from serialized settings + config = new KcpConfig(DualMode, RecvBufferSize, SendBufferSize, MTU, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout, MaxRetransmit); + + // client (NonAlloc version is not necessary anymore) + client = new KcpClient( + () => OnClientConnected.Invoke(), + (message, channel) => OnClientDataReceived.Invoke(message, FromKcpChannel(channel)), + () => OnClientDisconnected?.Invoke(), // may be null in StopHost(): https://github.com/MirrorNetworking/Mirror/issues/3708 + (error, reason) => OnClientError.Invoke(ToTransportError(error), reason), + config + ); + + // server + server = new KcpServer( + (connectionId, endPoint) => OnServerConnectedWithAddress.Invoke(connectionId, endPoint.PrettyAddress()), + (connectionId, message, channel) => OnServerDataReceived.Invoke(connectionId, message, FromKcpChannel(channel)), + (connectionId) => OnServerDisconnected.Invoke(connectionId), + (connectionId, error, reason) => OnServerError.Invoke(connectionId, ToTransportError(error), reason), + config + ); + + if (statisticsLog) + InvokeRepeating(nameof(OnLogStatistics), 1, 1); + + Log.Info("KcpTransport initialized!"); + } + + protected virtual void OnValidate() + { + // show max message sizes in inspector for convenience. + // 'config' isn't available in edit mode yet, so use MTU define. + ReliableMaxMessageSize = KcpPeer.ReliableMaxMessageSize(MTU, ReceiveWindowSize); + UnreliableMaxMessageSize = KcpPeer.UnreliableMaxMessageSize(MTU); + } + + // all except WebGL + // Do not change this back to using Application.platform + // because that doesn't work in the Editor! + public override bool Available() => +#if UNITY_WEBGL + false; +#else + true; +#endif + + // client + public override bool ClientConnected() => client.connected; + public override void ClientConnect(string address) + { + client.Connect(address, Port); + } + public override void ClientConnect(Uri uri) + { + if (uri.Scheme != Scheme) + throw new ArgumentException($"Invalid url {uri}, use {Scheme}://host:port instead", nameof(uri)); + + int serverPort = uri.IsDefaultPort ? Port : uri.Port; + client.Connect(uri.Host, (ushort)serverPort); + } + public override void ClientSend(ArraySegment segment, int channelId) + { + client.Send(segment, ToKcpChannel(channelId)); + + // call event. might be null if no statistics are listening etc. + OnClientDataSent?.Invoke(segment, channelId); + } + public override void ClientDisconnect() => client.Disconnect(); + // process incoming in early update + public override void ClientEarlyUpdate() + { + // only process messages while transport is enabled. + // scene change messsages disable it to stop processing. + // (see also: https://github.com/vis2k/Mirror/pull/379) + if (enabled) client.TickIncoming(); + } + // process outgoing in late update + public override void ClientLateUpdate() => client.TickOutgoing(); + + // server + public override Uri ServerUri() + { + UriBuilder builder = new UriBuilder(); + builder.Scheme = Scheme; + builder.Host = Dns.GetHostName(); + builder.Port = Port; + return builder.Uri; + } + public override bool ServerActive() => server.IsActive(); + public override void ServerStart() => server.Start(Port); + public override void ServerSend(int connectionId, ArraySegment segment, int channelId) + { + server.Send(connectionId, segment, ToKcpChannel(channelId)); + + // call event. might be null if no statistics are listening etc. + OnServerDataSent?.Invoke(connectionId, segment, channelId); + } + public override void ServerDisconnect(int connectionId) => server.Disconnect(connectionId); + public override string ServerGetClientAddress(int connectionId) + { + IPEndPoint endPoint = server.GetClientEndPoint(connectionId); + return endPoint.PrettyAddress(); + } + public override void ServerStop() => server.Stop(); + public override void ServerEarlyUpdate() + { + // only process messages while transport is enabled. + // scene change messsages disable it to stop processing. + // (see also: https://github.com/vis2k/Mirror/pull/379) + if (enabled) server.TickIncoming(); + } + // process outgoing in late update + public override void ServerLateUpdate() => server.TickOutgoing(); + + // common + public override void Shutdown() {} + + // max message size + public override int GetMaxPacketSize(int channelId = Channels.Reliable) + { + // switch to kcp channel. + // unreliable or reliable. + // default to reliable just to be sure. + switch (channelId) + { + case Channels.Unreliable: + return KcpPeer.UnreliableMaxMessageSize(config.Mtu); + default: + return KcpPeer.ReliableMaxMessageSize(config.Mtu, ReceiveWindowSize); + } + } + + // kcp reliable channel max packet size is MTU * WND_RCV + // this allows 144kb messages. but due to head of line blocking, all + // other messages would have to wait until the maxed size one is + // delivered. batching 144kb messages each time would be EXTREMELY slow + // and fill the send queue nearly immediately when using it over the + // network. + // => instead we always use MTU sized batches. + // => people can still send maxed size if needed. + public override int GetBatchThreshold(int channelId) => + KcpPeer.UnreliableMaxMessageSize(config.Mtu); + + // server statistics + // LONG to avoid int overflows with connections.Sum. + // see also: https://github.com/vis2k/Mirror/pull/2777 + public long GetAverageMaxSendRate() => + server.connections.Count > 0 + ? server.connections.Values.Sum(conn => conn.MaxSendRate) / server.connections.Count + : 0; + public long GetAverageMaxReceiveRate() => + server.connections.Count > 0 + ? server.connections.Values.Sum(conn => conn.MaxReceiveRate) / server.connections.Count + : 0; + long GetTotalSendQueue() => + server.connections.Values.Sum(conn => conn.SendQueueCount); + long GetTotalReceiveQueue() => + server.connections.Values.Sum(conn => conn.ReceiveQueueCount); + long GetTotalSendBuffer() => + server.connections.Values.Sum(conn => conn.SendBufferCount); + long GetTotalReceiveBuffer() => + server.connections.Values.Sum(conn => conn.ReceiveBufferCount); + + // PrettyBytes function from DOTSNET + // pretty prints bytes as KB/MB/GB/etc. + // long to support > 2GB + // divides by floats to return "2.5MB" etc. + public static string PrettyBytes(long bytes) + { + // bytes + if (bytes < 1024) + return $"{bytes} B"; + // kilobytes + else if (bytes < 1024L * 1024L) + return $"{(bytes / 1024f):F2} KB"; + // megabytes + else if (bytes < 1024 * 1024L * 1024L) + return $"{(bytes / (1024f * 1024f)):F2} MB"; + // gigabytes + return $"{(bytes / (1024f * 1024f * 1024f)):F2} GB"; + } + + protected virtual void OnGUIStatistics() + { + GUILayout.BeginArea(new Rect(5, 110, 300, 300)); + + if (ServerActive()) + { + GUILayout.BeginVertical("Box"); + GUILayout.Label("SERVER"); + GUILayout.Label($" connections: {server.connections.Count}"); + GUILayout.Label($" MaxSendRate (avg): {PrettyBytes(GetAverageMaxSendRate())}/s"); + GUILayout.Label($" MaxRecvRate (avg): {PrettyBytes(GetAverageMaxReceiveRate())}/s"); + GUILayout.Label($" SendQueue: {GetTotalSendQueue()}"); + GUILayout.Label($" ReceiveQueue: {GetTotalReceiveQueue()}"); + GUILayout.Label($" SendBuffer: {GetTotalSendBuffer()}"); + GUILayout.Label($" ReceiveBuffer: {GetTotalReceiveBuffer()}"); + GUILayout.EndVertical(); + } + + if (ClientConnected()) + { + GUILayout.BeginVertical("Box"); + GUILayout.Label("CLIENT"); + GUILayout.Label($" MaxSendRate: {PrettyBytes(client.MaxSendRate)}/s"); + GUILayout.Label($" MaxRecvRate: {PrettyBytes(client.MaxReceiveRate)}/s"); + GUILayout.Label($" SendQueue: {client.SendQueueCount}"); + GUILayout.Label($" ReceiveQueue: {client.ReceiveQueueCount}"); + GUILayout.Label($" SendBuffer: {client.SendBufferCount}"); + GUILayout.Label($" ReceiveBuffer: {client.ReceiveBufferCount}"); + GUILayout.EndVertical(); + } + + GUILayout.EndArea(); + } + +// OnGUI allocates even if it does nothing. avoid in release. +#if UNITY_EDITOR || DEVELOPMENT_BUILD + protected virtual void OnGUI() + { + if (statisticsGUI) OnGUIStatistics(); + } +#endif + + protected virtual void OnLogStatistics() + { + if (ServerActive()) + { + string log = "kcp SERVER @ time: " + NetworkTime.localTime + "\n"; + log += $" connections: {server.connections.Count}\n"; + log += $" MaxSendRate (avg): {PrettyBytes(GetAverageMaxSendRate())}/s\n"; + log += $" MaxRecvRate (avg): {PrettyBytes(GetAverageMaxReceiveRate())}/s\n"; + log += $" SendQueue: {GetTotalSendQueue()}\n"; + log += $" ReceiveQueue: {GetTotalReceiveQueue()}\n"; + log += $" SendBuffer: {GetTotalSendBuffer()}\n"; + log += $" ReceiveBuffer: {GetTotalReceiveBuffer()}\n\n"; + Log.Info(log); + } + + if (ClientConnected()) + { + string log = "kcp CLIENT @ time: " + NetworkTime.localTime + "\n"; + log += $" MaxSendRate: {PrettyBytes(client.MaxSendRate)}/s\n"; + log += $" MaxRecvRate: {PrettyBytes(client.MaxReceiveRate)}/s\n"; + log += $" SendQueue: {client.SendQueueCount}\n"; + log += $" ReceiveQueue: {client.ReceiveQueueCount}\n"; + log += $" SendBuffer: {client.SendBufferCount}\n"; + log += $" ReceiveBuffer: {client.ReceiveBufferCount}\n\n"; + Log.Info(log); + } + } + + public override string ToString() => $"KCP [{port}]"; + } +} +//#endif MIRROR <- commented out because MIRROR isn't defined on first import yet diff --git a/Assets/Mirror/Transports/KCP-unmanaged/KcpTransport.cs.meta b/Assets/Mirror/Transports/KCP-unmanaged/KcpTransport.cs.meta new file mode 100644 index 0000000000..8f8e821638 --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/KcpTransport.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9b4b7c4aecdaf824cb300c034c53e9cd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/KCP-unmanaged/ThreadedKcpTransport.cs b/Assets/Mirror/Transports/KCP-unmanaged/ThreadedKcpTransport.cs new file mode 100644 index 0000000000..a0509e121a --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/ThreadedKcpTransport.cs @@ -0,0 +1,327 @@ +// Threaded version of our KCP transport. +// Elevates a few milliseconds of transport computations into a worker thread. +// +//#if MIRROR <- commented out because MIRROR isn't defined on first import yet +using System; +using System.Net; +using Mirror; +using UnityEngine; +using UnityEngine.Serialization; + +namespace kcp2k.unmanaged +{ + [HelpURL("https://mirror-networking.gitbook.io/docs/transports/kcp-transport")] + [DisallowMultipleComponent] + public class ThreadedKcpTransport : ThreadedTransport, PortTransport + { + // scheme used by this transport + public const string Scheme = "kcp"; + + // common + [Header("Transport Configuration")] + [FormerlySerializedAs("Port")] + public ushort port = 7777; + public ushort Port { get => port; set => port=value; } + [Tooltip("DualMode listens to IPv6 and IPv4 simultaneously. Disable if the platform only supports IPv4.")] + public bool DualMode = true; + [Tooltip("NoDelay is recommended to reduce latency. This also scales better without buffers getting full.")] + public bool NoDelay = true; + [Tooltip("KCP internal update interval. 100ms is KCP default, but a lower interval is recommended to minimize latency and to scale to more networked entities.")] + public uint Interval = 10; + [Tooltip("KCP timeout in milliseconds. Note that KCP sends a ping automatically.")] + public int Timeout = 10000; + [Tooltip("Socket receive buffer size. Large buffer helps support more connections. Increase operating system socket buffer size limits if needed.")] + public int RecvBufferSize = 1024 * 1027 * 7; + [Tooltip("Socket send buffer size. Large buffer helps support more connections. Increase operating system socket buffer size limits if needed.")] + public int SendBufferSize = 1024 * 1027 * 7; + + [Header("Advanced")] + [Tooltip("KCP fastresend parameter. Faster resend for the cost of higher bandwidth. 0 in normal mode, 2 in turbo mode.")] + public int FastResend = 2; + [Tooltip("KCP congestion window. Restricts window size to reduce congestion. Results in only 2-3 MTU messages per Flush even on loopback. Best to keept his disabled.")] + /*public*/ bool CongestionWindow = false; // KCP 'NoCongestionWindow' is false by default. here we negate it for ease of use. + [Tooltip("KCP window size can be modified to support higher loads. This also increases max message size.")] + public uint ReceiveWindowSize = 4096; //Kcp.WND_RCV; 128 by default. Mirror sends a lot, so we need a lot more. + [Tooltip("KCP window size can be modified to support higher loads.")] + public uint SendWindowSize = 4096; //Kcp.WND_SND; 32 by default. Mirror sends a lot, so we need a lot more. + [Tooltip("KCP will try to retransmit lost messages up to MaxRetransmit (aka dead_link) before disconnecting.")] + public uint MaxRetransmit = Kcp.DEADLINK * 2; // default prematurely disconnects a lot of people (#3022). use 2x. + [Tooltip("Enable to automatically set client & server send/recv buffers to OS limit. Avoids issues with too small buffers under heavy load, potentially dropping connections. Increase the OS limit if this is still too small.")] + [FormerlySerializedAs("MaximizeSendReceiveBuffersToOSLimit")] + public bool MaximizeSocketBuffers = true; + + [Header("Allowed Max Message Sizes\nBased on Receive Window Size")] + [Tooltip("KCP reliable max message size shown for convenience. Can be changed via ReceiveWindowSize.")] + [ReadOnly] public int ReliableMaxMessageSize = 0; // readonly, displayed from OnValidate + [Tooltip("KCP unreliable channel max message size for convenience. Not changeable.")] + [ReadOnly] public int UnreliableMaxMessageSize = 0; // readonly, displayed from OnValidate + + // config is created from the serialized properties above. + // we can expose the config directly in the future. + // for now, let's not break people's old settings. + protected KcpConfig config; + + // use default MTU for this transport. + const int MTU = Kcp.MTU_DEF; + + // server & client + KcpServer server; // USED IN WORKER THREAD. DON'T TOUCH FROM MAIN THREAD! + KcpClient client; // USED IN WORKER THREAD. DON'T TOUCH FROM MAIN THREAD! + + // copy MonoBehaviour.enabled for thread safe access + volatile bool enabledCopy = true; + + // debugging + [Header("Debug")] + public bool debugLog; + // show statistics in OnGUI + public bool statisticsGUI; + // log statistics for headless servers that can't show them in GUI + public bool statisticsLog; + + protected override void Awake() + { + // logging + // Log.Info should use Debug.Log if enabled, or nothing otherwise + // (don't want to spam the console on headless servers) + // THREAD SAFE thanks to ThreadLog.cs + if (debugLog) + Log.Info = Debug.Log; + else + Log.Info = _ => {}; + Log.Warning = Debug.LogWarning; + Log.Error = Debug.LogError; + + // create config from serialized settings + config = new KcpConfig(DualMode, RecvBufferSize, SendBufferSize, MTU, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout, MaxRetransmit); + + // client (NonAlloc version is not necessary anymore) + client = new KcpClient( + OnThreadedClientConnected, + (message, channel) => OnThreadedClientReceive(message, KcpTransport.FromKcpChannel(channel)), + OnThreadedClientDisconnected, + (error, reason) => OnThreadedClientError(KcpTransport.ToTransportError(error), reason), + config + ); + + // server + server = new KcpServer( + OnThreadedServerConnected, + (connectionId, message, channel) => OnThreadedServerReceive(connectionId, message, KcpTransport.FromKcpChannel(channel)), + OnThreadedServerDisconnected, + (connectionId, error, reason) => OnThreadedServerError(connectionId, KcpTransport.ToTransportError(error), reason), + config + ); + + if (statisticsLog) + InvokeRepeating(nameof(OnLogStatistics), 1, 1); + + // call base after creating kcp. + // it'll be used by the created thread immediately. + base.Awake(); + + Log.Info("ThreadedKcpTransport initialized!"); + } + + protected virtual void OnValidate() + { + // show max message sizes in inspector for convenience. + // 'config' isn't available in edit mode yet, so use MTU define. + ReliableMaxMessageSize = KcpPeer.ReliableMaxMessageSize(MTU, ReceiveWindowSize); + UnreliableMaxMessageSize = KcpPeer.UnreliableMaxMessageSize(MTU); + } + + // copy MonoBehaviour.enabled for thread safe use + void OnEnable() => enabledCopy = true; + void OnDisable() => enabledCopy = true; + + // all except WebGL + // Do not change this back to using Application.platform + // because that doesn't work in the Editor! + public override bool Available() => +#if UNITY_WEBGL + false; +#else + true; +#endif + + protected override void ThreadedClientConnect(string address) => client.Connect(address, Port); + protected override void ThreadedClientConnect(Uri uri) + { + if (uri.Scheme != Scheme) + throw new ArgumentException($"Invalid url {uri}, use {Scheme}://host:port instead", nameof(uri)); + + int serverPort = uri.IsDefaultPort ? Port : uri.Port; + client.Connect(uri.Host, (ushort)serverPort); + } + protected override void ThreadedClientSend(ArraySegment segment, int channelId) + { + client.Send(segment, KcpTransport.ToKcpChannel(channelId)); + + // thread safe version for statistics + OnThreadedClientSend(segment, channelId); + } + protected override void ThreadedClientDisconnect() => client.Disconnect(); + // process incoming in early update + protected override void ThreadedClientEarlyUpdate() + { + // only process messages while transport is enabled. + // scene change messsages disable it to stop processing. + // (see also: https://github.com/vis2k/Mirror/pull/379) + // => enabledCopy for thread safe use + if (enabledCopy) client.TickIncoming(); + } + // process outgoing in late update + protected override void ThreadedClientLateUpdate() => client.TickOutgoing(); + + // server thread overrides + public override Uri ServerUri() + { + UriBuilder builder = new UriBuilder(); + builder.Scheme = Scheme; + builder.Host = Dns.GetHostName(); + builder.Port = Port; + return builder.Uri; + } + protected override void ThreadedServerStart() => server.Start(Port); + protected override void ThreadedServerSend(int connectionId, ArraySegment segment, int channelId) + { + server.Send(connectionId, segment, KcpTransport.ToKcpChannel(channelId)); + + // thread safe version for statistics + OnThreadedServerSend(connectionId, segment, channelId); + } + protected override void ThreadedServerDisconnect(int connectionId) => server.Disconnect(connectionId); + /* NOT THREAD SAFE. ThreadedTransport version throws NotImplementedException for this. + public override string ServerGetClientAddress(int connectionId) + { + IPEndPoint endPoint = server.GetClientEndPoint(connectionId); + return endPoint != null + // Map to IPv4 if "IsIPv4MappedToIPv6" + // "::ffff:127.0.0.1" -> "127.0.0.1" + ? (endPoint.Address.IsIPv4MappedToIPv6 + ? endPoint.Address.MapToIPv4().ToString() + : endPoint.Address.ToString()) + : ""; + } + */ + protected override void ThreadedServerStop() => server.Stop(); + protected override void ThreadedServerEarlyUpdate() + { + // only process messages while transport is enabled. + // scene change messsages disable it to stop processing. + // (see also: https://github.com/vis2k/Mirror/pull/379) + // => enabledCopy for thread safe use + if (enabledCopy) server.TickIncoming(); + } + // process outgoing in late update + protected override void ThreadedServerLateUpdate() => server.TickOutgoing(); + + protected override void ThreadedShutdown() {} + + // max message size + public override int GetMaxPacketSize(int channelId = Channels.Reliable) + { + // switch to kcp channel. + // unreliable or reliable. + // default to reliable just to be sure. + switch (channelId) + { + case Channels.Unreliable: + return KcpPeer.UnreliableMaxMessageSize(config.Mtu); + default: + return KcpPeer.ReliableMaxMessageSize(config.Mtu, ReceiveWindowSize); + } + } + + // kcp reliable channel max packet size is MTU * WND_RCV + // this allows 144kb messages. but due to head of line blocking, all + // other messages would have to wait until the maxed size one is + // delivered. batching 144kb messages each time would be EXTREMELY slow + // and fill the send queue nearly immediately when using it over the + // network. + // => instead we always use MTU sized batches. + // => people can still send maxed size if needed. + public override int GetBatchThreshold(int channelId) => + KcpPeer.UnreliableMaxMessageSize(config.Mtu); + + protected virtual void OnGUIStatistics() + { + // TODO not thread safe + /* + GUILayout.BeginArea(new Rect(5, 110, 300, 300)); + + if (ServerActive()) + { + GUILayout.BeginVertical("Box"); + GUILayout.Label("SERVER"); + GUILayout.Label($" connections: {server.connections.Count}"); + GUILayout.Label($" MaxSendRate (avg): {KcpTransport.PrettyBytes(GetAverageMaxSendRate())}/s"); + GUILayout.Label($" MaxRecvRate (avg): {KcpTransport.PrettyBytes(GetAverageMaxReceiveRate())}/s"); + GUILayout.Label($" SendQueue: {GetTotalSendQueue()}"); + GUILayout.Label($" ReceiveQueue: {GetTotalReceiveQueue()}"); + GUILayout.Label($" SendBuffer: {GetTotalSendBuffer()}"); + GUILayout.Label($" ReceiveBuffer: {GetTotalReceiveBuffer()}"); + GUILayout.EndVertical(); + } + + if (ClientConnected()) + { + GUILayout.BeginVertical("Box"); + GUILayout.Label("CLIENT"); + GUILayout.Label($" MaxSendRate: {KcpTransport.PrettyBytes(client.peer.MaxSendRate)}/s"); + GUILayout.Label($" MaxRecvRate: {KcpTransport.PrettyBytes(client.peer.MaxReceiveRate)}/s"); + GUILayout.Label($" SendQueue: {client.peer.SendQueueCount}"); + GUILayout.Label($" ReceiveQueue: {client.peer.ReceiveQueueCount}"); + GUILayout.Label($" SendBuffer: {client.peer.SendBufferCount}"); + GUILayout.Label($" ReceiveBuffer: {client.peer.ReceiveBufferCount}"); + GUILayout.EndVertical(); + } + + GUILayout.EndArea(); + */ + } + +// OnGUI allocates even if it does nothing. avoid in release. +#if UNITY_EDITOR || DEVELOPMENT_BUILD + protected virtual void OnGUI() + { + if (statisticsGUI) OnGUIStatistics(); + } +#endif + + protected virtual void OnLogStatistics() + { + // TODO not thread safe + /* + if (ServerActive()) + { + string log = "kcp SERVER @ time: " + NetworkTime.localTime + "\n"; + log += $" connections: {server.connections.Count}\n"; + log += $" MaxSendRate (avg): {KcpTransport.PrettyBytes(GetAverageMaxSendRate())}/s\n"; + log += $" MaxRecvRate (avg): {KcpTransport.PrettyBytes(GetAverageMaxReceiveRate())}/s\n"; + log += $" SendQueue: {GetTotalSendQueue()}\n"; + log += $" ReceiveQueue: {GetTotalReceiveQueue()}\n"; + log += $" SendBuffer: {GetTotalSendBuffer()}\n"; + log += $" ReceiveBuffer: {GetTotalReceiveBuffer()}\n\n"; + Log.Info(log); + } + + if (ClientConnected()) + { + string log = "kcp CLIENT @ time: " + NetworkTime.localTime + "\n"; + log += $" MaxSendRate: {KcpTransport.PrettyBytes(client.peer.MaxSendRate)}/s\n"; + log += $" MaxRecvRate: {KcpTransport.PrettyBytes(client.peer.MaxReceiveRate)}/s\n"; + log += $" SendQueue: {client.peer.SendQueueCount}\n"; + log += $" ReceiveQueue: {client.peer.ReceiveQueueCount}\n"; + log += $" SendBuffer: {client.peer.SendBufferCount}\n"; + log += $" ReceiveBuffer: {client.peer.ReceiveBufferCount}\n\n"; + Log.Info(log); + } + */ + } + + public override string ToString() => $"ThreadedKCP {port}"; + } +} +//#endif MIRROR <- commented out because MIRROR isn't defined on first import yet diff --git a/Assets/Mirror/Transports/KCP-unmanaged/ThreadedKcpTransport.cs.meta b/Assets/Mirror/Transports/KCP-unmanaged/ThreadedKcpTransport.cs.meta new file mode 100644 index 0000000000..ad2edd1068 --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/ThreadedKcpTransport.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 89fae3f597eb82e4ab2d2015dcf622dd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k.meta b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k.meta new file mode 100644 index 0000000000..ce0551f111 --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e7ecb3de9e167ff4bbea6449fe86e888 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/KCP.asmdef b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/KCP.asmdef new file mode 100644 index 0000000000..23ee2b2c3d --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/KCP.asmdef @@ -0,0 +1,16 @@ +{ + "name": "kcp2k.unmanaged", + "rootNamespace": "", + "references": [ + "GUID:30817c1a0e6d646d99c048fc403f5979" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": true, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/KCP.asmdef.meta b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/KCP.asmdef.meta new file mode 100644 index 0000000000..cd8d349e10 --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/KCP.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a531e8e15c89e614dad06d5d7a3ef1c2 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/LICENSE.txt b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/LICENSE.txt new file mode 100644 index 0000000000..c77582e851 --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/LICENSE.txt @@ -0,0 +1,24 @@ +MIT License + +Copyright (c) 2016 limpo1989 +Copyright (c) 2020 Paul Pacheco +Copyright (c) 2020 Lymdun +Copyright (c) 2020 vis2k + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/LICENSE.txt.meta b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/LICENSE.txt.meta new file mode 100644 index 0000000000..9cc6c7ceb8 --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/LICENSE.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: bf0adfd231883624992ed6aa80ad0440 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/VERSION.txt b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/VERSION.txt new file mode 100644 index 0000000000..43aafcfb6c --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/VERSION.txt @@ -0,0 +1,261 @@ +V1.41 [2024-04-28] +- fix: KcpHeader is now parsed safely, handling attackers potentially sending values out of enum range +- fix: KcpClient RawSend may throw ConnectionRefused SocketException when OnDisconnected calls SendDisconnect(), which is fine +- fix: less scary cookie message and better explanation + +V1.40 [2024-01-03] +- added [KCP] to all log messages +- fix: #3704 remove old fix for #2353 which caused log spam and isn't needed anymore since the + original Mirror issue is long gone +- fix: KcpClient.RawSend now returns if socket wasn't created yet +- fix: https://github.com/MirrorNetworking/Mirror/issues/3591 KcpPeer.SendDisconnect now rapid + fires several unreliable messages instead of sending reliable. Fixes disconnect message not + going through if the connection is closed & removed immediately after. + +V1.39 [2023-10-31] +- fix: https://github.com/MirrorNetworking/Mirror/issues/3611 Windows UDP socket exceptions + on server if one of the clients died + +V1.38 [2023-10-29] +- fix: #54 mismatching cookie race condition. cookie is now included in all messages. +- feature: Exposed local end point on KcpClient/Server +- refactor: KcpPeer refactored as abstract class to remove KcpServer initialization workarounds + +V1.37 [2023-07-31] +- fix: #47 KcpServer.Stop now clears connections so they aren't carried over to the next session +- fix: KcpPeer doesn't log 'received unreliable message while not authenticated' anymore. + +V1.36 [2023-06-08] +- fix: #49 KcpPeer.RawInput message size check now considers cookie as well +- kcp.cs cleanups + +V1.35 [2023-04-05] +- fix: KcpClients now need to validate with a secure cookie in order to protect against + UDP spoofing. fixes: + https://github.com/MirrorNetworking/Mirror/issues/3286 + [disclosed by IncludeSec] +- KcpClient/Server: change callbacks to protected so inheriting classes can use them too +- KcpClient/Server: change config visibility to protected + +V1.34 [2023-03-15] +- Send/SendTo/Receive/ReceiveFrom NonBlocking extensions. + to encapsulate WouldBlock allocations, exceptions, etc. + allows for reuse when overwriting KcpServer/Client (i.e. for relays). + +V1.33 [2023-03-14] +- perf: KcpServer/Client RawReceive now call socket.Poll to avoid non-blocking + socket's allocating a new SocketException in case they WouldBlock. + fixes https://github.com/MirrorNetworking/Mirror/issues/3413 +- perf: KcpServer/Client RawSend now call socket.Poll to avoid non-blocking + socket's allocating a new SocketException in case they WouldBlock. + fixes https://github.com/MirrorNetworking/Mirror/issues/3413 + +V1.32 [2023-03-12] +- fix: KcpPeer RawInput now doesn't disconnect in case of random internet noise + +V1.31 [2023-03-05] +- KcpClient: Tick/Incoming/Outgoing can now be overwritten (virtual) +- breaking: KcpClient now takes KcpConfig in constructor instead of in Connect. + cleaner, and prepares for KcpConfig.MTU setting. +- KcpConfig now includes MTU; KcpPeer now works with KcpConfig's MTU, KcpServer/Client + buffers are now created with config's MTU. + +V1.30 [2023-02-20] +- fix: set send/recv buffer sizes directly instead of iterating to find the limit. + fixes: https://github.com/MirrorNetworking/Mirror/issues/3390 +- fix: server & client sockets are now always non-blocking to ensure main thread never + blocks on socket.recv/send. Send() now also handles WouldBlock. +- fix: socket.Receive/From directly with non-blocking sockets and handle WouldBlock, + instead of socket.Poll. faster, more obvious, and fixes Poll() looping forever while + socket is in error state. fixes: https://github.com/MirrorNetworking/Mirror/issues/2733 + +V1.29 [2023-01-28] +- fix: KcpServer.CreateServerSocket now handles NotSupportedException when setting DualMode + https://github.com/MirrorNetworking/Mirror/issues/3358 + +V1.28 [2023-01-28] +- fix: KcpClient.Connect now resolves hostname before creating peer + https://github.com/MirrorNetworking/Mirror/issues/3361 + +V1.27 [2023-01-08] +- KcpClient.Connect: invoke own events directly instead of going through peer, + which calls our own events anyway +- fix: KcpPeer/Client/Server callbacks are readonly and assigned in constructor + to ensure they are safe to use at all times. + fixes https://github.com/MirrorNetworking/Mirror/issues/3337 + +V1.26 [2022-12-22] +- KcpPeer.RawInput: fix compile error in old Unity Mono versions +- fix: KcpServer sets up a new connection's OnError immediately. + fixes KcpPeer throwing NullReferenceException when attempting to call OnError + after authentication errors. +- improved log messages + +V1.25 [2022-12-14] +- breaking: removed where-allocation. use IL2CPP on servers instead. +- breaking: KcpConfig to simplify configuration +- high level cleanups + +V1.24 [2022-12-14] +- KcpClient: fixed NullReferenceException when connection without a server. + added test coverage to ensure this never happens again. + +V1.23 [2022-12-07] +- KcpClient: rawReceiveBuffer exposed +- fix: KcpServer RawSend uses connection.remoteEndPoint instead of the helper + 'newClientEP'. fixes clients receiving the wrong messages meant for others. + https://github.com/MirrorNetworking/Mirror/issues/3296 + +V1.22 [2022-11-30] +- high level refactor, part two. + +V1.21 [2022-11-24] +- high level refactor, part one. + - KcpPeer instead of KcpConnection, KcpClientConnection, KcpServerConnection + - RawSend/Receive can now easily be overwritten in KcpClient/Server. + for non-alloc, relays, etc. + +V1.20 [2022-11-22] +- perf: KcpClient receive allocation was removed entirely. + reduces Mirror benchmark client sided allocations from 4.9 KB / 1.7 KB (non-alloc) to 0B. +- fix: KcpConnection.Disconnect does not check socket.Connected anymore. + UDP sockets don't have a connection. + fixes Disconnects not being sent to clients in netcore. +- KcpConnection.SendReliable: added OnError instead of logs + +V1.19 [2022-05-12] +- feature: OnError ErrorCodes + +V1.18 [2022-05-08] +- feature: OnError to allow higher level to show popups etc. +- feature: KcpServer.GetClientAddress is now GetClientEndPoint in order to + expose more details +- ResolveHostname: include exception in log for easier debugging +- fix: KcpClientConnection.RawReceive now logs the SocketException even if + it was expected. makes debugging easier. +- fix: KcpServer.TickIncoming now logs the SocketException even if it was + expected. makes debugging easier. +- fix: KcpClientConnection.RawReceive now calls Disconnect() if the other end + has closed the connection. better than just remaining in a state with unusable + sockets. + +V1.17 [2022-01-09] +- perf: server/client MaximizeSendReceiveBuffersToOSLimit option to set send/recv + buffer sizes to OS limit. avoids drops due to small buffers under heavy load. + +V1.16 [2022-01-06] +- fix: SendUnreliable respects ArraySegment.Offset +- fix: potential bug with negative length (see PR #2) +- breaking: removed pause handling because it's not necessary for Mirror anymore + +V1.15 [2021-12-11] +- feature: feature: MaxRetransmits aka dead_link now configurable +- dead_link disconnect message improved to show exact retransmit count + +V1.14 [2021-11-30] +- fix: Send() now throws an exception for messages which require > 255 fragments +- fix: ReliableMaxMessageSize is now limited to messages which require <= 255 fragments + +V1.13 [2021-11-28] +- fix: perf: uncork max message size from 144 KB to as much as we want based on + receive window size. + fixes https://github.com/vis2k/kcp2k/issues/22 + fixes https://github.com/skywind3000/kcp/pull/291 +- feature: OnData now includes channel it was received on + +V1.12 [2021-07-16] +- Tests: don't depend on Unity anymore +- fix: #26 - Kcp now catches exception if host couldn't be resolved, and calls + OnDisconnected to let the user now. +- fix: KcpServer.DualMode is now configurable in the constructor instead of + using #if UNITY_SWITCH. makes it run on all other non dual mode platforms too. +- fix: where-allocation made optional via virtuals and inheriting + KcpServer/Client/Connection NonAlloc classes. fixes a bug where some platforms + might not support where-allocation. + +V1.11 rollback [2021-06-01] +- perf: Segment MemoryStream initial capacity set to MTU to avoid early runtime + resizing/allocations + +V1.10 [2021-05-28] +- feature: configurable Timeout +- allocations explained with comments (C# ReceiveFrom / IPEndPoint.GetHashCode) +- fix: #17 KcpConnection.ReceiveNextReliable now assigns message default so it + works in .net too +- fix: Segment pool is not static anymore. Each kcp instance now has it's own + Pool. fixes #18 concurrency issues + +V1.9 [2021-03-02] +- Tick() split into TickIncoming()/TickOutgoing() to use in Mirror's new update + functions. allows to minimize latency. + => original Tick() is still supported for convenience. simply processes both! + +V1.8 [2021-02-14] +- fix: Unity IPv6 errors on Nintendo Switch +- fix: KcpConnection now disconnects if data message was received without content. + previously it would call OnData with an empty ArraySegment, causing all kinds of + weird behaviour in Mirror/DOTSNET. Added tests too. +- fix: KcpConnection.SendData: don't allow sending empty messages anymore. disconnect + and log a warning to make it completely obvious. + +V1.7 [2021-01-13] +- fix: unreliable messages reset timeout now too +- perf: KcpConnection OnCheckEnabled callback changed to a simple 'paused' boolean. + This is faster than invoking a Func every time and allows us to fix #8 more + easily later by calling .Pause/.Unpause from OnEnable/OnDisable in MirrorTransport. +- fix #8: Unpause now resets timeout to fix a bug where Mirror would pause kcp, + change the scene which took >10s, then unpause and kcp would detect the lack of + any messages for >10s as timeout. Added test to make sure it never happens again. +- MirrorTransport: statistics logging for headless servers +- Mirror Transport: Send/Receive window size increased once more from 2048 to 4096. + +V1.6 [2021-01-10] +- Unreliable channel added! +- perf: KcpHeader byte added to every kcp message to indicate + Handshake/Data/Ping/Disconnect instead of scanning each message for Hello/Byte/Ping + content via SegmentEquals. It's a lot cleaner, should be faster and should avoid + edge cases where a message content would equal Hello/Ping/Bye sequence accidentally. +- Kcp.Input: offset moved to parameters for cases where it's needed +- Kcp.SetMtu from original Kcp.c + +V1.5 [2021-01-07] +- KcpConnection.MaxSend/ReceiveRate calculation based on the article +- MirrorTransport: large send/recv window size defaults to avoid high latencies caused + by packets not being processed fast enough +- MirrorTransport: show MaxSend/ReceiveRate in debug gui +- MirrorTransport: don't Log.Info to console in headless mode if debug log is disabled + +V1.4 [2020-11-27] +- fix: OnCheckEnabled added. KcpConnection message processing while loop can now + be interrupted immediately. fixes Mirror Transport scene changes which need to stop + processing any messages immediately after a scene message) +- perf: Mirror KcpTransport: FastResend enabled by default. turbo mode according to: + https://github.com/skywind3000/kcp/blob/master/README.en.md#protocol-configuration +- perf: Mirror KcpTransport: CongestionControl disabled by default (turbo mode) + +V1.3 [2020-11-17] +- Log.Info/Warning/Error so logging doesn't depend on UnityEngine anymore +- fix: Server.Tick catches SocketException which happens if Android client is killed +- MirrorTransport: debugLog option added that can be checked in Unity Inspector +- Utils.Clamp so Kcp.cs doesn't depend on UnityEngine +- Utils.SegmentsEqual: use Linq SequenceEqual so it doesn't depend on UnityEngine +=> kcp2k can now be used in any C# project even without Unity + +V1.2 [2020-11-10] +- more tests added +- fix: raw receive buffers are now all of MTU size +- fix: raw receive detects error where buffer was too small for msgLength and + result in excess data being dropped silently +- KcpConnection.MaxMessageSize added for use in high level +- KcpConnection.MaxMessageSize increased from 1200 bytes to to maximum allowed + message size of 145KB for kcp (based on mtu, overhead, wnd_rcv) + +V1.1 [2020-10-30] +- high level cleanup, fixes, improvements + +V1.0 [2020-10-22] +- Kcp.cs now mirrors original Kcp.c behaviour + (this fixes dozens of bugs) + +V0.1 +- initial kcp-csharp based version \ No newline at end of file diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/VERSION.txt.meta b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/VERSION.txt.meta new file mode 100644 index 0000000000..6c461b3cbf --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/VERSION.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3bd73c0a194f4844c9fefd82d2f199e0 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel.meta b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel.meta new file mode 100644 index 0000000000..a743b9026c --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 95eb3e07317933541a29b1df37ca1a1c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/Common.cs b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/Common.cs new file mode 100644 index 0000000000..8632d4a683 --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/Common.cs @@ -0,0 +1,75 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Security.Cryptography; + +namespace kcp2k.unmanaged +{ + public static class Common + { + // helper function to resolve host to IPAddress + public static bool ResolveHostname(string hostname, out IPAddress[] addresses) + { + try + { + // NOTE: dns lookup is blocking. this can take a second. + addresses = Dns.GetHostAddresses(hostname); + return addresses.Length >= 1; + } + catch (SocketException exception) + { + Log.Info($"[KCP] Failed to resolve host: {hostname} reason: {exception}"); + addresses = null; + return false; + } + } + + // if connections drop under heavy load, increase to OS limit. + // if still not enough, increase the OS limit. + public static void ConfigureSocketBuffers(Socket socket, int recvBufferSize, int sendBufferSize) + { + // log initial size for comparison. + // remember initial size for log comparison + int initialReceive = socket.ReceiveBufferSize; + int initialSend = socket.SendBufferSize; + + // set to configured size + try + { + socket.ReceiveBufferSize = recvBufferSize; + socket.SendBufferSize = sendBufferSize; + } + catch (SocketException) + { + Log.Warning($"[KCP] failed to set Socket RecvBufSize = {recvBufferSize} SendBufSize = {sendBufferSize}"); + } + + + Log.Info($"[KCP] RecvBuf = {initialReceive}=>{socket.ReceiveBufferSize} ({socket.ReceiveBufferSize/initialReceive}x) SendBuf = {initialSend}=>{socket.SendBufferSize} ({socket.SendBufferSize/initialSend}x)"); + } + + // generate a connection hash from IP+Port. + // + // NOTE: IPEndPoint.GetHashCode() allocates. + // it calls m_Address.GetHashCode(). + // m_Address is an IPAddress. + // GetHashCode() allocates for IPv6: + // https://github.com/mono/mono/blob/bdd772531d379b4e78593587d15113c37edd4a64/mcs/class/referencesource/System/net/System/Net/IPAddress.cs#L699 + // + // => using only newClientEP.Port wouldn't work, because + // different connections can have the same port. + public static int ConnectionHash(EndPoint endPoint) => + endPoint.GetHashCode(); + + // cookies need to be generated with a secure random generator. + // we don't want them to be deterministic / predictable. + // RNG is cached to avoid runtime allocations. + static readonly RNGCryptoServiceProvider cryptoRandom = new RNGCryptoServiceProvider(); + static readonly byte[] cryptoRandomBuffer = new byte[4]; + public static uint GenerateCookie() + { + cryptoRandom.GetBytes(cryptoRandomBuffer); + return BitConverter.ToUInt32(cryptoRandomBuffer, 0); + } + } +} diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/Common.cs.meta b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/Common.cs.meta new file mode 100644 index 0000000000..22f019a70c --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/Common.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6e7cdc5c396d7aa4bb966e3a58fbb380 +timeCreated: 1669135138 \ No newline at end of file diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/ErrorCode.cs b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/ErrorCode.cs new file mode 100644 index 0000000000..9853360978 --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/ErrorCode.cs @@ -0,0 +1,15 @@ +// kcp specific error codes to allow for error switching, localization, +// translation to Mirror errors, etc. +namespace kcp2k.unmanaged +{ + public enum ErrorCode : byte + { + DnsResolve, // failed to resolve a host name + Timeout, // ping timeout or dead link + Congestion, // more messages than transport / network can process + InvalidReceive, // recv invalid packet (possibly intentional attack) + InvalidSend, // user tried to send invalid data + ConnectionClosed, // connection closed voluntarily or lost involuntarily + Unexpected // unexpected error / exception, requires fix. + } +} \ No newline at end of file diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/ErrorCode.cs.meta b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/ErrorCode.cs.meta new file mode 100644 index 0000000000..9a68780b68 --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/ErrorCode.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c45c4643ab87c0143bbf6cf345e293dd +timeCreated: 1652320712 \ No newline at end of file diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/Extensions.cs b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/Extensions.cs new file mode 100644 index 0000000000..23e2fe7baf --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/Extensions.cs @@ -0,0 +1,166 @@ +using System; +using System.Net; +using System.Net.Sockets; + +namespace kcp2k.unmanaged +{ + public static class Extensions + { + // ArraySegment as HexString for convenience + public static string ToHexString(this ArraySegment segment) => + BitConverter.ToString(segment.Array, segment.Offset, segment.Count); + + // non-blocking UDP send. + // allows for reuse when overwriting KcpServer/Client (i.e. for relays). + // => wrapped with Poll to avoid WouldBlock allocating new SocketException. + // => wrapped with try-catch to ignore WouldBlock exception. + // make sure to set socket.Blocking = false before using this! + public static bool SendToNonBlocking(this Socket socket, ArraySegment data, EndPoint remoteEP) + { + try + { + // when using non-blocking sockets, SendTo may return WouldBlock. + // in C#, WouldBlock throws a SocketException, which is expected. + // unfortunately, creating the SocketException allocates in C#. + // let's poll first to avoid the WouldBlock allocation. + // note that this entirely to avoid allocations. + // non-blocking UDP doesn't need Poll in other languages. + // and the code still works without the Poll call. + if (!socket.Poll(0, SelectMode.SelectWrite)) return false; + + // send to the the endpoint. + // do not send to 'newClientEP', as that's always reused. + // fixes https://github.com/MirrorNetworking/Mirror/issues/3296 + socket.SendTo(data.Array, data.Offset, data.Count, SocketFlags.None, remoteEP); + return true; + } + catch (SocketException e) + { + // for non-blocking sockets, SendTo may throw WouldBlock. + // in that case, simply drop the message. it's UDP, it's fine. + if (e.SocketErrorCode == SocketError.WouldBlock) return false; + + // otherwise it's a real socket error. throw it. + throw; + } + } + + // non-blocking UDP send. + // allows for reuse when overwriting KcpServer/Client (i.e. for relays). + // => wrapped with Poll to avoid WouldBlock allocating new SocketException. + // => wrapped with try-catch to ignore WouldBlock exception. + // make sure to set socket.Blocking = false before using this! + public static bool SendNonBlocking(this Socket socket, ArraySegment data) + { + try + { + // when using non-blocking sockets, SendTo may return WouldBlock. + // in C#, WouldBlock throws a SocketException, which is expected. + // unfortunately, creating the SocketException allocates in C#. + // let's poll first to avoid the WouldBlock allocation. + // note that this entirely to avoid allocations. + // non-blocking UDP doesn't need Poll in other languages. + // and the code still works without the Poll call. + if (!socket.Poll(0, SelectMode.SelectWrite)) return false; + + // SendTo allocates. we used bound Send. + socket.Send(data.Array, data.Offset, data.Count, SocketFlags.None); + return true; + } + catch (SocketException e) + { + // for non-blocking sockets, SendTo may throw WouldBlock. + // in that case, simply drop the message. it's UDP, it's fine. + if (e.SocketErrorCode == SocketError.WouldBlock) return false; + + // otherwise it's a real socket error. throw it. + throw; + } + } + + // non-blocking UDP receive. + // allows for reuse when overwriting KcpServer/Client (i.e. for relays). + // => wrapped with Poll to avoid WouldBlock allocating new SocketException. + // => wrapped with try-catch to ignore WouldBlock exception. + // make sure to set socket.Blocking = false before using this! + public static bool ReceiveFromNonBlocking(this Socket socket, byte[] recvBuffer, out ArraySegment data, ref EndPoint remoteEP) + { + data = default; + + try + { + // when using non-blocking sockets, ReceiveFrom may return WouldBlock. + // in C#, WouldBlock throws a SocketException, which is expected. + // unfortunately, creating the SocketException allocates in C#. + // let's poll first to avoid the WouldBlock allocation. + // note that this entirely to avoid allocations. + // non-blocking UDP doesn't need Poll in other languages. + // and the code still works without the Poll call. + if (!socket.Poll(0, SelectMode.SelectRead)) return false; + + // NOTE: ReceiveFrom allocates. + // we pass our IPEndPoint to ReceiveFrom. + // receive from calls newClientEP.Create(socketAddr). + // IPEndPoint.Create always returns a new IPEndPoint. + // https://github.com/mono/mono/blob/f74eed4b09790a0929889ad7fc2cf96c9b6e3757/mcs/class/System/System.Net.Sockets/Socket.cs#L1761 + // + // throws SocketException if datagram was larger than buffer. + // https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.receive?view=net-6.0 + int size = socket.ReceiveFrom(recvBuffer, 0, recvBuffer.Length, SocketFlags.None, ref remoteEP); + data = new ArraySegment(recvBuffer, 0, size); + return true; + } + catch (SocketException e) + { + // for non-blocking sockets, Receive throws WouldBlock if there is + // no message to read. that's okay. only log for other errors. + if (e.SocketErrorCode == SocketError.WouldBlock) return false; + + // otherwise it's a real socket error. throw it. + throw; + } + } + + // non-blocking UDP receive. + // allows for reuse when overwriting KcpServer/Client (i.e. for relays). + // => wrapped with Poll to avoid WouldBlock allocating new SocketException. + // => wrapped with try-catch to ignore WouldBlock exception. + // make sure to set socket.Blocking = false before using this! + public static bool ReceiveNonBlocking(this Socket socket, byte[] recvBuffer, out ArraySegment data) + { + data = default; + + try + { + // when using non-blocking sockets, ReceiveFrom may return WouldBlock. + // in C#, WouldBlock throws a SocketException, which is expected. + // unfortunately, creating the SocketException allocates in C#. + // let's poll first to avoid the WouldBlock allocation. + // note that this entirely to avoid allocations. + // non-blocking UDP doesn't need Poll in other languages. + // and the code still works without the Poll call. + if (!socket.Poll(0, SelectMode.SelectRead)) return false; + + // ReceiveFrom allocates. we used bound Receive. + // returns amount of bytes written into buffer. + // throws SocketException if datagram was larger than buffer. + // https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.receive?view=net-6.0 + // + // throws SocketException if datagram was larger than buffer. + // https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.receive?view=net-6.0 + int size = socket.Receive(recvBuffer, 0, recvBuffer.Length, SocketFlags.None); + data = new ArraySegment(recvBuffer, 0, size); + return true; + } + catch (SocketException e) + { + // for non-blocking sockets, Receive throws WouldBlock if there is + // no message to read. that's okay. only log for other errors. + if (e.SocketErrorCode == SocketError.WouldBlock) return false; + + // otherwise it's a real socket error. throw it. + throw; + } + } + } +} \ No newline at end of file diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/Extensions.cs.meta b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/Extensions.cs.meta new file mode 100644 index 0000000000..123688204a --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/Extensions.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 23520eb321fb817478941a75a2e9f33a +timeCreated: 1641701011 \ No newline at end of file diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpChannel.cs b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpChannel.cs new file mode 100644 index 0000000000..abed409bb0 --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpChannel.cs @@ -0,0 +1,10 @@ +namespace kcp2k.unmanaged +{ + // channel type and header for raw messages + public enum KcpChannel : byte + { + // don't react on 0x00. might help to filter out random noise. + Reliable = 1, + Unreliable = 2 + } +} \ No newline at end of file diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpChannel.cs.meta b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpChannel.cs.meta new file mode 100644 index 0000000000..8ec0afb9b4 --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpChannel.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 493b2b65c9b07354ca0863a7741dd84b +timeCreated: 1610081248 \ No newline at end of file diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpClient.cs b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpClient.cs new file mode 100644 index 0000000000..e3fa57ccd0 --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpClient.cs @@ -0,0 +1,292 @@ +// kcp client logic abstracted into a class. +// for use in Mirror, DOTSNET, testing, etc. +using System; +using System.Net; +using System.Net.Sockets; + +namespace kcp2k.unmanaged +{ + public class KcpClient : KcpPeer + { + // IO + protected Socket socket; + public EndPoint remoteEndPoint; + + // expose local endpoint for users / relays / nat traversal etc. + public EndPoint LocalEndPoint => socket?.LocalEndPoint; + + // config + protected readonly KcpConfig config; + + // raw receive buffer always needs to be of 'MTU' size, even if + // MaxMessageSize is larger. kcp always sends in MTU segments and having + // a buffer smaller than MTU would silently drop excess data. + // => we need the MTU to fit channel + message! + // => protected because someone may overwrite RawReceive but still wants + // to reuse the buffer. + protected readonly byte[] rawReceiveBuffer; + + // callbacks + // even for errors, to allow liraries to show popups etc. + // instead of logging directly. + // (string instead of Exception for ease of use and to avoid user panic) + // + // events are readonly, set in constructor. + // this ensures they are always initialized when used. + // fixes https://github.com/MirrorNetworking/Mirror/issues/3337 and more + protected readonly Action OnConnectedCallback; + protected readonly Action, KcpChannel> OnDataCallback; + protected readonly Action OnDisconnectedCallback; + protected readonly Action OnErrorCallback; + + // state + bool active = false; // active between when connect() and disconnect() are called + public bool connected; + + public KcpClient(Action OnConnected, + Action, KcpChannel> OnData, + Action OnDisconnected, + Action OnError, + KcpConfig config) + : base(config, 0) // client has no cookie yet + { + // initialize callbacks first to ensure they can be used safely. + OnConnectedCallback = OnConnected; + OnDataCallback = OnData; + OnDisconnectedCallback = OnDisconnected; + OnErrorCallback = OnError; + this.config = config; + + // create mtu sized receive buffer + rawReceiveBuffer = new byte[config.Mtu]; + } + + // callbacks /////////////////////////////////////////////////////////// + // some callbacks need to wrapped with some extra logic + protected override void OnAuthenticated() + { + Log.Info($"[KCP] Client: OnConnected"); + connected = true; + OnConnectedCallback(); + } + + protected override void OnData(ArraySegment message, KcpChannel channel) => + OnDataCallback(message, channel); + + protected override void OnError(ErrorCode error, string message) => + OnErrorCallback(error, message); + + protected override void OnDisconnected() + { + Log.Info($"[KCP] Client: OnDisconnected"); + connected = false; + socket?.Close(); + socket = null; + remoteEndPoint = null; + OnDisconnectedCallback(); + active = false; + } + + //////////////////////////////////////////////////////////////////////// + public void Connect(string address, ushort port) + { + if (connected) + { + Log.Warning("[KCP] Client: already connected!"); + return; + } + + // resolve host name before creating peer. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3361 + if (!Common.ResolveHostname(address, out IPAddress[] addresses)) + { + // pass error to user callback. no need to log it manually. + OnError(ErrorCode.DnsResolve, $"Failed to resolve host: {address}"); + OnDisconnectedCallback(); + return; + } + + // create fresh peer for each new session + // client doesn't need secure cookie. + Reset(config); + + Log.Info($"[KCP] Client: connect to {address}:{port}"); + + // create socket + remoteEndPoint = new IPEndPoint(addresses[0], port); + socket = new Socket(remoteEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp); + active = true; + + // recv & send are called from main thread. + // need to ensure this never blocks. + // even a 1ms block per connection would stop us from scaling. + socket.Blocking = false; + + // configure buffer sizes + Common.ConfigureSocketBuffers(socket, config.RecvBufferSize, config.SendBufferSize); + + // bind to endpoint so we can use send/recv instead of sendto/recvfrom. + socket.Connect(remoteEndPoint); + + // immediately send a hello message to the server. + // server will call OnMessage and add the new connection. + // note that this still has cookie=0 until we receive the server's hello. + SendHello(); + } + + // io - input. + // virtual so it may be modified for relays, etc. + // call this while it returns true, to process all messages this tick. + // returned ArraySegment is valid until next call to RawReceive. + protected virtual bool RawReceive(out ArraySegment segment) + { + segment = default; + if (socket == null) return false; + + try + { + return socket.ReceiveNonBlocking(rawReceiveBuffer, out segment); + } + // for non-blocking sockets, Receive throws WouldBlock if there is + // no message to read. that's okay. only log for other errors. + catch (SocketException e) + { + // the other end closing the connection is not an 'error'. + // but connections should never just end silently. + // at least log a message for easier debugging. + // for example, his can happen when connecting without a server. + // see test: ConnectWithoutServer(). + Log.Info($"[KCP] Client.RawReceive: looks like the other end has closed the connection. This is fine: {e}"); + base.Disconnect(); + return false; + } + } + + // io - output. + // virtual so it may be modified for relays, etc. + protected override void RawSend(ArraySegment data) + { + // only if socket was connected / created yet. + // users may call send functions without having connected, causing NRE. + if (socket == null) return; + + try + { + socket.SendNonBlocking(data); + } + catch (SocketException e) + { + // SendDisconnect() sometimes gets a SocketException with + // 'Connection Refused' if the other end already closed. + // this is not an 'error', it's expected to happen. + // but connections should never just end silently. + // at least log a message for easier debugging. + Log.Info($"[KCP] Client.RawSend: looks like the other end has closed the connection. This is fine: {e}"); + // base.Disconnect(); <- don't call this, would deadlock if SendDisconnect() already throws + + } + } + + public void Send(ArraySegment segment, KcpChannel channel) + { + if (!connected) + { + Log.Warning("[KCP] Client: can't send because not connected!"); + return; + } + + SendData(segment, channel); + } + + // insert raw IO. usually from socket.Receive. + // offset is useful for relays, where we may parse a header and then + // feed the rest to kcp. + public void RawInput(ArraySegment segment) + { + // ensure valid size: at least 1 byte for channel + 4 bytes for cookie + if (segment.Count <= 5) return; + + // parse channel + // byte channel = segment[0]; ArraySegment[i] isn't supported in some older Unity Mono versions + byte channel = segment.Array[segment.Offset + 0]; + + // server messages always contain the security cookie. + // parse it, assign if not assigned, warn if suddenly different. + Utils.Decode32U(segment.Array, segment.Offset + 1, out uint messageCookie); + if (messageCookie == 0) + { + Log.Error($"[KCP] Client: received message with cookie=0, this should never happen. Server should always include the security cookie."); + } + + if (cookie == 0) + { + cookie = messageCookie; + Log.Info($"[KCP] Client: received initial cookie: {cookie}"); + } + else if (cookie != messageCookie) + { + Log.Warning($"[KCP] Client: dropping message with mismatching cookie: {messageCookie} expected: {cookie}."); + return; + } + + // parse message + ArraySegment message = new ArraySegment(segment.Array, segment.Offset + 1+4, segment.Count - 1-4); + + switch (channel) + { + case (byte)KcpChannel.Reliable: + { + OnRawInputReliable(message); + break; + } + case (byte)KcpChannel.Unreliable: + { + OnRawInputUnreliable(message); + break; + } + default: + { + // invalid channel indicates random internet noise. + // servers may receive random UDP data. + // just ignore it, but log for easier debugging. + Log.Warning($"[KCP] Client: invalid channel header: {channel}, likely internet noise"); + break; + } + } + } + + // process incoming messages. should be called before updating the world. + // virtual because relay may need to inject their own ping or similar. + public override void TickIncoming() + { + // recv on socket first, then process incoming + // (even if we didn't receive anything. need to tick ping etc.) + // (connection is null if not active) + if (active) + { + while (RawReceive(out ArraySegment segment)) + RawInput(segment); + } + + // RawReceive may have disconnected peer. active check again. + if (active) base.TickIncoming(); + } + + // process outgoing messages. should be called after updating the world. + // virtual because relay may need to inject their own ping or similar. + public override void TickOutgoing() + { + // process outgoing while active + if (active) base.TickOutgoing(); + } + + // process incoming and outgoing for convenience + // => ideally call ProcessIncoming() before updating the world and + // ProcessOutgoing() after updating the world for minimum latency + public virtual void Tick() + { + TickIncoming(); + TickOutgoing(); + } + } +} diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpClient.cs.meta b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpClient.cs.meta new file mode 100644 index 0000000000..fa21cda973 --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpClient.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d8a72893b59812c45aa31613db1142d9 +timeCreated: 1603786960 \ No newline at end of file diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpConfig.cs b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpConfig.cs new file mode 100644 index 0000000000..bee842f229 --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpConfig.cs @@ -0,0 +1,98 @@ +// common config struct, instead of passing 10 parameters manually every time. +using System; +using KCP; + +namespace kcp2k.unmanaged +{ + // [Serializable] to show it in Unity inspector. + // 'class' so we can set defaults easily. + [Serializable] + public class KcpConfig + { + // socket configuration //////////////////////////////////////////////// + // DualMode uses both IPv6 and IPv4. not all platforms support it. + // (Nintendo Switch, etc.) + public bool DualMode; + + // UDP servers use only one socket. + // maximize buffer to handle as many connections as possible. + // + // M1 mac pro: + // recv buffer default: 786896 (771 KB) + // send buffer default: 9216 (9 KB) + // max configurable: ~7 MB + public int RecvBufferSize; + public int SendBufferSize; + + // kcp configuration /////////////////////////////////////////////////// + // configurable MTU in case kcp sits on top of other abstractions like + // encrypted transports, relays, etc. + public int Mtu; + + // NoDelay is recommended to reduce latency. This also scales better + // without buffers getting full. + public bool NoDelay; + + // KCP internal update interval. 100ms is KCP default, but a lower + // interval is recommended to minimize latency and to scale to more + // networked entities. + public uint Interval; + + // KCP fastresend parameter. Faster resend for the cost of higher + // bandwidth. + public int FastResend; + + // KCP congestion window heavily limits messages flushed per update. + // congestion window may actually be broken in kcp: + // - sending max sized message @ M1 mac flushes 2-3 messages per update + // - even with super large send/recv window, it requires thousands of + // update calls + // best to leave this disabled, as it may significantly increase latency. + public bool CongestionWindow; + + // KCP window size can be modified to support higher loads. + // for example, Mirror Benchmark requires: + // 128, 128 for 4k monsters + // 512, 512 for 10k monsters + // 8192, 8192 for 20k monsters + public uint SendWindowSize; + public uint ReceiveWindowSize; + + // timeout in milliseconds + public int Timeout; + + // maximum retransmission attempts until dead_link + public uint MaxRetransmits; + + // constructor ///////////////////////////////////////////////////////// + // constructor with defaults for convenience. + // makes it easy to define "new KcpConfig(DualMode=false)" etc. + public KcpConfig( + bool DualMode = true, + int RecvBufferSize = 1024 * 1024 * 7, + int SendBufferSize = 1024 * 1024 * 7, + int Mtu = (int)KCPBASIC.MTU_DEF, + bool NoDelay = true, + uint Interval = 10, + int FastResend = 0, + bool CongestionWindow = false, + uint SendWindowSize = KCPBASIC.WND_SND, + uint ReceiveWindowSize = KCPBASIC.WND_RCV, + int Timeout = KcpPeer.DEFAULT_TIMEOUT, + uint MaxRetransmits = KCPBASIC.DEADLINK) + { + this.DualMode = DualMode; + this.RecvBufferSize = RecvBufferSize; + this.SendBufferSize = SendBufferSize; + this.Mtu = Mtu; + this.NoDelay = NoDelay; + this.Interval = Interval; + this.FastResend = FastResend; + this.CongestionWindow = CongestionWindow; + this.SendWindowSize = SendWindowSize; + this.ReceiveWindowSize = ReceiveWindowSize; + this.Timeout = Timeout; + this.MaxRetransmits = MaxRetransmits; + } + } +} diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpConfig.cs.meta b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpConfig.cs.meta new file mode 100644 index 0000000000..7e0627dcb9 --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpConfig.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 89bd96ffe1099fc45b6c200f04168008 +timeCreated: 1670946969 \ No newline at end of file diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpHeader.cs b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpHeader.cs new file mode 100644 index 0000000000..2568295453 --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpHeader.cs @@ -0,0 +1,57 @@ +using System; + +namespace kcp2k.unmanaged +{ + // header for messages processed by kcp. + // this is NOT for the raw receive messages(!) because handshake/disconnect + // need to be sent reliably. it's not enough to have those in rawreceive + // because those messages might get lost without being resent! + public enum KcpHeaderReliable : byte + { + // don't react on 0x00. might help to filter out random noise. + Hello = 1, + // ping goes over reliable & KcpHeader for now. could go over unreliable + // too. there is no real difference except that this is easier because + // we already have a KcpHeader for reliable messages. + // ping is only used to keep it alive, so latency doesn't matter. + Ping = 2, + Data = 3, + } + + public enum KcpHeaderUnreliable : byte + { + // users may send unreliable messages + Data = 4, + // disconnect always goes through rapid fire unreliable (glenn fielder) + Disconnect = 5, + } + + // save convert the enums from/to byte. + // attackers may attempt to send invalid values, so '255' may not convert. + public static class KcpHeader + { + public static bool ParseReliable(byte value, out KcpHeaderReliable header) + { + if (Enum.IsDefined(typeof(KcpHeaderReliable), value)) + { + header = (KcpHeaderReliable)value; + return true; + } + + header = KcpHeaderReliable.Ping; // any default + return false; + } + + public static bool ParseUnreliable(byte value, out KcpHeaderUnreliable header) + { + if (Enum.IsDefined(typeof(KcpHeaderUnreliable), value)) + { + header = (KcpHeaderUnreliable)value; + return true; + } + + header = KcpHeaderUnreliable.Disconnect; // any default + return false; + } + } +} diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpHeader.cs.meta b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpHeader.cs.meta new file mode 100644 index 0000000000..97032b5dc9 --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpHeader.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 67f2415a5a9a0e5478492cf53b1defd7 +timeCreated: 1610081248 \ No newline at end of file diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpPeer.cs b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpPeer.cs new file mode 100644 index 0000000000..da2a83db15 --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpPeer.cs @@ -0,0 +1,792 @@ +// Kcp Peer, similar to UDP Peer but wrapped with reliability, channels, +// timeouts, authentication, state, etc. +// +// still IO agnostic to work with udp, nonalloc, relays, native, etc. +using System; +using System.Diagnostics; +using System.Net.Sockets; +using KCP; + +namespace kcp2k.unmanaged +{ + public abstract class KcpPeer + { + // kcp reliability algorithm + internal Kcp kcp; + + // security cookie to prevent UDP spoofing. + // credits to IncludeSec for disclosing the issue. + // + // server passes the expected cookie to the client's KcpPeer. + // KcpPeer sends cookie to the connected client. + // KcpPeer only accepts packets which contain the cookie. + // => cookie can be a random number, but it needs to be cryptographically + // secure random that can't be easily predicted. + // => cookie can be hash(ip, port) BUT only if salted to be not predictable + internal uint cookie; + + // state: connected as soon as we create the peer. + // leftover from KcpConnection. remove it after refactoring later. + protected KcpState state = KcpState.Connected; + + // If we don't receive anything these many milliseconds + // then consider us disconnected + public const int DEFAULT_TIMEOUT = 10000; + public int timeout; + uint lastReceiveTime; + + // internal time. + // StopWatch offers ElapsedMilliSeconds and should be more precise than + // Unity's time.deltaTime over long periods. + readonly Stopwatch watch = new Stopwatch(); + + // buffer to receive kcp's processed messages (avoids allocations). + // IMPORTANT: this is for KCP messages. so it needs to be of size: + // 1 byte header + MaxMessageSize content + readonly byte[] kcpMessageBuffer;// = new byte[1 + ReliableMaxMessageSize]; + + // send buffer for handing user messages to kcp for processing. + // (avoids allocations). + // IMPORTANT: needs to be of size: + // 1 byte header + MaxMessageSize content + readonly byte[] kcpSendBuffer;// = new byte[1 + ReliableMaxMessageSize]; + + // raw send buffer is exactly MTU. + readonly byte[] rawSendBuffer; + + // send a ping occasionally so we don't time out on the other end. + // for example, creating a character in an MMO could easily take a + // minute of no data being sent. which doesn't mean we want to time out. + // same goes for slow paced card games etc. + public const int PING_INTERVAL = 1000; + uint lastPingTime; + + // if we send more than kcp can handle, we will get ever growing + // send/recv buffers and queues and minutes of latency. + // => if a connection can't keep up, it should be disconnected instead + // to protect the server under heavy load, and because there is no + // point in growing to gigabytes of memory or minutes of latency! + // => 2k isn't enough. we reach 2k when spawning 4k monsters at once + // easily, but it does recover over time. + // => 10k seems safe. + // + // note: we have a ChokeConnectionAutoDisconnects test for this too! + internal const int QueueDisconnectThreshold = 10000; + + // getters for queue and buffer counts, used for debug info + public int SendQueueCount => (int)kcp.SendQueueCount; + public int ReceiveQueueCount => (int)kcp.ReceiveQueueCount; + public int SendBufferCount => (int)kcp.SendBufferCount; + public int ReceiveBufferCount => (int)kcp.ReceiveBufferCount; + + // we need to subtract the channel and cookie bytes from every + // MaxMessageSize calculation. + // we also need to tell kcp to use MTU-1 to leave space for the byte. + public const int CHANNEL_HEADER_SIZE = 1; + public const int COOKIE_HEADER_SIZE = 4; + public const int METADATA_SIZE = CHANNEL_HEADER_SIZE + COOKIE_HEADER_SIZE; + + // reliable channel (= kcp) MaxMessageSize so the outside knows largest + // allowed message to send. the calculation in Send() is not obvious at + // all, so let's provide the helper here. + // + // kcp does fragmentation, so max message is way larger than MTU. + // + // -> runtime MTU changes are disabled: mss is always MTU_DEF-OVERHEAD + // -> Send() checks if fragment count < rcv_wnd, so we use rcv_wnd - 1. + // NOTE that original kcp has a bug where WND_RCV default is used + // instead of configured rcv_wnd, limiting max message size to 144 KB + // https://github.com/skywind3000/kcp/pull/291 + // we fixed this in kcp2k. + // -> we add 1 byte KcpHeader enum to each message, so -1 + // + // IMPORTANT: max message is MTU * rcv_wnd, in other words it completely + // fills the receive window! due to head of line blocking, + // all other messages have to wait while a maxed size message + // is being delivered. + // => in other words, DO NOT use max size all the time like + // for batching. + // => sending UNRELIABLE max message size most of the time is + // best for performance (use that one for batching!) + static int ReliableMaxMessageSize_Unconstrained(int mtu, uint rcv_wnd) => + (mtu - (int)KCPBASIC.OVERHEAD - METADATA_SIZE) * ((int)rcv_wnd - 1) - 1; + + // kcp encodes 'frg' as 1 byte. + // max message size can only ever allow up to 255 fragments. + // WND_RCV gives 127 fragments. + // WND_RCV * 2 gives 255 fragments. + // so we can limit max message size by limiting rcv_wnd parameter. + public static int ReliableMaxMessageSize(int mtu, uint rcv_wnd) => + ReliableMaxMessageSize_Unconstrained(mtu, Math.Min(rcv_wnd, KCPBASIC.FRG_LIMIT)); + + // unreliable max message size is simply MTU - channel header - kcp header + public static int UnreliableMaxMessageSize(int mtu) => + mtu - METADATA_SIZE - 1; + + // maximum send rate per second can be calculated from kcp parameters + // source: https://translate.google.com/translate?sl=auto&tl=en&u=https://wetest.qq.com/lab/view/391.html + // + // KCP can send/receive a maximum of WND*MTU per interval. + // multiple by 1000ms / interval to get the per-second rate. + // + // example: + // WND(32) * MTU(1400) = 43.75KB + // => 43.75KB * 1000 / INTERVAL(10) = 4375KB/s + // + // returns bytes/second! + public uint MaxSendRate => kcp.SendWindowSize * kcp.MaximumTransmissionUnit * 1000 / kcp.Interval; + public uint MaxReceiveRate => kcp.ReceiveWindowSize * kcp.MaximumTransmissionUnit * 1000 / kcp.Interval; + + // calculate max message sizes based on mtu and wnd only once + public readonly int unreliableMax; + public readonly int reliableMax; + + // SetupKcp creates and configures a new KCP instance. + // => useful to start from a fresh state every time the client connects + // => NoDelay, interval, wnd size are the most important configurations. + // let's force require the parameters so we don't forget it anywhere. + protected KcpPeer(KcpConfig config, uint cookie) + { + // initialize variable state in extra function so we can reuse it + // when reconnecting to reset state + Reset(config); + + // set the cookie after resetting state so it's not overwritten again. + // with log message for debugging in case of cookie issues. + this.cookie = cookie; + Log.Info($"[KCP] {GetType()}: created with cookie={cookie}"); + + // create mtu sized send buffer + rawSendBuffer = new byte[config.Mtu]; + + // calculate max message sizes once + unreliableMax = UnreliableMaxMessageSize(config.Mtu); + reliableMax = ReliableMaxMessageSize(config.Mtu, config.ReceiveWindowSize); + + // create message buffers AFTER window size is set + // see comments on buffer definition for the "+1" part + kcpMessageBuffer = new byte[1 + reliableMax]; + kcpSendBuffer = new byte[1 + reliableMax]; + } + + // Reset all state once. + // useful for KcpClient to reconned with a fresh kcp state. + protected void Reset(KcpConfig config) + { + // reset state + cookie = 0; + state = KcpState.Connected; + lastReceiveTime = 0; + lastPingTime = 0; + watch.Restart(); // start at 0 each time + + // set up kcp over reliable channel (that's what kcp is for) + kcp = new Kcp(0, RawSendReliable); + + // set nodelay. + // note that kcp uses 'nocwnd' internally so we negate the parameter + kcp.SetNoDelay((int)(config.NoDelay ? 1u : 0u), (int)config.Interval, config.FastResend, !config.CongestionWindow ? 1 : 0); + kcp.SetWindowSize((int)config.SendWindowSize, (int)config.ReceiveWindowSize); + + // IMPORTANT: high level needs to add 1 channel byte to each raw + // message. so while Kcp.MTU_DEF is perfect, we actually need to + // tell kcp to use MTU-1 so we can still put the header into the + // message afterwards. + kcp.SetMtu((int)((uint)config.Mtu - METADATA_SIZE)); + + // set maximum retransmits (aka dead_link) + //kcp.dead_link = config.MaxRetransmits; + timeout = config.Timeout; + } + + // callbacks /////////////////////////////////////////////////////////// + // events are abstract, guaranteed to be implemented. + // this ensures they are always initialized when used. + // fixes https://github.com/MirrorNetworking/Mirror/issues/3337 and more + protected abstract void OnAuthenticated(); + protected abstract void OnData(ArraySegment message, KcpChannel channel); + protected abstract void OnDisconnected(); + + // error callback instead of logging. + // allows libraries to show popups etc. + // (string instead of Exception for ease of use and to avoid user panic) + protected abstract void OnError(ErrorCode error, string message); + protected abstract void RawSend(ArraySegment data); + + //////////////////////////////////////////////////////////////////////// + + void HandleTimeout(uint time) + { + // note: we are also sending a ping regularly, so timeout should + // only ever happen if the connection is truly gone. + if (time >= lastReceiveTime + timeout) + { + // pass error to user callback. no need to log it manually. + // GetType() shows Server/ClientConn instead of just Connection. + OnError(ErrorCode.Timeout, $"{GetType()}: Connection timed out after not receiving any message for {timeout}ms. Disconnecting."); + Disconnect(); + } + } + + void HandleDeadLink() + { + // kcp has 'dead_link' detection. might as well use it. + if (kcp.State == -1) + { + // pass error to user callback. no need to log it manually. + // GetType() shows Server/ClientConn instead of just Connection. + OnError(ErrorCode.Timeout, $"{GetType()}: dead_link detected: a message was retransmitted {KCPBASIC.DEADLINK} times without ack. Disconnecting."); + Disconnect(); + } + } + + // send a ping occasionally in order to not time out on the other end. + void HandlePing(uint time) + { + // enough time elapsed since last ping? + if (time >= lastPingTime + PING_INTERVAL) + { + // ping again and reset time + //Log.Debug("[KCP] sending ping..."); + SendPing(); + lastPingTime = time; + } + } + + void HandleChoked() + { + // disconnect connections that can't process the load. + // see QueueSizeDisconnect comments. + // => include all of kcp's buffers and the unreliable queue! + int total = (int)(kcp.ReceiveQueueCount + kcp.SendQueueCount + + kcp.ReceiveBufferCount + kcp.SendBufferCount); + if (total >= QueueDisconnectThreshold) + { + // pass error to user callback. no need to log it manually. + // GetType() shows Server/ClientConn instead of just Connection. + OnError(ErrorCode.Congestion, + $"{GetType()}: disconnecting connection because it can't process data fast enough.\n" + + $"Queue total {total}>{QueueDisconnectThreshold}. rcv_queue={kcp.ReceiveQueueCount} snd_queue={kcp.SendQueueCount} rcv_buf={kcp.ReceiveBufferCount} snd_buf={kcp.SendBufferCount}\n" + + $"* Try to Enable NoDelay, decrease INTERVAL, disable Congestion Window (= enable NOCWND!), increase SEND/RECV WINDOW or compress data.\n" + + $"* Or perhaps the network is simply too slow on our end, or on the other end."); + + // let's clear all pending sends before disconnting with 'Bye'. + // otherwise a single Flush in Disconnect() won't be enough to + // flush thousands of messages to finally deliver 'Bye'. + // this is just faster and more robust. + //kcp.snd_queue.Clear(); + + Disconnect(); + } + } + + // reads the next reliable message type & content from kcp. + // -> to avoid buffering, unreliable messages call OnData directly. + bool ReceiveNextReliable(out KcpHeaderReliable header, out ArraySegment message) + { + message = default; + header = KcpHeaderReliable.Ping; + + int msgSize = kcp.PeekSize(); + if (msgSize <= 0) return false; + + // only allow receiving up to buffer sized messages. + // otherwise we would get BlockCopy ArgumentException anyway. + if (msgSize > kcpMessageBuffer.Length) + { + // we don't allow sending messages > Max, so this must be an + // attacker. let's disconnect to avoid allocation attacks etc. + // pass error to user callback. no need to log it manually. + OnError(ErrorCode.InvalidReceive, $"{GetType()}: possible allocation attack for msgSize {msgSize} > buffer {kcpMessageBuffer.Length}. Disconnecting the connection."); + Disconnect(); + return false; + } + + // receive from kcp + int received = kcp.Receive(kcpMessageBuffer, msgSize); + if (received < 0) + { + // if receive failed, close everything + // pass error to user callback. no need to log it manually. + // GetType() shows Server/ClientConn instead of just Connection. + OnError(ErrorCode.InvalidReceive, $"{GetType()}: Receive failed with error={received}. closing connection."); + Disconnect(); + return false; + } + + // safely extract header. attackers may send values out of enum range. + byte headerByte = kcpMessageBuffer[0]; + if (!KcpHeader.ParseReliable(headerByte, out header)) + { + OnError(ErrorCode.InvalidReceive, $"{GetType()}: Receive failed to parse header: {headerByte} is not defined in {typeof(KcpHeaderReliable)}."); + Disconnect(); + return false; + } + + // extract content without header + message = new ArraySegment(kcpMessageBuffer, 1, msgSize - 1); + lastReceiveTime = (uint)watch.ElapsedMilliseconds; + return true; + } + + void TickIncoming_Connected(uint time) + { + // detect common events & ping + HandleTimeout(time); + HandleDeadLink(); + HandlePing(time); + HandleChoked(); + + // any reliable kcp message received? + if (ReceiveNextReliable(out KcpHeaderReliable header, out ArraySegment message)) + { + // message type FSM. no default so we never miss a case. + switch (header) + { + case KcpHeaderReliable.Hello: + { + // we were waiting for a Hello message. + // it proves that the other end speaks our protocol. + + // log with previously parsed cookie + Log.Info($"[KCP] {GetType()}: received hello with cookie={cookie}"); + state = KcpState.Authenticated; + OnAuthenticated(); + break; + } + case KcpHeaderReliable.Ping: + { + // ping keeps kcp from timing out. do nothing. + break; + } + case KcpHeaderReliable.Data: + { + // everything else is not allowed during handshake! + // pass error to user callback. no need to log it manually. + // GetType() shows Server/ClientConn instead of just Connection. + OnError(ErrorCode.InvalidReceive, $"[KCP] {GetType()}: received invalid header {header} while Connected. Disconnecting the connection."); + Disconnect(); + break; + } + } + } + } + + void TickIncoming_Authenticated(uint time) + { + // detect common events & ping + HandleTimeout(time); + HandleDeadLink(); + HandlePing(time); + HandleChoked(); + + // process all received messages + while (ReceiveNextReliable(out KcpHeaderReliable header, out ArraySegment message)) + { + // message type FSM. no default so we never miss a case. + switch (header) + { + case KcpHeaderReliable.Hello: + { + // should never receive another hello after auth + // GetType() shows Server/ClientConn instead of just Connection. + Log.Warning($"{GetType()}: received invalid header {header} while Authenticated. Disconnecting the connection."); + Disconnect(); + break; + } + case KcpHeaderReliable.Data: + { + // call OnData IF the message contained actual data + if (message.Count > 0) + { + //Log.Warning($"Kcp recv msg: {BitConverter.ToString(message.Array, message.Offset, message.Count)}"); + OnData(message, KcpChannel.Reliable); + } + // empty data = attacker, or something went wrong + else + { + // pass error to user callback. no need to log it manually. + // GetType() shows Server/ClientConn instead of just Connection. + OnError(ErrorCode.InvalidReceive, $"{GetType()}: received empty Data message while Authenticated. Disconnecting the connection."); + Disconnect(); + } + break; + } + case KcpHeaderReliable.Ping: + { + // ping keeps kcp from timing out. do nothing. + break; + } + } + } + } + + public virtual void TickIncoming() + { + uint time = (uint)watch.ElapsedMilliseconds; + + try + { + switch (state) + { + case KcpState.Connected: + { + TickIncoming_Connected(time); + break; + } + case KcpState.Authenticated: + { + TickIncoming_Authenticated(time); + break; + } + case KcpState.Disconnected: + { + // do nothing while disconnected + break; + } + } + } + // TODO KcpConnection is IO agnostic. move this to outside later. + catch (SocketException exception) + { + // this is ok, the connection was closed + // pass error to user callback. no need to log it manually. + // GetType() shows Server/ClientConn instead of just Connection. + OnError(ErrorCode.ConnectionClosed, $"{GetType()}: Disconnecting because {exception}. This is fine."); + Disconnect(); + } + catch (ObjectDisposedException exception) + { + // fine, socket was closed + // pass error to user callback. no need to log it manually. + // GetType() shows Server/ClientConn instead of just Connection. + OnError(ErrorCode.ConnectionClosed, $"{GetType()}: Disconnecting because {exception}. This is fine."); + Disconnect(); + } + catch (Exception exception) + { + // unexpected + // pass error to user callback. no need to log it manually. + // GetType() shows Server/ClientConn instead of just Connection. + OnError(ErrorCode.Unexpected, $"{GetType()}: unexpected Exception: {exception}"); + Disconnect(); + } + } + + public virtual void TickOutgoing() + { + uint time = (uint)watch.ElapsedMilliseconds; + + try + { + switch (state) + { + case KcpState.Connected: + case KcpState.Authenticated: + { + // update flushes out messages + kcp.Update(time); + break; + } + case KcpState.Disconnected: + { + // do nothing while disconnected + break; + } + } + } + // TODO KcpConnection is IO agnostic. move this to outside later. + catch (SocketException exception) + { + // this is ok, the connection was closed + // pass error to user callback. no need to log it manually. + // GetType() shows Server/ClientConn instead of just Connection. + OnError(ErrorCode.ConnectionClosed, $"{GetType()}: Disconnecting because {exception}. This is fine."); + Disconnect(); + } + catch (ObjectDisposedException exception) + { + // fine, socket was closed + // pass error to user callback. no need to log it manually. + // GetType() shows Server/ClientConn instead of just Connection. + OnError(ErrorCode.ConnectionClosed, $"{GetType()}: Disconnecting because {exception}. This is fine."); + Disconnect(); + } + catch (Exception exception) + { + // unexpected + // pass error to user callback. no need to log it manually. + // GetType() shows Server/ClientConn instead of just Connection. + OnError(ErrorCode.Unexpected, $"{GetType()}: unexpected exception: {exception}"); + Disconnect(); + } + } + + protected void OnRawInputReliable(ArraySegment message) + { + // input into kcp, but skip channel byte + int input = kcp.Input(message.Array, message.Offset, message.Count); + if (input != 0) + { + // GetType() shows Server/ClientConn instead of just Connection. + Log.Warning($"[KCP] {GetType()}: Input failed with error={input} for buffer with length={message.Count - 1}"); + } + } + + protected void OnRawInputUnreliable(ArraySegment message) + { + // need at least one byte for the KcpHeader enum + if (message.Count < 1) return; + + // safely extract header. attackers may send values out of enum range. + byte headerByte = message.Array[message.Offset + 0]; + if (!KcpHeader.ParseUnreliable(headerByte, out KcpHeaderUnreliable header)) + { + OnError(ErrorCode.InvalidReceive, $"{GetType()}: Receive failed to parse header: {headerByte} is not defined in {typeof(KcpHeaderUnreliable)}."); + Disconnect(); + return; + } + + // subtract header from message content + // (above we already ensure it's at least 1 byte long) + message = new ArraySegment(message.Array, message.Offset + 1, message.Count - 1); + + switch (header) + { + case KcpHeaderUnreliable.Data: + { + // ideally we would queue all unreliable messages and + // then process them in ReceiveNext() together with the + // reliable messages, but: + // -> queues/allocations/pools are slow and complex. + // -> DOTSNET 10k is actually slower if we use pooled + // unreliable messages for transform messages. + // + // DOTSNET 10k benchmark: + // reliable-only: 170 FPS + // unreliable queued: 130-150 FPS + // unreliable direct: 183 FPS(!) + // + // DOTSNET 50k benchmark: + // reliable-only: FAILS (queues keep growing) + // unreliable direct: 18-22 FPS(!) + // + // -> all unreliable messages are DATA messages anyway. + // -> let's skip the magic and call OnData directly if + // the current state allows it. + if (state == KcpState.Authenticated) + { + OnData(message, KcpChannel.Unreliable); + + // set last receive time to avoid timeout. + // -> we do this in ANY case even if not enabled. + // a message is a message. + // -> we set last receive time for both reliable and + // unreliable messages. both count. + // otherwise a connection might time out even + // though unreliable were received, but no + // reliable was received. + lastReceiveTime = (uint)watch.ElapsedMilliseconds; + } + else + { + // it's common to receive unreliable messages before being + // authenticated, for example: + // - random internet noise + // - game server may send an unreliable message after authenticating, + // and the unreliable message arrives on the client before the + // 'auth_ok' message. this can be avoided by sending a final + // 'ready' message after being authenticated, but this would + // add another 'round trip time' of latency to the handshake. + // + // it's best to simply ignore invalid unreliable messages here. + // Log.Info($"{GetType()}: received unreliable message while not authenticated."); + } + break; + } + case KcpHeaderUnreliable.Disconnect: + { + // GetType() shows Server/ClientConn instead of just Connection. + Log.Info($"[KCP] {GetType()}: received disconnect message"); + Disconnect(); + break; + } + } + } + + // raw send called by kcp + void RawSendReliable(byte[] data, int length) + { + // write channel header + // from 0, with 1 byte + data[0] = (byte)KcpChannel.Reliable; + + // write handshake cookie to protect against UDP spoofing. + // from 1, with 4 bytes + Utils.Encode32U(data, 1, cookie); // allocation free + + // write data + // from 5, with N bytes + //Buffer.BlockCopy(data, 0, rawSendBuffer, 1+4, length); + + // IO send + ArraySegment segment = new ArraySegment(data, 0, length + 1+4); + RawSend(segment); + } + + void SendReliable(KcpHeaderReliable header, ArraySegment content) + { + // 1 byte header + content needs to fit into send buffer + if (1 + content.Count > kcpSendBuffer.Length) // TODO + { + // otherwise content is larger than MaxMessageSize. let user know! + // GetType() shows Server/ClientConn instead of just Connection. + OnError(ErrorCode.InvalidSend, $"{GetType()}: Failed to send reliable message of size {content.Count} because it's larger than ReliableMaxMessageSize={reliableMax}"); + return; + } + + // write channel header + kcpSendBuffer[0] = (byte)header; + + // write data (if any) + if (content.Count > 0) + Buffer.BlockCopy(content.Array, content.Offset, kcpSendBuffer, 1, content.Count); + + // send to kcp for processing + int sent = kcp.Send(kcpSendBuffer, 0, 1 + content.Count); + if (sent < 0) + { + // GetType() shows Server/ClientConn instead of just Connection. + OnError(ErrorCode.InvalidSend, $"{GetType()}: Send failed with error={sent} for content with length={content.Count}"); + } + } + + void SendUnreliable(KcpHeaderUnreliable header, ArraySegment content) + { + // message size needs to be <= unreliable max size + if (content.Count > unreliableMax) + { + // otherwise content is larger than MaxMessageSize. let user know! + // GetType() shows Server/ClientConn instead of just Connection. + Log.Error($"[KCP] {GetType()}: Failed to send unreliable message of size {content.Count} because it's larger than UnreliableMaxMessageSize={unreliableMax}"); + return; + } + + // write channel header + // from 0, with 1 byte + rawSendBuffer[0] = (byte)KcpChannel.Unreliable; + + // write handshake cookie to protect against UDP spoofing. + // from 1, with 4 bytes + Utils.Encode32U(rawSendBuffer, 1, cookie); // allocation free + + // write kcp header + rawSendBuffer[5] = (byte)header; + + // write data (if any) + // from 6, with N bytes + if (content.Count > 0) + Buffer.BlockCopy(content.Array, content.Offset, rawSendBuffer, 1 + 4 + 1, content.Count); + + // IO send + ArraySegment segment = new ArraySegment(rawSendBuffer, 0, content.Count + 1 + 4 + 1); + RawSend(segment); + } + + // server & client need to send handshake at different times, so we need + // to expose the function. + // * client should send it immediately. + // * server should send it as reply to client's handshake, not before + // (server should not reply to random internet messages with handshake) + // => handshake info needs to be delivered, so it goes over reliable. + public void SendHello() + { + // send an empty message with 'Hello' header. + // cookie is automatically included in all messages. + + // GetType() shows Server/ClientConn instead of just Connection. + Log.Info($"[KCP] {GetType()}: sending handshake to other end with cookie={cookie}"); + SendReliable(KcpHeaderReliable.Hello, default); + } + + public void SendData(ArraySegment data, KcpChannel channel) + { + // sending empty segments is not allowed. + // nobody should ever try to send empty data. + // it means that something went wrong, e.g. in Mirror/DOTSNET. + // let's make it obvious so it's easy to debug. + if (data.Count == 0) + { + // pass error to user callback. no need to log it manually. + // GetType() shows Server/ClientConn instead of just Connection. + OnError(ErrorCode.InvalidSend, $"{GetType()}: tried sending empty message. This should never happen. Disconnecting."); + Disconnect(); + return; + } + + switch (channel) + { + case KcpChannel.Reliable: + SendReliable(KcpHeaderReliable.Data, data); + break; + case KcpChannel.Unreliable: + SendUnreliable(KcpHeaderUnreliable.Data, data); + break; + } + } + + // ping goes through kcp to keep it from timing out, so it goes over the + // reliable channel. + void SendPing() => SendReliable(KcpHeaderReliable.Ping, default); + + // send disconnect message + void SendDisconnect() + { + // sending over reliable to ensure delivery seems like a good idea: + // but if we close the connection immediately, it often doesn't get + // fully delivered: https://github.com/MirrorNetworking/Mirror/issues/3591 + // SendReliable(KcpHeader.Disconnect, default); + // + // instead, rapid fire a few unreliable messages. + // they are sent immediately even if we close the connection after. + // this way we don't need to keep the connection alive for a while. + // (glenn fiedler method) + for (int i = 0; i < 5; ++i) + SendUnreliable(KcpHeaderUnreliable.Disconnect, default); + } + + // disconnect this connection + public virtual void Disconnect() + { + // only if not disconnected yet + if (state == KcpState.Disconnected) + return; + + // send a disconnect message + try + { + SendDisconnect(); + } + // TODO KcpConnection is IO agnostic. move this to outside later. + catch (SocketException) + { + // this is ok, the connection was already closed + } + catch (ObjectDisposedException) + { + // this is normal when we stop the server + // the socket is stopped so we can't send anything anymore + // to the clients + + // the clients will eventually timeout and realize they + // were disconnected + } + + // set as Disconnected, call event + // GetType() shows Server/ClientConn instead of just Connection. + Log.Info($"[KCP] {GetType()}: Disconnected."); + state = KcpState.Disconnected; + OnDisconnected(); + } + } +} diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpPeer.cs.meta b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpPeer.cs.meta new file mode 100644 index 0000000000..f38de562f1 --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpPeer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f426cdf1932bbfe4185672e7797d104a +timeCreated: 1602600432 \ No newline at end of file diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpServer.cs b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpServer.cs new file mode 100644 index 0000000000..17edbe97d2 --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpServer.cs @@ -0,0 +1,412 @@ +// kcp server logic abstracted into a class. +// for use in Mirror, DOTSNET, testing, etc. +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Runtime.InteropServices; + +namespace kcp2k.unmanaged +{ + public class KcpServer + { + // callbacks + // even for errors, to allow liraries to show popups etc. + // instead of logging directly. + // (string instead of Exception for ease of use and to avoid user panic) + // + // events are readonly, set in constructor. + // this ensures they are always initialized when used. + // fixes https://github.com/MirrorNetworking/Mirror/issues/3337 and more + protected readonly Action OnConnected; // connectionId, address + protected readonly Action, KcpChannel> OnData; + protected readonly Action OnDisconnected; + protected readonly Action OnError; + + // configuration + protected readonly KcpConfig config; + + // state + protected Socket socket; + EndPoint newClientEP; + + // expose local endpoint for users / relays / nat traversal etc. + public EndPoint LocalEndPoint => socket?.LocalEndPoint; + + // raw receive buffer always needs to be of 'MTU' size, even if + // MaxMessageSize is larger. kcp always sends in MTU segments and having + // a buffer smaller than MTU would silently drop excess data. + // => we need the mtu to fit channel + message! + protected readonly byte[] rawReceiveBuffer; + + // connections where connectionId is EndPoint.GetHashCode + public Dictionary connections = + new Dictionary(); + + public KcpServer(Action OnConnected, + Action, KcpChannel> OnData, + Action OnDisconnected, + Action OnError, + KcpConfig config) + { + // initialize callbacks first to ensure they can be used safely. + this.OnConnected = OnConnected; + this.OnData = OnData; + this.OnDisconnected = OnDisconnected; + this.OnError = OnError; + this.config = config; + + // create mtu sized receive buffer + rawReceiveBuffer = new byte[config.Mtu]; + + // create newClientEP either IPv4 or IPv6 + newClientEP = config.DualMode + ? new IPEndPoint(IPAddress.IPv6Any, 0) + : new IPEndPoint(IPAddress.Any, 0); + } + + public virtual bool IsActive() => socket != null; + + static Socket CreateServerSocket(bool DualMode, ushort port) + { + if (DualMode) + { + // IPv6 socket with DualMode @ "::" : port + Socket socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp); + + // enabling DualMode may throw: + // https://learn.microsoft.com/en-us/dotnet/api/System.Net.Sockets.Socket.DualMode?view=net-7.0 + // attempt it, otherwise log but continue + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3358 + try + { + socket.DualMode = true; + } + catch (NotSupportedException e) + { + Log.Warning($"[KCP] Failed to set Dual Mode, continuing with IPv6 without Dual Mode. Error: {e}"); + } + + // for windows sockets, there's a rare issue where when using + // a server socket with multiple clients, if one of the clients + // is closed, the single server socket throws exceptions when + // sending/receiving. even if the socket is made for N clients. + // + // this actually happened to one of our users: + // https://github.com/MirrorNetworking/Mirror/issues/3611 + // + // here's the in-depth explanation & solution: + // + // "As you may be aware, if a host receives a packet for a UDP + // port that is not currently bound, it may send back an ICMP + // "Port Unreachable" message. Whether or not it does this is + // dependent on the firewall, private/public settings, etc. + // On localhost, however, it will pretty much always send this + // packet back. + // + // Now, on Windows (and only on Windows), by default, a received + // ICMP Port Unreachable message will close the UDP socket that + // sent it; hence, the next time you try to receive on the + // socket, it will throw an exception because the socket has + // been closed by the OS. + // + // Obviously, this causes a headache in the multi-client, + // single-server socket set-up you have here, but luckily there + // is a fix: + // + // You need to utilise the not-often-required SIO_UDP_CONNRESET + // Winsock control code, which turns off this built-in behaviour + // of automatically closing the socket. + // + // Note that this ioctl code is only supported on Windows + // (XP and later), not on Linux, since it is provided by the + // Winsock extensions. Of course, since the described behavior + // is only the default behavior on Windows, this omission is not + // a major loss. If you are attempting to create a + // cross-platform library, you should cordon this off as + // Windows-specific code." + // https://stackoverflow.com/questions/74327225/why-does-sending-via-a-udpclient-cause-subsequent-receiving-to-fail + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + const uint IOC_IN = 0x80000000U; + const uint IOC_VENDOR = 0x18000000U; + const int SIO_UDP_CONNRESET = unchecked((int)(IOC_IN | IOC_VENDOR | 12)); + socket.IOControl(SIO_UDP_CONNRESET, new byte[] { 0x00 }, null); + } + + socket.Bind(new IPEndPoint(IPAddress.IPv6Any, port)); + return socket; + } + else + { + // IPv4 socket @ "0.0.0.0" : port + Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + socket.Bind(new IPEndPoint(IPAddress.Any, port)); + return socket; + } + } + + public virtual void Start(ushort port) + { + // only start once + if (socket != null) + { + Log.Warning("[KCP] Server: already started!"); + return; + } + + // listen + socket = CreateServerSocket(config.DualMode, port); + + // recv & send are called from main thread. + // need to ensure this never blocks. + // even a 1ms block per connection would stop us from scaling. + socket.Blocking = false; + + // configure buffer sizes + Common.ConfigureSocketBuffers(socket, config.RecvBufferSize, config.SendBufferSize); + } + + public void Send(int connectionId, ArraySegment segment, KcpChannel channel) + { + if (connections.TryGetValue(connectionId, out KcpServerConnection connection)) + { + connection.SendData(segment, channel); + } + } + + public void Disconnect(int connectionId) + { + if (connections.TryGetValue(connectionId, out KcpServerConnection connection)) + { + connection.Disconnect(); + } + } + + // expose the whole IPEndPoint, not just the IP address. some need it. + public IPEndPoint GetClientEndPoint(int connectionId) + { + if (connections.TryGetValue(connectionId, out KcpServerConnection connection)) + { + return connection.remoteEndPoint as IPEndPoint; + } + return null; + } + + // io - input. + // virtual so it may be modified for relays, nonalloc workaround, etc. + // https://github.com/vis2k/where-allocation + // bool return because not all receives may be valid. + // for example, relay may expect a certain header. + protected virtual bool RawReceiveFrom(out ArraySegment segment, out int connectionId) + { + segment = default; + connectionId = 0; + if (socket == null) return false; + + try + { + if (socket.ReceiveFromNonBlocking(rawReceiveBuffer, out segment, ref newClientEP)) + { + // set connectionId to hash from endpoint + connectionId = Common.ConnectionHash(newClientEP); + return true; + } + } + catch (SocketException e) + { + // NOTE: SocketException is not a subclass of IOException. + // the other end closing the connection is not an 'error'. + // but connections should never just end silently. + // at least log a message for easier debugging. + Log.Info($"[KCP] Server: ReceiveFrom failed: {e}"); + } + + return false; + } + + // io - out. + // virtual so it may be modified for relays, nonalloc workaround, etc. + // relays may need to prefix connId (and remoteEndPoint would be same for all) + protected virtual void RawSend(int connectionId, ArraySegment data) + { + // get the connection's endpoint + if (!connections.TryGetValue(connectionId, out KcpServerConnection connection)) + { + Log.Warning($"[KCP] Server: RawSend invalid connectionId={connectionId}"); + return; + } + + try + { + socket.SendToNonBlocking(data, connection.remoteEndPoint); + } + catch (SocketException e) + { + Log.Error($"[KCP] Server: SendTo failed: {e}"); + } + } + + protected virtual KcpServerConnection CreateConnection(int connectionId) + { + // generate a random cookie for this connection to avoid UDP spoofing. + // needs to be random, but without allocations to avoid GC. + uint cookie = Common.GenerateCookie(); + + // create empty connection without peer first. + // we need it to set up peer callbacks. + // afterwards we assign the peer. + // events need to be wrapped with connectionIds + KcpServerConnection connection = new KcpServerConnection( + OnConnectedCallback, + (message, channel) => OnData(connectionId, message, channel), + OnDisconnectedCallback, + (error, reason) => OnError(connectionId, error, reason), + (data) => RawSend(connectionId, data), + config, + cookie, + newClientEP); + + return connection; + + // setup authenticated event that also adds to connections + void OnConnectedCallback(KcpServerConnection conn) + { + // add to connections dict after being authenticated. + connections.Add(connectionId, conn); + Log.Info($"[KCP] Server: added connection({connectionId})"); + + // setup Data + Disconnected events only AFTER the + // handshake. we don't want to fire OnServerDisconnected + // every time we receive invalid random data from the + // internet. + + // setup data event + + // finally, call mirror OnConnected event + Log.Info($"[KCP] Server: OnConnected({connectionId})"); + IPEndPoint endPoint = conn.remoteEndPoint as IPEndPoint; + OnConnected(connectionId, endPoint); + } + + void OnDisconnectedCallback() + { + // flag for removal + // (can't remove directly because connection is updated + // and event is called while iterating all connections) + connectionsToRemove.Add(connectionId); + + // call mirror event + Log.Info($"[KCP] Server: OnDisconnected({connectionId})"); + OnDisconnected(connectionId); + } + } + + // receive + add + process once. + // best to call this as long as there is more data to receive. + void ProcessMessage(ArraySegment segment, int connectionId) + { + //Log.Info($"[KCP] server raw recv {msgLength} bytes = {BitConverter.ToString(buffer, 0, msgLength)}"); + + // is this a new connection? + if (!connections.TryGetValue(connectionId, out KcpServerConnection connection)) + { + // create a new KcpConnection based on last received + // EndPoint. can be overwritten for where-allocation. + connection = CreateConnection(connectionId); + + // DO NOT add to connections yet. only if the first message + // is actually the kcp handshake. otherwise it's either: + // * random data from the internet + // * or from a client connection that we just disconnected + // but that hasn't realized it yet, still sending data + // from last session that we should absolutely ignore. + // + // + // TODO this allocates a new KcpConnection for each new + // internet connection. not ideal, but C# UDP Receive + // already allocated anyway. + // + // expecting a MAGIC byte[] would work, but sending the raw + // UDP message without kcp's reliability will have low + // probability of being received. + // + // for now, this is fine. + + + // now input the message & process received ones + // connected event was set up. + // tick will process the first message and adds the + // connection if it was the handshake. + connection.RawInput(segment); + connection.TickIncoming(); + + // again, do not add to connections. + // if the first message wasn't the kcp handshake then + // connection will simply be garbage collected. + } + // existing connection: simply input the message into kcp + else + { + connection.RawInput(segment); + } + } + + // process incoming messages. should be called before updating the world. + // virtual because relay may need to inject their own ping or similar. + readonly HashSet connectionsToRemove = new HashSet(); + public virtual void TickIncoming() + { + // input all received messages into kcp + while (RawReceiveFrom(out ArraySegment segment, out int connectionId)) + { + ProcessMessage(segment, connectionId); + } + + // process inputs for all server connections + // (even if we didn't receive anything. need to tick ping etc.) + foreach (KcpServerConnection connection in connections.Values) + { + connection.TickIncoming(); + } + + // remove disconnected connections + // (can't do it in connection.OnDisconnected because Tick is called + // while iterating connections) + foreach (int connectionId in connectionsToRemove) + { + connections.Remove(connectionId); + } + connectionsToRemove.Clear(); + } + + // process outgoing messages. should be called after updating the world. + // virtual because relay may need to inject their own ping or similar. + public virtual void TickOutgoing() + { + // flush all server connections + foreach (KcpServerConnection connection in connections.Values) + { + connection.TickOutgoing(); + } + } + + // process incoming and outgoing for convenience. + // => ideally call ProcessIncoming() before updating the world and + // ProcessOutgoing() after updating the world for minimum latency + public virtual void Tick() + { + TickIncoming(); + TickOutgoing(); + } + + public virtual void Stop() + { + // need to clear connections, otherwise they are in next session. + // fixes https://github.com/vis2k/kcp2k/pull/47 + connections.Clear(); + socket?.Close(); + socket = null; + } + } +} diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpServer.cs.meta b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpServer.cs.meta new file mode 100644 index 0000000000..30663a0653 --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpServer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c11b6c4902cd7164d9ad269cf13803ff +timeCreated: 1603787747 \ No newline at end of file diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpServerConnection.cs b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpServerConnection.cs new file mode 100644 index 0000000000..c7a48ba752 --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpServerConnection.cs @@ -0,0 +1,126 @@ +// server needs to store a separate KcpPeer for each connection. +// as well as remoteEndPoint so we know where to send data to. +using System; +using System.Net; + +namespace kcp2k.unmanaged +{ + public class KcpServerConnection : KcpPeer + { + public readonly EndPoint remoteEndPoint; + + // callbacks + // even for errors, to allow liraries to show popups etc. + // instead of logging directly. + // (string instead of Exception for ease of use and to avoid user panic) + // + // events are readonly, set in constructor. + // this ensures they are always initialized when used. + // fixes https://github.com/MirrorNetworking/Mirror/issues/3337 and more + protected readonly Action OnConnectedCallback; + protected readonly Action, KcpChannel> OnDataCallback; + protected readonly Action OnDisconnectedCallback; + protected readonly Action OnErrorCallback; + protected readonly Action> RawSendCallback; + + public KcpServerConnection( + Action OnConnected, + Action, KcpChannel> OnData, + Action OnDisconnected, + Action OnError, + Action> OnRawSend, + KcpConfig config, + uint cookie, + EndPoint remoteEndPoint) + : base(config, cookie) + { + OnConnectedCallback = OnConnected; + OnDataCallback = OnData; + OnDisconnectedCallback = OnDisconnected; + OnErrorCallback = OnError; + RawSendCallback = OnRawSend; + + this.remoteEndPoint = remoteEndPoint; + } + + // callbacks /////////////////////////////////////////////////////////// + protected override void OnAuthenticated() + { + // once we receive the first client hello, + // immediately reply with hello so the client knows the security cookie. + SendHello(); + OnConnectedCallback(this); + } + + protected override void OnData(ArraySegment message, KcpChannel channel) => + OnDataCallback(message, channel); + + protected override void OnDisconnected() => + OnDisconnectedCallback(); + + protected override void OnError(ErrorCode error, string message) => + OnErrorCallback(error, message); + + protected override void RawSend(ArraySegment data) => + RawSendCallback(data); + //////////////////////////////////////////////////////////////////////// + + // insert raw IO. usually from socket.Receive. + // offset is useful for relays, where we may parse a header and then + // feed the rest to kcp. + public void RawInput(ArraySegment segment) + { + // ensure valid size: at least 1 byte for channel + 4 bytes for cookie + if (segment.Count <= 5) return; + + // parse channel + // byte channel = segment[0]; ArraySegment[i] isn't supported in some older Unity Mono versions + byte channel = segment.Array[segment.Offset + 0]; + + // all server->client messages include the server's security cookie. + // all client->server messages except for the initial 'hello' include it too. + // parse the cookie and make sure it matches (except for initial hello). + Utils.Decode32U(segment.Array, segment.Offset + 1, out uint messageCookie); + + // security: messages after authentication are expected to contain the cookie. + // this protects against UDP spoofing. + // simply drop the message if the cookie doesn't match. + if (state == KcpState.Authenticated) + { + if (messageCookie != cookie) + { + // Info is enough, don't scare users. + // => this can happen for malicious messages + // => it can also happen if client's Hello message was retransmitted multiple times, which is totally normal. + Log.Info($"[KCP] ServerConnection: dropped message with invalid cookie: {messageCookie} from {remoteEndPoint} expected: {cookie} state: {state}. This can happen if the client's Hello message was transmitted multiple times, or if an attacker attempted UDP spoofing."); + return; + } + } + + // parse message + ArraySegment message = new ArraySegment(segment.Array, segment.Offset + 1+4, segment.Count - 1-4); + + switch (channel) + { + case (byte)KcpChannel.Reliable: + { + OnRawInputReliable(message); + break; + } + case (byte)KcpChannel.Unreliable: + { + OnRawInputUnreliable(message); + break; + } + default: + { + // invalid channel indicates random internet noise. + // servers may receive random UDP data. + // just ignore it, but log for easier debugging. + Log.Warning($"[KCP] ServerConnection: invalid channel header: {channel}, likely internet noise"); + break; + } + } + } + } +} diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpServerConnection.cs.meta b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpServerConnection.cs.meta new file mode 100644 index 0000000000..d08cad7dbc --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpServerConnection.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 76efd12644a2f4440bab84a0cd25f544 +timeCreated: 1602601483 \ No newline at end of file diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpState.cs b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpState.cs new file mode 100644 index 0000000000..5c34d4d527 --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpState.cs @@ -0,0 +1,4 @@ +namespace kcp2k.unmanaged +{ + public enum KcpState { Connected, Authenticated, Disconnected } +} diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpState.cs.meta b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpState.cs.meta new file mode 100644 index 0000000000..fc82f02f00 --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/KcpState.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 44b10e20f6eeb2a4682f4cc98cf9c921 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/Log.cs b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/Log.cs new file mode 100644 index 0000000000..c397d5c8c1 --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/Log.cs @@ -0,0 +1,14 @@ +// A simple logger class that uses Console.WriteLine by default. +// Can also do Logger.LogMethod = Debug.Log for Unity etc. +// (this way we don't have to depend on UnityEngine) +using System; + +namespace kcp2k.unmanaged +{ + public static class Log + { + public static Action Info = Console.WriteLine; + public static Action Warning = Console.WriteLine; + public static Action Error = Console.Error.WriteLine; + } +} diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/Log.cs.meta b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/Log.cs.meta new file mode 100644 index 0000000000..725edc569d --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/highlevel/Log.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ae2ee9c3ebfa2244680e9892241104bc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/kcp.meta b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/kcp.meta new file mode 100644 index 0000000000..9236b5c99b --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/kcp.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 773e0a4127cd4fb4599c008a9a0ae456 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/kcp/Kcp.cs b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/kcp/Kcp.cs new file mode 100644 index 0000000000..3db1d9e838 --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/kcp/Kcp.cs @@ -0,0 +1,626 @@ +#if UNITY_2021_3_OR_NEWER || GODOT +using System; +using System.Threading; +#endif +using static KCP.IKCP; + +#pragma warning disable CS8601 +#pragma warning disable CS8602 +#pragma warning disable CS8625 + +// ReSharper disable IdentifierTypo +// ReSharper disable GrammarMistakeInComment +// ReSharper disable PossibleNullReferenceException +// ReSharper disable ConvertToAutoPropertyWithPrivateSetter + +namespace KCP +{ + /// + /// Kcp + /// + public sealed unsafe class Kcp : IDisposable + { + /// + /// Kcp + /// + private IKCPCB* _kcp; + + /// + /// Output function + /// + private KcpCallback _output; + + /// + /// Buffer + /// + private byte[] _buffer; + + /// + /// Disposed + /// + private int _disposed; + + /// + /// Structure + /// + /// Output + public Kcp(KcpCallback output) : this(0, output) + { + } + + /// + /// Structure + /// + /// ConversationId + /// Output + public Kcp(uint conv, KcpCallback output) + { + _kcp = ikcp_create(conv, ref _buffer); + _output = output; + } + + /// + /// Set + /// + public bool IsSet => _kcp != null; + + /// + /// Conversation id + /// + public uint ConversationId => _kcp->conv; + + /// + /// Maximum transmission unit + /// + public uint MaximumTransmissionUnit => _kcp->mtu; + + /// + /// Maximum segment size + /// + public uint MaximumSegmentSize => _kcp->mss; + + /// + /// Connection state + /// + public int State => _kcp->state; + + /// + /// The sequence number of the first unacknowledged packet + /// + public uint SendUna => _kcp->snd_una; + + /// + /// The sequence number for the next packet to be sent + /// + public uint SendNext => _kcp->snd_nxt; + + /// + /// The sequence number for the next packet expected to be received + /// + public uint ReceiveNext => _kcp->rcv_nxt; + + /// + /// Slow start threshold for congestion control + /// + public uint SlowStartThreshold => _kcp->ssthresh; + + /// + /// Round-trip time variance + /// + public int RxRttval => _kcp->rx_rttval; + + /// + /// Smoothed round-trip time + /// + public int RxSrtt => _kcp->rx_srtt; + + /// + /// Retransmission timeout + /// + public int RxRto => _kcp->rx_rto; + + /// + /// Minimum retransmission timeout + /// + public int RxMinrto => _kcp->rx_minrto; + + /// + /// Send window size + /// + public uint SendWindowSize => _kcp->snd_wnd; + + /// + /// Receive window size + /// + public uint ReceiveWindowSize => _kcp->rcv_wnd; + + /// + /// Remote window size + /// + public uint RemoteWindowSize => _kcp->rmt_wnd; + + /// + /// Congestion window size + /// + public uint CongestionWindowSize => _kcp->cwnd; + + /// + /// Probe variable for fast recovery + /// + public uint Probe => _kcp->probe; + + /// + /// Current timestamp + /// + public uint Current => _kcp->current; + + /// + /// Flush interval + /// + public uint Interval => _kcp->interval; + + /// + /// Timestamp for the next flush + /// + public uint TimestampFlush => _kcp->ts_flush; + + /// + /// Number of retransmissions + /// + public uint Transmissions => _kcp->xmit; + + /// + /// Number of packets in the receive buffer + /// + public uint ReceiveBufferCount => _kcp->nrcv_buf; + + /// + /// Number of packets in the receive queue + /// + public uint ReceiveQueueCount => _kcp->nrcv_que; + + /// + /// Number of packets wait to receive + /// + public uint WaitReceiveCount => _kcp->nrcv_buf + _kcp->nrcv_que; + + /// + /// Number of packets in the send buffer + /// + public uint SendBufferCount => _kcp->nsnd_buf; + + /// + /// Number of packets in the send queue + /// + public uint SendQueueCount => _kcp->nsnd_que; + + /// + /// Number of packets wait to send + /// + public uint WaitSendCount => _kcp->nsnd_buf + _kcp->nsnd_que; + + /// + /// Whether Nagle's algorithm is disabled + /// + public uint NoDelay => _kcp->nodelay; + + /// + /// Whether the KCP connection has been updated + /// + public uint Updated => _kcp->updated; + + /// + /// Timestamp for the next probe + /// + public uint TimestampProbe => _kcp->ts_probe; + + /// + /// Probe wait time + /// + public uint ProbeWait => _kcp->probe_wait; + + /// + /// Incremental increase + /// + public uint Increment => _kcp->incr; + + /// + /// Pointer to the acknowledge list + /// + public uint* AckList => _kcp->acklist; + + /// + /// Count of acknowledges + /// + public uint AckCount => _kcp->ackcount; + + /// + /// Number of acknowledge blocks + /// + public uint AckBlock => _kcp->ackblock; + + /// + /// Buffer + /// + public byte[] Buffer => _buffer; + + /// + /// Fast resend trigger count + /// + public int FastResend => _kcp->fastresend; + + /// + /// Fast resend limit + /// + public int FastResendLimit => _kcp->fastlimit; + + /// + /// Whether congestion control is disabled + /// + public int NoCongestionWindow => _kcp->nocwnd; + + /// + /// Whether stream mode is enabled + /// + public int StreamMode => _kcp->stream; + + /// + /// Output function pointer + /// + public KcpCallback Output => _output; + + /// + /// Dispose + /// + public void Dispose() + { + if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0) + return; + ikcp_release(_kcp); + _kcp = null; + _output = null; + _buffer = null; + GC.SuppressFinalize(this); + } + + /// + /// Set output + /// + /// Output + public void SetOutput(KcpCallback output) => _output = output; + + /// + /// Destructure + /// + ~Kcp() => Dispose(); + + /// + /// Send + /// + /// Buffer + /// Sent bytes + public int Send(byte[] buffer) + { + fixed (byte* src = &buffer[0]) + return ikcp_send(_kcp, src, buffer.Length); + } + + /// + /// Send + /// + /// Buffer + /// Length + /// Sent bytes + public int Send(byte[] buffer, int length) + { + fixed (byte* src = &buffer[0]) + return ikcp_send(_kcp, src, length); + } + + /// + /// Send + /// + /// Buffer + /// Offset + /// Length + /// Sent bytes + public int Send(byte[] buffer, int offset, int length) + { + fixed (byte* src = &buffer[offset]) + return ikcp_send(_kcp, src, length); + } + + /// + /// Send + /// + /// Buffer + /// Sent bytes + public int Send(ReadOnlySpan buffer) + { + fixed (byte* src = &buffer[0]) + return ikcp_send(_kcp, src, buffer.Length); + } + + /// + /// Send + /// + /// Buffer + /// Sent bytes + public int Send(ReadOnlyMemory buffer) + { + fixed (byte* src = &buffer.Span[0]) + return ikcp_send(_kcp, src, buffer.Length); + } + + /// + /// Send + /// + /// Buffer + /// Sent bytes + public int Send(ArraySegment buffer) + { + fixed (byte* src = &buffer.Array[buffer.Offset]) + return ikcp_send(_kcp, src, buffer.Count); + } + + /// + /// Send + /// + /// Buffer + /// Length + /// Sent bytes + public int Send(byte* buffer, int length) => ikcp_send(_kcp, buffer, length); + + /// + /// Send + /// + /// Buffer + /// Offset + /// Length + /// Sent bytes + public int Send(byte* buffer, int offset, int length) => ikcp_send(_kcp, buffer + offset, length); + + /// + /// Input + /// + /// Buffer + /// Input bytes + public int Input(byte[] buffer) + { + fixed (byte* src = &buffer[0]) + return ikcp_input(_kcp, src, buffer.Length); + } + + /// + /// Input + /// + /// Buffer + /// Length + /// Input bytes + public int Input(byte[] buffer, int length) + { + fixed (byte* src = &buffer[0]) + return ikcp_input(_kcp, src, length); + } + + /// + /// Input + /// + /// Buffer + /// Offset + /// Length + /// Input bytes + public int Input(byte[] buffer, int offset, int length) + { + fixed (byte* src = &buffer[offset]) + return ikcp_input(_kcp, src, length); + } + + /// + /// Input + /// + /// Buffer + /// Input bytes + public int Input(ReadOnlySpan buffer) + { + fixed (byte* src = &buffer[0]) + return ikcp_input(_kcp, src, buffer.Length); + } + + /// + /// Input + /// + /// Buffer + /// Input bytes + public int Input(ReadOnlyMemory buffer) + { + fixed (byte* src = &buffer.Span[0]) + return ikcp_input(_kcp, src, buffer.Length); + } + + /// + /// Input + /// + /// Buffer + /// Input bytes + public int Input(ArraySegment buffer) + { + fixed (byte* src = &buffer.Array[buffer.Offset]) + return ikcp_input(_kcp, src, buffer.Count); + } + + /// + /// Input + /// + /// Buffer + /// Length + /// Input bytes + public int Input(byte* buffer, int length) => ikcp_input(_kcp, buffer, length); + + /// + /// Input + /// + /// Buffer + /// Offset + /// Length + /// Input bytes + public int Input(byte* buffer, int offset, int length) => ikcp_input(_kcp, buffer + offset, length); + + /// + /// Peek size + /// + /// Peeked size + public int PeekSize() => ikcp_peeksize(_kcp); + + /// + /// Receive + /// + /// Buffer + /// Received bytes + public int Receive(byte[] buffer) + { + fixed (byte* dest = &buffer[0]) + return ikcp_recv(_kcp, dest, buffer.Length); + } + + /// + /// Receive + /// + /// Buffer + /// Length + /// Received bytes + public int Receive(byte[] buffer, int length) + { + fixed (byte* dest = &buffer[0]) + return ikcp_recv(_kcp, dest, length); + } + + /// + /// Receive + /// + /// Buffer + /// Offset + /// Length + /// Received bytes + public int Receive(byte[] buffer, int offset, int length) + { + fixed (byte* dest = &buffer[offset]) + return ikcp_recv(_kcp, dest, length); + } + + /// + /// Receive + /// + /// Buffer + /// Received bytes + public int Receive(Span buffer) + { + fixed (byte* dest = &buffer[0]) + return ikcp_recv(_kcp, dest, buffer.Length); + } + + /// + /// Receive + /// + /// Buffer + /// Received bytes + public int Receive(Memory buffer) + { + fixed (byte* dest = &buffer.Span[0]) + return ikcp_recv(_kcp, dest, buffer.Length); + } + + /// + /// Receive + /// + /// Buffer + /// Received bytes + public int Receive(ArraySegment buffer) + { + fixed (byte* dest = &buffer.Array[buffer.Offset]) + return ikcp_recv(_kcp, dest, buffer.Count); + } + + /// + /// Receive + /// + /// Buffer + /// Length + /// Received bytes + public int Receive(byte* buffer, int length) => ikcp_recv(_kcp, buffer, length); + + /// + /// Receive + /// + /// Buffer + /// Offset + /// Length + /// Received bytes + public int Receive(byte* buffer, int offset, int length) => ikcp_recv(_kcp, buffer + offset, length); + + /// + /// Update + /// + /// Timestamp + public void Update(uint current) => ikcp_update(_kcp, current, _output, _buffer); + + /// + /// Check + /// + /// Timestamp + /// Next flush timestamp + public uint Check(uint current) => ikcp_check(_kcp, current); + + /// + /// Flush + /// + public void Flush() => ikcp_flush(_kcp, _output, _buffer); + + /// + /// Set maximum transmission unit + /// + /// Maximum transmission unit + /// Set + public int SetMtu(int mtu) => ikcp_setmtu(_kcp, mtu, ref _buffer); + + /// + /// Set flush interval + /// + /// Flush interval + public void SetInterval(int interval) => ikcp_interval(_kcp, interval); + + /// + /// Set no delay + /// + /// Whether Nagle's algorithm is disabled + /// Flush interval + /// Fast resend trigger count + /// No congestion window + public void SetNoDelay(int nodelay, int interval, int resend, int nc) => ikcp_nodelay(_kcp, nodelay, interval, resend, nc); + + /// + /// Set window size + /// + /// Send window size + /// Receive window size + public void SetWindowSize(int sndwnd, int rcvwnd) => ikcp_wndsize(_kcp, sndwnd, rcvwnd); + + /// + /// Set fast resend limit + /// + /// Fast resend limit + public void SetFastResendLimit(int fastlimit) => ikcp_fastresendlimit(_kcp, fastlimit); + + /// + /// Set whether stream mode is enabled + /// + /// Whether stream mode is enabled + public void SetStreamMode(int stream) => ikcp_streammode(_kcp, stream); + + /// + /// Set minimum retransmission timeout + /// + /// Minimum retransmission timeout + public void SetMinrto(int minrto) => ikcp_minrto(_kcp, minrto); + } +} \ No newline at end of file diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/kcp/Kcp.cs.meta b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/kcp/Kcp.cs.meta new file mode 100644 index 0000000000..4d3e62aac5 --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/kcp/Kcp.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 469c6dacc869a48469e0aa33c524bbcd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/kcp/Utils.cs b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/kcp/Utils.cs new file mode 100644 index 0000000000..2cb74628c1 --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/kcp/Utils.cs @@ -0,0 +1,76 @@ +using System.Runtime.CompilerServices; + +namespace kcp2k +{ + public static partial class Utils + { + // Clamp so we don't have to depend on UnityEngine + public static int Clamp(int value, int min, int max) + { + if (value < min) return min; + if (value > max) return max; + return value; + } + + // encode 8 bits unsigned int + public static int Encode8u(byte[] p, int offset, byte value) + { + p[0 + offset] = value; + return 1; + } + + // decode 8 bits unsigned int + public static int Decode8u(byte[] p, int offset, out byte value) + { + value = p[0 + offset]; + return 1; + } + + // encode 16 bits unsigned int (lsb) + public static int Encode16U(byte[] p, int offset, ushort value) + { + p[0 + offset] = (byte)(value >> 0); + p[1 + offset] = (byte)(value >> 8); + return 2; + } + + // decode 16 bits unsigned int (lsb) + public static int Decode16U(byte[] p, int offset, out ushort value) + { + ushort result = 0; + result |= p[0 + offset]; + result |= (ushort)(p[1 + offset] << 8); + value = result; + return 2; + } + + // encode 32 bits unsigned int (lsb) + public static int Encode32U(byte[] p, int offset, uint value) + { + p[0 + offset] = (byte)(value >> 0); + p[1 + offset] = (byte)(value >> 8); + p[2 + offset] = (byte)(value >> 16); + p[3 + offset] = (byte)(value >> 24); + return 4; + } + + // decode 32 bits unsigned int (lsb) + public static int Decode32U(byte[] p, int offset, out uint value) + { + uint result = 0; + result |= p[0 + offset]; + result |= (uint)(p[1 + offset] << 8); + result |= (uint)(p[2 + offset] << 16); + result |= (uint)(p[3 + offset] << 24); + value = result; + return 4; + } + + // timediff was a macro in original Kcp. let's inline it if possible. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int TimeDiff(uint later, uint earlier) + { + return (int)(later - earlier); + } + } +} diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/kcp/Utils.cs.meta b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/kcp/Utils.cs.meta new file mode 100644 index 0000000000..cf2dc45f0f --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/kcp/Utils.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 57bdd3ec692551a4fba15613e3bfe28c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/kcp/ikcpc.cs b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/kcp/ikcpc.cs new file mode 100644 index 0000000000..104254714a --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/kcp/ikcpc.cs @@ -0,0 +1,1210 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using static KCP.IQUEUEHEAD; +using static KCP.KCPBASIC; + +#pragma warning disable CS8600 +#pragma warning disable CS8602 +#pragma warning disable CS8981 + +// ReSharper disable IdentifierTypo +// ReSharper disable InconsistentNaming +// ReSharper disable ConvertIfStatementToSwitchStatement + +namespace KCP +{ + internal static unsafe class IKCP + { + private static void memcpy(void* dest, void* src, int n) => Buffer.MemoryCopy(src, dest, n, n); + + private static void memcpy(void* dest, void* src, uint n) => Buffer.MemoryCopy(src, dest, n, n); + + private static void* malloc(nint size) => +#if !UNITY_2021_3_OR_NEWER || NET6_0_OR_GREATER + NativeMemory.Alloc((nuint)size); +#else + (void*)Marshal.AllocHGlobal(size); +#endif + + private static void* malloc(nuint size) => +#if !UNITY_2021_3_OR_NEWER || NET6_0_OR_GREATER + NativeMemory.Alloc(size); +#else + (void*)Marshal.AllocHGlobal((nint)size); +#endif + + private static void free(void* ptr) => +#if !UNITY_2021_3_OR_NEWER || NET6_0_OR_GREATER + NativeMemory.Free(ptr); +#else + Marshal.FreeHGlobal((nint)ptr); +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte* ikcp_encode8u(byte* p, byte c) + { + *p++ = c; + return p; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte* ikcp_decode8u(byte* p, byte* c) + { + *c = *p++; + return p; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte* ikcp_encode16u(byte* p, ushort w) + { + memcpy(p, &w, 2); + p += 2; + return p; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte* ikcp_decode16u(byte* p, ushort* w) + { + memcpy(w, p, 2); + p += 2; + return p; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte* ikcp_encode32u(byte* p, uint l) + { + memcpy(p, &l, 4); + p += 4; + return p; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte* ikcp_decode32u(byte* p, uint* l) + { + memcpy(l, p, 4); + p += 4; + return p; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint _imin_(uint a, uint b) => a <= b ? a : b; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint _imax_(uint a, uint b) => a >= b ? a : b; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint _ibound_(uint lower, uint middle, uint upper) => _imin_(_imax_(lower, middle), upper); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int _iclamp_(int x, uint min, uint max) => x < min ? (int)min : x > max ? (int)max : x; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint _iceilpow2_(uint x) + { + x--; + x |= x >> 1; + x |= x >> 2; + x |= x >> 4; + x |= x >> 8; + x |= x >> 16; + x++; + return x; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int _itimediff(uint later, uint earlier) => (int)(later - earlier); + + private static void* ikcp_malloc(nint size) => malloc(size); + + private static void* ikcp_malloc(nuint size) => malloc(size); + + private static void ikcp_free(void* ptr) => free(ptr); + + private static IKCPSEG* ikcp_segment_new(IKCPCB* kcp, int size) => (IKCPSEG*)ikcp_malloc(sizeof(IKCPSEG) + size); + + private static void ikcp_segment_delete(IKCPCB* kcp, IKCPSEG* seg) => ikcp_free(seg); + + private static void ikcp_output(KcpCallback output, byte[] data, int size) + { + if (size == 0) + return; + output(data, size); + } + + public static IKCPCB* ikcp_create(uint conv, ref byte[] buffer) + { + var kcp = (IKCPCB*)ikcp_malloc(sizeof(IKCPCB)); + kcp->conv = conv; + kcp->snd_una = 0; + kcp->snd_nxt = 0; + kcp->rcv_nxt = 0; + kcp->ts_probe = 0; + kcp->probe_wait = 0; + kcp->snd_wnd = WND_SND; + kcp->rcv_wnd = WND_RCV; + kcp->rmt_wnd = WND_RCV; + kcp->cwnd = 0; + kcp->incr = 0; + kcp->probe = 0; + kcp->mtu = MTU_DEF; + kcp->mss = kcp->mtu - OVERHEAD; + kcp->stream = 0; + buffer = new byte[REVERSED_HEAD + (kcp->mtu + OVERHEAD) * 3]; + iqueue_init(&kcp->snd_queue); + iqueue_init(&kcp->rcv_queue); + iqueue_init(&kcp->snd_buf); + iqueue_init(&kcp->rcv_buf); + kcp->nrcv_buf = 0; + kcp->nsnd_buf = 0; + kcp->nrcv_que = 0; + kcp->nsnd_que = 0; + kcp->state = 0; + kcp->acklist = null; + kcp->ackblock = 0; + kcp->ackcount = 0; + kcp->rx_srtt = 0; + kcp->rx_rttval = 0; + kcp->rx_rto = (int)RTO_DEF; + kcp->rx_minrto = (int)RTO_MIN; + kcp->current = 0; + kcp->interval = INTERVAL; + kcp->ts_flush = INTERVAL; + kcp->nodelay = 0; + kcp->updated = 0; + kcp->ssthresh = THRESH_INIT; + kcp->fastresend = 0; + kcp->fastlimit = (int)FASTACK_LIMIT; + kcp->nocwnd = 0; + kcp->xmit = 0; + return kcp; + } + + public static void ikcp_release(IKCPCB* kcp) + { + if (kcp != null) + { + IKCPSEG* seg; + while (!iqueue_is_empty(&kcp->snd_buf)) + { + seg = iqueue_entry(kcp->snd_buf.next); + iqueue_del(&seg->node); + ikcp_segment_delete(kcp, seg); + } + + while (!iqueue_is_empty(&kcp->rcv_buf)) + { + seg = iqueue_entry(kcp->rcv_buf.next); + iqueue_del(&seg->node); + ikcp_segment_delete(kcp, seg); + } + + while (!iqueue_is_empty(&kcp->snd_queue)) + { + seg = iqueue_entry(kcp->snd_queue.next); + iqueue_del(&seg->node); + ikcp_segment_delete(kcp, seg); + } + + while (!iqueue_is_empty(&kcp->rcv_queue)) + { + seg = iqueue_entry(kcp->rcv_queue.next); + iqueue_del(&seg->node); + ikcp_segment_delete(kcp, seg); + } + + if (kcp->acklist != null) + ikcp_free(kcp->acklist); + kcp->nrcv_buf = 0; + kcp->nsnd_buf = 0; + kcp->nrcv_que = 0; + kcp->nsnd_que = 0; + kcp->ackcount = 0; + kcp->acklist = null; + ikcp_free(kcp); + } + } + + public static int ikcp_recv(IKCPCB* kcp, byte* buffer, int len) + { + if (iqueue_is_empty(&kcp->rcv_queue)) + return -1; + var peeksize = ikcp_peeksize_internal(kcp); + if (peeksize < 0) + return -2; + int recover; + IQUEUEHEAD* p; + IKCPSEG* seg; + if (len < 0) + { + len = -len; + if (peeksize > len) + return -3; + recover = kcp->nrcv_que >= kcp->rcv_wnd ? 1 : 0; + p = kcp->rcv_queue.next; + for (len = 0; p != &kcp->rcv_queue;) + { + seg = iqueue_entry(p); + p = p->next; + if (buffer != null) + { + memcpy(buffer, seg->data, seg->len); + buffer += seg->len; + } + + len += (int)seg->len; + var fragment = (int)seg->frg; + if (fragment == 0) + break; + } + } + else + { + if (peeksize > len) + return -3; + recover = kcp->nrcv_que >= kcp->rcv_wnd ? 1 : 0; + p = kcp->rcv_queue.next; + for (len = 0; p != &kcp->rcv_queue;) + { + seg = iqueue_entry(p); + p = p->next; + if (buffer != null) + { + memcpy(buffer, seg->data, seg->len); + buffer += seg->len; + } + + len += (int)seg->len; + var fragment = (int)seg->frg; + iqueue_del(&seg->node); + ikcp_segment_delete(kcp, seg); + kcp->nrcv_que--; + if (fragment == 0) + break; + } + } + + while (!iqueue_is_empty(&kcp->rcv_buf)) + { + seg = iqueue_entry(kcp->rcv_buf.next); + if (seg->sn == kcp->rcv_nxt && kcp->nrcv_que < kcp->rcv_wnd) + { + iqueue_del(&seg->node); + kcp->nrcv_buf--; + iqueue_add_tail(&seg->node, &kcp->rcv_queue); + kcp->nrcv_que++; + kcp->rcv_nxt++; + } + else + { + break; + } + } + + if (kcp->nrcv_que < kcp->rcv_wnd && recover != 0) + kcp->probe |= ASK_TELL; + return len; + } + + public static int ikcp_peeksize(IKCPCB* kcp) => iqueue_is_empty(&kcp->rcv_queue) ? -1 : ikcp_peeksize_internal(kcp); + + private static int ikcp_peeksize_internal(IKCPCB* kcp) + { + var seg = iqueue_entry(kcp->rcv_queue.next); + if (seg->frg == 0) + return (int)seg->len; + if (kcp->nrcv_que < seg->frg + 1) + return -1; + IQUEUEHEAD* p; + var length = 0; + for (p = kcp->rcv_queue.next; p != &kcp->rcv_queue; p = p->next) + { + seg = iqueue_entry(p); + length += (int)seg->len; + if (seg->frg == 0) + break; + } + + return length; + } + + public static int ikcp_send(IKCPCB* kcp, byte* buffer, int len) + { + if (len < 0) + return -1; + IKCPSEG* seg; + var sent = 0; + if (kcp->stream != 0) + { + if (!iqueue_is_empty(&kcp->snd_queue)) + { + var old = iqueue_entry(kcp->snd_queue.prev); + if (old->len < kcp->mss) + { + var capacity = (int)kcp->mss - (int)old->len; + var extend = len < capacity ? len : capacity; + seg = ikcp_segment_new(kcp, (int)old->len + extend); + iqueue_add_tail(&seg->node, &kcp->snd_queue); + memcpy(seg->data, old->data, old->len); + if (buffer != null) + { + memcpy(seg->data + old->len, buffer, extend); + buffer += extend; + } + + seg->len = old->len + (uint)extend; + seg->frg = 0; + len -= extend; + iqueue_del_init(&old->node); + ikcp_segment_delete(kcp, old); + sent = extend; + } + } + + if (len <= 0) + return sent; + int count; + if (len <= (int)kcp->mss) + { + count = 1; + } + else + { + count = (int)((len + kcp->mss - 1) / kcp->mss); + if (count >= (int)kcp->rcv_wnd) + return sent > 0 ? sent : -2; + if (count == 0) + count = 1; + } + + int i; + for (i = 0; i < count; ++i) + { + var size = len > (int)kcp->mss ? (int)kcp->mss : len; + seg = ikcp_segment_new(kcp, size); + if (buffer != null && len > 0) + memcpy(seg->data, buffer, size); + seg->len = (uint)size; + seg->frg = 0; + iqueue_init(&seg->node); + iqueue_add_tail(&seg->node, &kcp->snd_queue); + kcp->nsnd_que++; + if (buffer != null) + buffer += size; + len -= size; + sent += size; + } + } + else + { + int count; + if (len <= (int)kcp->mss) + { + count = 1; + } + else + { + count = (int)((len + kcp->mss - 1) / kcp->mss); + if (count > FRG_LIMIT || count >= (int)kcp->rcv_wnd) + return -2; + if (count == 0) + count = 1; + } + + int i; + for (i = 0; i < count; ++i) + { + var size = len > (int)kcp->mss ? (int)kcp->mss : len; + seg = ikcp_segment_new(kcp, size); + if (buffer != null && len > 0) + memcpy(seg->data, buffer, size); + seg->len = (uint)size; + seg->frg = (uint)(count - i - 1); + iqueue_init(&seg->node); + iqueue_add_tail(&seg->node, &kcp->snd_queue); + kcp->nsnd_que++; + if (buffer != null) + buffer += size; + len -= size; + sent += size; + } + } + + return sent; + } + + private static void ikcp_update_ack(IKCPCB* kcp, int rtt) + { + if (kcp->rx_srtt == 0) + { + kcp->rx_srtt = rtt; + kcp->rx_rttval = rtt / 2; + } + else + { + var delta = rtt - kcp->rx_srtt; + if (delta < 0) + delta = -delta; + kcp->rx_rttval = (3 * kcp->rx_rttval + delta) / 4; + kcp->rx_srtt = (7 * kcp->rx_srtt + rtt) / 8; + if (kcp->rx_srtt < 1) + kcp->rx_srtt = 1; + } + + var rto = (int)(kcp->rx_srtt + _imax_(kcp->interval, (uint)(4 * kcp->rx_rttval))); + kcp->rx_rto = (int)_ibound_((uint)kcp->rx_minrto, (uint)rto, RTO_MAX); + } + + private static void ikcp_shrink_buf(IKCPCB* kcp) + { + var p = kcp->snd_buf.next; + if (p != &kcp->snd_buf) + { + var seg = iqueue_entry(p); + kcp->snd_una = seg->sn; + } + else + { + kcp->snd_una = kcp->snd_nxt; + } + } + + private static void ikcp_parse_ack(IKCPCB* kcp, uint sn) + { + if (_itimediff(sn, kcp->snd_una) < 0 || _itimediff(sn, kcp->snd_nxt) >= 0) + return; + IQUEUEHEAD* p, next; + for (p = kcp->snd_buf.next; p != &kcp->snd_buf; p = next) + { + var seg = iqueue_entry(p); + next = p->next; + if (sn == seg->sn) + { + iqueue_del(p); + ikcp_segment_delete(kcp, seg); + kcp->nsnd_buf--; + break; + } + + if (_itimediff(sn, seg->sn) < 0) + break; + } + } + + private static void ikcp_parse_una(IKCPCB* kcp, uint una) + { + IQUEUEHEAD* p, next; + for (p = kcp->snd_buf.next; p != &kcp->snd_buf; p = next) + { + var seg = iqueue_entry(p); + next = p->next; + if (_itimediff(una, seg->sn) > 0) + { + iqueue_del(p); + ikcp_segment_delete(kcp, seg); + kcp->nsnd_buf--; + } + else + { + break; + } + } + } + + private static void ikcp_parse_fastack(IKCPCB* kcp, uint sn, uint ts) + { + if (_itimediff(sn, kcp->snd_una) < 0 || _itimediff(sn, kcp->snd_nxt) >= 0) + return; + IQUEUEHEAD* p, next; + for (p = kcp->snd_buf.next; p != &kcp->snd_buf; p = next) + { + var seg = iqueue_entry(p); + next = p->next; + if (_itimediff(sn, seg->sn) < 0) + break; + if (sn != seg->sn) + { +#if KCP_FASTACK_CONSERVE + seg->fastack++; +#else + if (_itimediff(ts, seg->ts) >= 0) + seg->fastack++; +#endif + } + } + } + + private static void ikcp_ack_push(IKCPCB* kcp, uint sn, uint ts) + { + var newsize = kcp->ackcount + 1; + if (newsize > kcp->ackblock) + { + var newblock = newsize <= 8 ? 8 : _iceilpow2_(newsize); + var acklist = (uint*)ikcp_malloc(newblock << 3); + if (kcp->acklist != null) + { + uint x; + for (x = 0; x < kcp->ackcount; ++x) + { + acklist[x * 2] = kcp->acklist[x * 2]; + acklist[x * 2 + 1] = kcp->acklist[x * 2 + 1]; + } + + ikcp_free(kcp->acklist); + } + + kcp->acklist = acklist; + kcp->ackblock = newblock; + } + + var ptr = &kcp->acklist[kcp->ackcount * 2]; + ptr[0] = sn; + ptr[1] = ts; + kcp->ackcount++; + } + + private static void ikcp_ack_get(IKCPCB* kcp, int p, uint* sn, uint* ts) + { + if (sn != null) + sn[0] = kcp->acklist[p * 2]; + if (ts != null) + ts[0] = kcp->acklist[p * 2 + 1]; + } + + private static void ikcp_parse_data(IKCPCB* kcp, IKCPSEG* newseg) + { + var sn = newseg->sn; + if (_itimediff(sn, kcp->rcv_nxt + kcp->rcv_wnd) >= 0 || _itimediff(sn, kcp->rcv_nxt) < 0) + { + ikcp_segment_delete(kcp, newseg); + return; + } + + IQUEUEHEAD* p, prev; + var repeat = 0; + for (p = kcp->rcv_buf.prev; p != &kcp->rcv_buf; p = prev) + { + var seg = iqueue_entry(p); + prev = p->prev; + if (seg->sn == sn) + { + repeat = 1; + break; + } + + if (_itimediff(sn, seg->sn) > 0) + break; + } + + if (repeat == 0) + { + iqueue_init(&newseg->node); + iqueue_add(&newseg->node, p); + kcp->nrcv_buf++; + } + else + { + ikcp_segment_delete(kcp, newseg); + } + + while (!iqueue_is_empty(&kcp->rcv_buf)) + { + var seg = iqueue_entry(kcp->rcv_buf.next); + if (seg->sn == kcp->rcv_nxt && kcp->nrcv_que < kcp->rcv_wnd) + { + iqueue_del(&seg->node); + kcp->nrcv_buf--; + iqueue_add_tail(&seg->node, &kcp->rcv_queue); + kcp->nrcv_que++; + kcp->rcv_nxt++; + } + else + { + break; + } + } + } + + public static int ikcp_input(IKCPCB* kcp, byte* data, int size) + { + if (data == null || size < (int)OVERHEAD) + return -1; + var prev_una = kcp->snd_una; + uint maxack = 0, latest_ts = 0; + var flag = 0; + while (true) + { + uint ts, sn, len, una, conv; + ushort wnd; + byte cmd, frg; + if (size < (int)OVERHEAD) + break; + data = ikcp_decode32u(data, &conv); + if (conv != kcp->conv) + return -1; + data = ikcp_decode8u(data, &cmd); + data = ikcp_decode8u(data, &frg); + data = ikcp_decode16u(data, &wnd); + data = ikcp_decode32u(data, &ts); + data = ikcp_decode32u(data, &sn); + data = ikcp_decode32u(data, &una); + data = ikcp_decode32u(data, &len); + size -= (int)OVERHEAD; + if (size < len || (int)len < 0) + return -2; + if (cmd != CMD_PUSH && cmd != CMD_ACK && cmd != CMD_WASK && cmd != CMD_WINS) + return -3; + kcp->rmt_wnd = wnd; + ikcp_parse_una(kcp, una); + ikcp_shrink_buf(kcp); + if (cmd == CMD_ACK) + { + if (_itimediff(kcp->current, ts) >= 0) + ikcp_update_ack(kcp, _itimediff(kcp->current, ts)); + ikcp_parse_ack(kcp, sn); + ikcp_shrink_buf(kcp); + if (flag == 0) + { + flag = 1; + maxack = sn; + latest_ts = ts; + } + else + { + if (_itimediff(sn, maxack) > 0) + { +#if KCP_FASTACK_CONSERVE + maxack = sn; + latest_ts = ts; +#else + if (_itimediff(ts, latest_ts) > 0) + { + maxack = sn; + latest_ts = ts; + } +#endif + } + } + } + else if (cmd == CMD_PUSH) + { + if (_itimediff(sn, kcp->rcv_nxt + kcp->rcv_wnd) < 0) + { + ikcp_ack_push(kcp, sn, ts); + if (_itimediff(sn, kcp->rcv_nxt) >= 0) + { + var seg = ikcp_segment_new(kcp, (int)len); + seg->conv = conv; + seg->cmd = cmd; + seg->frg = frg; + seg->wnd = wnd; + seg->ts = ts; + seg->sn = sn; + seg->una = una; + seg->len = len; + if (len > 0) + memcpy(seg->data, data, len); + ikcp_parse_data(kcp, seg); + } + } + } + else if (cmd == CMD_WASK) + { + kcp->probe |= ASK_TELL; + } + else if (cmd != CMD_WINS) + { + return -3; + } + + data += len; + size -= (int)len; + } + + if (flag != 0) + ikcp_parse_fastack(kcp, maxack, latest_ts); + if (_itimediff(kcp->snd_una, prev_una) > 0) + { + if (kcp->cwnd < kcp->rmt_wnd) + { + var mss = kcp->mss; + if (kcp->cwnd < kcp->ssthresh) + { + kcp->cwnd++; + kcp->incr += mss; + } + else + { + if (kcp->incr < mss) + kcp->incr = mss; + kcp->incr += mss * mss / kcp->incr + mss / 16; + if ((kcp->cwnd + 1) * mss <= kcp->incr) + kcp->cwnd = (kcp->incr + mss - 1) / (mss > 0 ? mss : 1); + } + + if (kcp->cwnd > kcp->rmt_wnd) + { + kcp->cwnd = kcp->rmt_wnd; + kcp->incr = kcp->rmt_wnd * mss; + } + } + } + + return 0; + } + + private static byte* ikcp_encode_seg(byte* ptr, IKCPSEG* seg) + { + ptr = ikcp_encode32u(ptr, seg->conv); + ptr = ikcp_encode8u(ptr, (byte)seg->cmd); + ptr = ikcp_encode8u(ptr, (byte)seg->frg); + ptr = ikcp_encode16u(ptr, (ushort)seg->wnd); + ptr = ikcp_encode32u(ptr, seg->ts); + ptr = ikcp_encode32u(ptr, seg->sn); + ptr = ikcp_encode32u(ptr, seg->una); + ptr = ikcp_encode32u(ptr, seg->len); + return ptr; + } + + private static int ikcp_wnd_unused(IKCPCB* kcp) => kcp->nrcv_que < kcp->rcv_wnd ? (int)(kcp->rcv_wnd - kcp->nrcv_que) : 0; + + public static void ikcp_flush(IKCPCB* kcp, KcpCallback output, byte[] bytes) + { + if (kcp->updated == 0) + return; + ikcp_flush_internal(kcp, output, bytes); + } + + private static void ikcp_flush_internal(IKCPCB* kcp, KcpCallback output, byte[] bytes) + { + var current = kcp->current; + fixed (byte* buffer = &bytes[REVERSED_HEAD]) + { + var ptr = buffer; + int size, i; + IQUEUEHEAD* p; + var change = 0; + var lost = 0; + IKCPSEG seg; + seg.conv = kcp->conv; + seg.cmd = CMD_ACK; + seg.frg = 0; + seg.wnd = (uint)ikcp_wnd_unused(kcp); + seg.una = kcp->rcv_nxt; + seg.len = 0; + seg.sn = 0; + seg.ts = 0; + var count = (int)kcp->ackcount; + for (i = 0; i < count; ++i) + { + size = (int)(ptr - buffer); + if (size + (int)OVERHEAD > (int)kcp->mtu) + { + ikcp_output(output, bytes, size); + ptr = buffer; + } + + ikcp_ack_get(kcp, i, &seg.sn, &seg.ts); + ptr = ikcp_encode_seg(ptr, &seg); + } + + kcp->ackcount = 0; + if (kcp->rmt_wnd == 0) + { + if (kcp->probe_wait == 0) + { + kcp->probe_wait = PROBE_INIT; + kcp->ts_probe = kcp->current + kcp->probe_wait; + } + else + { + if (_itimediff(kcp->current, kcp->ts_probe) >= 0) + { + if (kcp->probe_wait < PROBE_INIT) + kcp->probe_wait = PROBE_INIT; + kcp->probe_wait += kcp->probe_wait / 2; + if (kcp->probe_wait > PROBE_LIMIT) + kcp->probe_wait = PROBE_LIMIT; + kcp->ts_probe = kcp->current + kcp->probe_wait; + kcp->probe |= ASK_SEND; + } + } + } + else + { + kcp->ts_probe = 0; + kcp->probe_wait = 0; + } + + if ((kcp->probe != 0) & (ASK_SEND != 0)) + { + seg.cmd = CMD_WASK; + size = (int)(ptr - buffer); + if (size + (int)OVERHEAD > (int)kcp->mtu) + { + ikcp_output(output, bytes, size); + ptr = buffer; + } + + ptr = ikcp_encode_seg(ptr, &seg); + } + + if ((kcp->probe != 0) & (ASK_TELL != 0)) + { + seg.cmd = CMD_WINS; + size = (int)(ptr - buffer); + if (size + (int)OVERHEAD > (int)kcp->mtu) + { + ikcp_output(output, bytes, size); + ptr = buffer; + } + + ptr = ikcp_encode_seg(ptr, &seg); + } + + kcp->probe = 0; + var cwnd = _imin_(kcp->snd_wnd, kcp->rmt_wnd); + if (kcp->nocwnd == 0) + cwnd = _imin_(kcp->cwnd, cwnd); + while (_itimediff(kcp->snd_nxt, kcp->snd_una + cwnd) < 0) + { + if (iqueue_is_empty(&kcp->snd_queue)) + break; + var newseg = iqueue_entry(kcp->snd_queue.next); + iqueue_del(&newseg->node); + iqueue_add_tail(&newseg->node, &kcp->snd_buf); + kcp->nsnd_que--; + kcp->nsnd_buf++; + newseg->conv = kcp->conv; + newseg->cmd = CMD_PUSH; + newseg->wnd = seg.wnd; + newseg->ts = current; + newseg->sn = kcp->snd_nxt++; + newseg->una = kcp->rcv_nxt; + newseg->resendts = current; + newseg->rto = (uint)kcp->rx_rto; + newseg->fastack = 0; + newseg->xmit = 0; + } + + var resent = kcp->fastresend > 0 ? (uint)kcp->fastresend : 4294967295; + if (kcp->nodelay == 0) + { + var rtomin = (uint)(kcp->rx_rto >> 3); + for (p = kcp->snd_buf.next; p != &kcp->snd_buf; p = p->next) + { + var segment = iqueue_entry(p); + var needsend = 0; + if (segment->xmit == 0) + { + needsend = 1; + segment->xmit++; + segment->rto = (uint)kcp->rx_rto; + segment->resendts = current + segment->rto + rtomin; + } + else if (_itimediff(current, segment->resendts) >= 0) + { + needsend = 1; + segment->xmit++; + kcp->xmit++; + segment->rto += _imax_(segment->rto, (uint)kcp->rx_rto); + segment->resendts = current + segment->rto; + lost = 1; + } + else if (segment->fastack >= resent) + { + if ((int)segment->xmit <= kcp->fastlimit || kcp->fastlimit == 0) + { + needsend = 1; + segment->xmit++; + segment->fastack = 0; + segment->resendts = current + segment->rto; + change++; + } + } + + if (needsend != 0) + { + segment->ts = current; + segment->wnd = seg.wnd; + segment->una = kcp->rcv_nxt; + size = (int)(ptr - buffer); + var need = (int)(OVERHEAD + segment->len); + if (size + need > (int)kcp->mtu) + { + ikcp_output(output, bytes, size); + ptr = buffer; + } + + ptr = ikcp_encode_seg(ptr, segment); + if (segment->len > 0) + { + memcpy(ptr, segment->data, segment->len); + ptr += segment->len; + } + + if (segment->xmit >= DEADLINK) + kcp->state = -1; + } + } + } + else if (kcp->nodelay == 1) + { + for (p = kcp->snd_buf.next; p != &kcp->snd_buf; p = p->next) + { + var segment = iqueue_entry(p); + var needsend = 0; + if (segment->xmit == 0) + { + needsend = 1; + segment->xmit++; + segment->rto = (uint)kcp->rx_rto; + segment->resendts = current + segment->rto; + } + else if (_itimediff(current, segment->resendts) >= 0) + { + needsend = 1; + segment->xmit++; + kcp->xmit++; + var step = (int)segment->rto; + segment->rto += (uint)(step / 2); + segment->resendts = current + segment->rto; + lost = 1; + } + else if (segment->fastack >= resent) + { + if ((int)segment->xmit <= kcp->fastlimit || kcp->fastlimit == 0) + { + needsend = 1; + segment->xmit++; + segment->fastack = 0; + segment->resendts = current + segment->rto; + change++; + } + } + + if (needsend != 0) + { + segment->ts = current; + segment->wnd = seg.wnd; + segment->una = kcp->rcv_nxt; + size = (int)(ptr - buffer); + var need = (int)(OVERHEAD + segment->len); + if (size + need > (int)kcp->mtu) + { + ikcp_output(output, bytes, size); + ptr = buffer; + } + + ptr = ikcp_encode_seg(ptr, segment); + if (segment->len > 0) + { + memcpy(ptr, segment->data, segment->len); + ptr += segment->len; + } + + if (segment->xmit >= DEADLINK) + kcp->state = -1; + } + } + } + else + { + for (p = kcp->snd_buf.next; p != &kcp->snd_buf; p = p->next) + { + var segment = iqueue_entry(p); + var needsend = 0; + if (segment->xmit == 0) + { + needsend = 1; + segment->xmit++; + segment->rto = (uint)kcp->rx_rto; + segment->resendts = current + segment->rto; + } + else if (_itimediff(current, segment->resendts) >= 0) + { + needsend = 1; + segment->xmit++; + kcp->xmit++; + var step = (int)segment->rto; + segment->rto += (uint)(step / 2); + segment->resendts = current + segment->rto; + lost = 1; + } + else if (segment->fastack >= resent) + { + if ((int)segment->xmit <= kcp->fastlimit || kcp->fastlimit == 0) + { + needsend = 1; + segment->xmit++; + segment->fastack = 0; + segment->resendts = current + segment->rto; + change++; + } + } + + if (needsend != 0) + { + segment->ts = current; + segment->wnd = seg.wnd; + segment->una = kcp->rcv_nxt; + size = (int)(ptr - buffer); + var need = (int)(OVERHEAD + segment->len); + if (size + need > (int)kcp->mtu) + { + ikcp_output(output, bytes, size); + ptr = buffer; + } + + ptr = ikcp_encode_seg(ptr, segment); + if (segment->len > 0) + { + memcpy(ptr, segment->data, segment->len); + ptr += segment->len; + } + + if (segment->xmit >= DEADLINK) + kcp->state = -1; + } + } + } + + size = (int)(ptr - buffer); + if (size > 0) + ikcp_output(output, bytes, size); + if (change != 0) + { + var inflight = kcp->snd_nxt - kcp->snd_una; + kcp->ssthresh = inflight / 2; + if (kcp->ssthresh < THRESH_MIN) + kcp->ssthresh = THRESH_MIN; + kcp->cwnd = kcp->ssthresh + resent; + kcp->incr = kcp->cwnd * kcp->mss; + } + + if (lost != 0) + { + kcp->ssthresh = cwnd / 2; + if (kcp->ssthresh < THRESH_MIN) + kcp->ssthresh = THRESH_MIN; + kcp->cwnd = 1; + kcp->incr = kcp->mss; + } + + if (kcp->cwnd < 1) + { + kcp->cwnd = 1; + kcp->incr = kcp->mss; + } + } + } + + public static void ikcp_update(IKCPCB* kcp, uint current, KcpCallback output, byte[] bytes) + { + kcp->current = current; + if (kcp->updated == 0) + { + kcp->updated = 1; + kcp->ts_flush = kcp->current; + } + + var slap = _itimediff(kcp->current, kcp->ts_flush); + if (slap >= 10000 || slap < -10000) + { + kcp->ts_flush = kcp->current; + slap = 0; + } + + if (slap >= 0) + { + kcp->ts_flush += kcp->interval; + if (_itimediff(kcp->current, kcp->ts_flush) >= 0) + kcp->ts_flush = kcp->current + kcp->interval; + ikcp_flush_internal(kcp, output, bytes); + } + } + + public static uint ikcp_check(IKCPCB* kcp, uint current) + { + if (kcp->updated == 0) + return current; + var ts_flush = kcp->ts_flush; + if (_itimediff(current, ts_flush) >= 10000 || _itimediff(current, ts_flush) < -10000) + ts_flush = current; + if (_itimediff(current, ts_flush) >= 0) + return current; + var tm_packet = 2147483647; + var tm_flush = _itimediff(ts_flush, current); + IQUEUEHEAD* p; + for (p = kcp->snd_buf.next; p != &kcp->snd_buf; p = p->next) + { + var seg = iqueue_entry(p); + var diff = _itimediff(seg->resendts, current); + if (diff <= 0) + return current; + if (diff < tm_packet) + tm_packet = diff; + } + + var minimal = (uint)(tm_packet < tm_flush ? tm_packet : tm_flush); + if (minimal >= kcp->interval) + minimal = kcp->interval; + return current + minimal; + } + + public static int ikcp_setmtu(IKCPCB* kcp, int mtu, ref byte[] buffer) + { + if (kcp->mtu == (uint)mtu) + return 0; + if (mtu < (int)OVERHEAD) + return -1; + buffer = new byte[REVERSED_HEAD + (mtu + OVERHEAD) * 3]; + kcp->mtu = (uint)mtu; + kcp->mss = kcp->mtu - OVERHEAD; + return 0; + } + + public static void ikcp_interval(IKCPCB* kcp, int interval) + { + interval = _iclamp_(interval, INTERVAL_MIN, INTERVAL_LIMIT); + kcp->interval = (uint)interval; + } + + public static void ikcp_nodelay(IKCPCB* kcp, int nodelay, int interval, int resend, int nc) + { + nodelay = _iclamp_(nodelay, NODELAY_MIN, NODELAY_LIMIT); + kcp->nodelay = (uint)nodelay; + if (nodelay != 0) + kcp->rx_minrto = (int)RTO_NDL; + else + kcp->rx_minrto = (int)RTO_MIN; + interval = _iclamp_(interval, INTERVAL_MIN, INTERVAL_LIMIT); + kcp->interval = (uint)interval; + resend = _iclamp_(resend, 0, 4294967295); + kcp->fastresend = resend; + kcp->nocwnd = nc == 1 ? 1 : 0; + } + + public static void ikcp_wndsize(IKCPCB* kcp, int sndwnd, int rcvwnd) + { + sndwnd = _iclamp_(sndwnd, WND_SND, 2147483647); + rcvwnd = _iclamp_(rcvwnd, WND_RCV, 2147483647); + kcp->snd_wnd = (uint)sndwnd; + kcp->rcv_wnd = (uint)rcvwnd; + } + + public static void ikcp_fastresendlimit(IKCPCB* kcp, int fastlimit) + { + fastlimit = _iclamp_(fastlimit, FASTACK_MIN, FASTACK_LIMIT); + kcp->fastlimit = fastlimit; + } + + public static void ikcp_streammode(IKCPCB* kcp, int stream) => kcp->stream = stream == 1 ? 1 : 0; + + public static void ikcp_minrto(IKCPCB* kcp, int minrto) + { + minrto = _iclamp_(minrto, INTERVAL_MIN, RTO_MAX); + kcp->rx_minrto = minrto; + } + } +} diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/kcp/ikcpc.cs.meta b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/kcp/ikcpc.cs.meta new file mode 100644 index 0000000000..e0fc101666 --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/kcp/ikcpc.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5b79dd366bf83a64c84e27a4ac3b9f81 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/kcp/ikcph.cs b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/kcp/ikcph.cs new file mode 100644 index 0000000000..29dc20f08f --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/kcp/ikcph.cs @@ -0,0 +1,159 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +#pragma warning disable CS1591 +#pragma warning disable CS8981 + +// ReSharper disable IdentifierTypo +// ReSharper disable InconsistentNaming + +namespace KCP +{ + public delegate void KcpCallback(byte[] buffer, int length); + + internal unsafe struct IQUEUEHEAD + { + public IQUEUEHEAD* next; + public IQUEUEHEAD* prev; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void iqueue_init(IQUEUEHEAD* ptr) + { + ptr->next = ptr; + ptr->prev = ptr; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IKCPSEG* iqueue_entry(IQUEUEHEAD* ptr) => (IKCPSEG*)(byte*)(IKCPSEG*)ptr; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool iqueue_is_empty(IQUEUEHEAD* entry) => entry == entry->next; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void iqueue_del(IQUEUEHEAD* entry) + { + entry->next->prev = entry->prev; + entry->prev->next = entry->next; + entry->next = null; + entry->prev = null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void iqueue_del_init(IQUEUEHEAD* entry) + { + iqueue_del(entry); + iqueue_init(entry); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void iqueue_add(IQUEUEHEAD* node, IQUEUEHEAD* head) + { + node->prev = head; + node->next = head->next; + head->next->prev = node; + head->next = node; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void iqueue_add_tail(IQUEUEHEAD* node, IQUEUEHEAD* head) + { + node->prev = head->prev; + node->next = head; + head->prev->next = node; + head->prev = node; + } + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct IKCPSEG + { + public IQUEUEHEAD node; + public uint conv; + public uint cmd; + public uint frg; + public uint wnd; + public uint ts; + public uint sn; + public uint una; + public uint len; + public uint resendts; + public uint rto; + public uint fastack; + public uint xmit; + public fixed byte data[1]; + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct IKCPCB + { + public uint conv, mtu, mss; + public int state; + public uint snd_una, snd_nxt, rcv_nxt; + public uint ssthresh; + public int rx_rttval, rx_srtt, rx_rto, rx_minrto; + public uint snd_wnd, rcv_wnd, rmt_wnd, cwnd, probe; + public uint current, interval, ts_flush, xmit; + public uint nrcv_buf, nsnd_buf; + public uint nrcv_que, nsnd_que; + public uint nodelay, updated; + public uint ts_probe, probe_wait; + public uint incr; + public IQUEUEHEAD snd_queue; + public IQUEUEHEAD rcv_queue; + public IQUEUEHEAD snd_buf; + public IQUEUEHEAD rcv_buf; + public uint* acklist; + public uint ackcount; + public uint ackblock; + public int fastresend; + public int fastlimit; + public int nocwnd, stream; + } + + public static class KCPBASIC + { + public const uint RTO_NDL = 30; + public const uint RTO_MIN = 100; + public const uint RTO_DEF = 200; + public const uint RTO_MAX = 60000; + public const uint CMD_PUSH = 81; + public const uint CMD_ACK = 82; + public const uint CMD_WASK = 83; + public const uint CMD_WINS = 84; + public const uint ASK_SEND = 1; + public const uint ASK_TELL = 2; + public const uint WND_SND = 32; + public const uint WND_RCV = 128; + public const uint MTU_DEF = 1400; + public const uint ACK_FAST = 3; + public const uint INTERVAL = 100; + public const uint INTERVAL_MIN = 1; + public const uint INTERVAL_LIMIT = 5000; + public const uint OVERHEAD = 24; + public const uint DEADLINK = 20; + public const uint THRESH_INIT = 2; + public const uint THRESH_MIN = 2; + public const uint PROBE_INIT = 7000; + public const uint PROBE_LIMIT = 120000; + public const uint FRG_LIMIT = 255; + public const uint NODELAY_MIN = 0; + public const uint NODELAY_LIMIT = 2; + public const uint FASTACK_MIN = 0; + public const uint FASTACK_LIMIT = 5; + public const uint OUTPUT = 1; + public const uint INPUT = 2; + public const uint SEND = 4; + public const uint RECV = 8; + public const uint IN_DATA = 16; + public const uint IN_ACK = 32; + public const uint IN_PROBE = 64; + public const uint IN_WINS = 128; + public const uint OUT_DATA = 256; + public const uint OUT_ACK = 512; + public const uint OUT_PROBE = 1024; + public const uint OUT_WINS = 2048; + + // TODO: remove it if not needed + public const uint REVERSED_HEAD = 5; + } +} diff --git a/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/kcp/ikcph.cs.meta b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/kcp/ikcph.cs.meta new file mode 100644 index 0000000000..1d2aab3e7e --- /dev/null +++ b/Assets/Mirror/Transports/KCP-unmanaged/kcp2k/kcp/ikcph.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e28d0140679abfc44b39edafa0a64021 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: