From 9b690037159741beebc37cb788ff22526206ea12 Mon Sep 17 00:00:00 2001 From: Texel Date: Tue, 24 Jan 2023 04:03:23 -0500 Subject: [PATCH] Initial commit of Unity files, Network Library, Project settings (Compiler flags for networking to work on Unity), Unity Version 2019.4 LTS --- .vsconfig | 6 + Assets/Runtime.meta | 8 + Assets/Runtime/Drawers.meta | 8 + .../Drawers/ArrayElementTitleAttribute.cs | 15 + .../ArrayElementTitleAttribute.cs.meta | 11 + Assets/Runtime/Drawers/Editor.meta | 8 + .../Drawers/Editor/ArrayElementTitleDrawer.cs | 71 + .../Editor/ArrayElementTitleDrawer.cs.meta | 11 + .../Runtime/Drawers/Editor/EnumFlagDrawer.cs | 35 + .../Drawers/Editor/EnumFlagDrawer.cs.meta | 11 + .../Runtime/Drawers/Editor/ReadOnlyDrawer.cs | 14 + .../Drawers/Editor/ReadOnlyDrawer.cs.meta | 11 + Assets/Runtime/Drawers/EnumFlagAttribute.cs | 11 + .../Runtime/Drawers/EnumFlagAttribute.cs.meta | 11 + Assets/Runtime/Drawers/ReadOnlyAttribute.cs | 5 + .../Runtime/Drawers/ReadOnlyAttribute.cs.meta | 11 + Assets/Runtime/Extensions.meta | 8 + Assets/Runtime/Extensions/DoubleDictionary.cs | 55 + .../Extensions/DoubleDictionary.cs.meta | 11 + Assets/Runtime/Extensions/StringExtension.cs | 21 + .../Extensions/StringExtension.cs.meta | 11 + Assets/Runtime/Networking.meta | 8 + Assets/Runtime/Networking/Entity.meta | 8 + .../Runtime/Networking/Entity/EntityBase.cs | 797 +++++ .../Networking/Entity/EntityBase.cs.meta | 11 + .../Networking/Entity/EntityManager.cs | 476 +++ .../Networking/Entity/EntityManager.cs.meta | 11 + Assets/Runtime/Networking/Helpers.meta | 8 + .../Networking/Helpers/HashtableExtension.cs | 52 + .../Helpers/HashtableExtension.cs.meta | 11 + .../Networking/Helpers/PhotonConstants.cs | 34 + .../Helpers/PhotonConstants.cs.meta | 11 + .../Networking/Helpers/StreamCustomTypes.cs | 139 + .../Helpers/StreamCustomTypes.cs.meta | 11 + Assets/Runtime/Networking/NetProperties.meta | 8 + .../NetProperties/PlayerProperties.cs | 282 ++ .../NetProperties/PlayerProperties.cs.meta | 11 + .../NetProperties/RoomProperties.cs | 130 + .../NetProperties/RoomProperties.cs.meta | 11 + Assets/Runtime/Networking/NetworkManager.cs | 298 ++ .../Runtime/Networking/NetworkManager.cs.meta | 11 + .../Runtime/Networking/NetworkManager.prefab | 336 +++ .../Networking/NetworkManager.prefab.meta | 7 + .../Runtime/Networking/NetworkManagerDebug.cs | 17 + .../Networking/NetworkManagerDebug.cs.meta | 11 + .../Networking/NetworkManagerDummy.prefab | 284 ++ .../NetworkManagerDummy.prefab.meta | 7 + Assets/Runtime/Networking/QuickJoin.cs | 63 + Assets/Runtime/Networking/QuickJoin.cs.meta | 11 + Assets/Runtime/Networking/RoomCode.prefab | 351 +++ .../Runtime/Networking/RoomCode.prefab.meta | 7 + Assets/Runtime/Networking/RoomCodeDisplay.cs | 19 + .../Networking/RoomCodeDisplay.cs.meta | 11 + Assets/Runtime/Photon.meta | 8 + .../Photon/PhotonLoadbalancingApi.meta | 8 + .../PhotonLoadbalancingApi/Extensions.cs | 180 ++ .../PhotonLoadbalancingApi/Extensions.cs.meta | 11 + .../PhotonLoadbalancingApi/FriendInfo.cs | 45 + .../PhotonLoadbalancingApi/FriendInfo.cs.meta | 11 + .../LoadBalancingClient.cs | 2582 +++++++++++++++++ .../LoadBalancingClient.cs.meta | 11 + .../LoadBalancingPeer.cs | 1893 ++++++++++++ .../LoadBalancingPeer.cs.meta | 11 + .../Photon/PhotonLoadbalancingApi/Player.cs | 430 +++ .../PhotonLoadbalancingApi/Player.cs.meta | 11 + .../Photon/PhotonLoadbalancingApi/Room.cs | 474 +++ .../PhotonLoadbalancingApi/Room.cs.meta | 11 + .../Photon/PhotonLoadbalancingApi/RoomInfo.cs | 261 ++ .../PhotonLoadbalancingApi/RoomInfo.cs.meta | 11 + .../Photon/PhotonLoadbalancingApi/WebRpc.cs | 167 ++ .../PhotonLoadbalancingApi/WebRpc.cs.meta | 11 + Assets/Runtime/Photon/Plugins.meta | 8 + .../Runtime/Photon/Plugins/Photon3Unity3D.dll | Bin 0 -> 151040 bytes .../Photon/Plugins/Photon3Unity3D.dll.meta | 113 + .../Runtime/Photon/Plugins/Photon3Unity3D.xml | 2109 ++++++++++++++ .../Photon/Plugins/Photon3Unity3D.xml.meta | 7 + .../Photon/Plugins/PhotonWebSocket.meta | 8 + .../Plugins/PhotonWebSocket/PingHttp.cs | 37 + .../Plugins/PhotonWebSocket/PingHttp.cs.meta | 11 + .../Readme-Photon-WebSocket.txt | 22 + .../Readme-Photon-WebSocket.txt.meta | 7 + .../PhotonWebSocket/SocketWebTcpCoroutine.cs | 312 ++ .../SocketWebTcpCoroutine.cs.meta | 11 + .../PhotonWebSocket/SocketWebTcpThread.cs | 268 ++ .../SocketWebTcpThread.cs.meta | 11 + .../Plugins/PhotonWebSocket/WebSocket.meta | 8 + .../PhotonWebSocket/WebSocket/WebSocket.cs | 155 + .../WebSocket/WebSocket.cs.meta | 11 + .../PhotonWebSocket/WebSocket/WebSocket.jslib | 116 + .../WebSocket/WebSocket.jslib.meta | 34 + .../WebSocket/websocket-sharp.README | 3 + .../WebSocket/websocket-sharp.README.meta | 7 + .../WebSocket/websocket-sharp.dll | Bin 0 -> 244736 bytes .../WebSocket/websocket-sharp.dll.meta | 30 + Assets/Runtime/Photon/Plugins/Wsa.meta | 8 + .../Photon/Plugins/Wsa/Photon3Unity3D.dll | Bin 0 -> 112640 bytes .../Plugins/Wsa/Photon3Unity3D.dll.meta | 114 + Packages/manifest.json | 49 + Packages/packages-lock.json | 404 +++ ProjectSettings/AudioManager.asset | 19 + ProjectSettings/ClusterInputManager.asset | 6 + ProjectSettings/DynamicsManager.asset | 34 + ProjectSettings/EditorBuildSettings.asset | 8 + ProjectSettings/EditorSettings.asset | 35 + ProjectSettings/GraphicsSettings.asset | 57 + ProjectSettings/InputManager.asset | 295 ++ ProjectSettings/NavMeshAreas.asset | 91 + ProjectSettings/NetworkManager.asset | 8 + ProjectSettings/PackageManagerSettings.asset | 38 + ProjectSettings/Physics2DSettings.asset | 56 + ProjectSettings/PresetManager.asset | 7 + ProjectSettings/ProjectSettings.asset | 684 +++++ ProjectSettings/ProjectVersion.txt | 2 + ProjectSettings/QualitySettings.asset | 192 ++ ProjectSettings/TagManager.asset | 43 + ProjectSettings/TimeManager.asset | 9 + ProjectSettings/UnityConnectSettings.asset | 34 + ProjectSettings/VFXManager.asset | 12 + ProjectSettings/XRSettings.asset | 10 + 119 files changed, 15444 insertions(+) create mode 100644 .vsconfig create mode 100644 Assets/Runtime.meta create mode 100644 Assets/Runtime/Drawers.meta create mode 100644 Assets/Runtime/Drawers/ArrayElementTitleAttribute.cs create mode 100644 Assets/Runtime/Drawers/ArrayElementTitleAttribute.cs.meta create mode 100644 Assets/Runtime/Drawers/Editor.meta create mode 100644 Assets/Runtime/Drawers/Editor/ArrayElementTitleDrawer.cs create mode 100644 Assets/Runtime/Drawers/Editor/ArrayElementTitleDrawer.cs.meta create mode 100644 Assets/Runtime/Drawers/Editor/EnumFlagDrawer.cs create mode 100644 Assets/Runtime/Drawers/Editor/EnumFlagDrawer.cs.meta create mode 100644 Assets/Runtime/Drawers/Editor/ReadOnlyDrawer.cs create mode 100644 Assets/Runtime/Drawers/Editor/ReadOnlyDrawer.cs.meta create mode 100644 Assets/Runtime/Drawers/EnumFlagAttribute.cs create mode 100644 Assets/Runtime/Drawers/EnumFlagAttribute.cs.meta create mode 100644 Assets/Runtime/Drawers/ReadOnlyAttribute.cs create mode 100644 Assets/Runtime/Drawers/ReadOnlyAttribute.cs.meta create mode 100644 Assets/Runtime/Extensions.meta create mode 100644 Assets/Runtime/Extensions/DoubleDictionary.cs create mode 100644 Assets/Runtime/Extensions/DoubleDictionary.cs.meta create mode 100644 Assets/Runtime/Extensions/StringExtension.cs create mode 100644 Assets/Runtime/Extensions/StringExtension.cs.meta create mode 100644 Assets/Runtime/Networking.meta create mode 100644 Assets/Runtime/Networking/Entity.meta create mode 100644 Assets/Runtime/Networking/Entity/EntityBase.cs create mode 100644 Assets/Runtime/Networking/Entity/EntityBase.cs.meta create mode 100644 Assets/Runtime/Networking/Entity/EntityManager.cs create mode 100644 Assets/Runtime/Networking/Entity/EntityManager.cs.meta create mode 100644 Assets/Runtime/Networking/Helpers.meta create mode 100644 Assets/Runtime/Networking/Helpers/HashtableExtension.cs create mode 100644 Assets/Runtime/Networking/Helpers/HashtableExtension.cs.meta create mode 100644 Assets/Runtime/Networking/Helpers/PhotonConstants.cs create mode 100644 Assets/Runtime/Networking/Helpers/PhotonConstants.cs.meta create mode 100644 Assets/Runtime/Networking/Helpers/StreamCustomTypes.cs create mode 100644 Assets/Runtime/Networking/Helpers/StreamCustomTypes.cs.meta create mode 100644 Assets/Runtime/Networking/NetProperties.meta create mode 100644 Assets/Runtime/Networking/NetProperties/PlayerProperties.cs create mode 100644 Assets/Runtime/Networking/NetProperties/PlayerProperties.cs.meta create mode 100644 Assets/Runtime/Networking/NetProperties/RoomProperties.cs create mode 100644 Assets/Runtime/Networking/NetProperties/RoomProperties.cs.meta create mode 100644 Assets/Runtime/Networking/NetworkManager.cs create mode 100644 Assets/Runtime/Networking/NetworkManager.cs.meta create mode 100644 Assets/Runtime/Networking/NetworkManager.prefab create mode 100644 Assets/Runtime/Networking/NetworkManager.prefab.meta create mode 100644 Assets/Runtime/Networking/NetworkManagerDebug.cs create mode 100644 Assets/Runtime/Networking/NetworkManagerDebug.cs.meta create mode 100644 Assets/Runtime/Networking/NetworkManagerDummy.prefab create mode 100644 Assets/Runtime/Networking/NetworkManagerDummy.prefab.meta create mode 100644 Assets/Runtime/Networking/QuickJoin.cs create mode 100644 Assets/Runtime/Networking/QuickJoin.cs.meta create mode 100644 Assets/Runtime/Networking/RoomCode.prefab create mode 100644 Assets/Runtime/Networking/RoomCode.prefab.meta create mode 100644 Assets/Runtime/Networking/RoomCodeDisplay.cs create mode 100644 Assets/Runtime/Networking/RoomCodeDisplay.cs.meta create mode 100644 Assets/Runtime/Photon.meta create mode 100644 Assets/Runtime/Photon/PhotonLoadbalancingApi.meta create mode 100644 Assets/Runtime/Photon/PhotonLoadbalancingApi/Extensions.cs create mode 100644 Assets/Runtime/Photon/PhotonLoadbalancingApi/Extensions.cs.meta create mode 100644 Assets/Runtime/Photon/PhotonLoadbalancingApi/FriendInfo.cs create mode 100644 Assets/Runtime/Photon/PhotonLoadbalancingApi/FriendInfo.cs.meta create mode 100644 Assets/Runtime/Photon/PhotonLoadbalancingApi/LoadBalancingClient.cs create mode 100644 Assets/Runtime/Photon/PhotonLoadbalancingApi/LoadBalancingClient.cs.meta create mode 100644 Assets/Runtime/Photon/PhotonLoadbalancingApi/LoadBalancingPeer.cs create mode 100644 Assets/Runtime/Photon/PhotonLoadbalancingApi/LoadBalancingPeer.cs.meta create mode 100644 Assets/Runtime/Photon/PhotonLoadbalancingApi/Player.cs create mode 100644 Assets/Runtime/Photon/PhotonLoadbalancingApi/Player.cs.meta create mode 100644 Assets/Runtime/Photon/PhotonLoadbalancingApi/Room.cs create mode 100644 Assets/Runtime/Photon/PhotonLoadbalancingApi/Room.cs.meta create mode 100644 Assets/Runtime/Photon/PhotonLoadbalancingApi/RoomInfo.cs create mode 100644 Assets/Runtime/Photon/PhotonLoadbalancingApi/RoomInfo.cs.meta create mode 100644 Assets/Runtime/Photon/PhotonLoadbalancingApi/WebRpc.cs create mode 100644 Assets/Runtime/Photon/PhotonLoadbalancingApi/WebRpc.cs.meta create mode 100644 Assets/Runtime/Photon/Plugins.meta create mode 100644 Assets/Runtime/Photon/Plugins/Photon3Unity3D.dll create mode 100644 Assets/Runtime/Photon/Plugins/Photon3Unity3D.dll.meta create mode 100644 Assets/Runtime/Photon/Plugins/Photon3Unity3D.xml create mode 100644 Assets/Runtime/Photon/Plugins/Photon3Unity3D.xml.meta create mode 100644 Assets/Runtime/Photon/Plugins/PhotonWebSocket.meta create mode 100644 Assets/Runtime/Photon/Plugins/PhotonWebSocket/PingHttp.cs create mode 100644 Assets/Runtime/Photon/Plugins/PhotonWebSocket/PingHttp.cs.meta create mode 100644 Assets/Runtime/Photon/Plugins/PhotonWebSocket/Readme-Photon-WebSocket.txt create mode 100644 Assets/Runtime/Photon/Plugins/PhotonWebSocket/Readme-Photon-WebSocket.txt.meta create mode 100644 Assets/Runtime/Photon/Plugins/PhotonWebSocket/SocketWebTcpCoroutine.cs create mode 100644 Assets/Runtime/Photon/Plugins/PhotonWebSocket/SocketWebTcpCoroutine.cs.meta create mode 100644 Assets/Runtime/Photon/Plugins/PhotonWebSocket/SocketWebTcpThread.cs create mode 100644 Assets/Runtime/Photon/Plugins/PhotonWebSocket/SocketWebTcpThread.cs.meta create mode 100644 Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket.meta create mode 100644 Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket/WebSocket.cs create mode 100644 Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket/WebSocket.cs.meta create mode 100644 Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket/WebSocket.jslib create mode 100644 Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket/WebSocket.jslib.meta create mode 100644 Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket/websocket-sharp.README create mode 100644 Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket/websocket-sharp.README.meta create mode 100644 Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket/websocket-sharp.dll create mode 100644 Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket/websocket-sharp.dll.meta create mode 100644 Assets/Runtime/Photon/Plugins/Wsa.meta create mode 100644 Assets/Runtime/Photon/Plugins/Wsa/Photon3Unity3D.dll create mode 100644 Assets/Runtime/Photon/Plugins/Wsa/Photon3Unity3D.dll.meta create mode 100644 Packages/manifest.json create mode 100644 Packages/packages-lock.json create mode 100644 ProjectSettings/AudioManager.asset create mode 100644 ProjectSettings/ClusterInputManager.asset create mode 100644 ProjectSettings/DynamicsManager.asset create mode 100644 ProjectSettings/EditorBuildSettings.asset create mode 100644 ProjectSettings/EditorSettings.asset create mode 100644 ProjectSettings/GraphicsSettings.asset create mode 100644 ProjectSettings/InputManager.asset create mode 100644 ProjectSettings/NavMeshAreas.asset create mode 100644 ProjectSettings/NetworkManager.asset create mode 100644 ProjectSettings/PackageManagerSettings.asset create mode 100644 ProjectSettings/Physics2DSettings.asset create mode 100644 ProjectSettings/PresetManager.asset create mode 100644 ProjectSettings/ProjectSettings.asset create mode 100644 ProjectSettings/ProjectVersion.txt create mode 100644 ProjectSettings/QualitySettings.asset create mode 100644 ProjectSettings/TagManager.asset create mode 100644 ProjectSettings/TimeManager.asset create mode 100644 ProjectSettings/UnityConnectSettings.asset create mode 100644 ProjectSettings/VFXManager.asset create mode 100644 ProjectSettings/XRSettings.asset diff --git a/.vsconfig b/.vsconfig new file mode 100644 index 0000000..d70cd98 --- /dev/null +++ b/.vsconfig @@ -0,0 +1,6 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.VisualStudio.Workload.ManagedGame" + ] +} diff --git a/Assets/Runtime.meta b/Assets/Runtime.meta new file mode 100644 index 0000000..ddb9dc5 --- /dev/null +++ b/Assets/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0763d456e1ae157498c954ce5f728954 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Drawers.meta b/Assets/Runtime/Drawers.meta new file mode 100644 index 0000000..e624c75 --- /dev/null +++ b/Assets/Runtime/Drawers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c948a1a65858f7947baef36c7dc71069 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Drawers/ArrayElementTitleAttribute.cs b/Assets/Runtime/Drawers/ArrayElementTitleAttribute.cs new file mode 100644 index 0000000..b688bcc --- /dev/null +++ b/Assets/Runtime/Drawers/ArrayElementTitleAttribute.cs @@ -0,0 +1,15 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +public class ArrayElementTitleAttribute : PropertyAttribute { + public string fieldName; + + public ArrayElementTitleAttribute() { + this.fieldName = "name"; + } + + public ArrayElementTitleAttribute(string fieldName) { + this.fieldName = fieldName; + } +} diff --git a/Assets/Runtime/Drawers/ArrayElementTitleAttribute.cs.meta b/Assets/Runtime/Drawers/ArrayElementTitleAttribute.cs.meta new file mode 100644 index 0000000..6ae356f --- /dev/null +++ b/Assets/Runtime/Drawers/ArrayElementTitleAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e8550d86f434d784e98b16c3530703d8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Drawers/Editor.meta b/Assets/Runtime/Drawers/Editor.meta new file mode 100644 index 0000000..1280787 --- /dev/null +++ b/Assets/Runtime/Drawers/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a0346cc8c59a10f4a966240dd9e440b3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Drawers/Editor/ArrayElementTitleDrawer.cs b/Assets/Runtime/Drawers/Editor/ArrayElementTitleDrawer.cs new file mode 100644 index 0000000..3909a76 --- /dev/null +++ b/Assets/Runtime/Drawers/Editor/ArrayElementTitleDrawer.cs @@ -0,0 +1,71 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; + +[CustomPropertyDrawer(typeof(ArrayElementTitleAttribute))] +public class ArrayElementTitleDrawer : PropertyDrawer { + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { + return EditorGUI.GetPropertyHeight(property, label, true); + } + + protected virtual ArrayElementTitleAttribute titleAttribute => (ArrayElementTitleAttribute)attribute; + + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { + string fullpath = property.propertyPath + "." + titleAttribute.fieldName; + var prop = property.serializedObject.FindProperty(fullpath); + string newlabel = GetTitle(prop); + if (string.IsNullOrEmpty(newlabel)) + newlabel = label.text; + + EditorGUI.PropertyField(position, property, new GUIContent(newlabel, label.tooltip), true); + } + + + private string GetTitle(SerializedProperty prop) { + switch (prop.propertyType) { + case SerializedPropertyType.Generic: + break; + case SerializedPropertyType.Integer: + return prop.intValue.ToString(); + case SerializedPropertyType.Boolean: + return prop.boolValue.ToString(); + case SerializedPropertyType.Float: + return prop.floatValue.ToString(); + case SerializedPropertyType.String: + return prop.stringValue; + case SerializedPropertyType.Color: + return prop.colorValue.ToString(); + case SerializedPropertyType.ObjectReference: + return prop.objectReferenceValue.ToString(); + case SerializedPropertyType.LayerMask: + break; + case SerializedPropertyType.Enum: + return prop.enumNames[prop.enumValueIndex]; + case SerializedPropertyType.Vector2: + return prop.vector2Value.ToString(); + case SerializedPropertyType.Vector3: + return prop.vector3Value.ToString(); + case SerializedPropertyType.Vector4: + return prop.vector4Value.ToString(); + case SerializedPropertyType.Rect: + break; + case SerializedPropertyType.ArraySize: + break; + case SerializedPropertyType.Character: + break; + case SerializedPropertyType.AnimationCurve: + break; + case SerializedPropertyType.Bounds: + break; + case SerializedPropertyType.Gradient: + break; + case SerializedPropertyType.Quaternion: + break; + default: + break; + } + return ""; + } +} \ No newline at end of file diff --git a/Assets/Runtime/Drawers/Editor/ArrayElementTitleDrawer.cs.meta b/Assets/Runtime/Drawers/Editor/ArrayElementTitleDrawer.cs.meta new file mode 100644 index 0000000..5f177ba --- /dev/null +++ b/Assets/Runtime/Drawers/Editor/ArrayElementTitleDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e22b2b5dabe661b4498b020007dbb838 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Drawers/Editor/EnumFlagDrawer.cs b/Assets/Runtime/Drawers/Editor/EnumFlagDrawer.cs new file mode 100644 index 0000000..87b8321 --- /dev/null +++ b/Assets/Runtime/Drawers/Editor/EnumFlagDrawer.cs @@ -0,0 +1,35 @@ +using System; +using System.Reflection; +using UnityEditor; +using UnityEngine; + +[CustomPropertyDrawer(typeof(EnumFlagAttribute))] +public class EnumFlagDrawer : PropertyDrawer { + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { + EnumFlagAttribute flagSettings = (EnumFlagAttribute)attribute; + Enum targetEnum = GetBaseProperty(property); + + string propName = flagSettings.enumName; + if (string.IsNullOrEmpty(propName)) + propName = property.displayName; + + EditorGUI.BeginProperty(position, label, property); + Enum enumNew = EditorGUI.EnumFlagsField(position, propName, targetEnum); + property.intValue = (int)Convert.ChangeType(enumNew, targetEnum.GetType()); + EditorGUI.EndProperty(); + } + + static T GetBaseProperty(SerializedProperty prop) { + // Separate the steps it takes to get to this property + string[] separatedPaths = prop.propertyPath.Split('.'); + + // Go down to the root of this serialized property + System.Object reflectionTarget = prop.serializedObject.targetObject as object; + // Walk down the path to get the target object + foreach (var path in separatedPaths) { + FieldInfo fieldInfo = reflectionTarget.GetType().GetField(path); + reflectionTarget = fieldInfo.GetValue(reflectionTarget); + } + return (T)reflectionTarget; + } +} \ No newline at end of file diff --git a/Assets/Runtime/Drawers/Editor/EnumFlagDrawer.cs.meta b/Assets/Runtime/Drawers/Editor/EnumFlagDrawer.cs.meta new file mode 100644 index 0000000..146c94c --- /dev/null +++ b/Assets/Runtime/Drawers/Editor/EnumFlagDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bde9bcb8cf46e7e4090257fbff464d17 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Drawers/Editor/ReadOnlyDrawer.cs b/Assets/Runtime/Drawers/Editor/ReadOnlyDrawer.cs new file mode 100644 index 0000000..83f2266 --- /dev/null +++ b/Assets/Runtime/Drawers/Editor/ReadOnlyDrawer.cs @@ -0,0 +1,14 @@ +using System; +using System.Reflection; +using UnityEditor; +using UnityEngine; + +[CustomPropertyDrawer(typeof(ReadOnlyAttribute))] +public class ReadOnlyDrawer : PropertyDrawer { + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { + GUI.enabled = false; + EditorGUI.PropertyField(position, property, label, true); + GUI.enabled = true; + } + +} \ No newline at end of file diff --git a/Assets/Runtime/Drawers/Editor/ReadOnlyDrawer.cs.meta b/Assets/Runtime/Drawers/Editor/ReadOnlyDrawer.cs.meta new file mode 100644 index 0000000..49f9ca5 --- /dev/null +++ b/Assets/Runtime/Drawers/Editor/ReadOnlyDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e772dac1e610df54fbbf25f1073012dc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Drawers/EnumFlagAttribute.cs b/Assets/Runtime/Drawers/EnumFlagAttribute.cs new file mode 100644 index 0000000..ff60765 --- /dev/null +++ b/Assets/Runtime/Drawers/EnumFlagAttribute.cs @@ -0,0 +1,11 @@ +using UnityEngine; + +public class EnumFlagAttribute : PropertyAttribute { + public string enumName; + + public EnumFlagAttribute() { } + + public EnumFlagAttribute(string name) { + enumName = name; + } +} \ No newline at end of file diff --git a/Assets/Runtime/Drawers/EnumFlagAttribute.cs.meta b/Assets/Runtime/Drawers/EnumFlagAttribute.cs.meta new file mode 100644 index 0000000..cd7d513 --- /dev/null +++ b/Assets/Runtime/Drawers/EnumFlagAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6c03bbb4c7afaf746ab43c4fbeb3ae9e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Drawers/ReadOnlyAttribute.cs b/Assets/Runtime/Drawers/ReadOnlyAttribute.cs new file mode 100644 index 0000000..b1a2340 --- /dev/null +++ b/Assets/Runtime/Drawers/ReadOnlyAttribute.cs @@ -0,0 +1,5 @@ +using UnityEngine; + +public class ReadOnlyAttribute : PropertyAttribute { + public ReadOnlyAttribute() { } +} \ No newline at end of file diff --git a/Assets/Runtime/Drawers/ReadOnlyAttribute.cs.meta b/Assets/Runtime/Drawers/ReadOnlyAttribute.cs.meta new file mode 100644 index 0000000..e096cda --- /dev/null +++ b/Assets/Runtime/Drawers/ReadOnlyAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2175955395688374191c019f19c355be +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Extensions.meta b/Assets/Runtime/Extensions.meta new file mode 100644 index 0000000..e71f354 --- /dev/null +++ b/Assets/Runtime/Extensions.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9bbd61e2d44aca241a19686e7dbb6203 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Extensions/DoubleDictionary.cs b/Assets/Runtime/Extensions/DoubleDictionary.cs new file mode 100644 index 0000000..f75a639 --- /dev/null +++ b/Assets/Runtime/Extensions/DoubleDictionary.cs @@ -0,0 +1,55 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +public static class DoubleDictionary{ + + static Dictionary keys; + static Dictionary values; + + static DoubleDictionary(){ + Create(); + } + + public static void Create(){ + keys = new Dictionary(); + values = new Dictionary(); + } + + public static void Set(Key key, Value value){ + keys.Add(key, value); + values.Add(value, key); + } + + public static void Remove(Key key){ + Value value; + if (keys.TryGetValue(key, out value)){ + keys.Remove(key); + values.Remove(value); + } + } + + public static void Remove(Value value){ + Key key; + if (values.TryGetValue(value, out key)){ + keys.Remove(key); + values.Remove(value); + } + } + + public static void Remove(Key key, Value value){ + keys.Remove(key); + values.Remove(value); + } + + public static Key Get(Value value){ + Key key; + return values.TryGetValue(value, out key) ? key : default; + } + + public static Value Get(Key key){ + Value value; + return keys.TryGetValue(key, out value) ? value : default; + } + +} diff --git a/Assets/Runtime/Extensions/DoubleDictionary.cs.meta b/Assets/Runtime/Extensions/DoubleDictionary.cs.meta new file mode 100644 index 0000000..0a2dab1 --- /dev/null +++ b/Assets/Runtime/Extensions/DoubleDictionary.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f87146a5f00172e4c8bde3d6fb4918df +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Extensions/StringExtension.cs b/Assets/Runtime/Extensions/StringExtension.cs new file mode 100644 index 0000000..4cdd510 --- /dev/null +++ b/Assets/Runtime/Extensions/StringExtension.cs @@ -0,0 +1,21 @@ +public static class StringExtensionMethods +{ + public static int GetStableHashCode(this string str) + { + unchecked + { + int hash1 = 5381; + int hash2 = hash1; + + for(int i = 0; i < str.Length && str[i] != '\0'; i += 2) + { + hash1 = ((hash1 << 5) + hash1) ^ str[i]; + if (i == str.Length - 1 || str[i+1] == '\0') + break; + hash2 = ((hash2 << 5) + hash2) ^ str[i+1]; + } + + return hash1 + (hash2*1566083941); + } + } +} \ No newline at end of file diff --git a/Assets/Runtime/Extensions/StringExtension.cs.meta b/Assets/Runtime/Extensions/StringExtension.cs.meta new file mode 100644 index 0000000..e579e4e --- /dev/null +++ b/Assets/Runtime/Extensions/StringExtension.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 91365f7329a827044a590282cb8a77c2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Networking.meta b/Assets/Runtime/Networking.meta new file mode 100644 index 0000000..9e9d3bf --- /dev/null +++ b/Assets/Runtime/Networking.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0b45bb8cb359039458f36fee5d02af4a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Networking/Entity.meta b/Assets/Runtime/Networking/Entity.meta new file mode 100644 index 0000000..4bb7a59 --- /dev/null +++ b/Assets/Runtime/Networking/Entity.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 488818778f517a047ad888c9ccfb7b5a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Networking/Entity/EntityBase.cs b/Assets/Runtime/Networking/Entity/EntityBase.cs new file mode 100644 index 0000000..a2ba2b0 --- /dev/null +++ b/Assets/Runtime/Networking/Entity/EntityBase.cs @@ -0,0 +1,797 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +using System.Reflection; +using System.Linq; + +using ExitGames.Client.Photon.LoadBalancing; +using ExitGames.Client.Photon; + +using Hashtable = ExitGames.Client.Photon.Hashtable; +using PConst = PhotonConstants; + + +using Attribute = System.Attribute; +using Type = System.Type; +using Action = System.Action; + +using EntityNetwork; + +public abstract class EntityBase : MonoBehaviour { + + /// + /// Entity ID for the entity, all entities require a unique ID. + /// + //[System.NonSerialized] + public int EntityID; + + /// + /// Player ID number who is considered the authority for the object. + /// isMine / isRemote / isUnclaimed are determined by the authority holder. + /// Defaults to -1 if unassigned. + /// + //[System.NonSerialized] + public int authorityID = -1; + + #region Helpers + /// + /// A helper that determines if the object is owned locally. + /// + /// + public bool isMine { + get { + // Everything is ours when we're not connected + if (NetworkManager.net == null) return true; + + // If we're the master and have the appropriate interface, ingore the Authority ID and use the master status + if (this is IMasterOwnsUnclaimed && isUnclaimed) { + return NetworkManager.isMaster; + } + + return NetworkManager.localID == authorityID; + } + } + + /// + /// A helper to determine if the object is remote. + /// Returns false if we're disconnected + /// + /// + public bool isRemote { + get { + if (NetworkManager.net == null) return false; + + // Similar to isMine, ignore the master status if unclaimed + if (this is IMasterOwnsUnclaimed && isUnclaimed) { + return !NetworkManager.isMaster; + } + + return NetworkManager.localID != authorityID; + } + } + + /// + /// Helper to evaluate our authority ID being -1. It should be -1 if unclaimed. + /// + public bool isUnclaimed { + get { + return authorityID == -1; + } + } + + /// + /// Query to see if we're registered. This is slightly expensive. + /// + /// true if is registered; otherwise, false. + public bool isRegistered { + get { + return EntityManager.Entity(authorityID) != null; + } + } + + public void AppendIDs(Hashtable h) { + h.Add(PConst.eidChar, EntityID); + h.Add(PConst.athChar, authorityID); + } + + public void Register() { + EntityManager.Register(this); + } + + public void RaiseEvent(char c, bool includeLocal, params object[] parameters) { + var h = new Hashtable(); + + AppendIDs(h); + + h.Add(0, c); + if (parameters != null) + h.Add(1, parameters); + + NetworkManager.netMessage(PhotonConstants.EntityEventCode, h, true); + + if (includeLocal) { + InternallyInvokeEvent(c, parameters); + } + } + + public static void RaiseStaticEvent(char c, bool includeLocal, params object[] parameters) where T : EntityBase{ + var h = new Hashtable(); + + // Given we have no instance ID's, we don't append IDs + + h.Add(0, c); + + if (parameters != null) + h.Add(1, parameters); + + //var name = typeof(T). + h.Add(2,IDfromType(typeof(T))); + + NetworkManager.netMessage(PhotonConstants.EntityEventCode, h, true); + + if (includeLocal) + InternallyInvokeStatic(typeof(T),c, parameters); + } + + static Dictionary EBTypeIDs; + static Dictionary IDToEBs; + static void buildTypeIDs(){ // Build a bidirectional lookup of all EntityBase's in the assembly and assign them unique ID's + EBTypeIDs = new Dictionary(); + IDToEBs = new Dictionary(); + + var ebType = typeof(EntityBase); + var derivedTypes = System.AppDomain.CurrentDomain.GetAssemblies().SelectMany(t => t.GetTypes()).Where(t=>ebType.IsAssignableFrom(t)); + + var sorted = derivedTypes.OrderBy(t => t.FullName); + int newID = 0; + foreach(var type in sorted) { + EBTypeIDs.Add(newID, type); + IDToEBs.Add(type, newID); + ++newID; + } + + if (Debug.isDebugBuild || Application.isEditor) { + var debugString = new System.Text.StringBuilder(); + foreach(var pair in EBTypeIDs) { + debugString.AppendFormat("{0} -> {1} \n", pair.Value, pair.Key); + } + + Debug.Log(debugString.ToString()); + } + } + + /// + /// Get a unique ID for this objects class that dervives from EntityBase + /// + public int typeID { + get { + return IDfromType(this.GetType()); + } + } + + public static int IDfromType(Type t) { + if (IDToEBs != null) + return IDToEBs[t]; + + buildTypeIDs(); + + return IDfromType(t); + } + public static Type TypeFromID(int id) { + if (EBTypeIDs != null) + return EBTypeIDs[id]; + + buildTypeIDs(); + + return TypeFromID(id); // Return the original request + } + #endregion + + #region Serializers + + /// + /// Serialize all tokens with the given label into HashTable h + /// Returns true if any contained token requires a reliable update + /// If you use IAutoSerialize, this is only neccessary for manual tokens + /// + protected bool SerializeToken(Hashtable h, params char[] ca) { + bool needsReliable = false; + + var tH = tokenHandler; + foreach(char c in ca) { + h.Add(c, tH.get(c, this)); + + // If we're not already reliable, check if we need reliable + if (!needsReliable) + needsReliable = tH.alwaysReliable[c]; + } + + return needsReliable; + } + + /// + /// Internally used for building and dispatching entity updates, build a full serialization of auto tokens and ID's + /// Due to inconsistent handling/calling contexts, ID's are added safely + /// + /// 0 if nothing is sent, 1 if there is content to send, 2 if content should be sent reliably + public int SerializeAuto(Hashtable h) { + var tH = tokenHandler; + + var time = Time.realtimeSinceStartup; + + bool reliableFlag = false; + bool isSending = false; + + foreach (var c in tH.autoTokens) { + if (this[c] < time) { + this[c] = time + tH.updateTimes[c]; + isSending = true; + + h.Add(c, tH.get(c, this)); + + if (!reliableFlag) + reliableFlag = tH.reliableTokens.Contains(c); + } + } + + if (isSending) { + SerializeAlwaysTokensSafely(h); + //toUpdate.AddRange(tH.alwaysSendTokens); + } + + // If none of the tokens actually updated, return 0 + // Otherwise, return 1 for a normal update, 2 for a reliable update + if (!isSending) + return 0; + + h.AddOrSet(PConst.eidChar, EntityID); + h.AddOrSet(PConst.athChar, authorityID); + + //SerializeToken(toUpdate.Distinct().ToArray()); + + return reliableFlag ? 2 : 1; + } + + /// + /// Read specified values out of the hashtable + /// In most cases, you'll want to use DeserializeFull instead + /// + protected void DeserializeToken(Hashtable h, params char[] ca) { + var tH = tokenHandler; + foreach(char c in ca) { + object value; + if (h.TryGetValue(c, out value)) + tH.set(c, this, value); + } + } + + /// + /// Read all attributed tokens fields of the hashtable and update corresponding values. + /// This will be called automatically if implementing IAutoDeserialize + /// + public void DeserializeFull(Hashtable h) { + var tH = tokenHandler; + foreach(char c in TokenList()) { + object value; + if (h.TryGetValue(c, out value)) + tH.set(c, this, value); + } + } + + /// + /// Key function describing what to serialize. Be sure to call Base.Serialize(h) + /// Helper SerializeToken will automatically write fields with matching tokens into the table + /// + public virtual void Serialize(Hashtable h) { + AppendIDs(h); + } + + /// + /// Deserialize the entity out of the provided hashtable. + /// Use helper function DeserializeToken automatically unpack any tokens + /// + public virtual void Deserialize(Hashtable h) { + h.SetOnKey(PConst.eidChar, ref EntityID); + h.SetOnKey(PConst.athChar, ref authorityID); + } + + /// + /// Check to see if the hashtable already contains each always send token, and if not, add it. + /// + private bool SerializeAlwaysTokensSafely(Hashtable h) { + var tH = tokenHandler; + foreach(var c in tH.alwaysSendTokens) { + // If the hashtable doesn't contain our token, add it in + if (!h.ContainsKey(c)) { + h.Add(c, tH.get(c,this)); + } + } + return tH.alwaysIsRelaible; + } + + #endregion + + /// + /// Send a reliable update with only the provided tokens, immediately. + /// This does not send the alwaysSend autotokens, and exists solely so that you can have a field update as soon as possible, + /// such as menu or input events + /// + public void UpdateExclusively(params char[] ca) { + var h = new Hashtable(); + + AppendIDs(h); + + SerializeToken(h, ca); + NetworkManager.netMessage(PConst.EntityUpdateCode, h, true); + } + + /// + /// Immediately sent a network update with our current state. This includes auto tokens if IAutoSerialize is implemented. + /// Reliable flag, though it defaults to false, may be forced true when sending always or reliable tokens. + /// + public void UpdateNow(bool reliable = false) { + var h = new Hashtable(); + + Serialize(h); + + if (this is IAutoSerialize) { + int autoCode = SerializeAuto(h); + if (autoCode == 2) reliable = true; + } else { + if (SerializeAlwaysTokensSafely(h)) + reliable = true; + } + + + NetworkManager.netMessage(PConst.EntityUpdateCode, h, reliable); + } + + #region timing + // updateTimers coordinates when each value's server representation 'expires' and should be resent + // For values which didn't specify an update time, this value is set to +inf, so that it will always be greater than the current time + + private Dictionary updateTimers = new Dictionary(); + + + float this[char c] { + get { + float t; + if(!updateTimers.TryGetValue(c, out t)) { + var updateTime = tokenHandler.updateTimes[c]; + updateTimers.Add(c, updateTime >= 0 ? 0 : Mathf.Infinity); + } + return updateTimers[c]; + } + set { + updateTimers[c] = value; + } + } + + #endregion + // Token management is a system that assigns a character token to each field for serialization, via attributes + // This is used to automatically pull get/set for variables to assist in auto serializing as much as possible and reducing the amount of manual network messaging + + [ContextMenu("Claim as mine")] + public bool ClaimAsMine() { + if (!NetworkManager.inRoom && NetworkManager.isReady) return false; + + authorityID = NetworkManager.localID; + + UpdateNow(true); + return true; + } + + #region TokenManagement + + /// TODO: Modifying a token at this level needs to clone the TokenHandler specially for this EntityBase object so changes don't propegate to other entities + + /// + /// Runtime modify the parameters of a token. Modifying the reliability of a token is slightly intensive. + /// + /// The token to be modified + /// Milliseconds between updates. 0 is every frame, use cautiously. Set negative to unsubcribe automaic updates. + /// If the token should always be sent with other tokens + /// If the token needs to be sent reliably. + public void ModifyToken(char token, int? updateMs = null, bool? alwaysSend = null, bool? isReliable = null) { + + var tH = tokenHandler; + if (tH.shared) { + _tokenHandler = tH.DeepClone(); + tH = _tokenHandler; + } + + + // If we have a value for reliability + if (isReliable.HasValue) { + if (tH.reliableTokens.Contains(token)){ + if (!isReliable.Value) + tH.reliableTokens.Remove(token); + } else { + if (isReliable.Value) + tH.reliableTokens.Add(token); + } + } + + // If we have a value for always sending + if (alwaysSend.HasValue) { + if (tH.alwaysSend.ContainsKey(token)){ + if (!alwaysSend.Value) + tH.alwaysSend.Remove(token); + } else { + if (alwaysSend.Value) + tH.alwaysSend.Add(token,alwaysSend.Value); + } + } + + if (alwaysSend.HasValue || isReliable.HasValue) + tH.ReEvalAlwaysIsReliable(); + + if (updateMs.HasValue) { + float fUpdateTime = updateMs.Value / 1000f; + tH.updateTimes[token] = fUpdateTime; + + // Unsubscribing + if (fUpdateTime < 0) { + if (tH.autoTokens.Contains(token)) + tH.autoTokens.Remove(token); + if (!tH.manualTokens.Contains(token)) + tH.manualTokens.Add(token); + + this[token] = Mathf.Infinity; // Never auto-update + } else { + if (!tH.autoTokens.Contains(token)) + tH.autoTokens.Add(token); + if (tH.manualTokens.Contains(token)) + tH.manualTokens.Remove(token); + + this[token] = 0; // Auto update next check + } + } + } + + /// + /// Invoke a labeled character event. You should never need to use this method manually. + /// + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] + public void InternallyInvokeEvent(char c, params object[] parameters) { + tokenHandler.NetEvents[c].Invoke(this, parameters); + } + + /// + /// Invoke a labeled character event when the event is static. You should hopefully never need to use this method manually. + /// + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] + public static void InternallyInvokeStatic(Type T, char c, params object[] parameters) { + if (!handlers.ContainsKey(T)) { + BuildTokenList(T); + } + + TokenHandler th = handlers[T]; + + th.NetEvents[c].Invoke(null, parameters); + } + + private static Dictionary> tokens = new Dictionary>(); + private static Dictionary handlers = new Dictionary(); + + /// + /// Cached token handler reference + /// + private TokenHandler _tokenHandler; + /// + /// Gets the token for class in question. Will generate the token list if it doesn't exist. + /// If the object modifies it's tokens parameters, it will clone a new handler specific to the object + /// + protected TokenHandler tokenHandler { + get { + if (_tokenHandler != null) return _tokenHandler; + + var T = GetType(); + + if (handlers.ContainsKey(T)) + return _tokenHandler = handlers[T]; + + BuildTokenList(T); + return _tokenHandler = handlers[T]; + } + } + + protected class TokenHandler { + private Dictionary> setters; + private Dictionary> getters; + + public Dictionary NetEvents = new Dictionary(); + + public TokenHandler() { + setters = new Dictionary>(); + getters = new Dictionary>(); + } + + public TokenHandler DeepClone() { + TokenHandler nTH = new TokenHandler(); + + nTH.setters = new Dictionary>(this.setters); + nTH.getters = new Dictionary>(this.getters); + + nTH.alwaysSend = new Dictionary(this.alwaysSend); + nTH.alwaysReliable = new Dictionary(this.alwaysReliable); + + nTH.alwaysIsRelaible = this.alwaysIsRelaible; + + nTH.reliableTokens.AddRange(this.reliableTokens); + nTH.alwaysSendTokens.AddRange(this.alwaysSendTokens); + nTH.autoTokens.AddRange(this.autoTokens); + nTH.manualTokens.AddRange(this.manualTokens); + + nTH.NetEvents = this.NetEvents; // This one we keep the reference for + + // Flag the new TokenHandler as not being shared + nTH.shared = false; + + return nTH; + } + + public bool shared = true; + + public object get(char c, EntityBase eb) { + return getter(c)(eb); + } + public void set(char c, EntityBase eb, object o) { + setter(c)(eb, o); + } + + public System.Func getter(char c){ + return getters[c]; + } + public System.Action setter(char c) { + return setters[c]; + } + + public Dictionary + alwaysSend = new Dictionary(), + alwaysReliable = new Dictionary(); + + /// + /// If any always send tokens are reliable, implicity, all of them are. + /// + public bool alwaysIsRelaible = false; + + public Dictionary updateTimes = new Dictionary(); + /// + /// Tokens that should always be sent reliably + /// + public List reliableTokens = new List(); + /// + /// Tokens that should always be sent whenever another token is sent + /// + public List alwaysSendTokens = new List(); + /// + /// Tokens that are automatically dispatched according to the update timer + /// + public List autoTokens = new List(); + /// + /// Tokens that are not always send or auto tokens. Useful for getting the subsection of tokens to serialize manually + /// + public List manualTokens = new List(); + + /// + /// Check each reliable token for if it needs to always be sent, and if any do, always sent tokens require reliable updates. + /// + public void ReEvalAlwaysIsReliable() { + alwaysIsRelaible = false; + + foreach(var token in reliableTokens) { + if (alwaysSend.ContainsKey(token)) { + alwaysIsRelaible = true; + return; + } + } + } + + public void RegisterField(char c, FieldInfo fi, NetVar nv) { + getters.Add(c, (e)=> { return fi.GetValue(e); }); + setters.Add(c, (e,v)=> { fi.SetValue(e,v); }); + alwaysSend.Add(c,nv.alwaysSend); + alwaysReliable.Add(c,nv.alwaysReliable); + updateTimes.Add(c, nv.updateTime); + + if (nv.alwaysSend) + alwaysSendTokens.Add(c); + if (nv.alwaysReliable) + reliableTokens.Add(c); + if (nv.updateTime >= 0f) { + autoTokens.Add(c); + } else if (!nv.alwaysSend) { + manualTokens.Add(c); + } + + if(nv.alwaysSend && nv.alwaysReliable) { + alwaysIsRelaible = true; + } + + Debug.LogFormat("{0} -> {1}", c, fi.Name); + } + } + + /// + /// Get a list of all tokens used in this class. + /// + public List TokenList() { + var T = this.GetType(); + + if (tokens.ContainsKey(T)) { + return tokens[T]; + } + + BuildTokenList(T); + return tokens[T]; + } + + static char AutoCharField(FieldInfo fi, ushort offset = 0) { + var strategies = new[] { + fi.Name[0], + fi.Name.ToLowerInvariant()[0], + fi.Name.ToUpperInvariant()[0] }; + + var suggest = strategies[offset % 3]; + // Advance the token if we are still colliding + if (offset > 3) + suggest = (char)(System.Convert.ToUInt16(suggest) + offset); + return suggest; + } + + static void BuildTokenList(Type T) { + if (!T.IsSubclassOf(typeof(EntityBase))) + throw new System.Exception("Cannot build a token list for a class that doesn't derive EntityBase"); + + // Setup the char list + List charList; + + TokenHandler th; + // Establish the token handler + if (!tokens.ContainsKey(T)) { + charList = new List(); + th = new TokenHandler(); + tokens.Add(T,charList); + + handlers.Add(T,th); + } else { + charList = tokens[T]; + th = handlers[T]; + } + + var fields = T.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + // Build the list of net vars, including ones without assigned chars + var pendingInfos = new List<(char c, FieldInfo fi, NetVar nv)>(); + foreach(FieldInfo fi in fields) { + var fieldInfo = fi; // Closure fi to prevent variable capture in lambdas + var netVar = fieldInfo.GetCustomAttributes(typeof(NetVar),true).FirstOrDefault() as NetVar; + if (netVar == null) continue; // This field has no netvar associated, skip it + + if (netVar.token != ' ') charList.Add(netVar.token); + pendingInfos.Add((netVar.token, fi, netVar)); + // Enforce order so we can generate tokens consistently + pendingInfos = pendingInfos.OrderBy(p => p.fi.Name).ToList(); + } + + + foreach(var (c, fi, nv) in pendingInfos) { + if (c != ' ') { + th.RegisterField(c, fi, nv); + continue; + } + + var suggest = AutoCharField(fi); + if (!charList.Contains(suggest)) { + charList.Add(suggest); + th.RegisterField(suggest, fi, nv); + continue; + } + + // Search for an unused key + ushort off = 0; + while(charList.Contains(suggest)) + suggest = AutoCharField(fi, ++off); + + charList.Add(suggest); + th.RegisterField(suggest, fi, nv); + } + + var methods = T.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + // Store all event method infos for remote invocation + foreach(MethodInfo mi in methods) { + var methodInfo = mi; // Closure + var netEvent = methodInfo.GetCustomAttributes(typeof(NetEvent),true).FirstOrDefault() as NetEvent; + + if (netEvent == null) { + //Debug.LogFormat("Skipping {0}'s {1}", T.Name, methodInfo.Name); + continue; + } + + //Debug.LogFormat("EVENT {0}'s {1} -> {2}", T.Name, methodInfo.Name, netEvent.token); + th.NetEvents.Add(netEvent.token, methodInfo); + } + + // Search for all static events on this type; In theory this could be merged with the non-static search, but at time of implementing I thought I may process them seperately. + var staticMethods = T.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); + foreach (MethodInfo mi in staticMethods) { + var smi = mi; // Closure + var netEvent = smi.GetCustomAttributes(typeof(NetEvent),true).FirstOrDefault() as NetEvent; + + if (netEvent == null) continue; + + th.NetEvents.Add(netEvent.token, smi); + } + + var autoTok = string.Join(",", th.autoTokens.Select(t => t.ToString()).ToArray()); + //Debug.LogFormat("{0} Auto Tokens: {1}", T.Name, autoTok); + var alTok = string.Join(",", th.alwaysSendTokens.Select(t => t.ToString()).ToArray()); + //Debug.LogFormat("{0} Alwy Tokens: {1}", T.Name, alTok); + } + + public virtual void Awake() { + if (this is IEarlyAutoRegister) + Register(); + + else if (this is IAutoRegister) + StartCoroutine(DeferredRegister()); + } + + IEnumerator DeferredRegister() { + while (!NetworkManager.inRoom) + yield return null; + Register(); + } + + /// + /// Network variable attribute, specifying the desired token. + /// Set alwaysReliable to hint that a reliable update is required + /// Set alwaysSend to always include the variable in all dispatches + /// + /// + /// Always Reliable -> Token must be sent reliably every time + /// Always Send -> Token will be sent whenever any other token is sent + /// updateMs -> If set, the token will automatically dispatch every updateMs milliseconds + /// + [System.AttributeUsage(System.AttributeTargets.Field,AllowMultiple=false)] + public class NetVar : Attribute { + public readonly char token; + public readonly bool alwaysReliable, alwaysSend; + public readonly float updateTime; + + public NetVar(char token = ' ', bool alwaysReliable = false, bool alwaysSend = false, int updateMs = -1) { + this.token = token; + this.alwaysReliable = alwaysReliable; + this.alwaysSend = alwaysSend; + this.updateTime = updateMs / 1000f; // Convert milliseconds to seconds + } + } + + /// + /// This attribute describes a networked event function; This function must be non-static and is called on a specific entity. + /// It may have any network serializable parameters + /// + [System.AttributeUsage(System.AttributeTargets.Method,AllowMultiple=false)] + public class NetEvent : Attribute { + public readonly char token; + + public NetEvent(char token) { + this.token = token; + } + } + + + /// + /// Attach to a static field returning either a GameObject or EntityBase + /// This function will be called to create a networked entity. + /// The method may contain any number of parameters that are serializable + /// The EntityID + /// + [System.AttributeUsage(System.AttributeTargets.Method)] + public class Instantiation : Attribute { } + + #endregion +} \ No newline at end of file diff --git a/Assets/Runtime/Networking/Entity/EntityBase.cs.meta b/Assets/Runtime/Networking/Entity/EntityBase.cs.meta new file mode 100644 index 0000000..4d31cd0 --- /dev/null +++ b/Assets/Runtime/Networking/Entity/EntityBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4c9b2b5002413374ca9dd3d2f5d26bcf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Networking/Entity/EntityManager.cs b/Assets/Runtime/Networking/Entity/EntityManager.cs new file mode 100644 index 0000000..1e04920 --- /dev/null +++ b/Assets/Runtime/Networking/Entity/EntityManager.cs @@ -0,0 +1,476 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +using System.Linq; + +using System.Reflection; + +using ExitGames.Client.Photon; +using ExitGames.Client.Photon.LoadBalancing; + +using Hashtable = ExitGames.Client.Photon.Hashtable; + +using Type = System.Type; +using Action = System.Action; + +namespace EntityNetwork { + + public static class EntityManager { + public static Dictionary entities = new Dictionary(); + + /// + /// Get an unused EntityID for a given PlayerID. + /// This ID is ensured to be unique for the client, assuming each client has a different PlayerID it will not collide. + /// Each playerID's ID space is about two hundred and sixty eight million IDs. + /// + /// An unused EntityBase ID number + /// The player number, ranged [0,127] + public static int GetUnusedID(int playerID) { + if (playerID > 127) + throw new System.ArgumentOutOfRangeException("playerID cannot exceed 127"); + if (playerID < 0) + throw new System.ArgumentOutOfRangeException("playerID cannot be less than zero"); + + + // Fill all but the topmost byte randomly, then the topmost byte will be an sbyte for player id + + int player = playerID << 28; + int randomInt = Random.Range(0, 0x0FFFFFFF); + + int proposedID = player | randomInt; + + // Recursively dig for new ID's on collision + while (entities.ContainsKey(proposedID)) { + proposedID = GetUnusedID(playerID); + } + + return proposedID; + } + + /// + /// Get a reference to an entity of a given ID. + /// There is a chance that this entity may have been deleted. + /// + /// Entity ID + public static EntityBase Entity(int id) { + EntityBase eb; + return entities.TryGetValue(id, out eb) ? eb : null; + } + + /// + /// Get a reference to an entity of a given ID. + /// There is a chance that this entity may have been deleted. + /// + /// + /// + /// + public static T Entity(int id) where T : EntityBase{ + return Entity(id) as T; + } + + private static void CleanEntities() { + var toRemove = new List(entities.Count); + + foreach(var pair in entities) + if (!pair.Value) toRemove.Add(pair.Key); + + foreach (var key in toRemove) + entities.Remove(key); + } + + /// + /// Register an entity to receive network events/updates. + /// This will fail if the EntityID is already in use. + /// + /// Registering an entity validates all existing entities and will unsubcribe dead entities. + public static void Register(EntityBase eb) { + CleanEntities(); + + //Debug.LogFormat("Registered Entity {0} : {1}",eb.name,eb.EntityID); + + //Debug.LogFormat("{0} -> {1}", eb.name, string.Join(", ", eb.GetType().GetInterfaces().Select(t => t.Name).ToArray())); + if (eb is IAutoSerialize) { + eb.StartCoroutine(autoDispatchEntity(eb)); + } + + if (entities.ContainsKey(eb.EntityID)) { + var otherEntity = Entity(eb.EntityID); + Debug.LogErrorFormat(eb, "{0} has attempted to register over an existing ID {1}, Which belongs to {2}", + eb.gameObject.name, eb.EntityID, otherEntity.gameObject.name); + throw new System.Exception("Entity ID already in use!"); + } + + entities.Add(eb.EntityID, eb); + } + + /// + /// Deregister an EntityBase. This requires enumeraing all entities and is slower than just destroying the EntityBase + /// However, in certain cases, like re-registering as a new pooled object or if the object must exist after being removed, it's worth using + /// + public static void DeRegister(EntityBase eb) { + // Grab all keyvaluepairs where the entity is the entity base being deregistered - ToArray is used to collapse the linq to avoid sync issues + var toRemove = entities.Select(t => new {id = t.Key, Entity = t.Value}).Where(t => t.Entity == eb).ToArray(); + foreach(var removal in toRemove) { + entities.Remove(removal.id); + } + + } + + public static IEnumerator autoDispatchEntity(EntityBase eb) { + Debug.LogFormat("Creating Serial Dispatcher for {0} : {1}", eb.name,eb.EntityID); + + Hashtable h = new Hashtable(); + + while (true) { + h.Clear(); + + if (eb.isMine) { + int code = eb.SerializeAuto(h); + + if (code != 0) { + // If code is 2, message should be reliable + // Debug.LogFormat("Dispatching {0}/{2}: {1}", eb.name, h.ToStringFull(), PhotonConstants.EntityUpdateCode); + NetworkManager.netMessage(PhotonConstants.EntityUpdateCode, h, code == 2); + } + } + + yield return null; + } + } + + static EntityManager() { + NetworkManager.netHook += OnNet; + NetworkManager.onLeave += AllowOrphanSuicidesAndCalls; + } + + // Hook the main events + static void OnNet(EventData ev) { + if (ev.Code == PhotonConstants.EntityUpdateCode) { + var h = (Hashtable)ev[ParameterCode.Data]; + + // Reject self-aimed events + if ((int)ev[ParameterCode.ActorNr] == NetworkManager.localID){ + // Show a red particle for an outgoing signal, before rejecting the event + var ebs = Entity((int)h[PhotonConstants.eidChar]); + NetworkManager.netParticle(ebs, Color.red); + return; + } + + var eb = Entity((int)h[PhotonConstants.eidChar]); + + if (eb) { + // Show a blue particle for an incoming singal + NetworkManager.netParticle(eb, Color.blue); + + if (eb is IAutoDeserialize) { + eb.DeserializeFull(h); + } + eb.Deserialize(h); + } + } + + if (ev.Code == PhotonConstants.EntityEventCode) { + var h = (Hashtable)ev[ParameterCode.Data]; + + // --- Static Events --- + // Param labeled 2 in the hashtable is the EntityBase's ID Type, if a static event call, so if the table contains key 2, run it as a static event + object idObject; + if (h.TryGetValue(2,out idObject)) { + var typeID = (int)idObject; + Type entityType; + try { + entityType = EntityBase.TypeFromID(typeID); + } catch { + throw new System.Exception("Attempting to call static event on a non-existant type"); + } + + var controlChar = (char)h[0]; + + object paramObject; + if (h.TryGetValue(1,out paramObject)) { + EntityBase.InternallyInvokeStatic(entityType, controlChar,(object[])paramObject); + } else { + EntityBase.InternallyInvokeStatic(entityType, controlChar, null); + } + + return; + } + + // --- Instance Events --- + var eb = Entity((int)h[PhotonConstants.eidChar]); + + if (eb) { + var controlChar = (char)h[0]; + + object paramObject; + if (h.TryGetValue(1,out paramObject)) { + eb.InternallyInvokeEvent(controlChar,(object[])paramObject); + } else { + eb.InternallyInvokeEvent(controlChar, null); + } + } + } + + if (ev.Code == PhotonConstants.EntityInstantiateCode) { + var h = (Hashtable)ev[ParameterCode.Data]; + DeserializeInstantiate(h); + } + } + + /// + /// Generate a hashtable describing an object instantiaton for use with DeserializeInstantiate + /// Use helper method Instantiate to automatically call and fire this as an event. + /// + /// + public static Hashtable SerializeInstantiate(int authID, Vector3 pos, Quaternion rot, params object[] param) { + var H = new Hashtable(); + + //H.Add('T', typeof(T).ToString()); + + H.Add('O', authID); + H.Add('I', GetUnusedID(authID)); + + H.Add('T', typeof(T).FullName); + + H.Add('P', pos); + H.Add('R', rot); + H.Add('p', param); + + return H; + } + + /// + /// Locally creates the instantiated object described in Hashtable H. + /// + /// + public static void DeserializeInstantiate(Hashtable H) { + CheckInstantiators(); + + //Debug.Log(H.ToStringFull()); + var type = typeLookup[H['T'] as string]; + + var eid = (int)H['I']; + var ID = (int)H['O']; + + var pos = (Vector3)H['P']; + var rot = (Quaternion)H['R']; + + var options = H['p'] as object[]; + + ActionInstantiate(ID, eid, type, pos, rot, options); + + //Instantiate(pos, rot, options); + } + + #region Instantiation + // Instantiation uses InstanceGameObject / InstanceGameEntity attributes + + // Actually construct an instantiator object + private static void ActionInstantiate(int authID, int entityID, Type T, Vector3 pos, Quaternion rot, object[] param) { + MethodInfo mi; + if (!InstantiateMethods.TryGetValue(T, out mi)) { + throw new System.Exception(string.Format("Type {0} doesn't have an Instantiate Attributed method and isn't Instantiable.", T.Name)); + } + + if (typeof(GameObject).IsAssignableFrom(mi.ReturnType)) { + var val = mi.Invoke(null, param) as GameObject; + + var go = Object.Instantiate(val, pos, rot); + + // Attempt to set the ID of the entitybase + var eb = go.GetComponentInChildren(); + + if (eb) { + eb.EntityID = entityID; + eb.authorityID = authID; + } + + go.SendMessage("OnInstantiate", SendMessageOptions.DontRequireReceiver); + + return; + } + + var rt = mi.ReturnType; + if (typeof(EntityBase).IsAssignableFrom(rt)) { + var eb = mi.Invoke(null, param) as EntityBase; + + eb.authorityID = authID; + eb.EntityID = entityID; + + var go = eb.gameObject; + var t = eb.transform; + + if (pos != Vector3.zero) + t.position = pos; + if (rot != Quaternion.identity) + t.rotation = rot; + + go.SendMessage("OnInstantiate", SendMessageOptions.DontRequireReceiver); + + return; + } + + throw new System.Exception(string.Format("Type {0}'s Instantiate Method doesn't return an EntityBase or GameObject", T.Name)); + } + + // Helper dictionaries. typeLookup is to help us send types over the wire, InstantiateMethods stores each types instantiator + static Dictionary typeLookup; + static Dictionary InstantiateMethods; + + + // This is a mess of autodocumentation, mostly due to usage of params and overloads. + + /// + /// Activate type T's EntityBase.Instantation attribute remotely with given parameters, Generating and assigning the appropriate actor ID + /// This method returns the HashTable describing the instantation request that can be used to also create the object locally. + /// + /// + public static Hashtable Instantiate(int authID, params object[] param) { return Instantiate(authID, Vector3.zero, Quaternion.identity, param); } + /// + /// Activate type T's EntityBase.Instantation attribute remotely with given parameters, Generating and assigning the appropriate actor ID + /// This method returns the HashTable describing the instantation request that can be used to also create the object locally. + /// + /// + public static Hashtable Instantiate(int authID, Vector3 pos, params object[] param) { return Instantiate(authID, pos, Quaternion.identity, param); } + /// + /// Activate type T's EntityBase.Instantation attribute remotely with given parameters, Generating and assigning the appropriate actor ID + /// This method returns the HashTable describing the instantation request that can be used to also create the object locally. + /// + /// + public static Hashtable Instantiate(int authID, Vector3 pos, Quaternion rot) { return Instantiate(authID, pos, rot, null); } + /// + /// Activate type T's EntityBase.Instantation attribute remotely with given parameters, Generating and assigning the appropriate actor ID + /// This method returns the HashTable describing the instantation request that can be used to also create the object locally. + /// + /// + public static Hashtable Instantiate(int authID, Vector3 pos, Quaternion rot,params object[] param) { + var table = SerializeInstantiate(authID, pos, rot, param); + + if (NetworkManager.isReady) { + NetworkManager.net.OpRaiseEvent(PhotonConstants.EntityInstantiateCode, table, true, RaiseEventOptions.Default); + } + + return table; + } + + static bool InstantiatorsBuilt = false; + static void CheckInstantiators() { + if (InstantiatorsBuilt) return; + + BuildInstantiators(); + InstantiatorsBuilt = true; + } + + // Gather all the instantiaton attributes on entity classes + static void BuildInstantiators() { + Debug.Log("Buiding Instantiator cache"); + + InstantiateMethods = new Dictionary(); + typeLookup = new Dictionary(); + + var ebT = typeof(EntityBase); + + var AllEntityTypes = + System.AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(s => s.GetTypes()) + .Where(t => ebT.IsAssignableFrom(t)); + + //var AllEntityTypes = Assembly.GetTypes().Where(t => ebT.IsAssignableFrom(t)); + + foreach(var entityType in AllEntityTypes) { + var methods = entityType.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + typeLookup.Add(entityType.FullName, entityType); + //Debug.LogFormat("Scanning Type {0}", entityType); + + foreach(var method in methods) { + //Debug.LogFormat("Scanning Method {0}", method.Name); + // First look for a GameObject instantiator + var ia = method.GetCustomAttributes(typeof(EntityBase.Instantiation),true).FirstOrDefault() as EntityBase.Instantiation; + if (ia != null) { + InstantiateMethods.Add(entityType, method); + Debug.LogFormat("Registering Instantiator {0} for {1} (R: {2})",method.Name,entityType.FullName,method.ReturnType.ToString()); + } + } + } + } + + static void AllowOrphanSuicidesAndCalls(EventData ev) { + var toKill = new List(); + var players = NetworkManager.net.CurrentRoom.Players.Select(t => t.Value.ID); + + foreach(var pair in entities) { + var e = pair.Value; + + if (e is IAutoKillOrphans) { + if (e.authorityID == -1) continue; + if (players.Contains(e.authorityID)) continue; + + toKill.Add(e.EntityID); + } + // Send out the orphan callbacks + if (e is IOrphanCallback) { + if (e.authorityID == -1) return; + if (players.Contains(e.authorityID)) continue; + + (e as IOrphanCallback).OnOrphaned(); + } + } + + // Kill the orphans + foreach(var killable in toKill) { + if (Application.isEditor || Debug.isDebugBuild) { + var killEntity = Entity(killable); + + Debug.LogFormat("Destroying orphaned entity {0} as it's owner {1} has left the room.",killEntity.gameObject.name,killEntity.authorityID); + } + Object.Destroy(Entity(killable).gameObject); + } + } + + #endregion + } + /// + /// Specify that deserialization should be automaticly handled. + /// All registered field tokens will be automaticly set using cached setters + /// This is not appropriate if you have custom serialization/deserialization logic + /// + public interface IAutoDeserialize {} + + /// + /// Specify that automatic token handling should be performed on the entity. + /// In most cases, this should remove the need to write custom serializers + /// This only applies to NetVar's with alwaysSend or updateTime set + /// + public interface IAutoSerialize {} + + /// + /// Only appropriate for Entities with fixed, pre-determined ID's. + /// The entity will attempt to register itself on Awake() + /// + public interface IAutoRegister {} + + /// + /// Only appropriate for Entities with fixed, pre-determined ID's. + /// The entity will absolutely to register itself on Awake() + /// + public interface IEarlyAutoRegister {} + + /// + /// Assign to an EntityBase so that any time an AuthorityID would be checked we instead check if we're the room master + /// Used to clarify network ownership for objects that aren't owned by a player but instead by the room itself + /// + public interface IMasterOwnsUnclaimed {} + + /// + /// When the authority player disconnects, destroy the entity and attached gameobject that aren't owned by players (EXCEPT with AuthID -1) + /// + public interface IAutoKillOrphans {} + + /// + /// Adds an OnOrphaned callback - Note this is run whenever a player quits and we are unclaimed without a -1 authority, not just when our authority quits. + /// + public interface IOrphanCallback { + void OnOrphaned(); + } +} \ No newline at end of file diff --git a/Assets/Runtime/Networking/Entity/EntityManager.cs.meta b/Assets/Runtime/Networking/Entity/EntityManager.cs.meta new file mode 100644 index 0000000..fbd7726 --- /dev/null +++ b/Assets/Runtime/Networking/Entity/EntityManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 118662652baff8841bb52034c1795789 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Networking/Helpers.meta b/Assets/Runtime/Networking/Helpers.meta new file mode 100644 index 0000000..e6e9a24 --- /dev/null +++ b/Assets/Runtime/Networking/Helpers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8c1f784b32aefb84cad6d24f7d2a37b5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Networking/Helpers/HashtableExtension.cs b/Assets/Runtime/Networking/Helpers/HashtableExtension.cs new file mode 100644 index 0000000..a9459a0 --- /dev/null +++ b/Assets/Runtime/Networking/Helpers/HashtableExtension.cs @@ -0,0 +1,52 @@ +using UnityEngine; +using ExitGames.Client.Photon.LoadBalancing; + +using Hashtable = ExitGames.Client.Photon.Hashtable; + +/// +/// Extensions of Exitgames Hashtable to allow for concise conditional setting logic +/// +public static class HashtableExtension { + /// + /// Checks if the hashtable contains key, if so, it will update toSet. Struct version + /// + /// Key to check for + /// Reference to the variable to set + /// Type to cast toSet to + public static void SetOnKey(this Hashtable h, object key, ref T toSet) where T : struct { + if (h.ContainsKey(key)) + toSet = (T)h[key]; + } + + public static void AddOrSet(this Hashtable h, object key, T val) where T : struct { + if (h.ContainsKey(key)) { + h[key] = val; + } else { + h.Add(key, val); + } + } + + /// + /// Add a value to the hashtable if and only if it mismatches the previous provided + /// Returns true if the replacement was made + /// + public static bool AddWithDirty(this Hashtable h, char key, T tracked, ref T previous) { + if (tracked.Equals(previous)) return false; + + h.Add (key,tracked); + previous = tracked; + return true; + } + + /// + /// Adds and updates the keys/value based on . + /// Any other keys are uneffected. + /// + /// + /// + public static void SetHashtable(this Hashtable h, Hashtable propertiesToSet){ + var customProps = propertiesToSet.StripToStringKeys() as Hashtable; + h.Merge(customProps); + h.StripKeysWithNullValues(); + } +} diff --git a/Assets/Runtime/Networking/Helpers/HashtableExtension.cs.meta b/Assets/Runtime/Networking/Helpers/HashtableExtension.cs.meta new file mode 100644 index 0000000..b6427bf --- /dev/null +++ b/Assets/Runtime/Networking/Helpers/HashtableExtension.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 91b1fc7f0e237bb4e8fc167e80d4b8e5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Networking/Helpers/PhotonConstants.cs b/Assets/Runtime/Networking/Helpers/PhotonConstants.cs new file mode 100644 index 0000000..0c9029e --- /dev/null +++ b/Assets/Runtime/Networking/Helpers/PhotonConstants.cs @@ -0,0 +1,34 @@ +using UnityEngine; +using System.Collections.Generic; + +public static class PhotonConstants { + public static readonly byte EntityUpdateCode = 110; + public static readonly byte EntityEventCode = 105; + public static readonly byte EntityInstantiateCode = 106; + public static readonly char eidChar = (char)206; // 'Î' + public static readonly char athChar = (char)238; // 'î' + public static readonly char insChar = (char)207; // 'Ï' + public static readonly char tpeChar = (char)208; + + public static readonly string propScene = "sc"; + + + /// + /// Region names strings + /// + public static readonly Dictionary RegionNames = new Dictionary() { + {"asia","Signapore"}, + {"au","Australia"}, + {"cae","Montreal"}, + {"cn","Shanghai"}, + {"eu","Europe"}, + {"in","India"}, + {"jp","Japan"}, + {"ru","Moscow"}, + {"rue","East Russia"}, + {"sa","Brazil"}, + {"kr","South Korea"}, + {"us","Eastern US"}, + {"usw","Western US"} + }; +} diff --git a/Assets/Runtime/Networking/Helpers/PhotonConstants.cs.meta b/Assets/Runtime/Networking/Helpers/PhotonConstants.cs.meta new file mode 100644 index 0000000..e21d9ed --- /dev/null +++ b/Assets/Runtime/Networking/Helpers/PhotonConstants.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3090fc43b95be2244909c218fbf35512 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Networking/Helpers/StreamCustomTypes.cs b/Assets/Runtime/Networking/Helpers/StreamCustomTypes.cs new file mode 100644 index 0000000..2a25ca8 --- /dev/null +++ b/Assets/Runtime/Networking/Helpers/StreamCustomTypes.cs @@ -0,0 +1,139 @@ +using System; +using System.IO; +using System.Linq; +using ExitGames.Client.Photon; +using UnityEngine; + +using Hashtable = ExitGames.Client.Photon.Hashtable; + +public static class StreamCustomTypes { + public static void Register() { + PhotonPeer.RegisterType(typeof(Vector2), (byte)'V', SerializeVector2, DeserializeVector2); + PhotonPeer.RegisterType(typeof(Vector3), (byte)'W', SerializeVector3, DeserializeVector3); + PhotonPeer.RegisterType(typeof(Quaternion), (byte)'Q', SerializeQuaternion, DeserializeQuaternion); + PhotonPeer.RegisterType(typeof(Color), (byte)'C', SerializeColor, DeserializeColor); + PhotonPeer.RegisterType(typeof(char), (byte)'c', SerializeChar, DeserializeChar); + } + + private static short SerializeVector2(StreamBuffer outStream, object customObj) { + var vo = (Vector2)customObj; + + var ms = new MemoryStream(2 * 4); + + ms.Write(BitConverter.GetBytes(vo.x), 0, 4); + ms.Write(BitConverter.GetBytes(vo.y), 0, 4); + + outStream.Write(ms.ToArray(), 0, 2 * 4); + return 2 * 4; + } + + private static object DeserializeVector2(StreamBuffer inStream, short length) { + var bytes = new Byte[2 * 4]; + inStream.Read(bytes, 0, 2 * 4); + return new + Vector2( + BitConverter.ToSingle(bytes, 0), + BitConverter.ToSingle(bytes, 4)); + + // As best as I can tell, the new Protocol.Serialize/Deserialize are written around WP8 restrictions + // It's not worth the pain. + + //int index = 0; + //float x, y; + //Protocol.Deserialize(out x, bytes, ref index); + //Protocol.Deserialize(out y, bytes, ref index); + + //return new Vector2(x, y); + } + + private static short SerializeVector3(StreamBuffer outStream, object customObj) { + Vector3 vo = (Vector3)customObj; + + var ms = new MemoryStream(3 * 4); + + ms.Write(BitConverter.GetBytes(vo.x), 0, 4); + ms.Write(BitConverter.GetBytes(vo.y), 0, 4); + ms.Write(BitConverter.GetBytes(vo.z), 0, 4); + + outStream.Write(ms.ToArray(), 0, 3 * 4); + return 3 * 4; + } + + private static object DeserializeVector3(StreamBuffer inStream, short length) { + var bytes = new byte[3 * 4]; + + inStream.Read(bytes, 0, 3 * 4); + + return new + Vector3( + BitConverter.ToSingle(bytes, 0), + BitConverter.ToSingle(bytes, 4), + BitConverter.ToSingle(bytes, 8)); + } + + private static short SerializeQuaternion(StreamBuffer outStream, object customObj) { + Quaternion vo = (Quaternion)customObj; + + var ms = new MemoryStream(4 * 4); + + ms.Write(BitConverter.GetBytes(vo.x), 0, 4); + ms.Write(BitConverter.GetBytes(vo.y), 0, 4); + ms.Write(BitConverter.GetBytes(vo.z), 0, 4); + ms.Write(BitConverter.GetBytes(vo.w), 0, 4); + + outStream.Write(ms.ToArray(), 0, 4 * 4); + return 4 * 4; + } + + private static object DeserializeQuaternion(StreamBuffer inStream, short length) { + var bytes = new byte[4 * 4]; + + inStream.Read(bytes, 0, 4 * 4); + + return new + Quaternion( + BitConverter.ToSingle(bytes, 0), + BitConverter.ToSingle(bytes, 4), + BitConverter.ToSingle(bytes, 8), + BitConverter.ToSingle(bytes, 12)); + } + + private static short SerializeColor(StreamBuffer outStream, object customObj) { + Color vo = (Color)customObj; + + var ms = new MemoryStream(4 * 4); + + ms.Write(BitConverter.GetBytes(vo.r), 0, 4); + ms.Write(BitConverter.GetBytes(vo.g), 0, 4); + ms.Write(BitConverter.GetBytes(vo.b), 0, 4); + ms.Write(BitConverter.GetBytes(vo.a), 0, 4); + + outStream.Write(ms.ToArray(), 0, 4 * 4); + return 4 * 4; + } + + private static object DeserializeColor(StreamBuffer inStream, short length) { + var bytes = new byte[4 * 4]; + + inStream.Read(bytes, 0, 4 * 4); + + return new + Color( + BitConverter.ToSingle(bytes, 0), + BitConverter.ToSingle(bytes, 4), + BitConverter.ToSingle(bytes, 8), + BitConverter.ToSingle(bytes, 12)); + } + + private static short SerializeChar(StreamBuffer outStream, object customObj) { + outStream.Write(new[]{ (byte)((char)customObj) }, 0, 1); + return 1; + } + + private static object DeserializeChar(StreamBuffer inStream, short Length) { + var bytes = new Byte[1]; + inStream.Read(bytes, 0, 1); + + return (char)bytes[0]; + } +} diff --git a/Assets/Runtime/Networking/Helpers/StreamCustomTypes.cs.meta b/Assets/Runtime/Networking/Helpers/StreamCustomTypes.cs.meta new file mode 100644 index 0000000..ba72072 --- /dev/null +++ b/Assets/Runtime/Networking/Helpers/StreamCustomTypes.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e4402087c29fe6f4888bd19ae6c229d3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Networking/NetProperties.meta b/Assets/Runtime/Networking/NetProperties.meta new file mode 100644 index 0000000..21bec1a --- /dev/null +++ b/Assets/Runtime/Networking/NetProperties.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d974f1ce0926d5e4e9e6b1d5bccacc0e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Networking/NetProperties/PlayerProperties.cs b/Assets/Runtime/Networking/NetProperties/PlayerProperties.cs new file mode 100644 index 0000000..78c8bc0 --- /dev/null +++ b/Assets/Runtime/Networking/NetProperties/PlayerProperties.cs @@ -0,0 +1,282 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +using Player = ExitGames.Client.Photon.LoadBalancing.Player; +using Hashtable = ExitGames.Client.Photon.Hashtable; + +public class PlayerPropertyEntry { + /// + /// Unique key for . + /// + public readonly string id; + + /// + /// Make sure is unique. + /// + /// + public PlayerPropertyEntry(string id){ + this.id = id; + } + + /// + /// Gets value from local player. + /// + /// + public T GetLocal(){ + return Get(NetworkManager.net.LocalPlayer); + } + + /// + /// Gets value from . + /// + /// + /// + public T Get(Player player){ + return (T)player.CustomProperties[id]; + } + + /// + /// Sets for local player. + /// + /// + public void SetLocal(T value){ + Set(NetworkManager.net.LocalPlayer, value); + } + + /// + /// Sets for . + /// + /// + /// + public void Set(Player player, T value){ + var h = new Hashtable(); + h.Add(id, value); + player.SetCustomProperties(h); + } + + /// + /// Sets with (, ). + /// + /// + /// + public void Initialilze(Hashtable hashtable, T value){ + hashtable.Add(id, value); + } + + // ---------------------------------------------------- + // Seems like the best place to put this + // dunno + // ---------------------------------------------------- + + /// + /// Gets value of in . + /// + /// + public int GetPlayerPrefInt() => PlayerPrefs.GetInt(id, 0); + + /// + /// Sets using into . + /// + /// + public void SetPlayerPrefInt(int value) => PlayerPrefs.SetInt(id, value); +} + +public static class PlayerProperties { + + /// + /// A huge list of a bunch of touhous. + /// + public static readonly string[] touhous = new[]{ + "Reimu", "Marsia", + "Rumia", "Daiyousei", "Cirno", "Hong", "Koakuma", "Patchouli", "Sakuya", "Flandre", + "Letty", "Chen", "Alice", "Lily", "Lyrica", "Lunasa", "Merlin", "Youmu", "Yuyuko", "Ran", "Yakari", + "Suika", + "Wriggle", "Mystia", "Keine", "Tewi", "Reisen", "Eirin", "Kaguya", "Mokou", + "Aya", "Medicine", "Yuuka", "Komachi", "Eiki", + "Shizuha", "Minoriko", "Hina", "Nitori", "Momiji", "Sanae", "Kanako", "Suwako", + "Iku", "Tenshi", "Hatate", "Kokoro", + "Kisume", "Yamame", "Parsee", "Yuugi", "Satori", "Rin", "Utsuho", "Koishi", + "Kyouko", "Yoshika", "Seiga", "Tojiko", "Futo", "Miko", "Mamizou", + "Wakasagihime", "Sekibanki", "Kagerou", "Benben", "Yatsuhashi", "Shinmyoumaru", "Raiko", + "Sumireko", + "Joon", "Shion", + "Seiran", "Ringo", "Doremy", "Sagume", "Clownpiece", "Junko", "Hecatia", + "Eternity", "Nemuno", "Auun", "Narumi", "Satono", "Mai", "Okina", + "Eika", "Urumi", "Kutaka", "Yachie", "Mayumi", "Keiki", "Saki", + "Rinnosuke", "Sunny", "Luna", "Star", "Chang'e", "Kasen", "Kosuzu" + }; + + /// + /// As name implies, gives a random touhou. + /// + public static string getRandomTouhou => touhous[Random.Range(0, touhous.Length)]; + + public static readonly string playerNickname = "nn"; + + /// + /// Player's status in lobby. (ready or not) + /// + public static readonly PlayerPropertyEntry lobbyStatus = new PlayerPropertyEntry("ls"); + /// + /// Player's status in loading a scene. (ready or not) + /// + public static readonly PlayerPropertyEntry gameStatus = new PlayerPropertyEntry("gs"); + + /// + /// Player's selected character. + /// + public static readonly PlayerPropertyEntry playerCharacter = new PlayerPropertyEntry("pc"); + /// + /// Player's selected team. + /// + public static readonly PlayerPropertyEntry playerTeam = new PlayerPropertyEntry("pt"); + + /// + /// Player's selected response. + /// + public static readonly PlayerPropertyEntry playerResponse = new PlayerPropertyEntry("pr"); + + public static Player localPlayer { + get { + return NetworkManager.net.LocalPlayer; + } + } + + /// + /// Initializes all custom properties for the local player. + /// + public static void CreatePlayerHashtable(){ + var h = new Hashtable(); + + localPlayer.NickName = GetPlayerNickname(); + + lobbyStatus.Initialilze(h, false); + gameStatus.Initialilze(h, false); + + playerCharacter.Initialilze(h, playerCharacter.GetPlayerPrefInt()); + playerTeam.Initialilze(h, playerCharacter.GetPlayerPrefInt()); + + playerResponse.Initialilze(h, -1); + + localPlayer.SetCustomProperties(h); + } + + /// + /// Resets a select few custom properties for the local player. + /// + public static void ResetPlayerHashtable(){ + var h = new Hashtable(); + + lobbyStatus.Initialilze(h, false); + playerResponse.Initialilze(h, -1); + + localPlayer.SetCustomProperties(h); + } + + #region Ready Status + + /// + /// If inside a room, returns if all players are lobby ready. + /// If not, returns if the local player is lobby ready. + /// + /// + public static bool GetAllLobbyStatus(){ + if (!NetworkManager.inRoom) return lobbyStatus.GetLocal(); + + var players = NetworkManager.net.CurrentRoom.Players.Values; + + if (players.Count < 2) return false; + + foreach(var p in players){ + if (!lobbyStatus.Get(p)) return false; + } + return true; + } + + /// + /// If inside a room, returns if all players are game ready. + /// If not, returns if the local player is game ready. + /// + /// + public static bool GetAllGameStatus() { + if (!NetworkManager.inRoom) return gameStatus.GetLocal(); + + var players = NetworkManager.net.CurrentRoom.Players.Values; + + if (players.Count < 2) return false; + + foreach (var p in players) { + if (!gameStatus.Get(p)) return false; + } + return true; + } + + /// + /// Returns the highest value player response from all players. + /// Returns -3 if no room is active. + /// Returns -2 if there is less than 2 players in the room. + /// + /// + public static int GetAllResponse(){ + if (!NetworkManager.inRoom) return -3; + + var players = NetworkManager.net.CurrentRoom.Players.Values; + + if (players.Count < 2) return -2; + + var response = int.MaxValue; + foreach (var p in players) { + response = Mathf.Min(playerResponse.Get(p), response); + } + return response; + } + + /// + /// If inside a room, returns true if team mode is inactive OR if team mode is active and there is 2+ unique teams. + /// If not inside a room, returns true. + /// + /// + public static bool GetAllTeamDifferent(){ + if (!NetworkManager.inRoom) return true; + if (!RoomProperties.teamStatus.Get()) return true; + + var uniqueTeams = NetworkManager.net.CurrentRoom.Players.Values.Select(p => playerTeam.Get(p)).Distinct(); + + return uniqueTeams.Count() > 1; + } + + #endregion + + #region Nickname + + /// + /// Gets local player's nickname from . + /// Returns a random touhou if no nickname is found. + /// + /// + public static string GetPlayerNickname(){ + var key = playerNickname; + if (PlayerPrefs.HasKey(key)){ + return PlayerPrefs.GetString(key); + } else { + var value = getRandomTouhou; + PlayerPrefs.SetString(key, value); + return value; + } + } + + /// + /// Sets into and . + /// + /// + public static void SetPlayerNickname(string nickname){ + var key = playerNickname; + PlayerPrefs.SetString(key, nickname); + localPlayer.NickName = key; + } + + #endregion + +} diff --git a/Assets/Runtime/Networking/NetProperties/PlayerProperties.cs.meta b/Assets/Runtime/Networking/NetProperties/PlayerProperties.cs.meta new file mode 100644 index 0000000..a759e35 --- /dev/null +++ b/Assets/Runtime/Networking/NetProperties/PlayerProperties.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0acc2d84cbeb940498c3e42bcfa41d67 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Networking/NetProperties/RoomProperties.cs b/Assets/Runtime/Networking/NetProperties/RoomProperties.cs new file mode 100644 index 0000000..a258553 --- /dev/null +++ b/Assets/Runtime/Networking/NetProperties/RoomProperties.cs @@ -0,0 +1,130 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.SceneManagement; + +using ExitGames.Client.Photon; +using ExitGames.Client.Photon.LoadBalancing; + +using Player = ExitGames.Client.Photon.LoadBalancing.Player; +using Hashtable = ExitGames.Client.Photon.Hashtable; + +public class RoomPropertyEntry{ + /// + /// Unique key for . + /// + public readonly string id; + + /// + /// Make sure is unique. + /// + /// + public RoomPropertyEntry(string id){ + this.id = id; + } + + /// + /// Sets for the room. + /// Performs a check if is true. + /// + /// + /// + public void Set(T value, bool masterOnly = true){ + if (masterOnly && !NetworkManager.isMaster) return; + + var h = new Hashtable(); + h.Add(id, value); + RoomProperties.UpdateRoomHashtable(h); + } + + /// + /// Gets value from room. + /// + /// + public T Get(){ + var prop = RoomProperties.GetRoomHashtable(); + return (T)prop[id]; + } + + /// + /// Sets with (, ). + /// + /// + /// + public void Initialize(Hashtable hashtable, T value){ + hashtable.Add(id, value); + } + +} + +public static class RoomProperties { + + private static Hashtable _localRoomProperties = new Hashtable(); + + public static RoomPropertyEntry teamStatus = new RoomPropertyEntry("ts"); + + public static RoomPropertyEntry sceneLoaded = new RoomPropertyEntry("sl"); + public static RoomPropertyEntry entities = new RoomPropertyEntry("et"); + public static RoomPropertyEntry entities2 = new RoomPropertyEntry("et2"); + + /// + /// Initalizes all custom properties of the room. + /// + /// + public static Hashtable CreateRoomHashtable(){ + _localRoomProperties.Clear(); + + sceneLoaded.Initialize(_localRoomProperties, "RoomScene1"); + entities.Initialize(_localRoomProperties, new Hashtable()); + entities2.Initialize(_localRoomProperties, ""); + + return _localRoomProperties; + } + + public static Hashtable GetRoomHashtable(){ + var room = NetworkManager.net != null ? NetworkManager.net.CurrentRoom : null; + if (room != null) { + _localRoomProperties = room.CustomProperties; + } + return _localRoomProperties; + } + + public static void UpdateRoomHashtable(Hashtable propertiesToSet){ + _localRoomProperties.SetHashtable(propertiesToSet); + var room = NetworkManager.net != null ? NetworkManager.net.CurrentRoom : null; + if (room != null){ + room.SetCustomProperties(propertiesToSet); + } + } + + public static void LoadRoomHashtable(){ + + // Put anything here + + /* + // load scene before doing anything else + + var roomscene = sceneLoaded.Get(); + + for(var i = 0; i < SceneManager.sceneCount; i++){ + var scene = SceneManager.GetSceneAt(i); + if (roomscene == scene.name) { + Debug.Log("Current room scene loaded"); + goto LoadRoomProperties; + } + } + + // The room scene we need loaded isn't loaded + // Force load it + Debug.Log("Loading room scene"); + SceneManager.LoadScene(roomscene); + return; + + // We have the current room loaded. Load other properties if needed + LoadRoomProperties: + + return; + */ + } + +} diff --git a/Assets/Runtime/Networking/NetProperties/RoomProperties.cs.meta b/Assets/Runtime/Networking/NetProperties/RoomProperties.cs.meta new file mode 100644 index 0000000..2391504 --- /dev/null +++ b/Assets/Runtime/Networking/NetProperties/RoomProperties.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 98ad96430aae9e241a4dedc659a0fa4f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Networking/NetworkManager.cs b/Assets/Runtime/Networking/NetworkManager.cs new file mode 100644 index 0000000..82fff38 --- /dev/null +++ b/Assets/Runtime/Networking/NetworkManager.cs @@ -0,0 +1,298 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +using ExitGames.Client.Photon.LoadBalancing; +using ExitGames.Client.Photon; + +using Hashtable = ExitGames.Client.Photon.Hashtable; +using System.Linq; + +public class NetworkManager : MonoBehaviour { + + #region Inspector + + public string serverAddress, appID, gameVersion; + + [Header("Debug")] + public ParticleSystem NetworkDebugParticles; + + public bool expectedOnline = false; + public byte expectedMaxPlayers = 2; + + #endregion + + #region Particles + public static bool visParticles = true; + // Spawn a particle on an entity, used for visualizing updates + public static void netParticle(EntityBase eb, Color pColor) { + if (!visParticles) return; + + if (instance.NetworkDebugParticles) { + var pparams = new ParticleSystem.EmitParams(); + pparams.position = eb.transform.position; + pparams.startColor = pColor; + instance.NetworkDebugParticles.Emit(pparams, 1); + } + } + + #endregion + + #region Network Helpers + public static NetLogic net { get; private set; } + + /// + /// A time value that is 'approximately' (+/- 10ms most of the time) synced across clients + /// + public static double serverTime { + get { + if (net == null || net.loadBalancingPeer == null) + return Time.realtimeSinceStartup; + + return net.loadBalancingPeer.ServerTimeInMilliSeconds * (1d/1000d); + } + } + + /// + /// On non WebSocketSecure platforms, encryption handshake must occur before opCustom can be sent. + /// This is important in cases such as getting the room or region list. + /// + public static bool delayForEncrypt { + get { + // We use secure websockets in UnityGL, so no encrypt handshake needs to occur + #if UNITY_WEBGL + return true; + #else + return !net.loadBalancingPeer.IsEncryptionAvailable; + #endif + } + } + + /// + /// Returns true if able to send network connections. + /// + public static bool isReady { + get { + if (net == null) return false; + + return net.IsConnectedAndReady; + } + } + + /// + /// If this client is considered owner of the room. + /// + public static bool isMaster { + get { + // If have no networking, we're the owner. + if (net == null) return true; + if (!inRoom) return true; + + return net.LocalPlayer.IsMasterClient; + } + } + + /// + /// Boolean for if we are on the name server. Used for awaiting the name server connection. + /// Can be set to true to connect to name server. + /// + /// true if on name server; otherwise, false.// + public static bool onNameServer { + get { + if (net == null) return false; + + return net.State.Equals(ClientState.ConnectedToNameServer); + } set { + if (value) net.ConnectToNameServer(); + } + } + + /// + /// Boolean to check if the network is in the master lobby, will be true after we've found a region. + /// + /// true if on master lobby; otherwise, false. + public static bool onMasterLobby { + get { + if (net == null) return false; + + return net.State.Equals(ClientState.JoinedLobby); + } + } + + /// + /// Boolean to check if we're in a room or not. + /// + /// true if in room; otherwise, false. + public static bool inRoom{ + get { + if (net == null) return false; + + return net.State.Equals(ClientState.Joined); + } + } + + /// + /// Returns all players in the current room sorted by . + /// + public static Player[] getSortedPlayers{ + get { + if (!inRoom) return new Player[0]; + + var players = NetworkManager.net.CurrentRoom.Players.Values; + var playersSorted = players.OrderBy(p => p.ID); + return playersSorted.ToArray(); + } + } + + /// + /// Enqueue a network update to be sent. Network events are processed both on Update and LateUpdate timings. + /// + public static bool netMessage(byte eventCode, object eventContent, bool sendReliable = false, RaiseEventOptions options = null) { + if (!inRoom || !isReady) return false; // Only actually send messages when in game and ready + + if (options == null) options = RaiseEventOptions.Default; + + return net.OpRaiseEvent(eventCode, eventContent, sendReliable, options); + } + + /// + /// Get the local player id. if isOnline isn't true, this will be -1 + /// + public static int localID { + get { + if (net == null) return 0; + + return net.LocalPlayer.ID; + } + } + + #endregion + + public static event System.Action netHook; + public static event System.Action onPropChanged; + public static event System.Action onLeave; + public static event System.Action onJoin; + + private static NetworkManager _instance; + public static NetworkManager instance { + get { + if (_instance) return _instance; + _instance = FindObjectOfType(); + if (_instance) return _instance; + throw new System.Exception("Network manager not instanced in scene"); + } + set { + _instance = value; + } + } + + public void Awake() { + if (_instance != null) { + Destroy(gameObject); + return; + } + + // Debug.Log("Aweak") + + instance = this; + DontDestroyOnLoad(gameObject); + + // Initialize network + net = new NetLogic(); + } + + void OnDestroy() { + if (net == null) return; + + // Only do service + if (_instance == this) { + net.Service(); // Service before disconnect to clear any blockers + net.Disconnect(); + } + } + + void Update() { + net.Service(); + } + + + void LateUpdate () { + net.Service(); + } + + public class NetLogic : LoadBalancingClient { + public NetLogic() { + // Setup and launch network service + AppId = NetworkManager.instance.appID; + AppVersion = NetworkManager.instance.gameVersion; + + AutoJoinLobby = true; + + // Register custom type handlers + StreamCustomTypes.Register(); + +#if UNITY_WEBGL + Debug.Log("Using secure websockets"); + this.TransportProtocol = ConnectionProtocol.WebSocketSecure; +#endif + + } + + public event System.Action GamelistRefresh; + + public override void OnEvent(EventData photonEvent) { + base.OnEvent(photonEvent); + + switch(photonEvent.Code) { + case EventCode.GameList: + case EventCode.GameListUpdate: + Debug.Log("Server List recieved"); + if (GamelistRefresh != null) GamelistRefresh(); + break; + case EventCode.PropertiesChanged: + onPropChanged?.Invoke(photonEvent); + break; + case EventCode.Join: + onJoin?.Invoke(photonEvent); + break; + case EventCode.Leave: + onLeave?.Invoke(photonEvent); + break; + } + + netHook?.Invoke(photonEvent); + + } + + /// + /// Joins a specific room by name. If the room doesn't exist (yet), it will be created implicitiy. + /// Creates custom properties automatically for the room. + /// + /// + /// + /// + /// + /// + public bool OpJoinOrCreateRoomWithProperties(string roomName, RoomOptions options, TypedLobby lobby) { + PlayerProperties.CreatePlayerHashtable(); + options.CustomRoomProperties = RoomProperties.GetRoomHashtable(); + + return OpJoinOrCreateRoom(roomName, options, lobby); + } + + public bool OpCreateRoomWithProperties(string roomName, RoomOptions options, TypedLobby lobby) { + PlayerProperties.CreatePlayerHashtable(); + options.CustomRoomProperties = RoomProperties.GetRoomHashtable(); + + return OpCreateRoom(roomName, options, lobby); + } + + public bool OpJoinRoomWithProperties(string roomName) { + PlayerProperties.CreatePlayerHashtable(); + + return OpJoinRoom(roomName); + } + + } +} + diff --git a/Assets/Runtime/Networking/NetworkManager.cs.meta b/Assets/Runtime/Networking/NetworkManager.cs.meta new file mode 100644 index 0000000..e39217b --- /dev/null +++ b/Assets/Runtime/Networking/NetworkManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ca2d3b1e793ff9643b979899bd8add03 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Networking/NetworkManager.prefab b/Assets/Runtime/Networking/NetworkManager.prefab new file mode 100644 index 0000000..a632140 --- /dev/null +++ b/Assets/Runtime/Networking/NetworkManager.prefab @@ -0,0 +1,336 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &925392911 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 925392912} + - component: {fileID: 925392914} + - component: {fileID: 925392913} + m_Layer: 0 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &925392912 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 925392911} + 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_Children: [] + m_Father: {fileID: 2129360324} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.9} + m_AnchorMax: {x: 0.5, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &925392914 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 925392911} + m_CullTransparentMesh: 0 +--- !u!114 &925392913 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 925392911} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, + Version=1.0.0.0, Culture=neutral, PublicKeyToken=null + m_text: In game + m_isRightToLeft: 0 + m_fontAsset: {fileID: 0} + m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4294967295 + m_fontColor: {r: 1, g: 1, b: 1, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_outlineColor: + serializedVersion: 2 + rgba: 4278190080 + m_fontSize: 96.65 + m_fontSizeBase: 36 + m_fontWeight: 400 + m_enableAutoSizing: 1 + m_fontSizeMin: 18 + m_fontSizeMax: 300 + m_fontStyle: 0 + m_textAlignment: 257 + m_characterSpacing: 0 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_enableWordWrapping: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_firstOverflowCharacterIndex: -1 + m_linkedTextComponent: {fileID: 0} + m_isLinkedTextComponent: 0 + m_isTextTruncated: 0 + m_enableKerning: 1 + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_ignoreRectMaskCulling: 0 + m_ignoreCulling: 1 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_VertexBufferAutoSizeReduction: 1 + m_firstVisibleCharacter: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_textInfo: + textComponent: {fileID: 925392913} + characterCount: 7 + spriteCount: 0 + spaceCount: 1 + wordCount: 2 + linkCount: 0 + lineCount: 1 + pageCount: 1 + materialCount: 1 + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_spriteAnimator: {fileID: 0} + m_hasFontAssetChanged: 0 + m_subTextObjects: + - {fileID: 0} + - {fileID: 0} + - {fileID: 0} + - {fileID: 0} + - {fileID: 0} + - {fileID: 0} + - {fileID: 0} + - {fileID: 0} + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} +--- !u!1 &2129360323 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2129360324} + - component: {fileID: 2129360327} + - component: {fileID: 2129360326} + - component: {fileID: 2129360325} + m_Layer: 0 + m_Name: Canvas + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2129360324 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2129360323} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 0, y: 0, z: 0} + m_Children: + - {fileID: 925392912} + m_Father: {fileID: 4214266351047742} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0, y: 0} +--- !u!223 &2129360327 +Canvas: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2129360323} + m_Enabled: 0 + serializedVersion: 3 + m_RenderMode: 0 + m_Camera: {fileID: 0} + m_PlaneDistance: 100 + m_PixelPerfect: 0 + m_ReceivesEvents: 1 + m_OverrideSorting: 0 + m_OverridePixelPerfect: 0 + m_SortingBucketNormalizedSize: 0 + m_AdditionalShaderChannelsFlag: 25 + m_SortingLayerID: 0 + m_SortingOrder: 0 + m_TargetDisplay: 0 +--- !u!114 &2129360326 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2129360323} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 1980459831, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_UiScaleMode: 0 + m_ReferencePixelsPerUnit: 100 + m_ScaleFactor: 1 + m_ReferenceResolution: {x: 800, y: 600} + m_ScreenMatchMode: 0 + m_MatchWidthOrHeight: 0 + m_PhysicalUnit: 3 + m_FallbackScreenDPI: 96 + m_DefaultSpriteDPI: 96 + m_DynamicPixelsPerUnit: 1 +--- !u!114 &2129360325 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2129360323} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 1301386320, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreReversedGraphics: 1 + m_BlockingObjects: 0 + m_BlockingMask: + serializedVersion: 2 + m_Bits: 4294967295 +--- !u!1 &1645469224734478 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4214266351047742} + - component: {fileID: 114738351746576220} + - component: {fileID: -8397950358197448299} + - component: {fileID: 5367125707474810989} + m_Layer: 0 + m_Name: NetworkManager + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &4214266351047742 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1645469224734478} + 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_Children: + - {fileID: 2129360324} + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &114738351746576220 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1645469224734478} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: ca2d3b1e793ff9643b979899bd8add03, type: 3} + m_Name: + m_EditorClassIdentifier: + serverAddress: + appID: f0c408f8-1fc7-43d9-8bfe-59156f86a6e8 + gameVersion: .1 + NetworkDebugParticles: {fileID: 0} + expectedOnline: 1 + expectedMaxPlayers: 2 +--- !u!114 &-8397950358197448299 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1645469224734478} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9a99da8b191429648a69535f4bf2e0d3, type: 3} + m_Name: + m_EditorClassIdentifier: + currentState: 0 +--- !u!114 &5367125707474810989 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1645469224734478} + m_Enabled: 0 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: ce8aa006a722b1048adbee6308ab38ff, type: 3} + m_Name: + m_EditorClassIdentifier: + allowQuickJoin: 1 diff --git a/Assets/Runtime/Networking/NetworkManager.prefab.meta b/Assets/Runtime/Networking/NetworkManager.prefab.meta new file mode 100644 index 0000000..1473c77 --- /dev/null +++ b/Assets/Runtime/Networking/NetworkManager.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: ad49179d7bafd344d948ab9643c4fdad +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Networking/NetworkManagerDebug.cs b/Assets/Runtime/Networking/NetworkManagerDebug.cs new file mode 100644 index 0000000..c068be0 --- /dev/null +++ b/Assets/Runtime/Networking/NetworkManagerDebug.cs @@ -0,0 +1,17 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +public class NetworkManagerDebug : MonoBehaviour { + // Debug out the current state for visibility + [Header("Current state")] + public ExitGames.Client.Photon.LoadBalancing.ClientState currentState; + + private void Update() { + if (NetworkManager.net != null) { + currentState = NetworkManager.net.State; + + GetComponentInChildren().text = currentState.ToString(); + } + } +} diff --git a/Assets/Runtime/Networking/NetworkManagerDebug.cs.meta b/Assets/Runtime/Networking/NetworkManagerDebug.cs.meta new file mode 100644 index 0000000..edce091 --- /dev/null +++ b/Assets/Runtime/Networking/NetworkManagerDebug.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9a99da8b191429648a69535f4bf2e0d3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Networking/NetworkManagerDummy.prefab b/Assets/Runtime/Networking/NetworkManagerDummy.prefab new file mode 100644 index 0000000..ccd683b --- /dev/null +++ b/Assets/Runtime/Networking/NetworkManagerDummy.prefab @@ -0,0 +1,284 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &1371084334 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1371084335} + - component: {fileID: 1371084339} + - component: {fileID: 1371084338} + - component: {fileID: 1371084337} + - component: {fileID: 1371084336} + m_Layer: 0 + m_Name: GameObject + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1371084335 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1371084334} + 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_Children: [] + m_Father: {fileID: 4214266351047742} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: 3} + m_SizeDelta: {x: 20, y: 5} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!23 &1371084339 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1371084334} + m_Enabled: 1 + m_CastShadows: 0 + m_ReceiveShadows: 0 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, 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: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + 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 +--- !u!33 &1371084338 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1371084334} + m_Mesh: {fileID: 0} +--- !u!222 &1371084337 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1371084334} + m_CullTransparentMesh: 0 +--- !u!114 &1371084336 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1371084334} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9541d86e2fd84c1d9990edf0852d74ab, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: State + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4294967295 + m_fontColor: {r: 1, g: 1, b: 1, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_outlineColor: + serializedVersion: 2 + rgba: 4278190080 + m_fontSize: 18 + m_fontSizeBase: 18 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 0 + m_textAlignment: 260 + m_characterSpacing: 0 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_enableWordWrapping: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_firstOverflowCharacterIndex: -1 + m_linkedTextComponent: {fileID: 0} + m_isLinkedTextComponent: 0 + m_isTextTruncated: 0 + m_enableKerning: 1 + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 0 + m_isCullingEnabled: 0 + m_ignoreRectMaskCulling: 0 + m_ignoreCulling: 1 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_VertexBufferAutoSizeReduction: 1 + m_firstVisibleCharacter: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_textInfo: + textComponent: {fileID: 1371084336} + characterCount: 5 + spriteCount: 0 + spaceCount: 0 + wordCount: 1 + linkCount: 0 + lineCount: 1 + pageCount: 1 + materialCount: 1 + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_spriteAnimator: {fileID: 0} + m_hasFontAssetChanged: 0 + m_renderer: {fileID: 1371084339} + m_subTextObjects: + - {fileID: 0} + - {fileID: 0} + - {fileID: 0} + - {fileID: 0} + - {fileID: 0} + - {fileID: 0} + - {fileID: 0} + - {fileID: 0} + m_maskType: 0 +--- !u!1 &1645469224734478 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4214266351047742} + - component: {fileID: 114738351746576220} + - component: {fileID: -8397950358197448299} + - component: {fileID: 14614541} + m_Layer: 0 + m_Name: NetworkManagerDummy + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &4214266351047742 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1645469224734478} + 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_Children: + - {fileID: 1371084335} + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &114738351746576220 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1645469224734478} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: ca2d3b1e793ff9643b979899bd8add03, type: 3} + m_Name: + m_EditorClassIdentifier: + serverAddress: + appID: 7c0120c9-54c3-4025-b11f-b7b153d681ed + gameVersion: .1 + NetworkDebugParticles: {fileID: 0} + expectedOnline: 0 + expectedMaxPlayers: 2 +--- !u!114 &-8397950358197448299 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1645469224734478} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9a99da8b191429648a69535f4bf2e0d3, type: 3} + m_Name: + m_EditorClassIdentifier: + currentState: 0 +--- !u!114 &14614541 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1645469224734478} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: ce8aa006a722b1048adbee6308ab38ff, type: 3} + m_Name: + m_EditorClassIdentifier: + allowQuickJoin: 1 diff --git a/Assets/Runtime/Networking/NetworkManagerDummy.prefab.meta b/Assets/Runtime/Networking/NetworkManagerDummy.prefab.meta new file mode 100644 index 0000000..1a2dc25 --- /dev/null +++ b/Assets/Runtime/Networking/NetworkManagerDummy.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 41564653a5d28dc4491420399b220d74 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Networking/QuickJoin.cs b/Assets/Runtime/Networking/QuickJoin.cs new file mode 100644 index 0000000..d3639eb --- /dev/null +++ b/Assets/Runtime/Networking/QuickJoin.cs @@ -0,0 +1,63 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEngine.SceneManagement; +using TMPro; +using ExitGames.Client.Photon.LoadBalancing; + +public class QuickJoin : MonoBehaviour { + // Update is called once per frame + public bool allowQuickJoin = true; + + void Update () { + if (allowQuickJoin && (Input.GetKey(KeyCode.LeftAlt) || Input.GetKey(KeyCode.RightAlt)) && Input.GetKeyDown(KeyCode.J)) { + if (NetworkManager.inRoom) return; + + var activeScene = "QUICKJOIN"; + + var ro = new RoomOptions(); + ro.IsVisible = false; + ro.IsOpen = true; + ro.MaxPlayers = NetworkManager.instance.expectedMaxPlayers; + + NetworkManager.net.OpJoinOrCreateRoomWithProperties(activeScene, ro, null); + } + } + + IEnumerator Start() { + while (!allowQuickJoin) yield return null; + + if (NetworkManager.net.ConnectToNameServer()){ + Debug.Log("Connecting to name server"); + } else { + Debug.Log("Name Server connection failed"); + yield break; + } + + while (!NetworkManager.onNameServer || !NetworkManager.isReady) yield return null; + Debug.Log("Connected to name server"); + + if (NetworkManager.net.OpGetRegions()){ + Debug.Log("Started region request"); + } else { + Debug.Log("Failed region request"); + yield break; + } + + while (NetworkManager.net.AvailableRegions == null) yield return null; + Debug.Log("Received region list"); + + if(NetworkManager.net.ConnectToRegionMaster("usw")){ + Debug.Log("Connecting to region master 'usw'"); + } else { + Debug.Log("Failed to connect to region master 'usw'"); + yield break; + } + + while (!NetworkManager.onMasterLobby) yield return null; + Debug.Log("Connected to region master"); + Debug.Log("You can quick join now"); + } + +} diff --git a/Assets/Runtime/Networking/QuickJoin.cs.meta b/Assets/Runtime/Networking/QuickJoin.cs.meta new file mode 100644 index 0000000..4fd5e7b --- /dev/null +++ b/Assets/Runtime/Networking/QuickJoin.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ce8aa006a722b1048adbee6308ab38ff +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Networking/RoomCode.prefab b/Assets/Runtime/Networking/RoomCode.prefab new file mode 100644 index 0000000..d3566fd --- /dev/null +++ b/Assets/Runtime/Networking/RoomCode.prefab @@ -0,0 +1,351 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &1108792484905544 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 224112458976610374} + - component: {fileID: 222721656503737066} + - component: {fileID: 114096038084053498} + m_Layer: 5 + m_Name: Image + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 0 +--- !u!224 &224112458976610374 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1108792484905544} + 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_Children: + - {fileID: 224945633132606452} + m_Father: {fileID: 224249934373868544} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.9} + m_AnchorMax: {x: 0.15, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &222721656503737066 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1108792484905544} + m_CullTransparentMesh: 0 +--- !u!114 &114096038084053498 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1108792484905544} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 0, b: 0, a: 1} + m_RaycastTarget: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, + Version=1.0.0.0, Culture=neutral, PublicKeyToken=null + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 +--- !u!1 &1569014553392932 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 224945633132606452} + - component: {fileID: 222745614857108624} + - component: {fileID: 114116331299675328} + m_Layer: 5 + m_Name: Title + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &224945633132606452 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1569014553392932} + 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_Children: [] + m_Father: {fileID: 224112458976610374} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &222745614857108624 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1569014553392932} + m_CullTransparentMesh: 0 +--- !u!114 &114116331299675328 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1569014553392932} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, + Version=1.0.0.0, Culture=neutral, PublicKeyToken=null + m_text: ABCD + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_sharedMaterial: {fileID: 0} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4294967295 + m_fontColor: {r: 1, g: 1, b: 1, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_outlineColor: + serializedVersion: 2 + rgba: 4278190080 + m_fontSize: 58 + m_fontSizeBase: 175 + m_fontWeight: 400 + m_enableAutoSizing: 1 + m_fontSizeMin: 50 + m_fontSizeMax: 500 + m_fontStyle: 0 + m_textAlignment: 258 + m_isAlignmentEnumConverted: 1 + m_characterSpacing: 0 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_enableWordWrapping: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_firstOverflowCharacterIndex: -1 + m_linkedTextComponent: {fileID: 0} + m_isLinkedTextComponent: 0 + m_isTextTruncated: 0 + m_enableKerning: 1 + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_ignoreRectMaskCulling: 0 + m_ignoreCulling: 1 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_firstVisibleCharacter: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_textInfo: + textComponent: {fileID: 0} + characterCount: 4 + spriteCount: 0 + spaceCount: 0 + wordCount: 1 + linkCount: 0 + lineCount: 1 + pageCount: 1 + materialCount: 1 + m_havePropertiesChanged: 0 + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_spriteAnimator: {fileID: 0} + m_isInputParsingRequired: 0 + m_inputSource: 0 + m_hasFontAssetChanged: 0 + m_subTextObjects: + - {fileID: 0} + - {fileID: 0} + - {fileID: 0} + - {fileID: 0} + - {fileID: 0} + - {fileID: 0} + - {fileID: 0} + - {fileID: 0} + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} +--- !u!1 &1935942590280396 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 224249934373868544} + - component: {fileID: 223742609007803358} + - component: {fileID: 114955499652235416} + - component: {fileID: 114786950351237876} + - component: {fileID: 6963261912793562539} + m_Layer: 5 + m_Name: RoomCode + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &224249934373868544 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1935942590280396} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 0, y: 0, z: 0} + m_Children: + - {fileID: 224112458976610374} + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0, y: 0} +--- !u!223 &223742609007803358 +Canvas: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1935942590280396} + m_Enabled: 1 + serializedVersion: 3 + m_RenderMode: 0 + m_Camera: {fileID: 0} + m_PlaneDistance: 100 + m_PixelPerfect: 0 + m_ReceivesEvents: 1 + m_OverrideSorting: 0 + m_OverridePixelPerfect: 0 + m_SortingBucketNormalizedSize: 0 + m_AdditionalShaderChannelsFlag: 25 + m_SortingLayerID: 0 + m_SortingOrder: 6 + m_TargetDisplay: 0 +--- !u!114 &114955499652235416 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1935942590280396} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 1980459831, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_UiScaleMode: 0 + m_ReferencePixelsPerUnit: 100 + m_ScaleFactor: 1 + m_ReferenceResolution: {x: 800, y: 600} + m_ScreenMatchMode: 0 + m_MatchWidthOrHeight: 0 + m_PhysicalUnit: 3 + m_FallbackScreenDPI: 96 + m_DefaultSpriteDPI: 96 + m_DynamicPixelsPerUnit: 1 +--- !u!114 &114786950351237876 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1935942590280396} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 1301386320, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreReversedGraphics: 1 + m_BlockingObjects: 0 + m_BlockingMask: + serializedVersion: 2 + m_Bits: 4294967295 +--- !u!114 &6963261912793562539 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1935942590280396} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 07421fd6321565a4e8aebdebfd61c79c, type: 3} + m_Name: + m_EditorClassIdentifier: + Display: {fileID: 1108792484905544} + Text: {fileID: 114116331299675328} diff --git a/Assets/Runtime/Networking/RoomCode.prefab.meta b/Assets/Runtime/Networking/RoomCode.prefab.meta new file mode 100644 index 0000000..254a3b4 --- /dev/null +++ b/Assets/Runtime/Networking/RoomCode.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 7857daa21c365d749b825ce14546bc09 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Networking/RoomCodeDisplay.cs b/Assets/Runtime/Networking/RoomCodeDisplay.cs new file mode 100644 index 0000000..6320309 --- /dev/null +++ b/Assets/Runtime/Networking/RoomCodeDisplay.cs @@ -0,0 +1,19 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +using TMPro; + +public class RoomCodeDisplay : MonoBehaviour { + + public TextMeshProUGUI Text; + + void Start() { + if (NetworkManager.inRoom){ + gameObject.SetActive(true); + Text.text = NetworkManager.net.CurrentRoom.Name; + } else { + gameObject.SetActive(false); + } + } +} diff --git a/Assets/Runtime/Networking/RoomCodeDisplay.cs.meta b/Assets/Runtime/Networking/RoomCodeDisplay.cs.meta new file mode 100644 index 0000000..7ad921c --- /dev/null +++ b/Assets/Runtime/Networking/RoomCodeDisplay.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 07421fd6321565a4e8aebdebfd61c79c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Photon.meta b/Assets/Runtime/Photon.meta new file mode 100644 index 0000000..9babe68 --- /dev/null +++ b/Assets/Runtime/Photon.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b45dd90f2a7976d4ab982b1f5d5986ad +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Photon/PhotonLoadbalancingApi.meta b/Assets/Runtime/Photon/PhotonLoadbalancingApi.meta new file mode 100644 index 0000000..b6c62d3 --- /dev/null +++ b/Assets/Runtime/Photon/PhotonLoadbalancingApi.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8a4408e5f601f774eb5f3fb98b07f009 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Photon/PhotonLoadbalancingApi/Extensions.cs b/Assets/Runtime/Photon/PhotonLoadbalancingApi/Extensions.cs new file mode 100644 index 0000000..d0b959e --- /dev/null +++ b/Assets/Runtime/Photon/PhotonLoadbalancingApi/Extensions.cs @@ -0,0 +1,180 @@ +// ---------------------------------------------------------------------------- +// +// Photon Extensions - Copyright (C) 2017 Exit Games GmbH +// +// +// Provides some helpful methods and extensions for Hashtables, etc. +// +// developer@photonengine.com +// ---------------------------------------------------------------------------- + +#if UNITY_4_7_OR_NEWER +#define UNITY +#endif + + +namespace ExitGames.Client.Photon.LoadBalancing +{ + using System.Collections; + + #if UNITY + using UnityEngine; + using Debug = UnityEngine.Debug; + #endif + #if UNITY || NETFX_CORE + using Hashtable = ExitGames.Client.Photon.Hashtable; + using SupportClass = ExitGames.Client.Photon.SupportClass; + #endif + + + /// + /// This static class defines some useful extension methods for several existing classes (e.g. Vector3, float and others). + /// + public static class Extensions + { + /// + /// Merges all keys from addHash into the target. Adds new keys and updates the values of existing keys in target. + /// + /// The IDictionary to update. + /// The IDictionary containing data to merge into target. + public static void Merge(this IDictionary target, IDictionary addHash) + { + if (addHash == null || target.Equals(addHash)) + { + return; + } + + foreach (object key in addHash.Keys) + { + target[key] = addHash[key]; + } + } + + /// + /// Merges keys of type string to target Hashtable. + /// + /// + /// Does not remove keys from target (so non-string keys CAN be in target if they were before). + /// + /// The target IDicitionary passed in plus all string-typed keys from the addHash. + /// A IDictionary that should be merged partly into target to update it. + public static void MergeStringKeys(this IDictionary target, IDictionary addHash) + { + if (addHash == null || target.Equals(addHash)) + { + return; + } + + foreach (object key in addHash.Keys) + { + // only merge keys of type string + if (key is string) + { + target[key] = addHash[key]; + } + } + } + + /// Helper method for debugging of IDictionary content, inlcuding type-information. Using this is not performant. + /// Should only be used for debugging as necessary. + /// Some Dictionary or Hashtable. + /// String of the content of the IDictionary. + public static string ToStringFull(this IDictionary origin) + { + return SupportClass.DictionaryToString(origin, false); + } + + + /// Helper method for debugging of object[] content. Using this is not performant. + /// Should only be used for debugging as necessary. + /// Any object[]. + /// A comma-separated string containing each value's ToString(). + public static string ToStringFull(this object[] data) + { + if (data == null) return "null"; + + string[] sb = new string[data.Length]; + for (int i = 0; i < data.Length; i++) + { + object o = data[i]; + sb[i] = (o != null) ? o.ToString() : "null"; + } + + return string.Join(", ", sb); + } + + + /// + /// This method copies all string-typed keys of the original into a new Hashtable. + /// + /// + /// Does not recurse (!) into hashes that might be values in the root-hash. + /// This does not modify the original. + /// + /// The original IDictonary to get string-typed keys from. + /// New Hashtable containing only string-typed keys of the original. + public static Hashtable StripToStringKeys(this IDictionary original) + { + Hashtable target = new Hashtable(); + if (original != null) + { + foreach (object key in original.Keys) + { + if (key is string) + { + target[key] = original[key]; + } + } + } + + return target; + } + + /// + /// This removes all key-value pairs that have a null-reference as value. + /// Photon properties are removed by setting their value to null. + /// Changes the original passed IDictionary! + /// + /// The IDictionary to strip of keys with null-values. + public static void StripKeysWithNullValues(this IDictionary original) + { + object[] keys = new object[original.Count]; + original.Keys.CopyTo(keys, 0); + + for (int index = 0; index < keys.Length; index++) + { + var key = keys[index]; + if (original[key] == null) + { + original.Remove(key); + } + } + } + + /// + /// Checks if a particular integer value is in an int-array. + /// + /// This might be useful to look up if a particular actorNumber is in the list of players of a room. + /// The array of ints to check. + /// The number to lookup in target. + /// True if nr was found in target. + public static bool Contains(this int[] target, int nr) + { + if (target == null) + { + return false; + } + + for (int index = 0; index < target.Length; index++) + { + if (target[index] == nr) + { + return true; + } + } + + return false; + } + } +} + diff --git a/Assets/Runtime/Photon/PhotonLoadbalancingApi/Extensions.cs.meta b/Assets/Runtime/Photon/PhotonLoadbalancingApi/Extensions.cs.meta new file mode 100644 index 0000000..a3e17aa --- /dev/null +++ b/Assets/Runtime/Photon/PhotonLoadbalancingApi/Extensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ee1c48ab6ff903f4c823e7cf48b423e5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Photon/PhotonLoadbalancingApi/FriendInfo.cs b/Assets/Runtime/Photon/PhotonLoadbalancingApi/FriendInfo.cs new file mode 100644 index 0000000..1d9b03d --- /dev/null +++ b/Assets/Runtime/Photon/PhotonLoadbalancingApi/FriendInfo.cs @@ -0,0 +1,45 @@ +// ---------------------------------------------------------------------------- +// +// Loadbalancing Framework for Photon - Copyright (C) 2013 Exit Games GmbH +// +// +// Collection of values related to a user / friend. +// +// developer@photonengine.com +// ---------------------------------------------------------------------------- + + + +#if UNITY_4_7_OR_NEWER +#define UNITY +#endif + + +namespace ExitGames.Client.Photon.LoadBalancing +{ + #if UNITY || NETFX_CORE + using Hashtable = ExitGames.Client.Photon.Hashtable; + using SupportClass = ExitGames.Client.Photon.SupportClass; + #endif + + + /// + /// Used to store info about a friend's online state and in which room he/she is. + /// + public class FriendInfo + { + public string Name { get; internal protected set; } + public bool IsOnline { get; internal protected set; } + public string Room { get; internal protected set; } + + public bool IsInRoom + { + get { return this.IsOnline && !string.IsNullOrEmpty(this.Room); } + } + + public override string ToString() + { + return string.Format("{0}\t is: {1}", this.Name, (!this.IsOnline) ? "offline" : this.IsInRoom ? "playing" : "on master"); + } + } +} diff --git a/Assets/Runtime/Photon/PhotonLoadbalancingApi/FriendInfo.cs.meta b/Assets/Runtime/Photon/PhotonLoadbalancingApi/FriendInfo.cs.meta new file mode 100644 index 0000000..ed679ed --- /dev/null +++ b/Assets/Runtime/Photon/PhotonLoadbalancingApi/FriendInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 723f710efc79ee0469dbe94bf0cc9ddb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Photon/PhotonLoadbalancingApi/LoadBalancingClient.cs b/Assets/Runtime/Photon/PhotonLoadbalancingApi/LoadBalancingClient.cs new file mode 100644 index 0000000..5275eeb --- /dev/null +++ b/Assets/Runtime/Photon/PhotonLoadbalancingApi/LoadBalancingClient.cs @@ -0,0 +1,2582 @@ +// ----------------------------------------------------------------------- +// +// Loadbalancing Framework for Photon - Copyright (C) 2011 Exit Games GmbH +// +// +// Provides the operations and a state for games using the +// Photon LoadBalancing server. +// +// developer@photonengine.com +// ---------------------------------------------------------------------------- + + +#if UNITY_4_7_OR_NEWER +#define UNITY +#endif + +namespace ExitGames.Client.Photon.LoadBalancing +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Diagnostics; + + #if UNITY + using UnityEngine; + using Debug = UnityEngine.Debug; + #endif + #if UNITY || NETFX_CORE + using Hashtable = ExitGames.Client.Photon.Hashtable; + using SupportClass = ExitGames.Client.Photon.SupportClass; + #endif + + + #region Enums + + /// + /// State values for a client, which handles switching Photon server types, some operations, etc. + /// + /// \ingroup publicApi + public enum ClientState + { + /// Peer is created but not used yet. + PeerCreated, + + /// Transition state while connecting to a server. On the Photon Cloud this sends the AppId and AuthenticationValues (UserID). + Authenticating, + + /// Transition state while connecting to a server. Leads to state ConnectedToMasterserver or JoinedLobby. + Authenticated, + + /// The client is in a lobby, connected to the MasterServer. Depending on the lobby, it gets room listings. + JoinedLobby, + + /// Transition from MasterServer to GameServer. + DisconnectingFromMasterserver, + + /// Transition to GameServer (client authenticates and joins/creates a room). + ConnectingToGameserver, + + /// Connected to GameServer (going to auth and join game). + ConnectedToGameserver, + + /// Transition state while joining or creating a room on GameServer. + Joining, + + /// The client entered a room. The CurrentRoom and Players are known and you can now raise events. + Joined, + + /// Transition state when leaving a room. + Leaving, + + /// Transition from GameServer to MasterServer (after leaving a room/game). + DisconnectingFromGameserver, + + /// Connecting to MasterServer (includes sending authentication values). + ConnectingToMasterserver, + + /// The client disconnects (from any server). This leads to state Disconnected. + Disconnecting, + + /// The client is no longer connected (to any server). Connect to MasterServer to go on. + Disconnected, + + /// Connected to MasterServer. You might use matchmaking or join a lobby now. + ConnectedToMasterserver, + + /// Connected to MasterServer. You might use matchmaking or join a lobby now. + [Obsolete("Renamed to ConnectedToMasterserver.")] + ConnectedToMaster = ConnectedToMasterserver, + + /// Client connects to the NameServer. This process includes low level connecting and setting up encryption. When done, state becomes ConnectedToNameServer. + ConnectingToNameServer, + + /// Client is connected to the NameServer and established enctryption already. You should call OpGetRegions or ConnectToRegionMaster. + ConnectedToNameServer, + + /// Clients disconnects (specifically) from the NameServer (usually to connect to the MasterServer). + DisconnectingFromNameServer + } + + + /// + /// Internal state, how this peer gets into a particular room (joining it or creating it). + /// + internal enum JoinType + { + /// This client creates a room, gets into it (no need to join) and can set room properties. + CreateRoom, + /// The room existed already and we join into it (not setting room properties). + JoinRoom, + /// Done on Master Server and (if successful) followed by a Join on Game Server. + JoinRandomRoom, + /// Client is either joining or creating a room. On Master- and Game-Server. + JoinOrCreateRoom + } + + /// Enumaration of causes for Disconnects (used in LoadBalancingClient.DisconnectedCause). + /// Read the individual descriptions to find out what to do about this type of disconnect. + public enum DisconnectCause + { + /// No error was tracked. + None, + /// OnStatusChanged: The CCUs count of your Photon Server License is exausted (temporarily). + DisconnectByServerUserLimit, + /// OnStatusChanged: The server is not available or the address is wrong. Make sure the port is provided and the server is up. + ExceptionOnConnect, + /// OnStatusChanged: The server disconnected this client. Most likely the server's send buffer is full (receiving too much from other clients). + DisconnectByServer, + /// OnStatusChanged: This client detected that the server's responses are not received in due time. Maybe you send / receive too much? + TimeoutDisconnect, + /// OnStatusChanged: Some internal exception caused the socket code to fail. Contact Exit Games. + Exception, + /// OnOperationResponse: Authenticate in the Photon Cloud with invalid AppId. Update your subscription or contact Exit Games. + InvalidAuthentication, + /// OnOperationResponse: Authenticate (temporarily) failed when using a Photon Cloud subscription without CCU Burst. Update your subscription. + MaxCcuReached, + /// OnOperationResponse: Authenticate when the app's Photon Cloud subscription is locked to some (other) region(s). Update your subscription or master server address. + InvalidRegion, + /// OnOperationResponse: Operation that's (currently) not available for this client (not authorized usually). Only tracked for op Authenticate. + OperationNotAllowedInCurrentState, + /// OnOperationResponse: Authenticate in the Photon Cloud with invalid client values or custom authentication setup in Cloud Dashboard. + CustomAuthenticationFailed, + /// OnStatusChanged: The server disconnected this client from within the room's logic (the C# code). + DisconnectByServerLogic, + /// The authentication ticket should provide access to any Photon Cloud server without doing another authentication-service call. However, the ticket expired. + AuthenticationTicketExpired + } + + /// Available server (types) for internally used field: server. + /// Photon uses 3 different roles of servers: Name Server, Master Server and Game Server. + public enum ServerConnection + { + /// This server is where matchmaking gets done and where clients can get lists of rooms in lobbies. + MasterServer, + /// This server handles a number of rooms to execute and relay the messages between players (in a room). + GameServer, + /// This server is used initially to get the address (IP) of a Master Server for a specific region. Not used for Photon OnPremise (self hosted). + NameServer + } + + /// + /// Defines how the communication gets encrypted. + /// + public enum EncryptionMode + { + /// + /// This is the default encryption mode: Messages get encrypted only on demand (when you send operations with the "encrypt" parameter set to true). + /// + PayloadEncryption, + /// + /// With this encryption mode for UDP, the connection gets setup and all further datagrams get encrypted almost entirely. On-demand message encryption (like in PayloadEncryption) is skipped. + /// + DatagramEncryption = 10, + } + + + public static class EncryptionDataParameters + { + /// + /// Key for encryption mode + /// + public const byte Mode = 0; + /// + /// Key for first secret + /// + public const byte Secret1 = 1; + /// + /// Key for second secret + /// + public const byte Secret2 = 2; + } + #endregion + + /// + /// This class implements the Photon LoadBalancing workflow by using a LoadBalancingPeer. + /// It keeps a state and will automatically execute transitions between the Master and Game Servers. + /// + /// + /// This class (and the Player class) should be extended to implement your own game logic. + /// You can override CreatePlayer as "factory" method for Players and return your own Player instances. + /// The State of this class is essential to know when a client is in a lobby (or just on the master) + /// and when in a game where the actual gameplay should take place. + /// Extension notes: + /// An extension of this class should override the methods of the IPhotonPeerListener, as they + /// are called when the state changes. Call base.method first, then pick the operation or state you + /// want to react to and put it in a switch-case. + /// We try to provide demo to each platform where this api can be used, so lookout for those. + /// + public class LoadBalancingClient : IPhotonPeerListener + { + /// + /// The client uses a LoadBalancingPeer as API to communicate with the server. + /// This is public for ease-of-use: Some methods like OpRaiseEvent are not relevant for the connection state and don't need a override. + /// + public LoadBalancingPeer loadBalancingPeer; + + /// The version of your client. A new version also creates a new "virtual app" to separate players from older client versions. + public string AppVersion { get; set; } + + /// The AppID as assigned from the Photon Cloud. If you host yourself, this is the "regular" Photon Server Application Name (most likely: "LoadBalancing"). + public string AppId { get; set; } + + /// A user's authentication values for authentication in Photon. + /// Set this property or pass AuthenticationValues by Connect(..., authValues). + public AuthenticationValues AuthValues { get; set; } + + + /// Enables the new Authentication workflow. + public AuthModeOption AuthMode = AuthModeOption.Auth; + + /// Defines how the communication gets encrypted. + public EncryptionMode EncryptionMode = EncryptionMode.PayloadEncryption; + + /// The protocol which will be used on Master- and Gameserver. + /// + /// When using AuthOnceWss, the client uses a wss-connection on the Nameserver but another protocol on the other servers. + /// As the Nameserver sends an address, which is different per protocol, it needs to know the expected protocol. + /// + private ConnectionProtocol ExpectedProtocol = ConnectionProtocol.Udp; + + /// Exposes the TransportProtocol of the used PhotonPeer. Settable while not connected. + public ConnectionProtocol TransportProtocol + { + get { return this.loadBalancingPeer.TransportProtocol; } + set + { + if (this.loadBalancingPeer == null || this.loadBalancingPeer.PeerState != PeerStateValue.Disconnected) + { + this.DebugReturn(DebugLevel.WARNING, "Can't set TransportProtocol. Disconnect first! " + ((this.loadBalancingPeer != null) ? "PeerState: " + this.loadBalancingPeer.PeerState : "loadBalancingPeer is null.")); + return; + } + this.loadBalancingPeer.TransportProtocol = value; + } + } + + /// Defines which IPhotonSocket class to use per ConnectionProtocol. + /// + /// Several platforms have special Socket implementations and slightly different APIs. + /// To accomodate this, switching the socket implementation for a network protocol was made available. + /// By default, UDP and TCP have socket implementations assigned. + /// + /// You only need to set the SocketImplementationConfig once, after creating a PhotonPeer + /// and before connecting. If you switch the TransportProtocol, the correct implementation is being used. + /// + public Dictionary SocketImplementationConfig + { + get { return this.loadBalancingPeer.SocketImplementationConfig; } + } + + + ///Simplifies getting the token for connect/init requests, if this feature is enabled. + private string TokenForInit + { + get + { + if (this.AuthMode == AuthModeOption.Auth) + { + return null; + } + return (this.AuthValues != null) ? this.AuthValues.Token : null; + } + } + + + /// True if this client uses a NameServer to get the Master Server address. + public bool IsUsingNameServer { get; protected internal set; } + + /// Name Server Host Name for Photon Cloud. Without port and without any prefix. + public string NameServerHost = "ns.exitgames.com"; + + /// Name Server for HTTP connections to the Photon Cloud. Includes prefix and port. + public string NameServerHttp = "http://ns.exitgames.com:80/photon/n"; + + /// Name Server port per protocol (the UDP port is different than TCP, etc). + private static readonly Dictionary ProtocolToNameServerPort = new Dictionary() { { ConnectionProtocol.Udp, 5058 }, { ConnectionProtocol.Tcp, 4533 }, { ConnectionProtocol.WebSocket, 9093 }, { ConnectionProtocol.WebSocketSecure, 19093 } }; //, { ConnectionProtocol.RHttp, 6063 } }; + + /// Name Server Address for Photon Cloud (based on current protocol). You can use the default values and usually won't have to set this value. + public string NameServerAddress { get { return this.GetNameServerAddress(); } } + + /// The currently used server address (if any). The type of server is define by Server property. + public string CurrentServerAddress { get { return this.loadBalancingPeer.ServerAddress; } } + + + /// Your Master Server address. In PhotonCloud, call ConnectToRegionMaster() to find your Master Server. + /// + /// In the Photon Cloud, explicit definition of a Master Server Address is not best practice. + /// The Photon Cloud has a "Name Server" which redirects clients to a specific Master Server (per Region and AppId). + /// + public string MasterServerAddress { get; protected internal set; } + + /// The game server's address for a particular room. In use temporarily, as assigned by master. + public string GameServerAddress { get; protected internal set; } + + /// The server this client is currently connected or connecting to. + /// + /// Each server (NameServer, MasterServer, GameServer) allow some operations and reject others. + /// + public ServerConnection Server { get; private set; } + + /// Backing field for property. + private ClientState state = ClientState.PeerCreated; + + /// Current state this client is in. Careful: several states are "transitions" that lead to other states. + public ClientState State + { + get + { + return this.state; + } + + protected internal set + { + this.state = value; + if (OnStateChangeAction != null) OnStateChangeAction(this.state); + } + } + + /// Returns if this client is currently connected or connecting to some type of server. + /// This is even true while switching servers. Use IsConnectedAndReady to check only for those states that enable you to send Operations. + public bool IsConnected { get { return this.loadBalancingPeer != null && this.State != ClientState.PeerCreated && this.State != ClientState.Disconnected; } } + + + /// + /// A refined version of IsConnected which is true only if your connection to the server is ready to accept operations. + /// + /// + /// Which operations are available, depends on the Server. For example, the NameServer allows OpGetRegions which is not available anywhere else. + /// The MasterServer does not allow you to send events (OpRaiseEvent) and on the GameServer you are unable to join a lobby (OpJoinLobby). + /// Check which server you are on with PhotonNetwork.Server. + /// + public bool IsConnectedAndReady + { + get + { + if (this.loadBalancingPeer == null) + { + return false; + } + + switch (this.State) + { + case ClientState.PeerCreated: + case ClientState.Disconnected: + case ClientState.Disconnecting: + case ClientState.Authenticating: + case ClientState.ConnectingToGameserver: + case ClientState.ConnectingToMasterserver: + case ClientState.ConnectingToNameServer: + case ClientState.Joining: + case ClientState.Leaving: + return false; // we are not ready to execute any operations + } + + return true; + } + } + + + /// Register a method to be called when this client's ClientState gets set. + /// This can be useful to react to being connected, joined into a room, etc. + public event Action OnStateChangeAction; + + /// Register a method to be called when an event got dispatched. Gets called at the end of OnEvent(). + /// + /// This is an alternative to extending LoadBalancingClient to override OnEvent(). + /// + /// Note that OnEvent is executing before your Action is called. + /// That means for example: Joining players will already be in the player list but leaving + /// players will already be removed from the room. + /// + public event Action OnEventAction; + + /// Register a method to be called when this client's ClientState gets set. + /// + /// This is an alternative to extending LoadBalancingClient to override OnOperationResponse(). + /// + /// Note that OnOperationResponse gets executed before your Action is called. + /// That means for example: The OpJoinLobby response already set the state to "JoinedLobby" + /// and the response to OpLeave already triggered the Disconnect before this is called. + /// + public event Action OnOpResponseAction; + + + /// Summarizes (aggregates) the different causes for disconnects of a client. + /// + /// A disconnect can be caused by: errors in the network connection or some vital operation failing + /// (which is considered "high level"). While operations always trigger a call to OnOperationResponse, + /// connection related changes are treated in OnStatusChanged. + /// The DisconnectCause is set in either case and summarizes the causes for any disconnect in a single + /// state value which can be used to display (or debug) the cause for disconnection. + /// + public DisconnectCause DisconnectedCause { get; protected set; } + + + /// Internal value if the client is in a lobby. + /// This is used to re-set this.State, when joining/creating a room fails. + private bool inLobby; + + /// The lobby this client currently uses. + public TypedLobby CurrentLobby { get; protected internal set; } + + /// Backing field for property. + private bool autoJoinLobby = true; + + /// If your client should join random games, you can skip joining the lobby. Call OpJoinRandomRoom and create a room if that fails. + public bool AutoJoinLobby + { + get + { + return this.autoJoinLobby; + } + + set + { + this.autoJoinLobby = value; + } + } + + /// + /// If set to true, the Master Server will report the list of used lobbies to the client. This sets and updates LobbyStatistics. + /// + /// + /// Lobby Statistics can be useful if a game uses multiple lobbies and you want + /// to show activity of each to players. + /// + /// LobbyStatistics are updated when you connect to the Master Server. + /// + public bool EnableLobbyStatistics; + + /// Internal lobby stats cache, used by LobbyStatistics. + private List lobbyStatistics = new List(); + + /// + /// If RequestLobbyStatistics is true, this provides a list of used lobbies (their name, type, room- and player-count) of this application, while on the Master Server. + /// + /// + /// If turned on, the Master Server will provide information about active lobbies for this application. + /// + /// Lobby Statistics can be useful if a game uses multiple lobbies and you want + /// to show activity of each to players. Per lobby, you get: name, type, room- and player-count. + /// + /// Lobby Statistics are not turned on by default. + /// Enable them by setting RequestLobbyStatistics to true before you connect. + /// + /// LobbyStatistics are updated when you connect to the Master Server. + /// You can check in OnEvent if EventCode.LobbyStats arrived. This the updates. + /// + public List LobbyStatistics + { + get { return this.lobbyStatistics; } + private set { this.lobbyStatistics = value; } + } + + + /// The local player is never null but not valid unless the client is in a room, too. The ID will be -1 outside of rooms. + public Player LocalPlayer { get; internal set; } + + /// + /// The nickname of the player (synced with others). Same as client.LocalPlayer.NickName. + /// + public string NickName + { + get + { + return this.LocalPlayer.NickName; + } + + set + { + if (this.LocalPlayer == null) + { + return; + } + + this.LocalPlayer.NickName = value; + } + } + + + /// An ID for this user. Sent in OpAuthenticate when you connect. If not set, the PlayerName is applied during connect. + /// + /// On connect, if the UserId is null or empty, the client will copy the PlayName to UserId. If PlayerName is not set either + /// (before connect), the server applies a temporary ID which stays unknown to this client and other clients. + /// + /// The UserId is what's used in FindFriends and for fetching data for your account (with WebHooks e.g.). + /// + /// By convention, set this ID before you connect, not while being connected. + /// There is no error but the ID won't change while being connected. + /// + public string UserId { + get + { + if (this.AuthValues != null) + { + return this.AuthValues.UserId; + } + return null; + } + set + { + if (this.AuthValues == null) + { + this.AuthValues = new AuthenticationValues(); + } + this.AuthValues.UserId = value; + } + } + + /// This "list" is populated while being in the lobby of the Master. It contains RoomInfo per roomName (keys). + public Dictionary RoomInfoList = new Dictionary(); + + /// The current room this client is connected to (null if none available). + public Room CurrentRoom; + + + /// Statistic value available on master server: Players on master (looking for games). + public int PlayersOnMasterCount { get; internal set; } + + /// Statistic value available on master server: Players in rooms (playing). + public int PlayersInRoomsCount { get; internal set; } + + /// Statistic value available on master server: Rooms currently created. + public int RoomsCount { get; internal set; } + + + /// Internally used to decide if a room must be created or joined on game server. + private JoinType lastJoinType; + + protected internal EnterRoomParams enterRoomParamsCache; + + /// Internally used to trigger OpAuthenticate when encryption was established after a connect. + private bool didAuthenticate; + + /// + /// List of friends, their online status and the room they are in. Null until initialized by OpFindFriends response. + /// + /// + /// Do not modify this list! It's internally handled by OpFindFriends and meant as read-only. + /// The value of FriendListAge gives you a hint how old the data is. Don't get this list more often than useful (> 10 seconds). + /// In best case, keep the list you fetch really short. You could (e.g.) get the full list only once, then request a few updates + /// only for friends who are online. After a while (e.g. 1 minute), you can get the full list again. + /// + public List FriendList { get; private set; } + + /// Contains the list of names of friends to look up their state on the server. + private string[] friendListRequested; + + /// + /// Age of friend list info (in milliseconds). It's 0 until a friend list is fetched. + /// + public int FriendListAge { get { return (this.isFetchingFriendList || this.friendListTimestamp == 0) ? 0 : Environment.TickCount - this.friendListTimestamp; } } + + /// Private timestamp (in ms) of the last friendlist update. + private int friendListTimestamp; + + /// Internal flag to know if the client currently fetches a friend list. + private bool isFetchingFriendList; + + + /// Internally used to check if a "Secret" is available to use. Sent by Photon Cloud servers, it simplifies authentication when switching servers. + protected bool IsAuthorizeSecretAvailable + { + get + { + return this.AuthValues != null && !string.IsNullOrEmpty(this.AuthValues.Token); + } + } + + /// A list of region names for the Photon Cloud. Set by the result of OpGetRegions(). + /// Put a "case OperationCode.GetRegions:" into your OnOperationResponse method to notice when the result is available. + public string[] AvailableRegions { get; private set; } + + /// A list of region server (IP addresses with port) for the Photon Cloud. Set by the result of OpGetRegions(). + /// Put a "case OperationCode.GetRegions:" into your OnOperationResponse method to notice when the result is available. + public string[] AvailableRegionsServers { get; private set; } + + /// The cloud region this client connects to. Set by ConnectToRegionMaster(). Not set if you don't use a NameServer! + public string CloudRegion { get; private set; } + + + /// Creates a LoadBalancingClient with UDP protocol or the one specified. + /// Specifies the network protocol to use for connections. + public LoadBalancingClient(ConnectionProtocol protocol = ConnectionProtocol.Udp) + { + this.loadBalancingPeer = new LoadBalancingPeer(this, protocol); + this.LocalPlayer = this.CreatePlayer(string.Empty, -1, true, null); + this.State = ClientState.PeerCreated; + } + + + /// Creates a LoadBalancingClient, setting various values needed before connecting. + /// The Master Server's address to connect to. Used in Connect. + /// The AppId of this title. Needed for the Photon Cloud. Find it in the Dashboard. + /// A version for this client/build. In the Photon Cloud, players are separated by AppId, GameVersion and Region. + /// Specifies the network protocol to use for connections. + public LoadBalancingClient(string masterAddress, string appId, string gameVersion, ConnectionProtocol protocol = ConnectionProtocol.Udp) : this(protocol) + { + this.MasterServerAddress = masterAddress; + this.AppId = appId; + this.AppVersion = gameVersion; + } + + /// + /// Gets the NameServer Address (with prefix and port), based on the set protocol (this.loadBalancingPeer.UsedProtocol). + /// + /// NameServer Address (with prefix and port). + private string GetNameServerAddress() + { + var protocolPort = 0; + ProtocolToNameServerPort.TryGetValue(this.loadBalancingPeer.TransportProtocol, out protocolPort); + + switch (this.loadBalancingPeer.TransportProtocol) + { + case ConnectionProtocol.Udp: + case ConnectionProtocol.Tcp: + return string.Format("{0}:{1}", NameServerHost, protocolPort); + #if RHTTP + case ConnectionProtocol.RHttp: + return NameServerHttp; + #endif + case ConnectionProtocol.WebSocket: + return string.Format("ws://{0}:{1}", NameServerHost, protocolPort); + case ConnectionProtocol.WebSocketSecure: + return string.Format("wss://{0}:{1}", NameServerHost, protocolPort); + default: + throw new ArgumentOutOfRangeException(); + } + } + + #region Operations and Commands + + /// + /// Starts the "process" to connect to the master server. Relevant connection-values parameters can be set via parameters. + /// + /// + /// The process to connect includes several steps: the actual connecting, establishing encryption, authentification + /// (of app and optionally the user) and joining a lobby (if AutoJoinLobby is true). + /// + /// Instead of providing all these parameters, you can also set the individual properties of a client before calling Connect(). + /// + /// Users can connect either anonymously or use "Custom Authentication" to verify each individual player's login. + /// Custom Authentication in Photon uses external services and communities to verify users. While the client provides a user's info, + /// the service setup is done in the Photon Cloud Dashboard. + /// The parameter authValues will set this.AuthValues and use them in the connect process. + /// + /// To connect to the Photon Cloud, a valid AppId must be provided. This is shown in the Photon Cloud Dashboard. + /// https://cloud.photonengine.com/dashboard + /// Connecting to the Photon Cloud might fail due to: + /// - Network issues (OnStatusChanged() StatusCode.ExceptionOnConnect) + /// - Region not available (OnOperationResponse() for OpAuthenticate with ReturnCode == ErrorCode.InvalidRegion) + /// - Subscription CCU limit reached (OnOperationResponse() for OpAuthenticate with ReturnCode == ErrorCode.MaxCcuReached) + /// More about the connection limitations: + /// http://doc.photonengine.com/photon-cloud/SubscriptionErrorCases/#cat-references + /// + /// Set a master server address instead of using the default. Uses default if null or empty. + /// Your application's name or the AppID assigned by Photon Cloud (as listed in Dashboard). Uses default if null or empty. + /// Can be used to separate users by their client's version (useful to add features without breaking older clients). Uses default if null or empty. + /// Optional name for this player. + /// Authentication values for this user. Optional. If you provide a unique userID it is used for FindFriends. + /// If the operation could be send (can be false for bad server urls). + public bool Connect(string masterServerAddress, string appId, string appVersion, string nickName, AuthenticationValues authValues) + { + if (!string.IsNullOrEmpty(masterServerAddress)) + { + this.MasterServerAddress = masterServerAddress; + } + + if (!string.IsNullOrEmpty(appId)) + { + this.AppId = appId; + } + + if (!string.IsNullOrEmpty(appVersion)) + { + this.AppVersion = appVersion; + } + + if (!string.IsNullOrEmpty(nickName)) + { + this.NickName = nickName; + } + + this.AuthValues = authValues; + + + // as this.Connect() checks usage of WebSockets for WebGL exports, this method doesn't + return this.Connect(); + } + + + /// + /// Starts the "process" to connect to a Master Server, using MasterServerAddress and AppId properties. + /// + /// + /// To connect to the Photon Cloud, use ConnectToRegionMaster(). + /// + /// The process to connect includes several steps: the actual connecting, establishing encryption, authentification + /// (of app and optionally the user) and joining a lobby (if AutoJoinLobby is true). + /// + /// Users can connect either anonymously or use "Custom Authentication" to verify each individual player's login. + /// Custom Authentication in Photon uses external services and communities to verify users. While the client provides a user's info, + /// the service setup is done in the Photon Cloud Dashboard. + /// The parameter authValues will set this.AuthValues and use them in the connect process. + /// + /// Connecting to the Photon Cloud might fail due to: + /// - Network issues (OnStatusChanged() StatusCode.ExceptionOnConnect) + /// - Region not available (OnOperationResponse() for OpAuthenticate with ReturnCode == ErrorCode.InvalidRegion) + /// - Subscription CCU limit reached (OnOperationResponse() for OpAuthenticate with ReturnCode == ErrorCode.MaxCcuReached) + /// More about the connection limitations: + /// http://doc.photonengine.com/photon-cloud/SubscriptionErrorCases/#cat-references + /// + public virtual bool Connect() + { + this.DisconnectedCause = DisconnectCause.None; + + #if UNITY_WEBGL + if (this.TransportProtocol == ConnectionProtocol.Tcp || this.TransportProtocol == ConnectionProtocol.Udp) + { + this.DebugReturn(DebugLevel.WARNING, "WebGL requires WebSockets. Switching TransportProtocol to WebSocketSecure."); + this.TransportProtocol = ConnectionProtocol.WebSocketSecure; + } + #endif + + if (this.loadBalancingPeer.Connect(this.MasterServerAddress, this.AppId, this.TokenForInit)) + { + this.State = ClientState.ConnectingToMasterserver; + return true; + } + + return false; + } + + + /// + /// Connects to the NameServer for Photon Cloud, where a region and server list can be obtained. + /// + /// + /// If the workflow was started or failed right away. + public bool ConnectToNameServer() + { + this.IsUsingNameServer = true; + this.CloudRegion = null; + + if (this.AuthMode == AuthModeOption.AuthOnceWss) + { + this.ExpectedProtocol = this.loadBalancingPeer.TransportProtocol; + this.loadBalancingPeer.TransportProtocol = ConnectionProtocol.WebSocketSecure; + } + + if (!this.loadBalancingPeer.Connect(NameServerAddress, "NameServer", this.TokenForInit)) + { + return false; + } + + this.State = ClientState.ConnectingToNameServer; + return true; + } + + + /// + /// Connects you to a specific region's Master Server, using the Name Server to find the IP. + /// + /// If the operation could be sent. If false, no operation was sent. + public bool ConnectToRegionMaster(string region) + { + this.IsUsingNameServer = true; + + if (this.State == ClientState.ConnectedToNameServer) + { + this.CloudRegion = region; + return this.CallAuthenticate(); + } + + this.loadBalancingPeer.Disconnect(); + this.CloudRegion = region; + + if (this.AuthMode == AuthModeOption.AuthOnceWss) + { + this.ExpectedProtocol = this.loadBalancingPeer.TransportProtocol; + this.loadBalancingPeer.TransportProtocol = ConnectionProtocol.WebSocketSecure; + } + + if (!this.loadBalancingPeer.Connect(this.NameServerAddress, "NameServer", null)) + { + return false; + } + + this.State = ClientState.ConnectingToNameServer; + return true; + } + + + /// Disconnects this client from any server and sets this.State if the connection is successfuly closed. + public void Disconnect() + { + if (this.State != ClientState.Disconnected) + { + this.State = ClientState.Disconnecting; + this.loadBalancingPeer.Disconnect(); + + //// we can set this high-level state if the low-level (connection)state is "disconnected" + //if (this.loadBalancingPeer.PeerState == PeerStateValue.Disconnected || this.loadBalancingPeer.PeerState == PeerStateValue.InitializingApplication) + //{ + // this.State = ClientState.Disconnected; + //} + } + } + + + private bool CallAuthenticate() + { + if (this.AuthMode == AuthModeOption.Auth) + { + return this.loadBalancingPeer.OpAuthenticate(this.AppId, this.AppVersion, this.AuthValues, this.CloudRegion, (this.EnableLobbyStatistics && this.Server == ServerConnection.MasterServer)); + } + else + { + return this.loadBalancingPeer.OpAuthenticateOnce(this.AppId, this.AppVersion, this.AuthValues, this.CloudRegion, this.EncryptionMode, this.ExpectedProtocol); + } + } + + + /// + /// This method dispatches all available incoming commands and then sends this client's outgoing commands. + /// It uses DispatchIncomingCommands and SendOutgoingCommands to do that. + /// + /// + /// The Photon client libraries are designed to fit easily into a game or application. The application + /// is in control of the context (thread) in which incoming events and responses are executed and has + /// full control of the creation of UDP/TCP packages. + /// + /// Sending packages and dispatching received messages are two separate tasks. Service combines them + /// into one method at the cost of control. It calls DispatchIncomingCommands and SendOutgoingCommands. + /// + /// Call this method regularly (2..20 times a second). + /// + /// This will Dispatch ANY received commands (unless a reliable command in-order is still missing) and + /// events AND will send queued outgoing commands. Fewer calls might be more effective if a device + /// cannot send many packets per second, as multiple operations might be combined into one package. + /// + /// + /// You could replace Service by: + /// + /// while (DispatchIncomingCommands()); //Dispatch until everything is Dispatched... + /// SendOutgoingCommands(); //Send a UDP/TCP package with outgoing messages + /// + /// + /// + public void Service() + { + if (this.loadBalancingPeer != null) + { + this.loadBalancingPeer.Service(); + } + } + + + /// + /// Private Disconnect variant that sets the state, too. + /// + private void DisconnectToReconnect() + { + switch (this.Server) + { + case ServerConnection.NameServer: + this.State = ClientState.DisconnectingFromNameServer; + break; + case ServerConnection.MasterServer: + this.State = ClientState.DisconnectingFromMasterserver; + break; + case ServerConnection.GameServer: + this.State = ClientState.DisconnectingFromGameserver; + break; + } + + this.loadBalancingPeer.Disconnect(); + } + + + /// + /// Privately used only. + /// Starts the "process" to connect to the game server (connect before a game is joined). + /// + private bool ConnectToGameServer() + { + if (this.loadBalancingPeer.Connect(this.GameServerAddress, this.AppId, this.TokenForInit)) + { + this.State = ClientState.ConnectingToGameserver; + return true; + } + + // TODO: handle error "cant connect to GS" + return false; + } + + /// + /// While on the NameServer, this gets you the list of regional servers (short names and their IPs to ping them). + /// + /// If the operation could be sent. If false, no operation was sent (e.g. while not connected to the NameServer). + public bool OpGetRegions() + { + if (this.Server != ServerConnection.NameServer) + { + return false; + } + + bool sent = this.loadBalancingPeer.OpGetRegions(this.AppId); + if (sent) + { + this.AvailableRegions = null; + } + + return sent; + } + + + /// + /// Request the rooms and online status for a list of friends. All clients should set a unique UserId before connecting. The result is available in this.FriendList. + /// + /// + /// Used on Master Server to find the rooms played by a selected list of users. + /// The result will be stored in LoadBalancingClient.FriendList, which is null before the first server response. + /// + /// Users identify themselves by setting a UserId in the LoadBalancingClient instance. + /// This will send the ID in OpAuthenticate during connect (to master and game servers). + /// Note: Changing a player's name doesn't make sense when using a friend list. + /// + /// The list of usernames must be fetched from some other source (not provided by Photon). + /// + /// + /// Internal: + /// The server response includes 2 arrays of info (each index matching a friend from the request): + /// ParameterCode.FindFriendsResponseOnlineList = bool[] of online states + /// ParameterCode.FindFriendsResponseRoomIdList = string[] of room names (empty string if not in a room) + /// + /// Array of friend's names (make sure they are unique). + /// If the operation could be sent (requires connection). + public bool OpFindFriends(string[] friendsToFind) + { + if (this.loadBalancingPeer == null) + { + return false; + } + + if (this.isFetchingFriendList || this.Server != ServerConnection.MasterServer) + { + return false; // fetching friends currently, so don't do it again (avoid changing the list while fetching friends) + } + + this.isFetchingFriendList = true; + this.friendListRequested = friendsToFind; + + return this.loadBalancingPeer.OpFindFriends(friendsToFind); + } + + /// + /// Joins the lobby on the Master Server, where you get a list of RoomInfos of currently open rooms. + /// This is an async request which triggers a OnOperationResponse() call. + /// + /// The lobby join to. Use null for default lobby. + /// If the operation could be sent (has to be connected). + public bool OpJoinLobby(TypedLobby lobby) + { + if (lobby == null) + { + lobby = TypedLobby.Default; + } + bool sent = this.loadBalancingPeer.OpJoinLobby(lobby); + if (sent) + { + this.CurrentLobby = lobby; + } + + return sent; + } + + + /// Opposite of joining a lobby. You don't have to explicitly leave a lobby to join another (client can be in one max, at any time). + /// If the operation could be sent (has to be connected). + public bool OpLeaveLobby() + { + return this.loadBalancingPeer.OpLeaveLobby(); + } + + + /// Operation to join a random room if available. You can use room properties to filter accepted rooms. + /// + /// You can use expectedCustomRoomProperties and expectedMaxPlayers as filters for accepting rooms. + /// If you set expectedCustomRoomProperties, a room must have the exact same key values set at Custom Properties. + /// You need to define which Custom Room Properties will be available for matchmaking when you create a room. + /// See: OpCreateRoom(string roomName, RoomOptions roomOptions, TypedLobby lobby) + /// + /// This operation fails if no rooms are fitting or available (all full, closed or not visible). + /// Override this class and implement OnOperationResponse(OperationResponse operationResponse). + /// + /// OpJoinRandomRoom can only be called while the client is connected to a Master Server. + /// You should check LoadBalancingClient.Server and LoadBalancingClient.IsConnectedAndReady before calling this method. + /// Alternatively, check the returned bool value. + /// + /// While the server is looking for a game, the State will be Joining. It's set immediately when this method sent the Operation. + /// + /// If successful, the LoadBalancingClient will get a Game Server Address and use it automatically + /// to switch servers and join the room. When you're in the room, this client's State will become + /// ClientState.Joined (both, for joining or creating it). + /// Set a OnStateChangeAction method to check for states. + /// + /// When joining a room, this client's Player Custom Properties will be sent to the room. + /// Use LocalPlayer.SetCustomProperties to set them, even while not yet in the room. + /// Note that the player properties will be cached locally and sent to any next room you would join, too. + /// + /// More about matchmaking: + /// http://doc.photonengine.com/en/realtime/current/reference/matchmaking-and-lobby + /// + /// You can define an array of expectedUsers, to block player slots in the room for these users. + /// The corresponding feature in Photon is called "Slot Reservation" and can be found in the doc pages. + /// + /// Optional. A room will only be joined, if it matches these custom properties (with string keys). + /// Filters for a particular maxplayer setting. Use 0 to accept any maxPlayer value. + /// Optional list of users (by UserId) who are expected to join this game and who you want to block a slot for. + /// If the operation could be sent currently (requires connection to Master Server). + public bool OpJoinRandomRoom(Hashtable expectedCustomRoomProperties, byte expectedMaxPlayers, string[] expectedUsers = null) + { + return OpJoinRandomRoom(expectedCustomRoomProperties, expectedMaxPlayers, MatchmakingMode.FillRoom, TypedLobby.Default, null, expectedUsers); + } + + + /// Operation to join a random room if available. You can use room properties to filter accepted rooms. + /// + /// You can use expectedCustomRoomProperties and expectedMaxPlayers as filters for accepting rooms. + /// If you set expectedCustomRoomProperties, a room must have the exact same key values set at Custom Properties. + /// You need to define which Custom Room Properties will be available for matchmaking when you create a room. + /// See: OpCreateRoom(string roomName, RoomOptions roomOptions, TypedLobby lobby) + /// + /// This operation fails if no rooms are fitting or available (all full, closed or not visible). + /// Override this class and implement OnOperationResponse(OperationResponse operationResponse). + /// + /// OpJoinRandomRoom can only be called while the client is connected to a Master Server. + /// You should check LoadBalancingClient.Server and LoadBalancingClient.IsConnectedAndReady before calling this method. + /// Alternatively, check the returned bool value. + /// + /// While the server is looking for a game, the State will be Joining. It's set immediately when this method sent the Operation. + /// + /// If successful, the LoadBalancingClient will get a Game Server Address and use it automatically + /// to switch servers and join the room. When you're in the room, this client's State will become + /// ClientState.Joined (both, for joining or creating it). + /// Set a OnStateChangeAction method to check for states. + /// + /// When joining a room, this client's Player Custom Properties will be sent to the room. + /// Use LocalPlayer.SetCustomProperties to set them, even while not yet in the room. + /// Note that the player properties will be cached locally and sent to any next room you would join, too. + /// + /// More about matchmaking: + /// http://doc.photonengine.com/en/realtime/current/reference/matchmaking-and-lobby + /// + /// Optional. A room will only be joined, if it matches these custom properties (with string keys). + /// Filters for a particular maxplayer setting. Use 0 to accept any maxPlayer value. + /// Selects one of the available matchmaking algorithms. See MatchmakingMode enum for options. + /// If the operation could be sent currently (requires connection to Master Server). + public bool OpJoinRandomRoom(Hashtable expectedCustomRoomProperties, byte expectedMaxPlayers, MatchmakingMode matchmakingMode) + { + return this.OpJoinRandomRoom(expectedCustomRoomProperties, expectedMaxPlayers, matchmakingMode, TypedLobby.Default, null); + } + + + /// Operation to join a random room if available. You can use room properties to filter accepted rooms. + /// + /// You can use expectedCustomRoomProperties and expectedMaxPlayers as filters for accepting rooms. + /// If you set expectedCustomRoomProperties, a room must have the exact same key values set at Custom Properties. + /// You need to define which Custom Room Properties will be available for matchmaking when you create a room. + /// See: OpCreateRoom(string roomName, RoomOptions roomOptions, TypedLobby lobby) + /// + /// This operation fails if no rooms are fitting or available (all full, closed or not visible). + /// Override this class and implement OnOperationResponse(OperationResponse operationResponse). + /// + /// OpJoinRandomRoom can only be called while the client is connected to a Master Server. + /// You should check LoadBalancingClient.Server and LoadBalancingClient.IsConnectedAndReady before calling this method. + /// Alternatively, check the returned bool value. + /// + /// While the server is looking for a game, the State will be Joining. + /// It's set immediately when this method sent the Operation. + /// + /// If successful, the LoadBalancingClient will get a Game Server Address and use it automatically + /// to switch servers and join the room. When you're in the room, this client's State will become + /// ClientState.Joined (both, for joining or creating it). + /// Set a OnStateChangeAction method to check for states. + /// + /// When joining a room, this client's Player Custom Properties will be sent to the room. + /// Use LocalPlayer.SetCustomProperties to set them, even while not yet in the room. + /// Note that the player properties will be cached locally and sent to any next room you would join, too. + /// + /// The parameter lobby can be null (using the defaul lobby) or a typed lobby you make up. + /// Lobbies are created on the fly, as required by the clients. If you organize matchmaking with lobbies, + /// keep in mind that they also fragment your matchmaking. Using more lobbies will put less rooms in each. + /// + /// The parameter sqlLobbyFilter can only be combined with the LobbyType.SqlLobby. In that case, it's used + /// to define a sql-like "WHERE" clause for filtering rooms. This is useful for skill-based matchmaking e.g.. + /// + /// More about matchmaking: + /// http://doc.photonengine.com/en/realtime/current/reference/matchmaking-and-lobby + /// + /// You can define an array of expectedUsers, to block player slots in the room for these users. + /// The corresponding feature in Photon is called "Slot Reservation" and can be found in the doc pages. + /// + /// Optional. A room will only be joined, if it matches these custom properties (with string keys). + /// Filters for a particular maxplayer setting. Use 0 to accept any maxPlayer value. + /// Selects one of the available matchmaking algorithms. See MatchmakingMode enum for options. + /// The lobby in which to find a room. Use null for default lobby. + /// Can be used with LobbyType.SqlLobby only. This is a "where" clause of a sql statement. Use null for random game. + /// Optional list of users (by UserId) who are expected to join this game and who you want to block a slot for. + /// If the operation could be sent currently (requires connection to Master Server). + public bool OpJoinRandomRoom(Hashtable expectedCustomRoomProperties, byte expectedMaxPlayers, MatchmakingMode matchmakingMode, TypedLobby lobby, string sqlLobbyFilter, string[] expectedUsers = null) + { + if (lobby == null) + { + lobby = TypedLobby.Default; + } + + this.State = ClientState.Joining; + this.lastJoinType = JoinType.JoinRandomRoom; + this.CurrentLobby = lobby; + + this.enterRoomParamsCache = new EnterRoomParams(); + this.enterRoomParamsCache.Lobby = lobby; + this.enterRoomParamsCache.ExpectedUsers = expectedUsers; + + OpJoinRandomRoomParams opParams = new OpJoinRandomRoomParams(); + opParams.ExpectedCustomRoomProperties = expectedCustomRoomProperties; + opParams.ExpectedMaxPlayers = expectedMaxPlayers; + opParams.MatchingType = matchmakingMode; + opParams.TypedLobby = lobby; + opParams.SqlLobbyFilter = sqlLobbyFilter; + opParams.ExpectedUsers = expectedUsers; + return this.loadBalancingPeer.OpJoinRandomRoom(opParams); + + //return this.loadBalancingPeer.OpJoinRandomRoom(expectedCustomRoomProperties, expectedMaxPlayers, playerPropsToSend, matchmakingMode, lobby, sqlLobbyFilter); + } + + + /// + /// Joins a room by roomName. Useful when using room lists in lobbies or when you know the name otherwise. + /// + /// + /// This method is useful when you are using a lobby to list rooms and know their names. + /// A room's name has to be unique (per region and game version), so it does not matter which lobby it's in. + /// + /// If the room is full, closed or not existing, this will fail. Override this class and implement + /// OnOperationResponse(OperationResponse operationResponse) to get the errors. + /// + /// OpJoinRoom can only be called while the client is connected to a Master Server. + /// You should check LoadBalancingClient.Server and LoadBalancingClient.IsConnectedAndReady before calling this method. + /// Alternatively, check the returned bool value. + /// + /// While the server is joining the game, the State will be ClientState.Joining. + /// It's set immediately when this method sends the Operation. + /// + /// If successful, the LoadBalancingClient will get a Game Server Address and use it automatically + /// to switch servers and join the room. When you're in the room, this client's State will become + /// ClientState.Joined (both, for joining or creating it). + /// Set a OnStateChangeAction method to check for states. + /// + /// When joining a room, this client's Player Custom Properties will be sent to the room. + /// Use LocalPlayer.SetCustomProperties to set them, even while not yet in the room. + /// Note that the player properties will be cached locally and sent to any next room you would join, too. + /// + /// It's usually better to use OpJoinOrCreateRoom for invitations. + /// Then it does not matter if the room is already setup. + /// + /// You can define an array of expectedUsers, to block player slots in the room for these users. + /// The corresponding feature in Photon is called "Slot Reservation" and can be found in the doc pages. + /// + /// The name of the room to join. Must be existing already, open and non-full or can't be joined. + /// Optional list of users (by UserId) who are expected to join this game and who you want to block a slot for. + /// If the operation could be sent currently (requires connection to Master Server). + public bool OpJoinRoom(string roomName, string[] expectedUsers = null) + { + this.State = ClientState.Joining; + this.lastJoinType = JoinType.JoinRoom; + bool onGameServer = this.Server == ServerConnection.GameServer; + + + EnterRoomParams opParams = new EnterRoomParams(); + this.enterRoomParamsCache = opParams; + opParams.RoomName = roomName; + opParams.OnGameServer = onGameServer; + opParams.ExpectedUsers = expectedUsers; + + return this.loadBalancingPeer.OpJoinRoom(opParams); + } + + /// + /// Rejoins a room by roomName (using the userID internally to return). Useful to return to a persisted room or after temporarily losing connection. + /// + public bool OpReJoinRoom(string roomName) + { + this.State = ClientState.Joining; + this.lastJoinType = JoinType.JoinRoom; + bool onGameServer = this.Server == ServerConnection.GameServer; + + + EnterRoomParams opParams = new EnterRoomParams(); + this.enterRoomParamsCache = opParams; + opParams.RoomName = roomName; + opParams.OnGameServer = onGameServer; + opParams.RejoinOnly = true; + + return this.loadBalancingPeer.OpJoinRoom(opParams); + } + + + /// + /// Joins a specific room by name. If the room does not exist (yet), it will be created implicitly. + /// + /// + /// Unlike OpJoinRoom, this operation does not fail if the room does not exist. + /// This can be useful when you send invitations to a room before actually creating it: + /// Any invited player (whoever is first) can call this and on demand, the room gets created implicitly. + /// + /// This operation does not allow you to re-join a game. To return to a room, use OpJoinRoom with + /// the actorNumber which was assigned previously. + /// + /// If you set room properties in RoomOptions, they get ignored when the room is existing already. + /// This avoids changing the room properties by late joining players. Only when the room gets created, + /// the RoomOptions are set in this case. + /// + /// If the room is full or closed, this will fail. Override this class and implement + /// OnOperationResponse(OperationResponse operationResponse) to get the errors. + /// + /// This method can only be called while the client is connected to a Master Server. + /// You should check LoadBalancingClient.Server and LoadBalancingClient.IsConnectedAndReady before + /// calling this method. Alternatively, check the returned bool value. + /// + /// While the server is joining the game, the State will be ClientState.Joining. + /// It's set immediately when this method sends the Operation. + /// + /// If successful, the LoadBalancingClient will get a Game Server Address and use it automatically + /// to switch servers and join the room. When you're in the room, this client's State will become + /// ClientState.Joined (both, for joining or creating it). + /// Set a OnStateChangeAction method to check for states. + /// + /// When entering the room, this client's Player Custom Properties will be sent to the room. + /// Use LocalPlayer.SetCustomProperties to set them, even while not yet in the room. + /// Note that the player properties will be cached locally and sent to any next room you would join, too. + /// + /// You can define an array of expectedUsers, to block player slots in the room for these users. + /// The corresponding feature in Photon is called "Slot Reservation" and can be found in the doc pages. + /// + /// The name of the room to join (might be created implicitly). + /// Contains the parameters and properties of the new room. See RoomOptions class for a description of each. + /// Typed lobby to be used if the roomname is not in use (and room gets created). If != null, it will also set CurrentLobby. + /// Optional list of users (by UserId) who are expected to join this game and who you want to block a slot for. + /// If the operation could be sent currently (requires connection to Master Server). + public bool OpJoinOrCreateRoom(string roomName, RoomOptions roomOptions, TypedLobby lobby, string[] expectedUsers = null) + { + this.State = ClientState.Joining; + this.lastJoinType = JoinType.JoinOrCreateRoom; + this.CurrentLobby = lobby; + bool onGameServer = this.Server == ServerConnection.GameServer; + + EnterRoomParams opParams = new EnterRoomParams(); + this.enterRoomParamsCache = opParams; + opParams.RoomName = roomName; + opParams.RoomOptions = roomOptions; + opParams.Lobby = lobby; + opParams.CreateIfNotExists = true; + opParams.OnGameServer = onGameServer; + opParams.ExpectedUsers = expectedUsers; + + return this.loadBalancingPeer.OpJoinRoom(opParams); + } + + + /// + /// Creates a new room on the server (or fails if the name is already in use). + /// + /// + /// If you don't want to create a unique room-name, pass null or "" as name and the server will assign a + /// roomName (a GUID as string). Room names are unique. + /// + /// A room will be attached to the specified lobby. Use null as lobby to attach the + /// room to the lobby you are now in. If you are in no lobby, the default lobby is used. + /// + /// Multiple lobbies can help separate players by map or skill or game type. Each room can only be found + /// in one lobby (no matter if defined by name and type or as default). + /// + /// This method can only be called while the client is connected to a Master Server. + /// You should check LoadBalancingClient.Server and LoadBalancingClient.IsConnectedAndReady before calling this method. + /// Alternatively, check the returned bool value. + /// + /// Even when sent, the Operation will fail (on the server) if the roomName is in use. + /// Override this class and implement OnOperationResponse(OperationResponse operationResponse) to get the errors. + /// + /// + /// While the server is creating the game, the State will be ClientState.Joining. + /// The state Joining is used because the client is on the way to enter a room (no matter if joining or creating). + /// It's set immediately when this method sends the Operation. + /// + /// If successful, the LoadBalancingClient will get a Game Server Address and use it automatically + /// to switch servers and enter the room. When you're in the room, this client's State will become + /// ClientState.Joined (both, for joining or creating it). + /// Set a OnStateChangeAction method to check for states. + /// + /// When entering the room, this client's Player Custom Properties will be sent to the room. + /// Use LocalPlayer.SetCustomProperties to set them, even while not yet in the room. + /// Note that the player properties will be cached locally and sent to any next room you would join, too. + /// + /// You can define an array of expectedUsers, to block player slots in the room for these users. + /// The corresponding feature in Photon is called "Slot Reservation" and can be found in the doc pages. + /// + /// The name to create a room with. Must be unique and not in use or can't be created. If null, the server will assign a GUID as name. + /// Contains the parameters and properties of the new room. See RoomOptions class for a description of each. + /// The lobby (name and type) in which to create the room. Null uses the current lobby or the default lobby (if not in a lobby). + /// Optional list of users (by UserId) who are expected to join this game and who you want to block a slot for. + /// If the operation could be sent currently (requires connection to Master Server). + public bool OpCreateRoom(string roomName, RoomOptions roomOptions, TypedLobby lobby, string[] expectedUsers = null) + { + this.State = ClientState.Joining; + this.lastJoinType = JoinType.CreateRoom; + this.CurrentLobby = lobby; + bool onGameServer = this.Server == ServerConnection.GameServer; + + EnterRoomParams opParams = new EnterRoomParams(); + this.enterRoomParamsCache = opParams; + opParams.RoomName = roomName; + opParams.RoomOptions = roomOptions; + opParams.Lobby = lobby; + opParams.OnGameServer = onGameServer; + opParams.ExpectedUsers = expectedUsers; + + return this.loadBalancingPeer.OpCreateRoom(opParams); + //return this.loadBalancingPeer.OpCreateRoom(roomName, roomOptions, lobby, playerPropsToSend, onGameServer); + } + + + /// + /// Leaves the CurrentRoom and returns to the Master server (back to the lobby). + /// OpLeaveRoom skips execution when the room is null or the server is not GameServer or the client is disconnecting from GS already. + /// OpLeaveRoom returns false in those cases and won't change the state, so check return of this method. + /// + /// + /// This method actually is not an operation per se. It sets a state and calls Disconnect(). + /// This is is quicker than calling OpLeave and then disconnect (which also triggers a leave). + /// + /// If the current room could be left (impossible while not in a room). + public bool OpLeaveRoom() + { + return OpLeaveRoom(false); //TURNBASED + } + + + /// + /// Leaves the current room, optionally telling the server that the user is just becoming inactive. + /// + /// + /// If true, this player becomes inactive in the game and can return later (if PlayerTTL of the room is > 0). + /// + /// OpLeaveRoom skips execution when the room is null or the server is not GameServer or the client is disconnecting from GS already. + /// OpLeaveRoom returns false in those cases and won't change the state, so check return of this method. + /// + /// In some cases, this method will skip the OpLeave call and just call Disconnect(), + /// which not only leaves the room but also the server. Disconnect also triggers a leave and so that workflow is is quicker. + /// + /// If the current room could be left (impossible while not in a room). + public bool OpLeaveRoom(bool becomeInactive) + { + if (this.CurrentRoom == null || this.Server != ServerConnection.GameServer || this.State == ClientState.DisconnectingFromGameserver) + { + return false; + } + + if (becomeInactive) + { + this.State = ClientState.DisconnectingFromGameserver; + this.loadBalancingPeer.Disconnect(); + } + else + { + this.State = ClientState.Leaving; + this.loadBalancingPeer.OpLeaveRoom(false); //TURNBASED users can leave a room forever or return later + } + + return true; + } + + + /// Gets a list of games matching a SQL-like where clause. + /// + /// Operation is only available for lobbies of type SqlLobby. + /// This is an async request which triggers a OnOperationResponse() call. + /// Returned game list is stored in RoomInfoList. + /// + /// + /// The lobby to query. Has to be of type SqlLobby. + /// The sql query statement. + /// If the operation could be sent (has to be connected). + public bool OpGetGameList(TypedLobby typedLobby, string sqlLobbyFilter) + { + return this.loadBalancingPeer.OpGetGameList(typedLobby, sqlLobbyFilter); + } + + + /// + /// Updates and synchronizes a Player's Custom Properties. Optionally, expectedProperties can be provided as condition. + /// + /// + /// Custom Properties are a set of string keys and arbitrary values which is synchronized + /// for the players in a Room. They are available when the client enters the room, as + /// they are in the response of OpJoin and OpCreate. + /// + /// Custom Properties either relate to the (current) Room or a Player (in that Room). + /// + /// Both classes locally cache the current key/values and make them available as + /// property: CustomProperties. This is provided only to read them. + /// You must use the method SetCustomProperties to set/modify them. + /// + /// Any client can set any Custom Properties anytime (when in a room). + /// It's up to the game logic to organize how they are best used. + /// + /// You should call SetCustomProperties only with key/values that are new or changed. This reduces + /// traffic and performance. + /// + /// Unless you define some expectedProperties, setting key/values is always permitted. + /// In this case, the property-setting client will not receive the new values from the server but + /// instead update its local cache in SetCustomProperties. + /// + /// If you define expectedProperties, the server will skip updates if the server property-cache + /// does not contain all expectedProperties with the same values. + /// In this case, the property-setting client will get an update from the server and update it's + /// cached key/values at about the same time as everyone else. + /// + /// The benefit of using expectedProperties can be only one client successfully sets a key from + /// one known value to another. + /// As example: Store who owns an item in a Custom Property "ownedBy". It's 0 initally. + /// When multiple players reach the item, they all attempt to change "ownedBy" from 0 to their + /// actorNumber. If you use expectedProperties {"ownedBy", 0} as condition, the first player to + /// take the item will have it (and the others fail to set the ownership). + /// + /// Properties get saved with the game state for Turnbased games (which use IsPersistent = true). + /// + /// Defines which player the Custom Properties belong to. ActorID of a player. + /// Hashtable of Custom Properties that changes. + /// Provide some keys/values to use as condition for setting the new values. Client must be in room. + /// Defines if the set properties should be forwarded to a WebHook. Client must be in room. + public bool OpSetCustomPropertiesOfActor(int actorNr, Hashtable propertiesToSet, Hashtable expectedProperties = null, WebFlags webFlags = null) + { + + if (this.CurrentRoom == null) + { + // if you attempt to set this player's values without conditions, then fine: + if (expectedProperties == null && webFlags == null && this.LocalPlayer != null && this.LocalPlayer.ID == actorNr) + { + this.LocalPlayer.SetCustomProperties(propertiesToSet); + return true; + } + + if (this.loadBalancingPeer.DebugOut >= DebugLevel.ERROR) + { + this.DebugReturn(DebugLevel.ERROR, "OpSetCustomPropertiesOfActor() failed. To use expectedProperties or webForward, you have to be in a room. State: " + this.State); + } + return false; + } + + Hashtable customActorProperties = new Hashtable(); + customActorProperties.MergeStringKeys(propertiesToSet); + + return this.OpSetPropertiesOfActor(actorNr, customActorProperties, expectedProperties, webFlags); + } + + /// Replaced by a newer version with WebFlags. + [Obsolete("Use the overload with WebFlags.")] + public bool OpSetCustomPropertiesOfActor(int actorNr, Hashtable propertiesToSet, Hashtable expectedProperties, bool webForward) + { + return this.OpSetCustomPropertiesOfActor(actorNr, propertiesToSet, expectedProperties, (webForward ? new WebFlags(WebFlags.HttpForwardConst) : null)); + } + + + /// Internally used to cache and set properties (including well known properties). + /// Requires being in a room (because this attempts to send an operation which will fail otherwise). + protected internal bool OpSetPropertiesOfActor(int actorNr, Hashtable actorProperties, Hashtable expectedProperties = null, WebFlags webFlags = null) + { + if (this.CurrentRoom == null) + { + if (this.loadBalancingPeer.DebugOut >= DebugLevel.ERROR) + { + this.DebugReturn(DebugLevel.ERROR, "OpSetPropertiesOfActor() failed because this client is not in a room currently. State: " + this.State); + } + return false; + } + + if (expectedProperties == null || expectedProperties.Count == 0) + { + Player target = this.CurrentRoom.GetPlayer(actorNr); + if (target != null) + { + target.InternalCacheProperties(actorProperties); + } + } + + return this.loadBalancingPeer.OpSetPropertiesOfActor(actorNr, actorProperties, expectedProperties, webFlags); + } + + + /// + /// Updates and synchronizes this Room's Custom Properties. Optionally, expectedProperties can be provided as condition. + /// + /// + /// Custom Properties are a set of string keys and arbitrary values which is synchronized + /// for the players in a Room. They are available when the client enters the room, as + /// they are in the response of OpJoin and OpCreate. + /// + /// Custom Properties either relate to the (current) Room or a Player (in that Room). + /// + /// Both classes locally cache the current key/values and make them available as + /// property: CustomProperties. This is provided only to read them. + /// You must use the method SetCustomProperties to set/modify them. + /// + /// Any client can set any Custom Properties anytime (when in a room). + /// It's up to the game logic to organize how they are best used. + /// + /// You should call SetCustomProperties only with key/values that are new or changed. This reduces + /// traffic and performance. + /// + /// Unless you define some expectedProperties, setting key/values is always permitted. + /// In this case, the property-setting client will not receive the new values from the server but + /// instead update its local cache in SetCustomProperties. + /// + /// If you define expectedProperties, the server will skip updates if the server property-cache + /// does not contain all expectedProperties with the same values. + /// In this case, the property-setting client will get an update from the server and update it's + /// cached key/values at about the same time as everyone else. + /// + /// The benefit of using expectedProperties can be only one client successfully sets a key from + /// one known value to another. + /// As example: Store who owns an item in a Custom Property "ownedBy". It's 0 initally. + /// When multiple players reach the item, they all attempt to change "ownedBy" from 0 to their + /// actorNumber. If you use expectedProperties {"ownedBy", 0} as condition, the first player to + /// take the item will have it (and the others fail to set the ownership). + /// + /// Properties get saved with the game state for Turnbased games (which use IsPersistent = true). + /// + /// Hashtable of Custom Properties that changes. + /// Provide some keys/values to use as condition for setting the new values. + /// Defines if the set properties should be forwarded to a WebHook. + public bool OpSetCustomPropertiesOfRoom(Hashtable propertiesToSet, Hashtable expectedProperties = null, WebFlags webFlags = null) + { + Hashtable customGameProps = new Hashtable(); + customGameProps.MergeStringKeys(propertiesToSet); + + return this.OpSetPropertiesOfRoom(customGameProps, expectedProperties, webFlags); + } + + /// Replaced by a newer version with WebFlags. + [Obsolete("Use the overload with WebFlags.")] + public bool OpSetCustomPropertiesOfRoom(Hashtable propertiesToSet, Hashtable expectedProperties, bool webForward) + { + return this.OpSetCustomPropertiesOfRoom(propertiesToSet, expectedProperties, (webForward ? new WebFlags(WebFlags.HttpForwardConst) : null)); + } + + + /// Internally used to cache and set properties (including well known properties). + /// Requires being in a room (because this attempts to send an operation which will fail otherwise). + protected internal bool OpSetPropertiesOfRoom(Hashtable gameProperties, Hashtable expectedProperties = null, WebFlags webFlags = null) + { + if (this.CurrentRoom == null) + { + if (this.loadBalancingPeer.DebugOut >= DebugLevel.ERROR) + { + this.DebugReturn(DebugLevel.ERROR, "OpSetPropertiesOfRoom() failed because this client is not in a room currently. State: " + this.State); + } + return false; + } + + if (expectedProperties == null || expectedProperties.Count == 0) + { + this.CurrentRoom.InternalCacheProperties(gameProperties); + } + return this.loadBalancingPeer.OpSetPropertiesOfRoom(gameProperties, expectedProperties, webFlags); + } + + + /// + /// Send an event with custom code/type and any content to the other players in the same room. + /// + /// This override explicitly uses another parameter order to not mix it up with the implementation for Hashtable only. + /// Identifies this type of event (and the content). Your game's event codes can start with 0. + /// Any serializable datatype (including Hashtable like the other OpRaiseEvent overloads). + /// If this event has to arrive reliably (potentially repeated if it's lost). + /// Contains (slightly) less often used options. If you pass null, the default options will be used. + /// If operation could be enqueued for sending. Sent when calling: Service or SendOutgoingCommands. + public virtual bool OpRaiseEvent(byte eventCode, object customEventContent, bool sendReliable, RaiseEventOptions raiseEventOptions) + { + if (this.loadBalancingPeer == null) + { + return false; + } + + return this.loadBalancingPeer.OpRaiseEvent(eventCode, customEventContent, sendReliable, raiseEventOptions); + } + + + /// + /// Operation to handle this client's interest groups (for events in room). + /// + /// + /// Note the difference between passing null and byte[0]: + /// null won't add/remove any groups. + /// byte[0] will add/remove all (existing) groups. + /// First, removing groups is executed. This way, you could leave all groups and join only the ones provided. + /// + /// Changes become active not immediately but when the server executes this operation (approximately RTT/2). + /// + /// Groups to remove from interest. Null will not remove any. A byte[0] will remove all. + /// Groups to add to interest. Null will not add any. A byte[0] will add all current. + /// If operation could be enqueued for sending. Sent when calling: Service or SendOutgoingCommands. + public virtual bool OpChangeGroups(byte[] groupsToRemove, byte[] groupsToAdd) + { + if (this.loadBalancingPeer == null) + { + return false; + } + + return this.loadBalancingPeer.OpChangeGroups(groupsToRemove, groupsToAdd); + } + + + #endregion + + #region Helpers + + /// + /// Privately used to read-out properties coming from the server in events and operation responses (which might be a bit tricky). + /// + private void ReadoutProperties(Hashtable gameProperties, Hashtable actorProperties, int targetActorNr) + { + // read game properties and cache them locally + if (this.CurrentRoom != null && gameProperties != null) + { + this.CurrentRoom.InternalCacheProperties(gameProperties); + } + + if (actorProperties != null && actorProperties.Count > 0) + { + if (targetActorNr > 0) + { + // we have a single entry in the actorProperties with one user's name + // targets MUST exist before you set properties + Player target = this.CurrentRoom.GetPlayer(targetActorNr); + if (target != null) + { + Hashtable props = this.ReadoutPropertiesForActorNr(actorProperties, targetActorNr); + target.InternalCacheProperties(props); + //SendMonoMessage(PhotonNetworkingMessage.OnPhotonPlayerPropertiesChanged, target, props); + } + } + else + { + // in this case, we've got a key-value pair per actor (each + // value is a hashtable with the actor's properties then) + int actorNr; + Hashtable props; + string newName; + Player target; + + foreach (object key in actorProperties.Keys) + { + actorNr = (int)key; + props = (Hashtable)actorProperties[key]; + newName = (string)props[ActorProperties.PlayerName]; + + target = this.CurrentRoom.GetPlayer(actorNr); + if (target == null) + { + target = this.CreatePlayer(newName, actorNr, false, props); + this.CurrentRoom.StorePlayer(target); + } + + target.InternalCacheProperties(props); + //SendMonoMessage(PhotonNetworkingMessage.OnPhotonPlayerPropertiesChanged, target, props); + } + } + } + } + + + /// + /// Privately used only to read properties for a distinct actor (which might be the hashtable OR a key-pair value IN the actorProperties). + /// + private Hashtable ReadoutPropertiesForActorNr(Hashtable actorProperties, int actorNr) + { + if (actorProperties.ContainsKey(actorNr)) + { + return (Hashtable)actorProperties[actorNr]; + } + + return actorProperties; + } + + /// + /// Internally used to set the LocalPlayer's ID (from -1 to the actual in-room ID). + /// + /// New actor ID (a.k.a actorNr) assigned when joining a room. + protected internal void ChangeLocalID(int newID) + { + if (this.LocalPlayer == null) + { + this.DebugReturn(DebugLevel.WARNING, string.Format("Local actor is null or not in mActors! mLocalActor: {0} mActors==null: {1} newID: {2}", this.LocalPlayer, this.CurrentRoom.Players == null, newID)); + } + + if (this.CurrentRoom == null) + { + // change to new actor/player ID and make sure the player does not have a room reference left + this.LocalPlayer.ChangeLocalID(newID); + this.LocalPlayer.RoomReference = null; + } + else + { + // remove old actorId from actor list + this.CurrentRoom.RemovePlayer(this.LocalPlayer); + + // change to new actor/player ID + this.LocalPlayer.ChangeLocalID(newID); + + // update the room's list with the new reference + this.CurrentRoom.StorePlayer(this.LocalPlayer); + } + } + + /// + /// Internally used to clean up local instances of players and room. + /// + private void CleanCachedValues() + { + this.ChangeLocalID(-1); + this.isFetchingFriendList = false; + + // if this is called on the gameserver, we clean the room we were in. on the master, we keep the room to get into it + if (this.Server == ServerConnection.GameServer || this.State == ClientState.Disconnecting || this.State == ClientState.PeerCreated) + { + this.CurrentRoom = null; // players get cleaned up inside this, too, except LocalPlayer (which we keep) + } + + // when we leave the master, we clean up the rooms list (which might be updated by the lobby when we join again) + if (this.Server == ServerConnection.MasterServer || this.State == ClientState.Disconnecting || this.State == ClientState.PeerCreated) + { + this.RoomInfoList.Clear(); + } + } + + + /// + /// Called internally, when a game was joined or created on the game server successfully. + /// + /// + /// This reads the response, finds out the local player's actorNumber (a.k.a. Player.ID) and applies properties of the room and players. + /// Errors for these operations are to be handled before this method is called. + /// + /// Contains the server's response for an operation called by this peer. + private void GameEnteredOnGameServer(OperationResponse operationResponse) + { + this.CurrentRoom = this.CreateRoom(this.enterRoomParamsCache.RoomName, this.enterRoomParamsCache.RoomOptions); + this.CurrentRoom.LoadBalancingClient = this; + this.CurrentRoom.IsLocalClientInside = true; + + // first change the local id, instead of first updating the actorList since actorList uses ID to update itself + + // the local player's actor-properties are not returned in join-result. add this player to the list + int localActorNr = (int)operationResponse[ParameterCode.ActorNr]; + this.ChangeLocalID(localActorNr); + + if (operationResponse.Parameters.ContainsKey(ParameterCode.ActorList)) + { + int[] actorsInRoom = (int[])operationResponse.Parameters[ParameterCode.ActorList]; + this.UpdatedActorList(actorsInRoom); + } + + + Hashtable actorProperties = (Hashtable)operationResponse[ParameterCode.PlayerProperties]; + Hashtable gameProperties = (Hashtable)operationResponse[ParameterCode.GameProperties]; + this.ReadoutProperties(gameProperties, actorProperties, 0); + + this.State = ClientState.Joined; + + switch (operationResponse.OperationCode) + { + case OperationCode.CreateGame: + // TODO: add callback "game created" + break; + case OperationCode.JoinGame: + case OperationCode.JoinRandomGame: + // "game joined" should be called in another place (the join ev contains important info for the room). + break; + } + } + + private void UpdatedActorList(int[] actorsInGame) + { + if (actorsInGame != null) + { + foreach (int userId in actorsInGame) + { + Player target = this.CurrentRoom.GetPlayer(userId); + if (target == null) + { + this.CurrentRoom.StorePlayer(this.CreatePlayer(string.Empty, userId, false, null)); + } + } + } + } + + /// + /// Factory method to create a player instance - override to get your own player-type with custom features. + /// + /// The name of the player to be created. + /// The player ID (a.k.a. actorNumber) of the player to be created. + /// Sets the distinction if the player to be created is your player or if its assigned to someone else. + /// The custom properties for this new player + /// The newly created player + protected internal virtual Player CreatePlayer(string actorName, int actorNumber, bool isLocal, Hashtable actorProperties) + { + Player newPlayer = new Player(actorName, actorNumber, isLocal, actorProperties); + return newPlayer; + } + + /// Internal "factory" method to create a room-instance. + protected internal virtual Room CreateRoom(string roomName, RoomOptions opt) + { + Room r = new Room(roomName, opt); + return r; + } + + + private byte[] encryptionSecret; + + #endregion + + #region Implementation of IPhotonPeerListener + + /// Debug output of low level api (and this client). + /// This method is not responsible to keep up the state of a LoadBalancingClient. Calling base.DebugReturn on overrides is optional. + public virtual void DebugReturn(DebugLevel level, string message) + { + #if !UNITY + Debug.WriteLine(message); + #else + if (level == DebugLevel.ERROR) + { + Debug.LogError(message); + } + else if (level == DebugLevel.WARNING) + { + Debug.LogWarning(message); + } + else if (level == DebugLevel.INFO) + { + Debug.Log(message); + } + else if (level == DebugLevel.ALL) + { + Debug.Log(message); + } + #endif + } + + + /// + /// Uses the OperationResponses provided by the server to advance the internal state and call ops as needed. + /// + /// + /// When this method finishes, it will call your OnOpResponseAction (if any). This way, you can get any + /// operation response without overriding this class. + /// + /// To implement a more complex game/app logic, you should implement your own class that inherits the + /// LoadBalancingClient. Override this method to use your own operation-responses easily. + /// + /// This method is essential to update the internal state of a LoadBalancingClient, so overriding methods + /// must call base.OnOperationResponse(). + /// + /// Contains the server's response for an operation called by this peer. + public virtual void OnOperationResponse(OperationResponse operationResponse) + { + // if (operationResponse.ReturnCode != 0) this.DebugReturn(DebugLevel.ERROR, operationResponse.ToStringFull()); + + // use the "secret" or "token" whenever we get it. doesn't really matter if it's in AuthResponse. + if (operationResponse.Parameters.ContainsKey(ParameterCode.Secret)) + { + if (this.AuthValues == null) + { + this.AuthValues = new AuthenticationValues(); + //this.DebugReturn(DebugLevel.ERROR, "Server returned secret. Created AuthValues."); + } + + this.AuthValues.Token = operationResponse[ParameterCode.Secret] as string; + } + + switch (operationResponse.OperationCode) + { + case OperationCode.Authenticate: + case OperationCode.AuthenticateOnce: + { + if (operationResponse.ReturnCode != 0) + { + this.DebugReturn(DebugLevel.ERROR, operationResponse.ToStringFull() + " Server: " + this.Server + " Address: " + this.loadBalancingPeer.ServerAddress); + + switch (operationResponse.ReturnCode) + { + case ErrorCode.InvalidAuthentication: + this.DisconnectedCause = DisconnectCause.InvalidAuthentication; + break; + case ErrorCode.CustomAuthenticationFailed: + this.DisconnectedCause = DisconnectCause.CustomAuthenticationFailed; + break; + case ErrorCode.InvalidRegion: + this.DisconnectedCause = DisconnectCause.InvalidRegion; + break; + case ErrorCode.MaxCcuReached: + this.DisconnectedCause = DisconnectCause.MaxCcuReached; + break; + case ErrorCode.OperationNotAllowedInCurrentState: + this.DisconnectedCause = DisconnectCause.OperationNotAllowedInCurrentState; + break; + } + this.State = ClientState.Disconnecting; + this.Disconnect(); + break; // if auth didn't succeed, we disconnect (above) and exit this operation's handling + } + + if (this.Server == ServerConnection.NameServer || this.Server == ServerConnection.MasterServer) + { + if (operationResponse.Parameters.ContainsKey(ParameterCode.UserId)) + { + string incomingId = (string)operationResponse.Parameters[ParameterCode.UserId]; + if (!string.IsNullOrEmpty(incomingId)) + { + this.UserId = incomingId; + this.DebugReturn(DebugLevel.INFO, string.Format("Received your UserID from server. Updating local value to: {0}", this.UserId)); + } + } + if (operationResponse.Parameters.ContainsKey(ParameterCode.NickName)) + { + this.NickName = (string)operationResponse.Parameters[ParameterCode.NickName]; + this.DebugReturn(DebugLevel.INFO, string.Format("Received your NickName from server. Updating local value to: {0}", this.NickName)); + } + + if (operationResponse.Parameters.ContainsKey(ParameterCode.EncryptionData)) + { + this.SetupEncryption((Dictionary)operationResponse.Parameters[ParameterCode.EncryptionData]); + } + } + + if (this.Server == ServerConnection.NameServer) + { + // on the NameServer, authenticate returns the MasterServer address for a region and we hop off to there + this.MasterServerAddress = operationResponse[ParameterCode.Address] as string; + if (this.AuthMode == AuthModeOption.AuthOnceWss) + { + this.DebugReturn(DebugLevel.INFO, string.Format("Due to AuthOnceWss, switching TransportProtocol to ExpectedProtocol: {0}.", this.ExpectedProtocol)); + this.loadBalancingPeer.TransportProtocol = this.ExpectedProtocol; + } + this.DisconnectToReconnect(); + } + else if (this.Server == ServerConnection.MasterServer) + { + this.State = ClientState.ConnectedToMasterserver; + + if (this.AuthMode != AuthModeOption.Auth) + { + this.loadBalancingPeer.OpSettings(this.EnableLobbyStatistics); + } + if (this.AutoJoinLobby) + { + this.loadBalancingPeer.OpJoinLobby(this.CurrentLobby); + } + } + else if (this.Server == ServerConnection.GameServer) + { + this.State = ClientState.Joining; + this.enterRoomParamsCache.PlayerProperties = this.LocalPlayer.AllProperties; + this.enterRoomParamsCache.OnGameServer = true; + + if (this.lastJoinType == JoinType.JoinRoom || this.lastJoinType == JoinType.JoinRandomRoom || this.lastJoinType == JoinType.JoinOrCreateRoom) + { + this.loadBalancingPeer.OpJoinRoom(this.enterRoomParamsCache); + } + else if (this.lastJoinType == JoinType.CreateRoom) + { + this.loadBalancingPeer.OpCreateRoom(this.enterRoomParamsCache); + } + break; + } + break; + } + + case OperationCode.GetRegions: + this.AvailableRegions = operationResponse[ParameterCode.Region] as string[]; + this.AvailableRegionsServers = operationResponse[ParameterCode.Address] as string[]; + break; + + case OperationCode.JoinRandomGame: // this happens only on the master server. on gameserver this is a "regular" join + case OperationCode.CreateGame: + case OperationCode.JoinGame: + + if (operationResponse.ReturnCode != 0) + { + //PhotonNetworkingMessage callback = PhotonNetworkingMessage.OnPhotonRandomJoinFailed; + //if (operationResponse.OperationCode == OperationCode.CreateGame) callback = PhotonNetworkingMessage.OnPhotonCreateRoomFailed; + //if (operationResponse.OperationCode == OperationCode.JoinGame) callback = PhotonNetworkingMessage.OnPhotonJoinRoomFailed; + //SendMonoMessage(callback, operationResponse.ReturnCode, operationResponse.DebugMessage); + + if (this.Server == ServerConnection.GameServer) + { + this.DisconnectToReconnect(); + } + else + { + this.State = (this.inLobby) ? ClientState.JoinedLobby : ClientState.ConnectedToMasterserver; + } + } + else + { + if (this.Server == ServerConnection.GameServer) + { + this.GameEnteredOnGameServer(operationResponse); + } + else + { + this.GameServerAddress = (string) operationResponse[ParameterCode.Address]; + string roomName = operationResponse[ParameterCode.RoomName] as string; + if (!string.IsNullOrEmpty(roomName)) + { + this.enterRoomParamsCache.RoomName = roomName; + } + + this.DisconnectToReconnect(); + } + } + break; + + case OperationCode.GetGameList: + if (operationResponse.ReturnCode != 0) + { + this.DebugReturn(DebugLevel.ERROR, "GetGameList failed: " + operationResponse.ToStringFull()); + break; + } + + this.RoomInfoList = new Dictionary(); + Hashtable games = (Hashtable)operationResponse[ParameterCode.GameList]; + foreach (string gameName in games.Keys) + { + RoomInfo game = new RoomInfo(gameName, (Hashtable)games[gameName]); + this.RoomInfoList[gameName] = game; + } + + // TODO: OnRoomListUpdate + break; + + case OperationCode.JoinLobby: + this.State = ClientState.JoinedLobby; + this.inLobby = true; + // TODO: OnJoinedLobby + break; + + case OperationCode.LeaveLobby: + this.State = ClientState.ConnectedToMasterserver; + this.inLobby = false; + break; + + case OperationCode.Leave: + //this.CleanCachedValues(); // this is done in status change on "disconnect" + this.DisconnectToReconnect(); + break; + + case OperationCode.FindFriends: + if (operationResponse.ReturnCode != 0) + { + this.DebugReturn(DebugLevel.ERROR, "OpFindFriends failed: " + operationResponse.ToStringFull()); + this.isFetchingFriendList = false; + break; + } + + bool[] onlineList = operationResponse[ParameterCode.FindFriendsResponseOnlineList] as bool[]; + string[] roomList = operationResponse[ParameterCode.FindFriendsResponseRoomIdList] as string[]; + + List friendList = new List(this.friendListRequested.Length); + for (int index = 0; index < this.friendListRequested.Length; index++) + { + FriendInfo friend = new FriendInfo(); + friend.Name = this.friendListRequested[index]; + friend.Room = roomList[index]; + friend.IsOnline = onlineList[index]; + friendList.Insert(index, friend); + } + this.FriendList = friendList; + + this.friendListRequested = null; + this.isFetchingFriendList = false; + this.friendListTimestamp = Environment.TickCount; + if (this.friendListTimestamp == 0) + { + this.friendListTimestamp = 1; // makes sure the timestamp is not accidentally 0 + } + break; + } + + if (this.OnOpResponseAction != null) this.OnOpResponseAction(operationResponse); + } + + /// + /// Uses the connection's statusCodes to advance the internal state and call operations as needed. + /// + /// This method is essential to update the internal state of a LoadBalancingClient. Overriding methods must call base.OnStatusChanged. + public virtual void OnStatusChanged(StatusCode statusCode) + { + switch (statusCode) + { + case StatusCode.Connect: + this.inLobby = false; + + if (this.State == ClientState.ConnectingToNameServer) + { + if (this.loadBalancingPeer.DebugOut >= DebugLevel.ALL) + { + this.DebugReturn(DebugLevel.ALL, "Connected to nameserver."); + } + + this.Server = ServerConnection.NameServer; + if (this.AuthValues != null) + { + this.AuthValues.Token = null; // when connecting to NameServer, invalidate the secret (only) + } + } + + if (this.State == ClientState.ConnectingToGameserver) + { + if (this.loadBalancingPeer.DebugOut >= DebugLevel.ALL) + { + this.DebugReturn(DebugLevel.ALL, "Connected to gameserver."); + } + + this.Server = ServerConnection.GameServer; + } + + if (this.State == ClientState.ConnectingToMasterserver) + { + if (this.loadBalancingPeer.DebugOut >= DebugLevel.ALL) + { + this.DebugReturn(DebugLevel.ALL, "Connected to masterserver."); + } + + this.Server = ServerConnection.MasterServer; + } + + + if (this.loadBalancingPeer.TransportProtocol != ConnectionProtocol.WebSocketSecure) + { + if (this.Server == ServerConnection.NameServer || this.AuthMode == AuthModeOption.Auth) + { + this.loadBalancingPeer.EstablishEncryption(); + } + } + else + { + goto case StatusCode.EncryptionEstablished; + } + + break; + + case StatusCode.EncryptionEstablished: + // on nameserver, the "process" is stopped here, so the developer/game can either get regions or authenticate with a specific region + if (this.Server == ServerConnection.NameServer) + { + this.State = ClientState.ConnectedToNameServer; + + // TODO: should we automatically get the regions?! + } + + if (this.Server != ServerConnection.NameServer && (this.AuthMode == AuthModeOption.AuthOnce || this.AuthMode == AuthModeOption.AuthOnceWss)) + { + // AuthMode "Once" means we only authenticate on the NameServer + break; + } + + + // on any other server we might now have to authenticate still, so the client can do anything at all + if (!this.didAuthenticate && (!this.IsUsingNameServer || this.CloudRegion != null)) + { + // once encryption is availble, the client should send one (secure) authenticate. it includes the AppId (which identifies your app on the Photon Cloud) + this.didAuthenticate = this.CallAuthenticate(); + + if (this.didAuthenticate) + { + this.State = ClientState.Authenticating; + } + else + { + this.DebugReturn(DebugLevel.ERROR, "Error calling OpAuthenticate! Did not work. Check log output, AuthValues and if you're connected. State: " + this.State); + } + } + break; + + case StatusCode.Disconnect: + // disconnect due to connection exception is handled below (don't connect to GS or master in that case) + + this.CleanCachedValues(); + this.didAuthenticate = false; // on connect, we know that we didn't + this.inLobby = false; + + switch (this.State) + { + case ClientState.PeerCreated: + case ClientState.Disconnecting: + if (this.AuthValues != null) + { + this.AuthValues.Token = null; // when leaving the server, invalidate the secret (but not the auth values) + } + this.State = ClientState.Disconnected; + break; + + case ClientState.DisconnectingFromGameserver: + case ClientState.DisconnectingFromNameServer: + this.Connect(); // this gets the client back to the Master Server + break; + + case ClientState.DisconnectingFromMasterserver: + this.ConnectToGameServer(); // this connects the client with the Game Server (when joining/creating a room) + break; + + default: + string stacktrace = ""; + #if DEBUG && !NETFX_CORE + stacktrace = new System.Diagnostics.StackTrace(true).ToString(); + #endif + this.DebugReturn(DebugLevel.WARNING, "Got a unexpected Disconnect in LoadBalancingClient State: " + this.State + ". Server: " + this.Server+ " Trace: " + stacktrace); + + if (this.AuthValues != null) + { + this.AuthValues.Token = null; // when leaving the server, invalidate the secret (but not the auth values) + } + this.State = ClientState.Disconnected; + + break; + } + break; + + case StatusCode.DisconnectByServerUserLimit: + this.DebugReturn(DebugLevel.ERROR, "The Photon license's CCU Limit was reached. Server rejected this connection. Wait and re-try."); + if (this.AuthValues != null) + { + this.AuthValues.Token = null; // when leaving the server, invalidate the secret (but not the auth values) + } + this.DisconnectedCause = DisconnectCause.DisconnectByServerUserLimit; + this.State = ClientState.Disconnected; + break; + case StatusCode.ExceptionOnConnect: + case StatusCode.SecurityExceptionOnConnect: + if (this.AuthValues != null) + { + this.AuthValues.Token = null; // when leaving the server, invalidate the secret (but not the auth values) + } + this.DisconnectedCause = DisconnectCause.ExceptionOnConnect; + this.State = ClientState.Disconnected; + break; + case StatusCode.DisconnectByServer: + if (this.AuthValues != null) + { + this.AuthValues.Token = null; // when leaving the server, invalidate the secret (but not the auth values) + } + this.DisconnectedCause = DisconnectCause.DisconnectByServer; + this.State = ClientState.Disconnected; + break; + case StatusCode.DisconnectByServerLogic: + if (this.AuthValues != null) + { + this.AuthValues.Token = null; // when leaving the server, invalidate the secret (but not the auth values) + } + this.DisconnectedCause = DisconnectCause.DisconnectByServerLogic; + this.State = ClientState.Disconnected; + break; + case StatusCode.TimeoutDisconnect: + if (this.AuthValues != null) + { + this.AuthValues.Token = null; // when leaving the server, invalidate the secret (but not the auth values) + } + this.DisconnectedCause = DisconnectCause.TimeoutDisconnect; + this.State = ClientState.Disconnected; + break; + case StatusCode.Exception: + case StatusCode.ExceptionOnReceive: + if (this.AuthValues != null) + { + this.AuthValues.Token = null; // when leaving the server, invalidate the secret (but not the auth values) + } + this.DisconnectedCause = DisconnectCause.Exception; + this.State = ClientState.Disconnected; + break; + } + } + + /// + /// Uses the photonEvent's provided by the server to advance the internal state and call ops as needed. + /// + /// This method is essential to update the internal state of a LoadBalancingClient. Overriding methods must call base.OnEvent. + public virtual void OnEvent(EventData photonEvent) + { + + int actorNr = 0; + Player originatingPlayer = null; + if (photonEvent.Parameters.ContainsKey(ParameterCode.ActorNr)) + { + actorNr = (int) photonEvent[ParameterCode.ActorNr]; + if (this.CurrentRoom != null) + { + originatingPlayer = this.CurrentRoom.GetPlayer(actorNr); + } + } + + + switch (photonEvent.Code) + { + case EventCode.GameList: + case EventCode.GameListUpdate: + if (photonEvent.Code == EventCode.GameList) + { + this.RoomInfoList = new Dictionary(); + } + + Hashtable games = (Hashtable)photonEvent[ParameterCode.GameList]; + foreach (string gameName in games.Keys) + { + RoomInfo game = new RoomInfo(gameName, (Hashtable)games[gameName]); + if (game.removedFromList) + { + this.RoomInfoList.Remove(gameName); + } + else + { + this.RoomInfoList[gameName] = game; + } + } + //SendMonoMessage(PhotonNetworkingMessage.OnReceivedRoomListUpdate); + break; + + case EventCode.Join: + Hashtable actorProperties = (Hashtable)photonEvent[ParameterCode.PlayerProperties]; + + if (originatingPlayer == null) + { + Player newPlayer = this.CreatePlayer(string.Empty, actorNr, false, actorProperties); + this.CurrentRoom.StorePlayer(newPlayer); + } + else + { + originatingPlayer.InternalCacheProperties(actorProperties); + originatingPlayer.IsInactive = false; + } + + if (actorNr == this.LocalPlayer.ID) + { + // in this player's own join event, we get a complete list of players in the room, so check if we know each of the + int[] actorsInRoom = (int[])photonEvent[ParameterCode.ActorList]; + this.UpdatedActorList(actorsInRoom); + } + break; + + case EventCode.Leave: + bool isInactive = false; + if (photonEvent.Parameters.ContainsKey(ParameterCode.IsInactive)) + { + isInactive = (bool)photonEvent.Parameters[ParameterCode.IsInactive]; + } + + if (isInactive) + { + originatingPlayer.IsInactive = true; + } + else + { + this.CurrentRoom.RemovePlayer(actorNr); + } + + if (photonEvent.Parameters.ContainsKey(ParameterCode.MasterClientId)) + { + int newMaster = (int)photonEvent[ParameterCode.MasterClientId]; + if (newMaster != 0) + { + this.CurrentRoom.masterClientId = newMaster; + } + } + break; + + case EventCode.PropertiesChanged: + // whenever properties are sent in-room, they can be broadcasted as event (which we handle here) + // we get PLAYERproperties if actorNr > 0 or ROOMproperties if actorNumber is not set or 0 + int targetActorNr = 0; + if (photonEvent.Parameters.ContainsKey(ParameterCode.TargetActorNr)) + { + targetActorNr = (int)photonEvent[ParameterCode.TargetActorNr]; + } + + Hashtable gameProperties = null; + Hashtable actorProps = null; + if (targetActorNr == 0) + { + gameProperties = (Hashtable)photonEvent[ParameterCode.Properties]; + } + else + { + actorProps = (Hashtable)photonEvent[ParameterCode.Properties]; + } + + this.ReadoutProperties(gameProperties, actorProps, targetActorNr); + break; + + case EventCode.AppStats: + // only the master server sends these in (1 minute) intervals + this.PlayersInRoomsCount = (int)photonEvent[ParameterCode.PeerCount]; + this.RoomsCount = (int)photonEvent[ParameterCode.GameCount]; + this.PlayersOnMasterCount = (int)photonEvent[ParameterCode.MasterPeerCount]; + break; + + case EventCode.LobbyStats: + string[] names = photonEvent[ParameterCode.LobbyName] as string[]; + byte[] types = photonEvent[ParameterCode.LobbyType] as byte[]; + int[] peers = photonEvent[ParameterCode.PeerCount] as int[]; + int[] rooms = photonEvent[ParameterCode.GameCount] as int[]; + + this.lobbyStatistics.Clear(); + for (int i = 0; i < names.Length; i++) + { + TypedLobbyInfo info = new TypedLobbyInfo(); + info.Name = names[i]; + info.Type = (LobbyType)types[i]; + info.PlayerCount = peers[i]; + info.RoomCount = rooms[i]; + + this.lobbyStatistics.Add(info); + } + + //SendMonoMessage(PhotonNetworkingMessage.OnLobbyStatisticsUpdate); + break; + + case EventCode.ErrorInfo: + if (this.OnEventAction != null) + { + this.OnEventAction(photonEvent); + } + break; + + case EventCode.AuthEvent: + if (this.AuthValues == null) + { + this.AuthValues = new AuthenticationValues(); + } + + this.AuthValues.Token = photonEvent[ParameterCode.Secret] as string; + break; + + } + + if (this.OnEventAction != null) this.OnEventAction(photonEvent); + } + + /// In Photon 4, "raw messages" will get their own callback method in the interface. Not used yet. + public virtual void OnMessage(object message) + { + this.DebugReturn(DebugLevel.ALL, string.Format("got OnMessage {0}", message)); + } + + #endregion + + + private void SetupEncryption(Dictionary encryptionData) + { + var mode = (EncryptionMode)(byte)encryptionData[EncryptionDataParameters.Mode]; + switch (mode) + { + case EncryptionMode.PayloadEncryption: + byte[] encryptionSecret = (byte[])encryptionData[EncryptionDataParameters.Secret1]; + this.loadBalancingPeer.InitPayloadEncryption(encryptionSecret); + break; + case EncryptionMode.DatagramEncryption: + { + byte[] secret1 = (byte[])encryptionData[EncryptionDataParameters.Secret1]; + byte[] secret2 = (byte[])encryptionData[EncryptionDataParameters.Secret2]; + this.loadBalancingPeer.InitDatagramEncryption(secret1, secret2); + } + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + + + /// + /// This operation makes Photon call your custom web-service by path/name with the given parameters (converted into Json). + /// + /// + /// A WebRPC calls a custom, http-based function on a server you provide. The uriPath is relative to a "base path" + /// which is configured server-side. The sent parameters get converted from C# types to Json. Vice versa, the response + /// of the web-service will be converted to C# types and sent back as normal operation response. + /// + /// + /// To use this feature, you have to setup your server: + /// + /// For a Photon Cloud application, + /// visit the Dashboard and setup "WebHooks". The BaseUrl is used for WebRPCs as well. + /// + /// + /// The response by Photon will call OnOperationResponse() with Code: OperationCode.WebRpc. + /// To get this response, you can derive the LoadBalancingClient, or (much easier) you set a suitable + /// OnOpResponseAction to be called. + /// + /// + /// It's important to understand that the OperationResponse tells you if the WebRPC could be called or not + /// but the content of the response will contain the values the web-service sent (if any). + /// If the web-service could not execute the request, it might return another error and a message. This is + /// inside the OperationResponse. + /// + /// The class WebRpcResponse is a helper-class that extracts the most valuable content from the WebRPC + /// response. + /// + /// + /// To get a WebRPC response, set a OnOpResponseAction: + /// + /// this.OnOpResponseAction = this.OpResponseHandler; + /// + /// It could look like this: + /// + /// public void OpResponseHandler(OperationResponse operationResponse) + /// { + /// if (operationResponse.OperationCode == OperationCode.WebRpc) + /// { + /// if (operationResponse.ReturnCode != 0) + /// { + /// Console.WriteLine("WebRpc failed. Response: " + operationResponse.ToStringFull()); + /// } + /// else + /// { + /// WebRpcResponse webResponse = new WebRpcResponse(operationResponse); + /// Console.WriteLine(webResponse.DebugMessage); // message from the webserver + /// + /// // do something with the response... + /// } + /// } + /// } + /// + /// The url path to call, relative to the baseUrl configured on Photon's server-side. + /// The parameters to send to the web-service method. + /// Defines if the authentication cookie gets sent to a WebHook (if setup). + public bool OpWebRpc(string uriPath, object parameters, bool sendAuthCookie = false) + { + Dictionary opParameters = new Dictionary(); + opParameters.Add(ParameterCode.UriPath, uriPath); + opParameters.Add(ParameterCode.WebRpcParameters, parameters); + if (sendAuthCookie) + { + opParameters.Add(ParameterCode.EventForward, WebFlags.SendAuthCookieConst); + } + return this.loadBalancingPeer.OpCustom(OperationCode.WebRpc, opParameters, true); + } + } + +} diff --git a/Assets/Runtime/Photon/PhotonLoadbalancingApi/LoadBalancingClient.cs.meta b/Assets/Runtime/Photon/PhotonLoadbalancingApi/LoadBalancingClient.cs.meta new file mode 100644 index 0000000..eb06bdf --- /dev/null +++ b/Assets/Runtime/Photon/PhotonLoadbalancingApi/LoadBalancingClient.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f9a8ceffad89d164e8b5ba037f5d34f7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Photon/PhotonLoadbalancingApi/LoadBalancingPeer.cs b/Assets/Runtime/Photon/PhotonLoadbalancingApi/LoadBalancingPeer.cs new file mode 100644 index 0000000..f70af97 --- /dev/null +++ b/Assets/Runtime/Photon/PhotonLoadbalancingApi/LoadBalancingPeer.cs @@ -0,0 +1,1893 @@ +// ---------------------------------------------------------------------------- +// +// Loadbalancing Framework for Photon - Copyright (C) 2016 Exit Games GmbH +// +// +// Provides operations to use the LoadBalancing and Cloud photon servers. +// No logic is implemented here. +// +// developer@photonengine.com +// ---------------------------------------------------------------------------- + +#define UNITY + +namespace ExitGames.Client.Photon.LoadBalancing +{ + using System; + using System.Collections; + using System.Collections.Generic; + using ExitGames.Client.Photon; + + #if UNITY + using UnityEngine; + using Debug = UnityEngine.Debug; + #endif + #if UNITY || NETFX_CORE + using Hashtable = ExitGames.Client.Photon.Hashtable; + using SupportClass = ExitGames.Client.Photon.SupportClass; + #endif + + + /// + /// A LoadbalancingPeer provides the operations and enum definitions needed to use the loadbalancing server application which is also used in Photon Cloud. + /// + /// + /// Internally used by PUN. + /// The LoadBalancingPeer does not keep a state, instead this is done by a LoadBalancingClient. + /// + public class LoadBalancingPeer : PhotonPeer + { + protected internal static Type PingImplementation = null; + + private readonly Dictionary opParameters = new Dictionary(); // used in OpRaiseEvent() (avoids lots of new Dictionary() calls) + + + /// + /// Creates a Peer with specified connection protocol. You need to set the Listener before using the peer. + /// + /// Each connection protocol has it's own default networking ports for Photon. + /// The preferred option is UDP. + public LoadBalancingPeer(ConnectionProtocol protocolType) : base(protocolType) + { + // this does not require a Listener, so: + // make sure to set this.Listener before using a peer! + + this.ConfigUnitySockets(); + } + + /// + /// Creates a Peer with specified connection protocol and a Listener for callbacks. + /// + public LoadBalancingPeer(IPhotonPeerListener listener, ConnectionProtocol protocolType) : this(protocolType) + { + this.Listener = listener; + } + + + // this sets up the socket implementations to use, depending on export + [System.Diagnostics.Conditional("UNITY")] + private void ConfigUnitySockets() + { + #pragma warning disable 0162 // the library variant defines if we should use PUN's SocketUdp variant (at all) + if (PhotonPeer.NoSocket) + { + #if !UNITY_EDITOR && (UNITY_PS3 || UNITY_ANDROID) + this.SocketImplementationConfig[ConnectionProtocol.Udp] = typeof(SocketUdpNativeDynamic); + PingImplementation = typeof(PingNativeDynamic); + #elif !UNITY_EDITOR && (UNITY_IPHONE || UNITY_SWITCH) + this.SocketImplementationConfig[ConnectionProtocol.Udp] = typeof(SocketUdpNativeStatic); + PingImplementation = typeof(PingNativeStatic); + #elif !UNITY_EDITOR && (UNITY_WINRT) + // this automatically uses a separate assembly-file with Win8-style Socket usage (not possible in Editor) + #else + Type udpSocket = Type.GetType("ExitGames.Client.Photon.SocketUdp, Assembly-CSharp"); + this.SocketImplementationConfig[ConnectionProtocol.Udp] = udpSocket; + if (udpSocket == null) + { + #if UNITY + UnityEngine.Debug.Log("Could not find a suitable C# socket class. This Photon3Unity3D.dll only supports native socket plugins."); + #endif + } + #endif + } + #pragma warning restore 0162 + + + PingImplementation = typeof(PingMono); + #if UNITY_WEBGL + Type pingType = Type.GetType("ExitGames.Client.Photon.PingHttp, Assembly-CSharp", false); + PingImplementation = pingType ?? Type.GetType("ExitGames.Client.Photon.PingHttp, Assembly-CSharp-firstpass", false); + #endif + #if !UNITY_EDITOR && UNITY_WINRT + PingImplementation = typeof(PingWindowsStore); + #endif + + + // to support WebGL export in Unity, we find and assign the SocketWebTcpThread or SocketWebTcpCoroutine class (if it's in the project). + Type websocketType = Type.GetType("ExitGames.Client.Photon.SocketWebTcpThread, Assembly-CSharp", false); + websocketType = websocketType ?? Type.GetType("ExitGames.Client.Photon.SocketWebTcpThread, Assembly-CSharp-firstpass", false); + websocketType = websocketType ?? Type.GetType("ExitGames.Client.Photon.SocketWebTcpCoroutine, Assembly-CSharp", false); + websocketType = websocketType ?? Type.GetType("ExitGames.Client.Photon.SocketWebTcpCoroutine, Assembly-CSharp-firstpass", false); + if (websocketType != null) + { + this.SocketImplementationConfig[ConnectionProtocol.WebSocket] = websocketType; + this.SocketImplementationConfig[ConnectionProtocol.WebSocketSecure] = websocketType; + } + } + + + public virtual bool OpGetRegions(string appId) + { + Dictionary parameters = new Dictionary(); + parameters[(byte)ParameterCode.ApplicationId] = appId; + + return this.OpCustom(OperationCode.GetRegions, parameters, true, 0, true); + } + + /// + /// Joins the lobby on the Master Server, where you get a list of RoomInfos of currently open rooms. + /// This is an async request which triggers a OnOperationResponse() call. + /// + /// The lobby join to. + /// If the operation could be sent (has to be connected). + public virtual bool OpJoinLobby(TypedLobby lobby = null) + { + if (this.DebugOut >= DebugLevel.INFO) + { + this.Listener.DebugReturn(DebugLevel.INFO, "OpJoinLobby()"); + } + + Dictionary parameters = null; + if (lobby != null && !lobby.IsDefault) + { + parameters = new Dictionary(); + parameters[(byte)ParameterCode.LobbyName] = lobby.Name; + parameters[(byte)ParameterCode.LobbyType] = (byte)lobby.Type; + } + + return this.OpCustom(OperationCode.JoinLobby, parameters, true); + } + + + /// + /// Leaves the lobby on the Master Server. + /// This is an async request which triggers a OnOperationResponse() call. + /// + /// If the operation could be sent (requires connection). + public virtual bool OpLeaveLobby() + { + if (this.DebugOut >= DebugLevel.INFO) + { + this.Listener.DebugReturn(DebugLevel.INFO, "OpLeaveLobby()"); + } + + return this.OpCustom(OperationCode.LeaveLobby, null, true); + } + + + /// Used in the RoomOptionFlags parameter, this bitmask toggles options in the room. + enum RoomOptionBit : int + { + CheckUserOnJoin = 0x01, // toggles a check of the UserId when joining (enabling returning to a game) + DeleteCacheOnLeave = 0x02, // deletes cache on leave + SuppressRoomEvents = 0x04, // suppresses all room events + PublishUserId = 0x08, // signals that we should publish userId + DeleteNullProps = 0x10, // signals that we should remove property if its value was set to null. see RoomOption to Delete Null Properties + BroadcastPropsChangeToAll = 0x20, // signals that we should send PropertyChanged event to all room players including initiator + } + + private void RoomOptionsToOpParameters(Dictionary op, RoomOptions roomOptions) + { + if (roomOptions == null) + { + roomOptions = new RoomOptions(); + } + + Hashtable gameProperties = new Hashtable(); + gameProperties[GamePropertyKey.IsOpen] = roomOptions.IsOpen; + gameProperties[GamePropertyKey.IsVisible] = roomOptions.IsVisible; + gameProperties[GamePropertyKey.PropsListedInLobby] = (roomOptions.CustomRoomPropertiesForLobby == null) ? new string[0] : roomOptions.CustomRoomPropertiesForLobby; + gameProperties.MergeStringKeys(roomOptions.CustomRoomProperties); + if (roomOptions.MaxPlayers > 0) + { + gameProperties[GamePropertyKey.MaxPlayers] = roomOptions.MaxPlayers; + } + op[ParameterCode.GameProperties] = gameProperties; + + + int flags = 0; // a new way to send the room options as bitwise-flags + op[ParameterCode.CleanupCacheOnLeave] = roomOptions.CleanupCacheOnLeave; // this is actually setting the room's config + flags = flags | (int)RoomOptionBit.DeleteCacheOnLeave; + + if (!roomOptions.CleanupCacheOnLeave) + { + gameProperties[GamePropertyKey.CleanupCacheOnLeave] = false; // this is only informational for the clients which join + } + + if (roomOptions.CheckUserOnJoin) + { + flags = flags | (int)RoomOptionBit.CheckUserOnJoin; + op[ParameterCode.CheckUserOnJoin] = true; //TURNBASED + } + + if (roomOptions.PlayerTtl > 0 || roomOptions.PlayerTtl == -1) + { + flags = flags | (int)RoomOptionBit.CheckUserOnJoin; + op[ParameterCode.CheckUserOnJoin] = true; // this affects rejoining a room. requires a userId to be used. added in v1.67 + op[ParameterCode.PlayerTTL] = roomOptions.PlayerTtl; // TURNBASED + } + + if (roomOptions.EmptyRoomTtl > 0) + { + op[ParameterCode.EmptyRoomTTL] = roomOptions.EmptyRoomTtl; //TURNBASED + } + + if (roomOptions.SuppressRoomEvents) + { + flags = flags | (int)RoomOptionBit.SuppressRoomEvents; + op[ParameterCode.SuppressRoomEvents] = true; + } + if (roomOptions.Plugins != null) + { + op[ParameterCode.Plugins] = roomOptions.Plugins; + } + if (roomOptions.PublishUserId) + { + flags = flags | (int)RoomOptionBit.PublishUserId; + op[ParameterCode.PublishUserId] = true; + } + if (roomOptions.DeleteNullProperties) + { + flags = flags | (int)RoomOptionBit.DeleteNullProps; // this is only settable as flag + } + + op[ParameterCode.RoomOptionFlags] = flags; + } + + + /// + /// Creates a room (on either Master or Game Server). + /// The OperationResponse depends on the server the peer is connected to: + /// Master will return a Game Server to connect to. + /// Game Server will return the joined Room's data. + /// This is an async request which triggers a OnOperationResponse() call. + /// + /// + /// If the room is already existing, the OperationResponse will have a returnCode of ErrorCode.GameAlreadyExists. + /// + public virtual bool OpCreateRoom(EnterRoomParams opParams) + { + if (this.DebugOut >= DebugLevel.INFO) + { + this.Listener.DebugReturn(DebugLevel.INFO, "OpCreateRoom()"); + } + + Dictionary op = new Dictionary(); + + if (!string.IsNullOrEmpty(opParams.RoomName)) + { + op[ParameterCode.RoomName] = opParams.RoomName; + } + if (opParams.Lobby != null && !string.IsNullOrEmpty(opParams.Lobby.Name)) + { + op[ParameterCode.LobbyName] = opParams.Lobby.Name; + op[ParameterCode.LobbyType] = (byte)opParams.Lobby.Type; + } + + if (opParams.ExpectedUsers != null && opParams.ExpectedUsers.Length > 0) + { + op[ParameterCode.Add] = opParams.ExpectedUsers; + } + if (opParams.OnGameServer) + { + if (opParams.PlayerProperties != null && opParams.PlayerProperties.Count > 0) + { + op[ParameterCode.PlayerProperties] = opParams.PlayerProperties; + op[ParameterCode.Broadcast] = true; // TODO: check if this also makes sense when creating a room?! // broadcast actor properties + } + + this.RoomOptionsToOpParameters(op, opParams.RoomOptions); + } + + //this.Listener.DebugReturn(DebugLevel.INFO, "CreateGame: " + SupportClass.DictionaryToString(op)); + return this.OpCustom(OperationCode.CreateGame, op, true); + } + + /// + /// Joins a room by name or creates new room if room with given name not exists. + /// The OperationResponse depends on the server the peer is connected to: + /// Master will return a Game Server to connect to. + /// Game Server will return the joined Room's data. + /// This is an async request which triggers a OnOperationResponse() call. + /// + /// + /// If the room is not existing (anymore), the OperationResponse will have a returnCode of ErrorCode.GameDoesNotExist. + /// Other possible ErrorCodes are: GameClosed, GameFull. + /// + /// If the operation could be sent (requires connection). + public virtual bool OpJoinRoom(EnterRoomParams opParams) + { + if (this.DebugOut >= DebugLevel.INFO) + { + this.Listener.DebugReturn(DebugLevel.INFO, "OpJoinRoom()"); + } + Dictionary op = new Dictionary(); + + if (!string.IsNullOrEmpty(opParams.RoomName)) + { + op[ParameterCode.RoomName] = opParams.RoomName; + } + + if (opParams.CreateIfNotExists) + { + op[ParameterCode.JoinMode] = (byte)JoinMode.CreateIfNotExists; + if (opParams.Lobby != null) + { + op[ParameterCode.LobbyName] = opParams.Lobby.Name; + op[ParameterCode.LobbyType] = (byte)opParams.Lobby.Type; + } + } + + if (opParams.RejoinOnly) + { + op[ParameterCode.JoinMode] = (byte)JoinMode.RejoinOnly; // changed from JoinMode.JoinOrRejoin + } + + if (opParams.ExpectedUsers != null && opParams.ExpectedUsers.Length > 0) + { + op[ParameterCode.Add] = opParams.ExpectedUsers; + } + + if (opParams.OnGameServer) + { + if (opParams.PlayerProperties != null && opParams.PlayerProperties.Count > 0) + { + op[ParameterCode.PlayerProperties] = opParams.PlayerProperties; + op[ParameterCode.Broadcast] = true; // broadcast actor properties + } + + if (opParams.CreateIfNotExists) + { + this.RoomOptionsToOpParameters(op, opParams.RoomOptions); + } + } + + //UnityEngine.Debug.Log("JoinGame: " + SupportClass.DictionaryToString(op)); + return this.OpCustom(OperationCode.JoinGame, op, true); + } + + + /// + /// Operation to join a random, available room. Overloads take additional player properties. + /// This is an async request which triggers a OnOperationResponse() call. + /// If all rooms are closed or full, the OperationResponse will have a returnCode of ErrorCode.NoRandomMatchFound. + /// If successful, the OperationResponse contains a gameserver address and the name of some room. + /// + /// If the operation could be sent currently (requires connection). + public virtual bool OpJoinRandomRoom(OpJoinRandomRoomParams opJoinRandomRoomParams) + { + if (this.DebugOut >= DebugLevel.INFO) + { + this.Listener.DebugReturn(DebugLevel.INFO, "OpJoinRandomRoom()"); + } + + Hashtable expectedRoomProperties = new Hashtable(); + expectedRoomProperties.MergeStringKeys(opJoinRandomRoomParams.ExpectedCustomRoomProperties); + if (opJoinRandomRoomParams.ExpectedMaxPlayers > 0) + { + expectedRoomProperties[GamePropertyKey.MaxPlayers] = opJoinRandomRoomParams.ExpectedMaxPlayers; + } + + Dictionary opParameters = new Dictionary(); + if (expectedRoomProperties.Count > 0) + { + opParameters[ParameterCode.GameProperties] = expectedRoomProperties; + } + + if (opJoinRandomRoomParams.MatchingType != MatchmakingMode.FillRoom) + { + opParameters[ParameterCode.MatchMakingType] = (byte)opJoinRandomRoomParams.MatchingType; + } + + if (opJoinRandomRoomParams.TypedLobby != null && !string.IsNullOrEmpty(opJoinRandomRoomParams.TypedLobby.Name)) + { + opParameters[ParameterCode.LobbyName] = opJoinRandomRoomParams.TypedLobby.Name; + opParameters[ParameterCode.LobbyType] = (byte)opJoinRandomRoomParams.TypedLobby.Type; + } + + if (!string.IsNullOrEmpty(opJoinRandomRoomParams.SqlLobbyFilter)) + { + opParameters[ParameterCode.Data] = opJoinRandomRoomParams.SqlLobbyFilter; + } + + if (opJoinRandomRoomParams.ExpectedUsers != null && opJoinRandomRoomParams.ExpectedUsers.Length > 0) + { + opParameters[ParameterCode.Add] = opJoinRandomRoomParams.ExpectedUsers; + } + + //this.Listener.DebugReturn(DebugLevel.INFO, "OpJoinRandom: " + SupportClass.DictionaryToString(opParameters)); + return this.OpCustom(OperationCode.JoinRandomGame, opParameters, true); + } + + + /// + /// Leaves a room with option to come back later or "for good". + /// + /// Async games can be re-joined (loaded) later on. Set to false, if you want to abandon a game entirely. + /// If the opteration can be send currently. + public virtual bool OpLeaveRoom(bool becomeInactive) + { + Dictionary opParameters = new Dictionary(); + if (becomeInactive) + { + opParameters[ParameterCode.IsInactive] = becomeInactive; + } + return this.OpCustom(OperationCode.Leave, opParameters, true); + } + + /// Gets a list of games matching a SQL-like where clause. + /// + /// Operation is only available in lobbies of type SqlLobby. + /// This is an async request which triggers a OnOperationResponse() call. + /// Returned game list is stored in RoomInfoList. + /// + /// + /// The lobby to query. Has to be of type SqlLobby. + /// The sql query statement. + /// If the operation could be sent (has to be connected). + public virtual bool OpGetGameList(TypedLobby lobby, string queryData) + { + if (this.DebugOut >= DebugLevel.INFO) + { + this.Listener.DebugReturn(DebugLevel.INFO, "OpGetGameList()"); + } + + if (lobby == null) + { + if (this.DebugOut >= DebugLevel.INFO) + { + this.Listener.DebugReturn(DebugLevel.INFO, "OpGetGameList not sent. Lobby cannot be null."); + } + return false; + } + + if (lobby.Type != LobbyType.SqlLobby) + { + if (this.DebugOut >= DebugLevel.INFO) + { + this.Listener.DebugReturn(DebugLevel.INFO, "OpGetGameList not sent. LobbyType must be SqlLobby."); + } + return false; + } + + Dictionary opParameters = new Dictionary(); + opParameters[(byte)ParameterCode.LobbyName] = lobby.Name; + opParameters[(byte)ParameterCode.LobbyType] = (byte)lobby.Type; + opParameters[(byte)ParameterCode.Data] = queryData; + + return this.OpCustom(OperationCode.GetGameList, opParameters, true); + } + + /// + /// Request the rooms and online status for a list of friends (each client must set a unique username via OpAuthenticate). + /// + /// + /// Used on Master Server to find the rooms played by a selected list of users. + /// Users identify themselves by using OpAuthenticate with a unique username. + /// The list of usernames must be fetched from some other source (not provided by Photon). + /// + /// The server response includes 2 arrays of info (each index matching a friend from the request): + /// ParameterCode.FindFriendsResponseOnlineList = bool[] of online states + /// ParameterCode.FindFriendsResponseRoomIdList = string[] of room names (empty string if not in a room) + /// + /// Array of friend's names (make sure they are unique). + /// If the operation could be sent (requires connection). + public virtual bool OpFindFriends(string[] friendsToFind) + { + Dictionary opParameters = new Dictionary(); + if (friendsToFind != null && friendsToFind.Length > 0) + { + opParameters[ParameterCode.FindFriendsRequestList] = friendsToFind; + } + + return this.OpCustom(OperationCode.FindFriends, opParameters, true); + } + + public bool OpSetCustomPropertiesOfActor(int actorNr, Hashtable actorProperties) + { + return this.OpSetPropertiesOfActor(actorNr, actorProperties.StripToStringKeys(), null); + } + + /// + /// Sets properties of a player / actor. + /// Internally this uses OpSetProperties, which can be used to either set room or player properties. + /// + /// The payer ID (a.k.a. actorNumber) of the player to attach these properties to. + /// The properties to add or update. + /// If set, these must be in the current properties-set (on the server) to set actorProperties: CAS. + /// Set these to forward the properties to a WebHook as defined for this app (in Dashboard). + /// If the operation could be sent (requires connection). + protected internal bool OpSetPropertiesOfActor(int actorNr, Hashtable actorProperties, Hashtable expectedProperties = null, WebFlags webflags = null) + { + if (this.DebugOut >= DebugLevel.INFO) + { + this.Listener.DebugReturn(DebugLevel.INFO, "OpSetPropertiesOfActor()"); + } + + if (actorNr <= 0 || actorProperties == null) + { + if (this.DebugOut >= DebugLevel.INFO) + { + this.Listener.DebugReturn(DebugLevel.INFO, "OpSetPropertiesOfActor not sent. ActorNr must be > 0 and actorProperties != null."); + } + return false; + } + + Dictionary opParameters = new Dictionary(); + opParameters.Add(ParameterCode.Properties, actorProperties); + opParameters.Add(ParameterCode.ActorNr, actorNr); + opParameters.Add(ParameterCode.Broadcast, true); + if (expectedProperties != null && expectedProperties.Count != 0) + { + opParameters.Add(ParameterCode.ExpectedValues, expectedProperties); + } + + if (webflags != null && webflags.HttpForward) + { + opParameters[ParameterCode.EventForward] = webflags.WebhookFlags; + } + + return this.OpCustom((byte)OperationCode.SetProperties, opParameters, true, 0, false); + } + + + protected void OpSetPropertyOfRoom(byte propCode, object value) + { + Hashtable properties = new Hashtable(); + properties[propCode] = value; + this.OpSetPropertiesOfRoom(properties); + } + + public bool OpSetCustomPropertiesOfRoom(Hashtable gameProperties) + { + return this.OpSetPropertiesOfRoom(gameProperties.StripToStringKeys()); + } + + /// + /// Sets properties of a room. + /// Internally this uses OpSetProperties, which can be used to either set room or player properties. + /// + /// The properties to add or update. + /// The properties expected when update occurs. (CAS : "Check And Swap") + /// WebFlag to indicate if request should be forwarded as "PathProperties" webhook or not. + /// If the operation could be sent (has to be connected). + protected internal bool OpSetPropertiesOfRoom(Hashtable gameProperties, Hashtable expectedProperties = null, WebFlags webflags = null) + { + if (this.DebugOut >= DebugLevel.INFO) + { + this.Listener.DebugReturn(DebugLevel.INFO, "OpSetPropertiesOfRoom()"); + } + + Dictionary opParameters = new Dictionary(); + opParameters.Add(ParameterCode.Properties, gameProperties); + opParameters.Add(ParameterCode.Broadcast, true); + if (expectedProperties != null && expectedProperties.Count != 0) + { + opParameters.Add(ParameterCode.ExpectedValues, expectedProperties); + } + + if (webflags!=null && webflags.HttpForward) + { + opParameters[ParameterCode.EventForward] = webflags.WebhookFlags; + } + + return this.OpCustom((byte)OperationCode.SetProperties, opParameters, true, 0, false); + } + + /// + /// Sends this app's appId and appVersion to identify this application server side. + /// This is an async request which triggers a OnOperationResponse() call. + /// + /// + /// This operation makes use of encryption, if that is established before. + /// See: EstablishEncryption(). Check encryption with IsEncryptionAvailable. + /// This operation is allowed only once per connection (multiple calls will have ErrorCode != Ok). + /// + /// Your application's name or ID to authenticate. This is assigned by Photon Cloud (webpage). + /// The client's version (clients with differing client appVersions are separated and players don't meet). + /// Contains all values relevant for authentication. Even without account system (external Custom Auth), the clients are allowed to identify themselves. + /// Optional region code, if the client should connect to a specific Photon Cloud Region. + /// Set to true on Master Server to receive "Lobby Statistics" events. + /// If the operation could be sent (has to be connected). + public virtual bool OpAuthenticate(string appId, string appVersion, AuthenticationValues authValues, string regionCode, bool getLobbyStatistics) + { + if (this.DebugOut >= DebugLevel.INFO) + { + this.Listener.DebugReturn(DebugLevel.INFO, "OpAuthenticate()"); + } + + Dictionary opParameters = new Dictionary(); + if (getLobbyStatistics) + { + // must be sent in operation, even if a Token is available + opParameters[ParameterCode.LobbyStats] = true; + } + + // shortcut, if we have a Token + if (authValues != null && authValues.Token != null) + { + opParameters[ParameterCode.Secret] = authValues.Token; + return this.OpCustom(OperationCode.Authenticate, opParameters, true, (byte)0, false); // we don't have to encrypt, when we have a token (which is encrypted) + } + + + // without a token, we send a complete op auth + + opParameters[ParameterCode.AppVersion] = appVersion; + opParameters[ParameterCode.ApplicationId] = appId; + + if (!string.IsNullOrEmpty(regionCode)) + { + opParameters[ParameterCode.Region] = regionCode; + } + + if (authValues != null) + { + + if (!string.IsNullOrEmpty(authValues.UserId)) + { + opParameters[ParameterCode.UserId] = authValues.UserId; + } + + if (authValues.AuthType != CustomAuthenticationType.None) + { + opParameters[ParameterCode.ClientAuthenticationType] = (byte)authValues.AuthType; + if (!string.IsNullOrEmpty(authValues.Token)) + { + opParameters[ParameterCode.Secret] = authValues.Token; + } + else + { + if (!string.IsNullOrEmpty(authValues.AuthGetParameters)) + { + opParameters[ParameterCode.ClientAuthenticationParams] = authValues.AuthGetParameters; + } + if (authValues.AuthPostData != null) + { + opParameters[ParameterCode.ClientAuthenticationData] = authValues.AuthPostData; + } + } + } + } + + return this.OpCustom(OperationCode.Authenticate, opParameters, true, (byte)0, this.IsEncryptionAvailable); + } + + + /// + /// Sends this app's appId and appVersion to identify this application server side. + /// This is an async request which triggers a OnOperationResponse() call. + /// + /// + /// This operation makes use of encryption, if that is established before. + /// See: EstablishEncryption(). Check encryption with IsEncryptionAvailable. + /// This operation is allowed only once per connection (multiple calls will have ErrorCode != Ok). + /// + /// Your application's name or ID to authenticate. This is assigned by Photon Cloud (webpage). + /// The client's version (clients with differing client appVersions are separated and players don't meet). + /// Optional authentication values. The client can set no values or a UserId or some parameters for Custom Authentication by a server. + /// Optional region code, if the client should connect to a specific Photon Cloud Region. + /// + /// + /// If the operation could be sent (has to be connected). + public virtual bool OpAuthenticateOnce(string appId, string appVersion, AuthenticationValues authValues, string regionCode, EncryptionMode encryptionMode, ConnectionProtocol expectedProtocol) + { + if (this.DebugOut >= DebugLevel.INFO) + { + this.Listener.DebugReturn(DebugLevel.INFO, "OpAuthenticate()"); + } + + + var opParameters = new Dictionary(); + + // shortcut, if we have a Token + if (authValues != null && authValues.Token != null) + { + opParameters[ParameterCode.Secret] = authValues.Token; + return this.OpCustom(OperationCode.AuthenticateOnce, opParameters, true, (byte)0, false); // we don't have to encrypt, when we have a token (which is encrypted) + } + + + opParameters[ParameterCode.ExpectedProtocol] = (byte)expectedProtocol; + opParameters[ParameterCode.EncryptionMode] = (byte)encryptionMode; + + opParameters[ParameterCode.AppVersion] = appVersion; + opParameters[ParameterCode.ApplicationId] = appId; + + if (!string.IsNullOrEmpty(regionCode)) + { + opParameters[ParameterCode.Region] = regionCode; + } + + if (authValues != null) + { + if (!string.IsNullOrEmpty(authValues.UserId)) + { + opParameters[ParameterCode.UserId] = authValues.UserId; + } + + if (authValues.AuthType != CustomAuthenticationType.None) + { + opParameters[ParameterCode.ClientAuthenticationType] = (byte)authValues.AuthType; + if (!string.IsNullOrEmpty(authValues.Token)) + { + opParameters[ParameterCode.Secret] = authValues.Token; + } + else + { + if (!string.IsNullOrEmpty(authValues.AuthGetParameters)) + { + opParameters[ParameterCode.ClientAuthenticationParams] = authValues.AuthGetParameters; + } + if (authValues.AuthPostData != null) + { + opParameters[ParameterCode.ClientAuthenticationData] = authValues.AuthPostData; + } + } + } + } + + return this.OpCustom(OperationCode.AuthenticateOnce, opParameters, true, (byte)0, this.IsEncryptionAvailable); + } + + /// + /// Operation to handle this client's interest groups (for events in room). + /// + /// + /// Note the difference between passing null and byte[0]: + /// null won't add/remove any groups. + /// byte[0] will add/remove all (existing) groups. + /// First, removing groups is executed. This way, you could leave all groups and join only the ones provided. + /// + /// Changes become active not immediately but when the server executes this operation (approximately RTT/2). + /// + /// Groups to remove from interest. Null will not remove any. A byte[0] will remove all. + /// Groups to add to interest. Null will not add any. A byte[0] will add all current. + /// If operation could be enqueued for sending. Sent when calling: Service or SendOutgoingCommands. + public virtual bool OpChangeGroups(byte[] groupsToRemove, byte[] groupsToAdd) + { + if (this.DebugOut >= DebugLevel.ALL) + { + this.Listener.DebugReturn(DebugLevel.ALL, "OpChangeGroups()"); + } + + Dictionary opParameters = new Dictionary(); + if (groupsToRemove != null) + { + opParameters[(byte)ParameterCode.Remove] = groupsToRemove; + } + if (groupsToAdd != null) + { + opParameters[(byte)ParameterCode.Add] = groupsToAdd; + } + + return this.OpCustom((byte)OperationCode.ChangeGroups, opParameters, true, 0); + } + + + /// + /// Send an event with custom code/type and any content to the other players in the same room. + /// + /// This override explicitly uses another parameter order to not mix it up with the implementation for Hashtable only. + /// Identifies this type of event (and the content). Your game's event codes can start with 0. + /// Any serializable datatype (including Hashtable like the other OpRaiseEvent overloads). + /// If this event has to arrive reliably (potentially repeated if it's lost). + /// Contains (slightly) less often used options. If you pass null, the default options will be used. + /// If operation could be enqueued for sending. Sent when calling: Service or SendOutgoingCommands. + public virtual bool OpRaiseEvent(byte eventCode, object customEventContent, bool sendReliable, RaiseEventOptions raiseEventOptions) + { + this.opParameters.Clear(); // re-used private variable to avoid many new Dictionary() calls (garbage collection) + this.opParameters[(byte)ParameterCode.Code] = (byte)eventCode; + if (customEventContent != null) + { + this.opParameters[(byte) ParameterCode.Data] = customEventContent; + } + + if (raiseEventOptions == null) + { + raiseEventOptions = RaiseEventOptions.Default; + } + else + { + if (raiseEventOptions.CachingOption != EventCaching.DoNotCache) + { + this.opParameters[(byte) ParameterCode.Cache] = (byte) raiseEventOptions.CachingOption; + } + if (raiseEventOptions.Receivers != ReceiverGroup.Others) + { + this.opParameters[(byte) ParameterCode.ReceiverGroup] = (byte) raiseEventOptions.Receivers; + } + if (raiseEventOptions.InterestGroup != 0) + { + this.opParameters[(byte) ParameterCode.Group] = (byte) raiseEventOptions.InterestGroup; + } + if (raiseEventOptions.TargetActors != null) + { + this.opParameters[(byte) ParameterCode.ActorList] = raiseEventOptions.TargetActors; + } + if (raiseEventOptions.Flags.HttpForward) + { + this.opParameters[(byte) ParameterCode.EventForward] = raiseEventOptions.Flags.WebhookFlags; //TURNBASED + } + } + + return this.OpCustom((byte) OperationCode.RaiseEvent, this.opParameters, sendReliable, raiseEventOptions.SequenceChannel, false); + } + + + /// + /// Internally used operation to set some "per server" settings. This is for the Master Server. + /// + /// Set to true, to get Lobby Statistics (lists of existing lobbies). + /// False if the operation could not be sent. + public virtual bool OpSettings(bool receiveLobbyStats) + { + if (this.DebugOut >= DebugLevel.ALL) + { + this.Listener.DebugReturn(DebugLevel.ALL, "OpSettings()"); + } + + // re-used private variable to avoid many new Dictionary() calls (garbage collection) + this.opParameters.Clear(); + + // implementation for Master Server: + if (receiveLobbyStats) + { + this.opParameters[(byte)0] = receiveLobbyStats; + } + + if (this.opParameters.Count == 0) + { + // no need to send op in case we set the default values + return true; + } + return this.OpCustom((byte)OperationCode.ServerSettings, this.opParameters, true); + } + } + + + + public class OpJoinRandomRoomParams + { + public Hashtable ExpectedCustomRoomProperties; + public byte ExpectedMaxPlayers; + public MatchmakingMode MatchingType; + public TypedLobby TypedLobby; + public string SqlLobbyFilter; + public string[] ExpectedUsers; + } + + public class EnterRoomParams + { + public string RoomName; + public RoomOptions RoomOptions; + public TypedLobby Lobby; + public Hashtable PlayerProperties; + public bool OnGameServer = true; // defaults to true! better send more parameter than too few (GS needs all) + public bool CreateIfNotExists; + public bool RejoinOnly; + public string[] ExpectedUsers; + } + + + /// + /// ErrorCode defines the default codes associated with Photon client/server communication. + /// + public class ErrorCode + { + /// (0) is always "OK", anything else an error or specific situation. + public const int Ok = 0; + + // server - Photon low(er) level: <= 0 + + /// + /// (-3) Operation can't be executed yet (e.g. OpJoin can't be called before being authenticated, RaiseEvent cant be used before getting into a room). + /// + /// + /// Before you call any operations on the Cloud servers, the automated client workflow must complete its authorization. + /// In PUN, wait until State is: JoinedLobby (with AutoJoinLobby = true) or ConnectedToMasterserver (AutoJoinLobby = false) + /// + public const int OperationNotAllowedInCurrentState = -3; + + /// (-2) The operation you called is not implemented on the server (application) you connect to. Make sure you run the fitting applications. + [Obsolete("Use InvalidOperation.")] + public const int InvalidOperationCode = -2; + + /// (-2) The operation you called could not be executed on the server. + /// + /// Make sure you are connected to the server you expect. + /// + /// This code is used in several cases: + /// The arguments/parameters of the operation might be out of range, missing entirely or conflicting. + /// The operation you called is not implemented on the server (application). Server-side plugins affect the available operations. + /// + public const int InvalidOperation = -2; + + /// (-1) Something went wrong in the server. Try to reproduce and contact Exit Games. + public const int InternalServerError = -1; + + // server - PhotonNetwork: 0x7FFF and down + // logic-level error codes start with short.max + + /// (32767) Authentication failed. Possible cause: AppId is unknown to Photon (in cloud service). + public const int InvalidAuthentication = 0x7FFF; + + /// (32766) GameId (name) already in use (can't create another). Change name. + public const int GameIdAlreadyExists = 0x7FFF - 1; + + /// (32765) Game is full. This rarely happens when some player joined the room before your join completed. + public const int GameFull = 0x7FFF - 2; + + /// (32764) Game is closed and can't be joined. Join another game. + public const int GameClosed = 0x7FFF - 3; + + [Obsolete("No longer used, cause random matchmaking is no longer a process.")] + public const int AlreadyMatched = 0x7FFF - 4; + + /// (32762) Not in use currently. + public const int ServerFull = 0x7FFF - 5; + + /// (32761) Not in use currently. + public const int UserBlocked = 0x7FFF - 6; + + /// (32760) Random matchmaking only succeeds if a room exists thats neither closed nor full. Repeat in a few seconds or create a new room. + public const int NoRandomMatchFound = 0x7FFF - 7; + + /// (32758) Join can fail if the room (name) is not existing (anymore). This can happen when players leave while you join. + public const int GameDoesNotExist = 0x7FFF - 9; + + /// (32757) Authorization on the Photon Cloud failed becaus the concurrent users (CCU) limit of the app's subscription is reached. + /// + /// Unless you have a plan with "CCU Burst", clients might fail the authentication step during connect. + /// Affected client are unable to call operations. Please note that players who end a game and return + /// to the master server will disconnect and re-connect, which means that they just played and are rejected + /// in the next minute / re-connect. + /// This is a temporary measure. Once the CCU is below the limit, players will be able to connect an play again. + /// + /// OpAuthorize is part of connection workflow but only on the Photon Cloud, this error can happen. + /// Self-hosted Photon servers with a CCU limited license won't let a client connect at all. + /// + public const int MaxCcuReached = 0x7FFF - 10; + + /// (32756) Authorization on the Photon Cloud failed because the app's subscription does not allow to use a particular region's server. + /// + /// Some subscription plans for the Photon Cloud are region-bound. Servers of other regions can't be used then. + /// Check your master server address and compare it with your Photon Cloud Dashboard's info. + /// https://www.photonengine.com/dashboard + /// + /// OpAuthorize is part of connection workflow but only on the Photon Cloud, this error can happen. + /// Self-hosted Photon servers with a CCU limited license won't let a client connect at all. + /// + public const int InvalidRegion = 0x7FFF - 11; + + /// + /// (32755) Custom Authentication of the user failed due to setup reasons (see Cloud Dashboard) or the provided user data (like username or token). Check error message for details. + /// + public const int CustomAuthenticationFailed = 0x7FFF - 12; + + /// (32753) The Authentication ticket expired. Usually, this is refreshed behind the scenes. Connect (and authorize) again. + public const int AuthenticationTicketExpired = 0x7FF1; + + /// + /// (32752) A server-side plugin (or webhook) failed to execute and reported an error. Check the OperationResponse.DebugMessage. + /// + public const int PluginReportedError = 0x7FFF - 15; + + /// + /// (32751) CreateGame/JoinGame/Join operation fails if expected plugin does not correspond to loaded one. + /// + public const int PluginMismatch = 0x7FFF - 16; + + /// + /// (32750) for join requests. Indicates the current peer already called join and is joined to the room. + /// + public const int JoinFailedPeerAlreadyJoined = 32750; // 0x7FFF - 17, + + /// + /// (32749) for join requests. Indicates the list of InactiveActors already contains an actor with the requested ActorNr or UserId. + /// + public const int JoinFailedFoundInactiveJoiner = 32749; // 0x7FFF - 18, + + /// + /// (32748) for join requests. Indicates the list of Actors (active and inactive) did not contain an actor with the requested ActorNr or UserId. + /// + public const int JoinFailedWithRejoinerNotFound = 32748; // 0x7FFF - 19, + + /// + /// (32747) for join requests. Note: for future use - Indicates the requested UserId was found in the ExcludedList. + /// + public const int JoinFailedFoundExcludedUserId = 32747; // 0x7FFF - 20, + + /// + /// (32746) for join requests. Indicates the list of ActiveActors already contains an actor with the requested ActorNr or UserId. + /// + public const int JoinFailedFoundActiveJoiner = 32746; // 0x7FFF - 21, + + /// + /// (32745) for SetProerties and Raisevent (if flag HttpForward is true) requests. Indicates the maximum allowd http requests per minute was reached. + /// + public const int HttpLimitReached = 32745; // 0x7FFF - 22, + + /// + /// (32744) for WebRpc requests. Indicates the the call to the external service failed. + /// + public const int ExternalHttpCallFailed = 32744; // 0x7FFF - 23, + + /// + /// (32742) Server error during matchmaking with slot reservation. E.g. the reserved slots can not exceed MaxPlayers. + /// + public const int SlotError = 32742; // 0x7FFF - 25, + + /// + /// (32741) Server will react with this error if invalid encryption parameters provided by token + /// + public const int InvalidEncryptionParameters = 32741; // 0x7FFF - 24, + +} + + + /// + /// Class for constants. These (byte) values define "well known" properties for an Actor / Player. + /// + /// + /// Pun uses these constants internally. + /// "Custom properties" have to use a string-type as key. They can be assigned at will. + /// + public class ActorProperties + { + /// (255) Name of a player/actor. + public const byte PlayerName = 255; // was: 1 + + /// (254) Tells you if the player is currently in this game (getting events live). + /// A server-set value for async games, where players can leave the game and return later. + public const byte IsInactive = 254; + + /// (253) UserId of the player. Sent when room gets created with RoomOptions.PublishUserId = true. + public const byte UserId = 253; + } + + + /// + /// Class for constants. These (byte) values are for "well known" room/game properties used in Photon Loadbalancing. + /// + /// + /// Pun uses these constants internally. + /// "Custom properties" have to use a string-type as key. They can be assigned at will. + /// + public class GamePropertyKey + { + /// (255) Max number of players that "fit" into this room. 0 is for "unlimited". + public const byte MaxPlayers = 255; + + /// (254) Makes this room listed or not in the lobby on master. + public const byte IsVisible = 254; + + /// (253) Allows more players to join a room (or not). + public const byte IsOpen = 253; + + /// (252) Current count of players in the room. Used only in the lobby on master. + public const byte PlayerCount = 252; + + /// (251) True if the room is to be removed from room listing (used in update to room list in lobby on master) + public const byte Removed = 251; + + /// (250) A list of the room properties to pass to the RoomInfo list in a lobby. This is used in CreateRoom, which defines this list once per room. + public const byte PropsListedInLobby = 250; + + /// (249) Equivalent of Operation Join parameter CleanupCacheOnLeave. + public const byte CleanupCacheOnLeave = 249; + + /// (248) Code for MasterClientId, which is synced by server. When sent as op-parameter this is (byte)203. As room property this is (byte)248. + /// Tightly related to ParameterCode.MasterClientId. + public const byte MasterClientId = (byte)248; + + /// (247) Code for ExpectedUsers in a room. Matchmaking keeps a slot open for the players with these userIDs. + public const byte ExpectedUsers = (byte)247; + } + + + /// + /// Class for constants. These values are for events defined by Photon Loadbalancing. + /// + /// They start at 255 and go DOWN. Your own in-game events can start at 0. Pun uses these constants internally. + public class EventCode + { + /// (230) Initial list of RoomInfos (in lobby on Master) + public const byte GameList = 230; + + /// (229) Update of RoomInfos to be merged into "initial" list (in lobby on Master) + public const byte GameListUpdate = 229; + + /// (228) Currently not used. State of queueing in case of server-full + public const byte QueueState = 228; + + /// (227) Currently not used. Event for matchmaking + public const byte Match = 227; + + /// (226) Event with stats about this application (players, rooms, etc) + public const byte AppStats = 226; + + /// (224) This event provides a list of lobbies with their player and game counts. + public const byte LobbyStats = 224; + + /// (210) Internally used in case of hosting by Azure + [Obsolete("TCP routing was removed after becoming obsolete.")] + public const byte AzureNodeInfo = 210; + + /// (255) Event Join: someone joined the game. The new actorNumber is provided as well as the properties of that actor (if set in OpJoin). + public const byte Join = (byte)255; + + /// (254) Event Leave: The player who left the game can be identified by the actorNumber. + public const byte Leave = (byte)254; + + /// (253) When you call OpSetProperties with the broadcast option "on", this event is fired. It contains the properties being set. + public const byte PropertiesChanged = (byte)253; + + /// (253) When you call OpSetProperties with the broadcast option "on", this event is fired. It contains the properties being set. + [Obsolete("Use PropertiesChanged now.")] + public const byte SetProperties = (byte)253; + + /// (252) When player left game unexpected and the room has a playerTtl > 0, this event is fired to let everyone know about the timeout. + /// Obsolete. Replaced by Leave. public const byte Disconnect = LiteEventCode.Disconnect; + + /// (251) Sent by Photon Cloud when a plugin-call or webhook-call failed. Usually, the execution on the server continues, despite the issue. Contains: ParameterCode.Info. + /// + public const byte ErrorInfo = 251; + + /// (250) Sent by Photon whent he event cache slice was changed. Done by OpRaiseEvent. + public const byte CacheSliceChanged = 250; + + /// (223) Sent by Photon to update a token before it times out. + public const byte AuthEvent = 223; + } + + + /// Class for constants. Codes for parameters of Operations and Events. + /// Pun uses these constants internally. + public class ParameterCode + { + /// (237) A bool parameter for creating games. If set to true, no room events are sent to the clients on join and leave. Default: false (and not sent). + public const byte SuppressRoomEvents = 237; + + /// (236) Time To Live (TTL) for a room when the last player leaves. Keeps room in memory for case a player re-joins soon. In milliseconds. + public const byte EmptyRoomTTL = 236; + + /// (235) Time To Live (TTL) for an 'actor' in a room. If a client disconnects, this actor is inactive first and removed after this timeout. In milliseconds. + public const byte PlayerTTL = 235; + + /// (234) Optional parameter of OpRaiseEvent and OpSetCustomProperties to forward the event/operation to a web-service. + public const byte EventForward = 234; + + /// (233) Optional parameter of OpLeave in async games. If false, the player does abandons the game (forever). By default players become inactive and can re-join. + [Obsolete("Use: IsInactive")] + public const byte IsComingBack = (byte)233; + + /// (233) Used in EvLeave to describe if a user is inactive (and might come back) or not. In rooms with PlayerTTL, becoming inactive is the default case. + public const byte IsInactive = (byte)233; + + /// (232) Used when creating rooms to define if any userid can join the room only once. + public const byte CheckUserOnJoin = (byte)232; + + /// (231) Code for "Check And Swap" (CAS) when changing properties. + public const byte ExpectedValues = (byte)231; + + /// (230) Address of a (game) server to use. + public const byte Address = 230; + + /// (229) Count of players in this application in a rooms (used in stats event) + public const byte PeerCount = 229; + + /// (228) Count of games in this application (used in stats event) + public const byte GameCount = 228; + + /// (227) Count of players on the master server (in this app, looking for rooms) + public const byte MasterPeerCount = 227; + + /// (225) User's ID + public const byte UserId = 225; + + /// (224) Your application's ID: a name on your own Photon or a GUID on the Photon Cloud + public const byte ApplicationId = 224; + + /// (223) Not used currently (as "Position"). If you get queued before connect, this is your position + public const byte Position = 223; + + /// (223) Modifies the matchmaking algorithm used for OpJoinRandom. Allowed parameter values are defined in enum MatchmakingMode. + public const byte MatchMakingType = 223; + + /// (222) List of RoomInfos about open / listed rooms + public const byte GameList = 222; + + /// (221) Internally used to establish encryption + public const byte Secret = 221; + + /// (220) Version of your application + public const byte AppVersion = 220; + + /// (210) Internally used in case of hosting by Azure + [Obsolete("TCP routing was removed after becoming obsolete.")] + public const byte AzureNodeInfo = 210; // only used within events, so use: EventCode.AzureNodeInfo + + /// (209) Internally used in case of hosting by Azure + [Obsolete("TCP routing was removed after becoming obsolete.")] + public const byte AzureLocalNodeId = 209; + + /// (208) Internally used in case of hosting by Azure + [Obsolete("TCP routing was removed after becoming obsolete.")] + public const byte AzureMasterNodeId = 208; + + /// (255) Code for the gameId/roomName (a unique name per room). Used in OpJoin and similar. + public const byte RoomName = (byte)255; + + /// (250) Code for broadcast parameter of OpSetProperties method. + public const byte Broadcast = (byte)250; + + /// (252) Code for list of players in a room. Currently not used. + public const byte ActorList = (byte)252; + + /// (254) Code of the Actor of an operation. Used for property get and set. + public const byte ActorNr = (byte)254; + + /// (249) Code for property set (Hashtable). + public const byte PlayerProperties = (byte)249; + + /// (245) Code of data/custom content of an event. Used in OpRaiseEvent. + public const byte CustomEventContent = (byte)245; + + /// (245) Code of data of an event. Used in OpRaiseEvent. + public const byte Data = (byte)245; + + /// (244) Code used when sending some code-related parameter, like OpRaiseEvent's event-code. + /// This is not the same as the Operation's code, which is no longer sent as part of the parameter Dictionary in Photon 3. + public const byte Code = (byte)244; + + /// (248) Code for property set (Hashtable). + public const byte GameProperties = (byte)248; + + /// + /// (251) Code for property-set (Hashtable). This key is used when sending only one set of properties. + /// If either ActorProperties or GameProperties are used (or both), check those keys. + /// + public const byte Properties = (byte)251; + + /// (253) Code of the target Actor of an operation. Used for property set. Is 0 for game + public const byte TargetActorNr = (byte)253; + + /// (246) Code to select the receivers of events (used in Lite, Operation RaiseEvent). + public const byte ReceiverGroup = (byte)246; + + /// (247) Code for caching events while raising them. + public const byte Cache = (byte)247; + + /// (241) Bool parameter of CreateGame Operation. If true, server cleans up roomcache of leaving players (their cached events get removed). + public const byte CleanupCacheOnLeave = (byte)241; + + /// (240) Code for "group" operation-parameter (as used in Op RaiseEvent). + public const byte Group = 240; + + /// (239) The "Remove" operation-parameter can be used to remove something from a list. E.g. remove groups from player's interest groups. + public const byte Remove = 239; + + /// (239) Used in Op Join to define if UserIds of the players are broadcast in the room. Useful for FindFriends and reserving slots for expected users. + public const byte PublishUserId = 239; + + /// (238) The "Add" operation-parameter can be used to add something to some list or set. E.g. add groups to player's interest groups. + public const byte Add = 238; + + /// (218) Content for EventCode.ErrorInfo and internal debug operations. + public const byte Info = 218; + + /// (217) This key's (byte) value defines the target custom authentication type/service the client connects with. Used in OpAuthenticate + public const byte ClientAuthenticationType = 217; + + /// (216) This key's (string) value provides parameters sent to the custom authentication type/service the client connects with. Used in OpAuthenticate + public const byte ClientAuthenticationParams = 216; + + /// (215) Makes the server create a room if it doesn't exist. OpJoin uses this to always enter a room, unless it exists and is full/closed. + // public const byte CreateIfNotExists = 215; + + /// (215) The JoinMode enum defines which variant of joining a room will be executed: Join only if available, create if not exists or re-join. + /// Replaces CreateIfNotExists which was only a bool-value. + public const byte JoinMode = 215; + + /// (214) This key's (string or byte[]) value provides parameters sent to the custom authentication service setup in Photon Dashboard. Used in OpAuthenticate + public const byte ClientAuthenticationData = 214; + + /// (203) Code for MasterClientId, which is synced by server. When sent as op-parameter this is code 203. + /// Tightly related to GamePropertyKey.MasterClientId. + public const byte MasterClientId = (byte)203; + + /// (1) Used in Op FindFriends request. Value must be string[] of friends to look up. + public const byte FindFriendsRequestList = (byte)1; + + /// (1) Used in Op FindFriends response. Contains bool[] list of online states (false if not online). + public const byte FindFriendsResponseOnlineList = (byte)1; + + /// (2) Used in Op FindFriends response. Contains string[] of room names ("" where not known or no room joined). + public const byte FindFriendsResponseRoomIdList = (byte)2; + + /// (213) Used in matchmaking-related methods and when creating a room to name a lobby (to join or to attach a room to). + public const byte LobbyName = (byte)213; + + /// (212) Used in matchmaking-related methods and when creating a room to define the type of a lobby. Combined with the lobby name this identifies the lobby. + public const byte LobbyType = (byte)212; + + /// (211) This (optional) parameter can be sent in Op Authenticate to turn on Lobby Stats (info about lobby names and their user- and game-counts). See: PhotonNetwork.Lobbies + public const byte LobbyStats = (byte)211; + + /// (210) Used for region values in OpAuth and OpGetRegions. + public const byte Region = (byte)210; + + /// (209) Path of the WebRPC that got called. Also known as "WebRpc Name". Type: string. + public const byte UriPath = 209; + + /// (208) Parameters for a WebRPC as: Dictionary<string, object>. This will get serialized to JSon. + public const byte WebRpcParameters = 208; + + /// (207) ReturnCode for the WebRPC, as sent by the web service (not by Photon, which uses ErrorCode). Type: byte. + public const byte WebRpcReturnCode = 207; + + /// (206) Message returned by WebRPC server. Analog to Photon's debug message. Type: string. + public const byte WebRpcReturnMessage = 206; + + /// (205) Used to define a "slice" for cached events. Slices can easily be removed from cache. Type: int. + public const byte CacheSliceIndex = 205; + + /// (204) Informs the server of the expected plugin setup. + /// + /// The operation will fail in case of a plugin mismatch returning error code PluginMismatch 32751(0x7FFF - 16). + /// Setting string[]{} means the client expects no plugin to be setup. + /// Note: for backwards compatibility null omits any check. + /// + public const byte Plugins = 204; + + /// (202) Used by the server in Operation Responses, when it sends the nickname of the client (the user's nickname). + public const byte NickName = 202; + + /// (201) Informs user about name of plugin load to game + public const byte PluginName = 201; + + /// (200) Informs user about version of plugin load to game + public const byte PluginVersion = 200; + + /// (195) Protocol which will be used by client to connect master/game servers. Used for nameserver. + public const byte ExpectedProtocol = 195; + + /// (194) Set of custom parameters which are sent in auth request. + public const byte CustomInitData = 194; + + /// (193) How are we going to encrypt data. + public const byte EncryptionMode = 193; + + /// (192) Parameter of Authentication, which contains encryption keys (depends on AuthMode and EncryptionMode). + public const byte EncryptionData = 192; + + /// (191) An int parameter summarizing several boolean room-options with bit-flags. + public const byte RoomOptionFlags = 191; + } + + + /// + /// Class for constants. Contains operation codes. + /// Pun uses these constants internally. + /// + public class OperationCode + { + [Obsolete("Exchanging encrpytion keys is done internally in the lib now. Don't expect this operation-result.")] + public const byte ExchangeKeysForEncryption = 250; + + /// (255) Code for OpJoin, to get into a room. + [Obsolete] + public const byte Join = 255; + + /// (231) Authenticates this peer and connects to a virtual application + public const byte AuthenticateOnce = 231; + + /// (230) Authenticates this peer and connects to a virtual application + public const byte Authenticate = 230; + + /// (229) Joins lobby (on master) + public const byte JoinLobby = 229; + + /// (228) Leaves lobby (on master) + public const byte LeaveLobby = 228; + + /// (227) Creates a game (or fails if name exists) + public const byte CreateGame = 227; + + /// (226) Join game (by name) + public const byte JoinGame = 226; + + /// (225) Joins random game (on master) + public const byte JoinRandomGame = 225; + + // public const byte CancelJoinRandom = 224; // obsolete, cause JoinRandom no longer is a "process". now provides result immediately + + /// (254) Code for OpLeave, to get out of a room. + public const byte Leave = (byte)254; + + /// (253) Raise event (in a room, for other actors/players) + public const byte RaiseEvent = (byte)253; + + /// (252) Set Properties (of room or actor/player) + public const byte SetProperties = (byte)252; + + /// (251) Get Properties + public const byte GetProperties = (byte)251; + + /// (248) Operation code to change interest groups in Rooms (Lite application and extending ones). + public const byte ChangeGroups = (byte)248; + + /// (222) Request the rooms and online status for a list of friends (by name, which should be unique). + public const byte FindFriends = 222; + + /// (221) Request statistics about a specific list of lobbies (their user and game count). + public const byte GetLobbyStats = 221; + + /// (220) Get list of regional servers from a NameServer. + public const byte GetRegions = 220; + + /// (219) WebRpc Operation. + public const byte WebRpc = 219; + + /// (218) Operation to set some server settings. Used with different parameters on various servers. + public const byte ServerSettings = 218; + + /// (217) Get the game list matching a supplied sql filter (SqlListLobby only) + public const byte GetGameList = 217; + } + + /// Defines possible values for OpJoinRoom and OpJoinOrCreate. It tells the server if the room can be only be joined normally, created implicitly or found on a web-service for Turnbased games. + /// These values are not directly used by a game but implicitly set. + public enum JoinMode : byte + { + /// Regular join. The room must exist. + Default = 0, + + /// Join or create the room if it's not existing. Used for OpJoinOrCreate for example. + CreateIfNotExists = 1, + + /// The room might be out of memory and should be loaded (if possible) from a Turnbased web-service. + JoinOrRejoin = 2, + + /// Only re-join will be allowed. If the user is not yet in the room, this will fail. + RejoinOnly = 3, + } + + /// + /// Options for matchmaking rules for OpJoinRandom. + /// + public enum MatchmakingMode : byte + { + /// Fills up rooms (oldest first) to get players together as fast as possible. Default. + /// Makes most sense with MaxPlayers > 0 and games that can only start with more players. + FillRoom = 0, + + /// Distributes players across available rooms sequentially but takes filter into account. Without filter, rooms get players evenly distributed. + SerialMatching = 1, + + /// Joins a (fully) random room. Expected properties must match but aside from this, any available room might be selected. + RandomMatching = 2 + } + + + /// + /// Lite - OpRaiseEvent lets you chose which actors in the room should receive events. + /// By default, events are sent to "Others" but you can overrule this. + /// + public enum ReceiverGroup : byte + { + /// Default value (not sent). Anyone else gets my event. + Others = 0, + + /// Everyone in the current room (including this peer) will get this event. + All = 1, + + /// The server sends this event only to the actor with the lowest actorNumber. + /// The "master client" does not have special rights but is the one who is in this room the longest time. + MasterClient = 2, + } + + /// + /// Lite - OpRaiseEvent allows you to cache events and automatically send them to joining players in a room. + /// Events are cached per event code and player: Event 100 (example!) can be stored once per player. + /// Cached events can be modified, replaced and removed. + /// + /// + /// Caching works only combination with ReceiverGroup options Others and All. + /// + public enum EventCaching : byte + { + /// Default value (not sent). + DoNotCache = 0, + + /// Will merge this event's keys with those already cached. + [Obsolete] + MergeCache = 1, + + /// Replaces the event cache for this eventCode with this event's content. + [Obsolete] + ReplaceCache = 2, + + /// Removes this event (by eventCode) from the cache. + [Obsolete] + RemoveCache = 3, + + /// Adds an event to the room's cache + AddToRoomCache = 4, + + /// Adds this event to the cache for actor 0 (becoming a "globally owned" event in the cache). + AddToRoomCacheGlobal = 5, + + /// Remove fitting event from the room's cache. + RemoveFromRoomCache = 6, + + /// Removes events of players who already left the room (cleaning up). + RemoveFromRoomCacheForActorsLeft = 7, + + /// Increase the index of the sliced cache. + SliceIncreaseIndex = 10, + + /// Set the index of the sliced cache. You must set RaiseEventOptions.CacheSliceIndex for this. + SliceSetIndex = 11, + + /// Purge cache slice with index. Exactly one slice is removed from cache. You must set RaiseEventOptions.CacheSliceIndex for this. + SlicePurgeIndex = 12, + + /// Purge cache slices with specified index and anything lower than that. You must set RaiseEventOptions.CacheSliceIndex for this. + SlicePurgeUpToIndex = 13, + } + + /// + /// Flags for "types of properties", being used as filter in OpGetProperties. + /// + [Flags] + public enum PropertyTypeFlag : byte + { + /// (0x00) Flag type for no property type. + None = 0x00, + + /// (0x01) Flag type for game-attached properties. + Game = 0x01, + + /// (0x02) Flag type for actor related propeties. + Actor = 0x02, + + /// (0x01) Flag type for game AND actor properties. Equal to 'Game' + GameAndActor = Game | Actor + } + + + /// Wraps up common room properties needed when you create rooms. Read the individual entries for more details. + /// This directly maps to the fields in the Room class. + public class RoomOptions + { + /// Defines if this room is listed in the lobby. If not, it also is not joined randomly. + /// + /// A room that is not visible will be excluded from the room lists that are sent to the clients in lobbies. + /// An invisible room can be joined by name but is excluded from random matchmaking. + /// + /// Use this to "hide" a room and simulate "private rooms". Players can exchange a roomname and create it + /// invisble to avoid anyone else joining it. + /// + public bool IsVisible { get { return this.isVisible; } set { this.isVisible = value; } } + private bool isVisible = true; + + /// Defines if this room can be joined at all. + /// + /// If a room is closed, no player can join this. As example this makes sense when 3 of 4 possible players + /// start their gameplay early and don't want anyone to join during the game. + /// The room can still be listed in the lobby (set isVisible to control lobby-visibility). + /// + public bool IsOpen { get { return this.isOpen; } set { this.isOpen = value; } } + private bool isOpen = true; + + /// Max number of players that can be in the room at any time. 0 means "no limit". + public byte MaxPlayers; + + /// Time To Live (TTL) for an 'actor' in a room. If a client disconnects, this actor is inactive first and removed after this timeout. In milliseconds. + public int PlayerTtl; + + /// Time To Live (TTL) for a room when the last player leaves. Keeps room in memory for case a player re-joins soon. In milliseconds. + public int EmptyRoomTtl; + + /// Activates UserId checks on joining - allowing a users to be only once in the room. + /// + /// Turnbased rooms should be created with this check turned on! They should also use custom authentication. + /// Disabled by default for backwards-compatibility. + /// + public bool CheckUserOnJoin { get; set; } + + /// Removes a user's events and properties from the room when a user leaves. + /// + /// This makes sense when in rooms where players can't place items in the room and just vanish entirely. + /// When you disable this, the event history can become too long to load if the room stays in use indefinitely. + /// Default: true. Cleans up the cache and props of leaving users. + /// + public bool CleanupCacheOnLeave { get { return this.cleanupCacheOnLeave; } set { this.cleanupCacheOnLeave = value; } } + private bool cleanupCacheOnLeave = true; + + /// The room's custom properties to set. Use string keys! + /// + /// Custom room properties are any key-values you need to define the game's setup. + /// The shorter your keys are, the better. + /// Example: Map, Mode (could be "m" when used with "Map"), TileSet (could be "t"). + /// + public Hashtable CustomRoomProperties; + + /// Defines the custom room properties that get listed in the lobby. + /// + /// Name the custom room properties that should be available to clients that are in a lobby. + /// Use with care. Unless a custom property is essential for matchmaking or user info, it should + /// not be sent to the lobby, which causes traffic and delays for clients in the lobby. + /// + /// Default: No custom properties are sent to the lobby. + /// + public string[] CustomRoomPropertiesForLobby = new string[0]; + + /// Informs the server of the expected plugin setup. + /// + /// The operation will fail in case of a plugin missmatch returning error code PluginMismatch 32757(0x7FFF - 10). + /// Setting string[]{} means the client expects no plugin to be setup. + /// Note: for backwards compatibility null omits any check. + /// + public string[] Plugins; + + /// + /// Tells the server to skip room events for joining and leaving players. + /// + /// + /// Using this makes the client unaware of the other players in a room. + /// That can save some traffic if you have some server logic that updates players + /// but it can also limit the client's usability. + /// + public bool SuppressRoomEvents { get; set; } + + /// + /// Defines if the UserIds of players get "published" in the room. Useful for FindFriends, if players want to play another game together. + /// + /// + /// When you set this to true, Photon will publish the UserIds of the players in that room. + /// In that case, you can use PhotonPlayer.userId, to access any player's userID. + /// This is useful for FindFriends and to set "expected users" to reserve slots in a room (see PhotonNetwork.JoinRoom e.g.). + /// + public bool PublishUserId { get; set; } + + /// Optionally, properties get deleted, when null gets assigned as value. Defaults to off / false. + /// + /// When Op SetProperties is setting a key's value to null, the server and clients should remove the key/value from the Custom Properties. + /// By default, the server keeps the keys (and null values) and sends them to joining players. + /// + /// Important: Only when SetProperties does a "broadcast", the change (key, value = null) is sent to clients to update accordingly. + /// This applies to Custom Properties for rooms and actors/players. + /// + public bool DeleteNullProperties { get; set; } + } + + + /// Aggregates several less-often used options for operation RaiseEvent. See field descriptions for usage details. + public class RaiseEventOptions + { + /// Default options: CachingOption: DoNotCache, InterestGroup: 0, targetActors: null, receivers: Others, sequenceChannel: 0. + public readonly static RaiseEventOptions Default = new RaiseEventOptions(); + + /// Defines if the server should simply send the event, put it in the cache or remove events that are like this one. + /// + /// When using option: SliceSetIndex, SlicePurgeIndex or SlicePurgeUpToIndex, set a CacheSliceIndex. All other options except SequenceChannel get ignored. + /// + public EventCaching CachingOption; + + /// The number of the Interest Group to send this to. 0 goes to all users but to get 1 and up, clients must subscribe to the group first. + public byte InterestGroup; + + /// A list of PhotonPlayer.IDs to send this event to. You can implement events that just go to specific users this way. + public int[] TargetActors; + + /// Sends the event to All, MasterClient or Others (default). Be careful with MasterClient, as the client might disconnect before it got the event and it gets lost. + public ReceiverGroup Receivers; + + /// Events are ordered per "channel". If you have events that are independent of others, they can go into another sequence or channel. + public byte SequenceChannel; + + /// Optional flags to be used in Photon client SDKs with Op RaiseEvent and Op SetProperties. + /// Introduced mainly for webhooks 1.2 to control behavior of forwarded HTTP requests. + public WebFlags Flags = WebFlags.Default; + + ///// Used along with CachingOption SliceSetIndex, SlicePurgeIndex or SlicePurgeUpToIndex if you want to set or purge a specific cache-slice. + //public int CacheSliceIndex; + + //public bool Encrypt; + } + + /// + /// Options of lobby types available. Lobby types might be implemented in certain Photon versions and won't be available on older servers. + /// + public enum LobbyType :byte + { + /// This lobby is used unless another is defined by game or JoinRandom. Room-lists will be sent and JoinRandomRoom can filter by matching properties. + Default = 0, + /// This lobby type lists rooms like Default but JoinRandom has a parameter for SQL-like "where" clauses for filtering. This allows bigger, less, or and and combinations. + SqlLobby = 2, + /// This lobby does not send lists of games. It is only used for OpJoinRandomRoom. It keeps rooms available for a while when there are only inactive users left. + AsyncRandomLobby = 3 + } + + /// Refers to a specific lobby (and type) on the server. + /// + /// The name and type are the unique identifier for a lobby.
+ /// Join a lobby via PhotonNetwork.JoinLobby(TypedLobby lobby).
+ /// The current lobby is stored in PhotonNetwork.lobby. + ///
+ public class TypedLobby + { + /// Name of the lobby this game gets added to. Default: null, attached to default lobby. Lobbies are unique per lobbyName plus lobbyType, so the same name can be used when several types are existing. + public string Name; + /// Type of the (named)lobby this game gets added to + public LobbyType Type; + + public static readonly TypedLobby Default = new TypedLobby(); + public bool IsDefault { get { return this.Type == LobbyType.Default && string.IsNullOrEmpty(this.Name); } } + + public TypedLobby() + { + this.Name = string.Empty; + this.Type = LobbyType.Default; + } + + public TypedLobby(string name, LobbyType type) + { + this.Name = name; + this.Type = type; + } + + public override string ToString() + { + return String.Format((string) "lobby '{0}'[{1}]", (object) this.Name, (object) this.Type); + } + } + + public class TypedLobbyInfo : TypedLobby + { + public int PlayerCount; + public int RoomCount; + + public override string ToString() + { + return string.Format("TypedLobbyInfo '{0}'[{1}] rooms: {2} players: {3}", this.Name, this.Type, this.RoomCount, this.PlayerCount); + } + } + + + /// + /// Options for authentication modes. From "classic" auth on each server to AuthOnce (on NameServer). + /// + public enum AuthModeOption { Auth, AuthOnce, AuthOnceWss } + + + /// + /// Options for optional "Custom Authentication" services used with Photon. Used by OpAuthenticate after connecting to Photon. + /// + public enum CustomAuthenticationType : byte + { + /// Use a custom authentification service. Currently the only implemented option. + Custom = 0, + + /// Authenticates users by their Steam Account. Set auth values accordingly! + Steam = 1, + + /// Authenticates users by their Facebook Account. Set auth values accordingly! + Facebook = 2, + + /// Authenticates users by their Oculus Account and token. + Oculus = 3, + + /// Authenticates users by their PSN Account and token. + PlayStation = 4, + + /// Authenticates users by their Xbox Account and XSTS token. + Xbox = 5, + + /// Disables custom authentification. Same as not providing any AuthenticationValues for connect (more precisely for: OpAuthenticate). + None = byte.MaxValue + } + + + /// + /// Container for user authentication in Photon. Set AuthValues before you connect - all else is handled. + /// + /// + /// On Photon, user authentication is optional but can be useful in many cases. + /// If you want to FindFriends, a unique ID per user is very practical. + /// + /// There are basically three options for user authentification: None at all, the client sets some UserId + /// or you can use some account web-service to authenticate a user (and set the UserId server-side). + /// + /// Custom Authentication lets you verify end-users by some kind of login or token. It sends those + /// values to Photon which will verify them before granting access or disconnecting the client. + /// + /// The AuthValues are sent in OpAuthenticate when you connect, so they must be set before you connect. + /// Should you not set any AuthValues, PUN will create them and set the playerName as userId in them. + /// If the AuthValues.userId is null or empty when it's sent to the server, then the Photon Server assigns a userId! + /// + /// The Photon Cloud Dashboard will let you enable this feature and set important server values for it. + /// https://www.photonengine.com/dashboard + /// + public class AuthenticationValues + { + /// See AuthType. + private CustomAuthenticationType authType = CustomAuthenticationType.None; + + /// The type of custom authentication provider that should be used. Currently only "Custom" or "None" (turns this off). + public CustomAuthenticationType AuthType + { + get { return authType; } + set { authType = value; } + } + + /// This string must contain any (http get) parameters expected by the used authentication service. By default, username and token. + /// Standard http get parameters are used here and passed on to the service that's defined in the server (Photon Cloud Dashboard). + public string AuthGetParameters { get; set; } + + /// Data to be passed-on to the auth service via POST. Default: null (not sent). Either string or byte[] (see setters). + public object AuthPostData { get; private set; } + + /// After initial authentication, Photon provides a token for this client / user, which is subsequently used as (cached) validation. + public string Token { get; set; } + + /// The UserId should be a unique identifier per user. This is for finding friends, etc.. + /// See remarks of AuthValues for info about how this is set and used. + public string UserId { get; set; } + + + /// Creates empty auth values without any info. + public AuthenticationValues() + { + } + + /// Creates minimal info about the user. If this is authenticated or not, depends on the set AuthType. + /// Some UserId to set in Photon. + public AuthenticationValues(string userId) + { + this.UserId = userId; + } + + /// Sets the data to be passed-on to the auth service via POST. + /// String data to be used in the body of the POST request. Null or empty string will set AuthPostData to null. + public virtual void SetAuthPostData(string stringData) + { + this.AuthPostData = (string.IsNullOrEmpty(stringData)) ? null : stringData; + } + + /// Sets the data to be passed-on to the auth service via POST. + /// Binary token / auth-data to pass on. + public virtual void SetAuthPostData(byte[] byteData) + { + this.AuthPostData = byteData; + } + + /// Adds a key-value pair to the get-parameters used for Custom Auth. + /// This method does uri-encoding for you. + /// Key for the value to set. + /// Some value relevant for Custom Authentication. + public virtual void AddAuthParameter(string key, string value) + { + string ampersand = string.IsNullOrEmpty(this.AuthGetParameters) ? "" : "&"; + this.AuthGetParameters = string.Format("{0}{1}{2}={3}", this.AuthGetParameters, ampersand, System.Uri.EscapeDataString(key), System.Uri.EscapeDataString(value)); + } + + public override string ToString() + { + return string.Format("AuthenticationValues UserId: {0}, GetParameters: {1} Token available: {2}", this.UserId, this.AuthGetParameters, this.Token != null); + } + } +} \ No newline at end of file diff --git a/Assets/Runtime/Photon/PhotonLoadbalancingApi/LoadBalancingPeer.cs.meta b/Assets/Runtime/Photon/PhotonLoadbalancingApi/LoadBalancingPeer.cs.meta new file mode 100644 index 0000000..8ab72fc --- /dev/null +++ b/Assets/Runtime/Photon/PhotonLoadbalancingApi/LoadBalancingPeer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a3aba55d840ae78459c990a41ed84f82 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Photon/PhotonLoadbalancingApi/Player.cs b/Assets/Runtime/Photon/PhotonLoadbalancingApi/Player.cs new file mode 100644 index 0000000..18566d1 --- /dev/null +++ b/Assets/Runtime/Photon/PhotonLoadbalancingApi/Player.cs @@ -0,0 +1,430 @@ +// ---------------------------------------------------------------------------- +// +// Loadbalancing Framework for Photon - Copyright (C) 2011 Exit Games GmbH +// +// +// Per client in a room, a Player is created. This client's Player is also +// known as PhotonClient.LocalPlayer and the only one you might change +// properties for. +// +// developer@photonengine.com +// ---------------------------------------------------------------------------- + +#define UNITY + +namespace ExitGames.Client.Photon.LoadBalancing +{ + using System; + using System.Collections; + using System.Collections.Generic; + using ExitGames.Client.Photon; + + #if UNITY + using UnityEngine; + #endif + #if UNITY || NETFX_CORE + using Hashtable = ExitGames.Client.Photon.Hashtable; + using SupportClass = ExitGames.Client.Photon.SupportClass; + #endif + + + /// + /// Summarizes a "player" within a room, identified (in that room) by ID (or "actorID"). + /// + /// + /// Each player has a actorID, valid for that room. It's -1 until assigned by server (and client logic). + /// + public class Player + { + /// + /// Used internally to identify the masterclient of a room. + /// + protected internal Room RoomReference { get; set; } + + + /// Backing field for property. + private int actorID = -1; + + /// Identifier of this player in current room. Also known as: actorNumber or actorID. It's -1 outside of rooms. + /// The ID is assigned per room and only valid in that context. It will change even on leave and re-join. IDs are never re-used per room. + public int ID + { + get { return this.actorID; } + } + + + /// Only one player is controlled by each client. Others are not local. + public readonly bool IsLocal; + + + /// Background field for nickName. + private string nickName; + + /// Non-unique nickname of this player. Synced automatically in a room. + /// + /// A player might change his own playername in a room (it's only a property). + /// Setting this value updates the server and other players (using an operation). + /// + public string NickName + { + get + { + return this.nickName; + } + set + { + if (!string.IsNullOrEmpty(this.nickName) && this.nickName.Equals(value)) + { + return; + } + + this.nickName = value; + + // update a room, if we changed our nickName (locally, while being in a room) + if (this.IsLocal && this.RoomReference != null && this.RoomReference.IsLocalClientInside) + { + this.SetPlayerNameProperty(); + } + } + } + + /// UserId of the player, available when the room got created with RoomOptions.PublishUserId = true. + /// Useful for PhotonNetwork.FindFriends and blocking slots in a room for expected players (e.g. in PhotonNetwork.CreateRoom). + public string UserId { get; internal set; } + + /// + /// True if this player is the Master Client of the current room. + /// + /// + /// See also: PhotonNetwork.masterClient. + /// + public bool IsMasterClient + { + get + { + if (this.RoomReference == null) + { + return false; + } + + return this.ID == this.RoomReference.MasterClientId; + } + } + + /// If this player is active in the room (and getting events which are currently being sent). + /// + /// Inactive players keep their spot in a room but otherwise behave as if offline (no matter what their actual connection status is). + /// The room needs a PlayerTTL > 0. If a player is inactive for longer than PlayerTTL, the server will remove this player from the room. + /// For a client "rejoining" a room, is the same as joining it: It gets properties, cached events and then the live events. + /// + public bool IsInactive { get; set; } + + /// Read-only cache for custom properties of player. Set via Player.SetCustomProperties. + /// + /// Don't modify the content of this Hashtable. Use SetCustomProperties and the + /// properties of this class to modify values. When you use those, the client will + /// sync values with the server. + /// + /// + public Hashtable CustomProperties { get; private set; } + + /// Creates a Hashtable with all properties (custom and "well known" ones). + /// Creates new Hashtables each time used, so if used more often, cache this. + public Hashtable AllProperties + { + get + { + Hashtable allProps = new Hashtable(); + allProps.Merge(this.CustomProperties); + allProps[ActorProperties.PlayerName] = this.nickName; + return allProps; + } + } + + /// Can be used to store a reference that's useful to know "by player". + /// Example: Set a player's character as Tag by assigning the GameObject on Instantiate. + public object TagObject; + + + /// + /// Creates a player instance. + /// To extend and replace this Player, override LoadBalancingPeer.CreatePlayer(). + /// + /// NickName of the player (a "well known property"). + /// ID or ActorNumber of this player in the current room (a shortcut to identify each player in room) + /// If this is the local peer's player (or a remote one). + protected internal Player(string nickName, int actorID, bool isLocal) : this(nickName, actorID, isLocal, null) + { + } + + /// + /// Creates a player instance. + /// To extend and replace this Player, override LoadBalancingPeer.CreatePlayer(). + /// + /// NickName of the player (a "well known property"). + /// ID or ActorNumber of this player in the current room (a shortcut to identify each player in room) + /// If this is the local peer's player (or a remote one). + /// A Hashtable of custom properties to be synced. Must use String-typed keys and serializable datatypes as values. + protected internal Player(string nickName, int actorID, bool isLocal, Hashtable playerProperties) + { + this.IsLocal = isLocal; + this.actorID = actorID; + this.NickName = nickName; + + this.CustomProperties = new Hashtable(); + this.InternalCacheProperties(playerProperties); + } + + + /// + /// Get a Player by ActorNumber (Player.ID). + /// + /// ActorNumber of the a player in this room. + /// Player or null. + public Player Get(int id) + { + if (this.RoomReference == null) + { + return null; + } + + return this.RoomReference.GetPlayer(id); + } + + /// Gets this Player's next Player, as sorted by ActorNumber (Player.ID). Wraps around. + /// Player or null. + public Player GetNext() + { + return GetNextFor(this.ID); + } + + /// Gets a Player's next Player, as sorted by ActorNumber (Player.ID). Wraps around. + /// Useful when you pass something to the next player. For example: passing the turn to the next player. + /// The Player for which the next is being needed. + /// Player or null. + public Player GetNextFor(Player currentPlayer) + { + if (currentPlayer == null) + { + return null; + } + return GetNextFor(currentPlayer.ID); + } + + /// Gets a Player's next Player, as sorted by ActorNumber (Player.ID). Wraps around. + /// Useful when you pass something to the next player. For example: passing the turn to the next player. + /// The ActorNumber (Player.ID) for which the next is being needed. + /// Player or null. + public Player GetNextFor(int currentPlayerId) + { + if (this.RoomReference == null || this.RoomReference.Players == null || this.RoomReference.Players.Count < 2) + { + return null; + } + + Dictionary players = this.RoomReference.Players; + int nextHigherId = int.MaxValue; // we look for the next higher ID + int lowestId = currentPlayerId; // if we are the player with the highest ID, there is no higher and we return to the lowest player's id + + foreach (int playerid in players.Keys) + { + if (playerid < lowestId) + { + lowestId = playerid; // less than any other ID (which must be at least less than this player's id). + } + else if (playerid > currentPlayerId && playerid < nextHigherId) + { + nextHigherId = playerid; // more than our ID and less than those found so far. + } + } + + //UnityEngine.Debug.LogWarning("Debug. " + currentPlayerId + " lower: " + lowestId + " higher: " + nextHigherId + " "); + //UnityEngine.Debug.LogWarning(this.RoomReference.GetPlayer(currentPlayerId)); + //UnityEngine.Debug.LogWarning(this.RoomReference.GetPlayer(lowestId)); + //if (nextHigherId != int.MaxValue) UnityEngine.Debug.LogWarning(this.RoomReference.GetPlayer(nextHigherId)); + return (nextHigherId != int.MaxValue) ? players[nextHigherId] : players[lowestId]; + } + + + /// Caches properties for new Players or when updates of remote players are received. Use SetCustomProperties() for a synced update. + /// + /// This only updates the CustomProperties and doesn't send them to the server. + /// Mostly used when creating new remote players, where the server sends their properties. + /// + public virtual void InternalCacheProperties(Hashtable properties) + { + if (properties == null || properties.Count == 0 || this.CustomProperties.Equals(properties)) + { + return; + } + + if (properties.ContainsKey(ActorProperties.PlayerName)) + { + string nameInServersProperties = (string)properties[ActorProperties.PlayerName]; + if (nameInServersProperties != null) + { + if (this.IsLocal) + { + // the local playername is different than in the properties coming from the server + // so the local nickName was changed and the server is outdated -> update server + // update property instead of using the outdated nickName coming from server + if (!nameInServersProperties.Equals(this.nickName)) + { + this.SetPlayerNameProperty(); + } + } + else + { + this.NickName = nameInServersProperties; + } + } + } + if (properties.ContainsKey(ActorProperties.UserId)) + { + this.UserId = (string)properties[ActorProperties.UserId]; + } + if (properties.ContainsKey(ActorProperties.IsInactive)) + { + this.IsInactive = (bool)properties[ActorProperties.IsInactive]; //TURNBASED new well-known propery for players + } + + this.CustomProperties.MergeStringKeys(properties); + this.CustomProperties.StripKeysWithNullValues(); + } + + + /// + /// Brief summary string of the Player. Includes name or player.ID and if it's the Master Client. + /// + public override string ToString() + { + return this.NickName + " " + SupportClass.DictionaryToString(this.CustomProperties); + } + + /// + /// String summary of the Player: player.ID, name and all custom properties of this user. + /// + /// + /// Use with care and not every frame! + /// Converts the customProperties to a String on every single call. + /// + public string ToStringFull() + { + return string.Format("#{0:00} '{1}'{2} {3}", this.ID, this.NickName, this.IsInactive ? " (inactive)" : "", this.CustomProperties.ToStringFull()); + } + + /// + /// If players are equal (by GetHasCode, which returns this.ID). + /// + public override bool Equals(object p) + { + Player pp = p as Player; + return (pp != null && this.GetHashCode() == pp.GetHashCode()); + } + + /// + /// Accompanies Equals, using the ID (actorNumber) as HashCode to return. + /// + public override int GetHashCode() + { + return this.ID; + } + + /// + /// Used internally, to update this client's playerID when assigned (doesn't change after assignment). + /// + protected internal void ChangeLocalID(int newID) + { + if (!this.IsLocal) + { + //Debug.LogError("ERROR You should never change Player IDs!"); + return; + } + + this.actorID = newID; + } + + + + /// + /// Updates and synchronizes this Player's Custom Properties. Optionally, expectedProperties can be provided as condition. + /// + /// + /// Custom Properties are a set of string keys and arbitrary values which is synchronized + /// for the players in a Room. They are available when the client enters the room, as + /// they are in the response of OpJoin and OpCreate. + /// + /// Custom Properties either relate to the (current) Room or a Player (in that Room). + /// + /// Both classes locally cache the current key/values and make them available as + /// property: CustomProperties. This is provided only to read them. + /// You must use the method SetCustomProperties to set/modify them. + /// + /// Any client can set any Custom Properties anytime (when in a room). + /// It's up to the game logic to organize how they are best used. + /// + /// You should call SetCustomProperties only with key/values that are new or changed. This reduces + /// traffic and performance. + /// + /// Unless you define some expectedProperties, setting key/values is always permitted. + /// In this case, the property-setting client will not receive the new values from the server but + /// instead update its local cache in SetCustomProperties. + /// + /// If you define expectedProperties, the server will skip updates if the server property-cache + /// does not contain all expectedProperties with the same values. + /// In this case, the property-setting client will get an update from the server and update it's + /// cached key/values at about the same time as everyone else. + /// + /// The benefit of using expectedProperties can be only one client successfully sets a key from + /// one known value to another. + /// As example: Store who owns an item in a Custom Property "ownedBy". It's 0 initally. + /// When multiple players reach the item, they all attempt to change "ownedBy" from 0 to their + /// actorNumber. If you use expectedProperties {"ownedBy", 0} as condition, the first player to + /// take the item will have it (and the others fail to set the ownership). + /// + /// Properties get saved with the game state for Turnbased games (which use IsPersistent = true). + /// + /// Hashtable of Custom Properties to be set. + /// If non-null, these are the property-values the server will check as condition for this update. + /// Defines if this SetCustomProperties-operation gets forwarded to your WebHooks. Client must be in room. + public void SetCustomProperties(Hashtable propertiesToSet, Hashtable expectedValues = null, WebFlags webFlags = null) + { + if (propertiesToSet == null) + { + return; + } + + Hashtable customProps = propertiesToSet.StripToStringKeys() as Hashtable; + Hashtable customPropsToCheck = expectedValues.StripToStringKeys() as Hashtable; + + + // no expected values -> set and callback + bool noCas = customPropsToCheck == null || customPropsToCheck.Count == 0; + + + if (noCas) + { + this.CustomProperties.Merge(customProps); + this.CustomProperties.StripKeysWithNullValues(); + } + + // send (sync) these new values if in room + if (this.RoomReference != null && this.RoomReference.IsLocalClientInside) + { + this.RoomReference.LoadBalancingClient.loadBalancingPeer.OpSetPropertiesOfActor(this.actorID, customProps, customPropsToCheck, webFlags); + } + } + + + /// Uses OpSetPropertiesOfActor to sync this player's NickName (server is being updated with this.NickName). + private void SetPlayerNameProperty() + { + if (this.RoomReference != null && this.RoomReference.IsLocalClientInside) + { + Hashtable properties = new Hashtable(); + properties[ActorProperties.PlayerName] = this.nickName; + this.RoomReference.LoadBalancingClient.loadBalancingPeer.OpSetPropertiesOfActor(this.ID, properties); + } + } + } +} \ No newline at end of file diff --git a/Assets/Runtime/Photon/PhotonLoadbalancingApi/Player.cs.meta b/Assets/Runtime/Photon/PhotonLoadbalancingApi/Player.cs.meta new file mode 100644 index 0000000..bc8a154 --- /dev/null +++ b/Assets/Runtime/Photon/PhotonLoadbalancingApi/Player.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0b0942472c9351047afc23b868b562f9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Photon/PhotonLoadbalancingApi/Room.cs b/Assets/Runtime/Photon/PhotonLoadbalancingApi/Room.cs new file mode 100644 index 0000000..979d83e --- /dev/null +++ b/Assets/Runtime/Photon/PhotonLoadbalancingApi/Room.cs @@ -0,0 +1,474 @@ +// ---------------------------------------------------------------------------- +// +// Loadbalancing Framework for Photon - Copyright (C) 2011 Exit Games GmbH +// +// +// The Room class resembles the properties known about the room in which +// a game/match happens. +// +// developer@photonengine.com +// ---------------------------------------------------------------------------- + +#define UNITY + + +namespace ExitGames.Client.Photon.LoadBalancing +{ + using System; + using System.Collections; + using System.Collections.Generic; + using ExitGames.Client.Photon; + + #if UNITY || NETFX_CORE + using Hashtable = ExitGames.Client.Photon.Hashtable; + using SupportClass = ExitGames.Client.Photon.SupportClass; + #endif + + + /// + /// This class represents a room a client joins/joined. + /// + /// + /// Contains a list of current players, their properties and those of this room, too. + /// A room instance has a number of "well known" properties like IsOpen, MaxPlayers which can be changed. + /// Your own, custom properties can be set via SetCustomProperties() while being in the room. + /// + /// Typically, this class should be extended by a game-specific implementation with logic and extra features. + /// + public class Room : RoomInfo + { + protected internal int PlayerTTL; + protected internal int RoomTTL; + + /// + /// A reference to the LoadbalancingClient which is currently keeping the connection and state. + /// + protected internal LoadBalancingClient LoadBalancingClient { get; set; } + + /// The name of a room. Unique identifier (per Loadbalancing group) for a room/match. + /// The name can't be changed once it's set by the server. + public new string Name + { + get + { + return this.name; + } + + internal set + { + this.name = value; + } + } + + /// + /// Defines if the room can be joined. + /// + /// + /// This does not affect listing in a lobby but joining the room will fail if not open. + /// If not open, the room is excluded from random matchmaking. + /// Due to racing conditions, found matches might become closed while users are trying to join. + /// Simply re-connect to master and find another. + /// Use property "IsVisible" to not list the room. + /// + /// As part of RoomInfo this can't be set. + /// As part of a Room (which the player joined), the setter will update the server and all clients. + /// + public new bool IsOpen + { + get + { + return this.isOpen; + } + + set + { + if (!this.IsLocalClientInside) + { + LoadBalancingClient.DebugReturn(DebugLevel.WARNING, "Can't set room properties when not in that room."); + } + + if (value != this.isOpen) + { + LoadBalancingClient.OpSetPropertiesOfRoom(new Hashtable() { { GamePropertyKey.IsOpen, value } }); + } + + this.isOpen = value; + } + } + + /// + /// Defines if the room is listed in its lobby. + /// + /// + /// Rooms can be created invisible, or changed to invisible. + /// To change if a room can be joined, use property: open. + /// + /// As part of RoomInfo this can't be set. + /// As part of a Room (which the player joined), the setter will update the server and all clients. + /// + public new bool IsVisible + { + get + { + return this.isVisible; + } + + set + { + if (!this.IsLocalClientInside) + { + LoadBalancingClient.DebugReturn(DebugLevel.WARNING, "Can't set room properties when not in that room."); + } + + if (value != this.isVisible) + { + LoadBalancingClient.OpSetPropertiesOfRoom(new Hashtable() { { GamePropertyKey.IsVisible, value } }); + } + + this.isVisible = value; + } + } + + + /// + /// Sets a limit of players to this room. This property is synced and shown in lobby, too. + /// If the room is full (players count == maxplayers), joining this room will fail. + /// + /// + /// As part of RoomInfo this can't be set. + /// As part of a Room (which the player joined), the setter will update the server and all clients. + /// + public new byte MaxPlayers + { + get + { + return this.maxPlayers; + } + + set + { + if (!this.IsLocalClientInside) + { + LoadBalancingClient.DebugReturn(DebugLevel.WARNING, "Can't set room properties when not in that room."); + } + + if (value != this.maxPlayers) + { + LoadBalancingClient.OpSetPropertiesOfRoom(new Hashtable() { { GamePropertyKey.MaxPlayers, value } }); + } + + this.maxPlayers = value; + } + } + + /// The count of players in this Room (using this.Players.Count). + public new byte PlayerCount + { + get + { + if (this.Players == null) + { + return 0; + } + + return (byte)this.Players.Count; + } + } + + /// While inside a Room, this is the list of players who are also in that room. + private Dictionary players = new Dictionary(); + + /// While inside a Room, this is the list of players who are also in that room. + public Dictionary Players + { + get + { + return players; + } + + private set + { + players = value; + } + } + + /// + /// List of users who are expected to join this room. In matchmaking, Photon blocks a slot for each of these UserIDs out of the MaxPlayers. + /// + /// + /// The corresponding feature in Photon is called "Slot Reservation" and can be found in the doc pages. + /// Define expected players in the PhotonNetwork methods: CreateRoom, JoinRoom and JoinOrCreateRoom. + /// + public string[] ExpectedUsers + { + get { return this.expectedUsers; } + } + + /// + /// The ID (actorID, actorNumber) of the player who's the master of this Room. + /// Note: This changes when the current master leaves the room. + /// + public int MasterClientId { get { return this.masterClientId; } } + + /// + /// Gets a list of custom properties that are in the RoomInfo of the Lobby. + /// This list is defined when creating the room and can't be changed afterwards. Compare: LoadBalancingClient.OpCreateRoom() + /// + /// You could name properties that are not set from the beginning. Those will be synced with the lobby when added later on. + public string[] PropertiesListedInLobby + { + get + { + return this.propertiesListedInLobby; + } + + private set + { + this.propertiesListedInLobby = value; + } + } + + /// + /// Gets if this room uses autoCleanUp to remove all (buffered) RPCs and instantiated GameObjects when a player leaves. + /// + public bool AutoCleanUp + { + get + { + return this.autoCleanUp; + } + } + + + /// Creates a Room (representation) with given name and properties and the "listing options" as provided by parameters. + /// Name of the room (can be null until it's actually created on server). + /// Room options. + protected internal Room(string roomName, RoomOptions options) : base(roomName, options != null ? options.CustomRoomProperties : null) + { + // base() sets name and (custom)properties. here we set "well known" properties + if (options != null) + { + this.isVisible = options.IsVisible; + this.isOpen = options.IsOpen; + this.maxPlayers = options.MaxPlayers; + this.propertiesListedInLobby = options.CustomRoomPropertiesForLobby; + this.PlayerTTL = options.PlayerTtl; + this.RoomTTL = options.EmptyRoomTtl; + } + } + + + /// + /// Updates and synchronizes this Room's Custom Properties. Optionally, expectedProperties can be provided as condition. + /// + /// + /// Custom Properties are a set of string keys and arbitrary values which is synchronized + /// for the players in a Room. They are available when the client enters the room, as + /// they are in the response of OpJoin and OpCreate. + /// + /// Custom Properties either relate to the (current) Room or a Player (in that Room). + /// + /// Both classes locally cache the current key/values and make them available as + /// property: CustomProperties. This is provided only to read them. + /// You must use the method SetCustomProperties to set/modify them. + /// + /// Any client can set any Custom Properties anytime (when in a room). + /// It's up to the game logic to organize how they are best used. + /// + /// You should call SetCustomProperties only with key/values that are new or changed. This reduces + /// traffic and performance. + /// + /// Unless you define some expectedProperties, setting key/values is always permitted. + /// In this case, the property-setting client will not receive the new values from the server but + /// instead update its local cache in SetCustomProperties. + /// + /// If you define expectedProperties, the server will skip updates if the server property-cache + /// does not contain all expectedProperties with the same values. + /// In this case, the property-setting client will get an update from the server and update it's + /// cached key/values at about the same time as everyone else. + /// + /// The benefit of using expectedProperties can be only one client successfully sets a key from + /// one known value to another. + /// As example: Store who owns an item in a Custom Property "ownedBy". It's 0 initally. + /// When multiple players reach the item, they all attempt to change "ownedBy" from 0 to their + /// actorNumber. If you use expectedProperties {"ownedBy", 0} as condition, the first player to + /// take the item will have it (and the others fail to set the ownership). + /// + /// Properties get saved with the game state for Turnbased games (which use IsPersistent = true). + /// + /// Hashtable of Custom Properties that changes. + /// Provide some keys/values to use as condition for setting the new values. Client must be in room. + /// Defines if this SetCustomProperties-operation gets forwarded to your WebHooks. Client must be in room. + public virtual void SetCustomProperties(Hashtable propertiesToSet, Hashtable expectedProperties = null, WebFlags webFlags = null) + { + Hashtable customProps = propertiesToSet.StripToStringKeys() as Hashtable; + + // merge (and delete null-values), unless we use CAS (expected props) + if (expectedProperties == null || expectedProperties.Count == 0) + { + this.CustomProperties.Merge(customProps); + this.CustomProperties.StripKeysWithNullValues(); + } + + // send (sync) these new values if in room + if (this.IsLocalClientInside) + { + this.LoadBalancingClient.loadBalancingPeer.OpSetPropertiesOfRoom(customProps, expectedProperties, webFlags); + } + } + + /// + /// Enables you to define the properties available in the lobby if not all properties are needed to pick a room. + /// + /// + /// Limit the amount of properties sent to users in the lobby to improve speed and stability. + /// + /// An array of custom room property names to forward to the lobby. + public void SetPropertiesListedInLobby(string[] propertiesListedInLobby) + { + Hashtable customProps = new Hashtable(); + customProps[GamePropertyKey.PropsListedInLobby] = propertiesListedInLobby; + + bool sent = this.LoadBalancingClient.OpSetPropertiesOfRoom(customProps); + + if (sent) + { + this.propertiesListedInLobby = propertiesListedInLobby; + } + } + + + /// + /// Removes a player from this room's Players Dictionary. + /// This is internally used by the LoadBalancing API. There is usually no need to remove players yourself. + /// This is not a way to "kick" players. + /// + protected internal virtual void RemovePlayer(Player player) + { + this.Players.Remove(player.ID); + player.RoomReference = null; + } + + /// + /// Removes a player from this room's Players Dictionary. + /// + protected internal virtual void RemovePlayer(int id) + { + this.RemovePlayer(this.GetPlayer(id)); + } + + /// + /// Asks the server to assign another player as Master Client of your current room. + /// + /// + /// RaiseEvent has the option to send messages only to the Master Client of a room. + /// SetMasterClient affects which client gets those messages. + /// + /// This method calls an operation on the server to set a new Master Client, which takes a roundtrip. + /// In case of success, this client and the others get the new Master Client from the server. + /// + /// SetMasterClient tells the server which current Master Client should be replaced with the new one. + /// It will fail, if anything switches the Master Client moments earlier. There is no callback for this + /// error. All clients should get the new Master Client assigned by the server anyways. + /// + /// See also: MasterClientId + /// + /// The player to become the next Master Client. + /// False when this operation couldn't be done currently. Requires a v4 Photon Server. + public bool SetMasterClient(Player masterClientPlayer) + { + if (!this.IsLocalClientInside) + { + this.LoadBalancingClient.DebugReturn(DebugLevel.WARNING, "SetMasterClient can only be called for the current room (being in one)."); + return false; + } + + Hashtable newProps = new Hashtable() { { GamePropertyKey.MasterClientId, masterClientPlayer.ID } }; + Hashtable prevProps = new Hashtable() { { GamePropertyKey.MasterClientId, this.MasterClientId} }; + return this.LoadBalancingClient.OpSetPropertiesOfRoom(newProps, prevProps); + } + + /// + /// Checks if the player is in the room's list already and calls StorePlayer() if not. + /// + /// The new player - identified by ID. + /// False if the player could not be added (cause it was in the list already). + public virtual bool AddPlayer(Player player) + { + if (!this.Players.ContainsKey(player.ID)) + { + this.StorePlayer(player); + return true; + } + + return false; + } + + /// + /// Updates a player reference in the Players dictionary (no matter if it existed before or not). + /// + /// The Player instance to insert into the room. + public virtual Player StorePlayer(Player player) + { + this.Players[player.ID] = player; + player.RoomReference = this; + + // while initializing the room, the players are not guaranteed to be added in-order + if (this.MasterClientId == 0 || player.ID < this.MasterClientId) + { + this.masterClientId = player.ID; + } + + return player; + } + + /// + /// Tries to find the player with given actorNumber (a.k.a. ID). + /// Only useful when in a Room, as IDs are only valid per Room. + /// + /// ID to look for. + /// The player with the ID or null. + public virtual Player GetPlayer(int id) + { + Player result = null; + this.Players.TryGetValue(id, out result); + + return result; + } + + /// + /// Attempts to remove all current expected users from the server's Slot Reservation list. + /// + /// + /// Note that this operation can conflict with new/other users joining. They might be + /// adding users to the list of expected users before or after this client called ClearExpectedUsers. + /// + /// This room's expectedUsers value will update, when the server sends a successful update. + /// + /// Internals: This methods wraps up setting the ExpectedUsers property of a room. + /// + public void ClearExpectedUsers() + { + Hashtable props = new Hashtable(); + props[GamePropertyKey.ExpectedUsers] = new string[0]; + Hashtable expected = new Hashtable(); + expected[GamePropertyKey.ExpectedUsers] = this.ExpectedUsers; + this.LoadBalancingClient.OpSetPropertiesOfRoom(props, expected); + } + + + /// Returns a summary of this Room instance as string. + /// Summary of this Room instance. + public override string ToString() + { + return string.Format("Room: '{0}' {1},{2} {4}/{3} players.", this.name, this.isVisible ? "visible" : "hidden", this.isOpen ? "open" : "closed", this.maxPlayers, this.PlayerCount); + } + + /// Returns a summary of this Room instance as longer string, including Custom Properties. + /// Summary of this Room instance. + public new string ToStringFull() + { + return string.Format("Room: '{0}' {1},{2} {4}/{3} players.\ncustomProps: {5}", this.name, this.isVisible ? "visible" : "hidden", this.isOpen ? "open" : "closed", this.maxPlayers, this.PlayerCount, this.CustomProperties.ToStringFull()); + } + } +} \ No newline at end of file diff --git a/Assets/Runtime/Photon/PhotonLoadbalancingApi/Room.cs.meta b/Assets/Runtime/Photon/PhotonLoadbalancingApi/Room.cs.meta new file mode 100644 index 0000000..e1ce8b1 --- /dev/null +++ b/Assets/Runtime/Photon/PhotonLoadbalancingApi/Room.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7f05b351233593247979ece22db7a9f4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Photon/PhotonLoadbalancingApi/RoomInfo.cs b/Assets/Runtime/Photon/PhotonLoadbalancingApi/RoomInfo.cs new file mode 100644 index 0000000..42655e2 --- /dev/null +++ b/Assets/Runtime/Photon/PhotonLoadbalancingApi/RoomInfo.cs @@ -0,0 +1,261 @@ +// ---------------------------------------------------------------------------- +// +// Loadbalancing Framework for Photon - Copyright (C) 2011 Exit Games GmbH +// +// +// This class resembles info about available rooms, as sent by the Master +// server's lobby. Consider all values as readonly. +// +// developer@photonengine.com +// ---------------------------------------------------------------------------- + +#if UNITY_4_7_OR_NEWER +#define UNITY +#endif + +namespace ExitGames.Client.Photon.LoadBalancing +{ + using System.Collections; + + #if UNITY || NETFX_CORE + using Hashtable = ExitGames.Client.Photon.Hashtable; + using SupportClass = ExitGames.Client.Photon.SupportClass; + #endif + + + /// + /// A simplified room with just the info required to list and join, used for the room listing in the lobby. + /// The properties are not settable (IsOpen, MaxPlayers, etc). + /// + /// + /// This class resembles info about available rooms, as sent by the Master server's lobby. + /// Consider all values as readonly. None are synced (only updated by events by server). + /// + public class RoomInfo + { + /// Used internally in lobby, to mark rooms that are no longer listed (for being full, closed or hidden). + protected internal bool removedFromList; + + /// Backing field for property. + private Hashtable customProperties = new Hashtable(); + + /// Backing field for property. + protected byte maxPlayers = 0; + + /// Backing field for property. + protected string[] expectedUsers; + + /// Backing field for property. + protected bool isOpen = true; + + /// Backing field for property. + protected bool isVisible = true; + + /// Backing field for property. False unless the GameProperty is set to true (else it's not sent). + protected bool autoCleanUp = true; + + /// Backing field for property. + protected string name; + + /// Backing field for master client id (actorNumber). defined by server in room props and ev leave. + protected internal int masterClientId; + + /// Backing field for property. + protected string[] propertiesListedInLobby; + + /// Read-only "cache" of custom properties of a room. Set via Room.SetCustomProperties (not available for RoomInfo class!). + /// All keys are string-typed and the values depend on the game/application. + /// + public Hashtable CustomProperties + { + get + { + return this.customProperties; + } + } + + /// The name of a room. Unique identifier for a room/match (per AppId + game-Version). + public string Name + { + get + { + return this.name; + } + } + + /// + /// Count of players currently in room. This property is overwritten by the Room class (used when you're in a Room). + /// + public int PlayerCount { get; private set; } + + /// + /// State if the local client is already in the game or still going to join it on gameserver (in lobby: false). + /// + public bool IsLocalClientInside { get; set; } + + /// + /// The limit of players for this room. This property is shown in lobby, too. + /// If the room is full (players count == maxplayers), joining this room will fail. + /// + /// + /// As part of RoomInfo this can't be set. + /// As part of a Room (which the player joined), the setter will update the server and all clients. + /// + public byte MaxPlayers + { + get + { + return this.maxPlayers; + } + } + + /// + /// Defines if the room can be joined. + /// This does not affect listing in a lobby but joining the room will fail if not open. + /// If not open, the room is excluded from random matchmaking. + /// Due to racing conditions, found matches might become closed even while you join them. + /// Simply re-connect to master and find another. + /// Use property "IsVisible" to not list the room. + /// + /// + /// As part of RoomInfo this can't be set. + /// As part of a Room (which the player joined), the setter will update the server and all clients. + /// + public bool IsOpen + { + get + { + return this.isOpen; + } + } + + /// + /// Defines if the room is listed in its lobby. + /// Rooms can be created invisible, or changed to invisible. + /// To change if a room can be joined, use property: open. + /// + /// + /// As part of RoomInfo this can't be set. + /// As part of a Room (which the player joined), the setter will update the server and all clients. + /// + public bool IsVisible + { + get + { + return this.isVisible; + } + } + + /// + /// Constructs a RoomInfo to be used in room listings in lobby. + /// + /// Name of the room and unique ID at the same time. + /// Properties for this room. + protected internal RoomInfo(string roomName, Hashtable roomProperties) + { + this.InternalCacheProperties(roomProperties); + + this.name = roomName; + } + + /// + /// Makes RoomInfo comparable (by name). + /// + public override bool Equals(object other) + { + RoomInfo otherRoomInfo = other as RoomInfo; + return (otherRoomInfo != null && this.Name.Equals(otherRoomInfo.name)); + } + + /// + /// Accompanies Equals, using the name's HashCode as return. + /// + /// + public override int GetHashCode() + { + return this.name.GetHashCode(); + } + + + /// Returns most interesting room values as string. + /// Summary of this RoomInfo instance. + public override string ToString() + { + return string.Format("Room: '{0}' {1},{2} {4}/{3} players.", this.name, this.isVisible ? "visible" : "hidden", this.isOpen ? "open" : "closed", this.maxPlayers, this.PlayerCount); + } + + /// Returns most interesting room values as string, including custom properties. + /// Summary of this RoomInfo instance. + public string ToStringFull() + { + return string.Format("Room: '{0}' {1},{2} {4}/{3} players.\ncustomProps: {5}", this.name, this.isVisible ? "visible" : "hidden", this.isOpen ? "open" : "closed", this.maxPlayers, this.PlayerCount, this.customProperties.ToStringFull()); + } + + /// Copies "well known" properties to fields (IsVisible, etc) and caches the custom properties (string-keys only) in a local hashtable. + /// New or updated properties to store in this RoomInfo. + protected internal virtual void InternalCacheProperties(Hashtable propertiesToCache) + { + if (propertiesToCache == null || propertiesToCache.Count == 0 || this.customProperties.Equals(propertiesToCache)) + { + return; + } + + // check of this game was removed from the list. in that case, we don't + // need to read any further properties + // list updates will remove this game from the game listing + if (propertiesToCache.ContainsKey(GamePropertyKey.Removed)) + { + this.removedFromList = (bool)propertiesToCache[GamePropertyKey.Removed]; + if (this.removedFromList) + { + return; + } + } + + // fetch the "well known" properties of the room, if available + if (propertiesToCache.ContainsKey(GamePropertyKey.MaxPlayers)) + { + this.maxPlayers = (byte)propertiesToCache[GamePropertyKey.MaxPlayers]; + } + + if (propertiesToCache.ContainsKey(GamePropertyKey.IsOpen)) + { + this.isOpen = (bool)propertiesToCache[GamePropertyKey.IsOpen]; + } + + if (propertiesToCache.ContainsKey(GamePropertyKey.IsVisible)) + { + this.isVisible = (bool)propertiesToCache[GamePropertyKey.IsVisible]; + } + + if (propertiesToCache.ContainsKey(GamePropertyKey.PlayerCount)) + { + this.PlayerCount = (int)((byte)propertiesToCache[GamePropertyKey.PlayerCount]); + } + + if (propertiesToCache.ContainsKey(GamePropertyKey.CleanupCacheOnLeave)) + { + this.autoCleanUp = (bool)propertiesToCache[GamePropertyKey.CleanupCacheOnLeave]; + } + + if (propertiesToCache.ContainsKey(GamePropertyKey.MasterClientId)) + { + this.masterClientId = (int)propertiesToCache[GamePropertyKey.MasterClientId]; + } + + if (propertiesToCache.ContainsKey(GamePropertyKey.PropsListedInLobby)) + { + this.propertiesListedInLobby = propertiesToCache[GamePropertyKey.PropsListedInLobby] as string[]; + } + + if (propertiesToCache.ContainsKey((byte)GamePropertyKey.ExpectedUsers)) + { + this.expectedUsers = (string[])propertiesToCache[GamePropertyKey.ExpectedUsers]; + } + + // merge the custom properties (from your application) to the cache (only string-typed keys will be kept) + this.customProperties.MergeStringKeys(propertiesToCache); + this.customProperties.StripKeysWithNullValues(); + } + } +} diff --git a/Assets/Runtime/Photon/PhotonLoadbalancingApi/RoomInfo.cs.meta b/Assets/Runtime/Photon/PhotonLoadbalancingApi/RoomInfo.cs.meta new file mode 100644 index 0000000..c4c70b8 --- /dev/null +++ b/Assets/Runtime/Photon/PhotonLoadbalancingApi/RoomInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 34e9ca7b04eb3424c915e47461a9ca43 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Photon/PhotonLoadbalancingApi/WebRpc.cs b/Assets/Runtime/Photon/PhotonLoadbalancingApi/WebRpc.cs new file mode 100644 index 0000000..896de0a --- /dev/null +++ b/Assets/Runtime/Photon/PhotonLoadbalancingApi/WebRpc.cs @@ -0,0 +1,167 @@ +// ---------------------------------------------------------------------------- +// +// Loadbalancing Framework for Photon - Copyright (C) 2016 Exit Games GmbH +// +// +// This class wraps responses of a Photon WebRPC call, coming from a +// third party web service. +// +// developer@photonengine.com +// ---------------------------------------------------------------------------- + +#if UNITY_4_7_OR_NEWER +#define UNITY +#endif + + +namespace ExitGames.Client.Photon.LoadBalancing +{ + using ExitGames.Client.Photon; + using System.Collections.Generic; + + #if UNITY || NETFX_CORE + using Hashtable = ExitGames.Client.Photon.Hashtable; + using SupportClass = ExitGames.Client.Photon.SupportClass; + #endif + + + /// Reads an operation response of a WebRpc and provides convenient access to most common values. + /// + /// See method PhotonNetwork.WebRpc.
+ /// Create a WebRpcResponse to access common result values.
+ /// The operationResponse.OperationCode should be: OperationCode.WebRpc.
+ ///
+ public class WebRpcResponse + { + /// Name of the WebRpc that was called. + public string Name { get; private set; } + + /// ReturnCode of the WebService that answered the WebRpc. + /// + /// 1 is: "OK" for WebRPCs.
+ /// -1 is: No ReturnCode by WebRpc service (check OperationResponse.ReturnCode).
+ /// Other ReturnCodes are defined by the individual WebRpc and service. + ///
+ public int ReturnCode { get; private set; } + + /// Might be empty or null. + public string DebugMessage { get; private set; } + + /// Other key/values returned by the webservice that answered the WebRpc. + public Dictionary Parameters { get; private set; } + + /// An OperationResponse for a WebRpc is needed to read it's values. + public WebRpcResponse(OperationResponse response) + { + object value; + response.Parameters.TryGetValue(ParameterCode.UriPath, out value); + this.Name = value as string; + + response.Parameters.TryGetValue(ParameterCode.WebRpcReturnCode, out value); + this.ReturnCode = (value != null) ? (byte)value : -1; + + response.Parameters.TryGetValue(ParameterCode.WebRpcParameters, out value); + this.Parameters = value as Dictionary; + + response.Parameters.TryGetValue(ParameterCode.WebRpcReturnMessage, out value); + this.DebugMessage = value as string; + } + + /// Turns the response into an easier to read string. + /// String resembling the result. + public string ToStringFull() + { + return string.Format("{0}={2}: {1} \"{3}\"", this.Name, SupportClass.DictionaryToString(this.Parameters), this.ReturnCode, this.DebugMessage); + } + } + + + /// + /// Optional flags to be used in Photon client SDKs with Op RaiseEvent and Op SetProperties. + /// Introduced mainly for webhooks 1.2 to control behavior of forwarded HTTP requests. + /// + public class WebFlags + { + + public readonly static WebFlags Default = new WebFlags(0); + public byte WebhookFlags; + /// + /// Indicates whether to forward HTTP request to web service or not. + /// + public bool HttpForward + { + get { return (WebhookFlags & HttpForwardConst) != 0; } + set { + if (value) + { + WebhookFlags |= HttpForwardConst; + } + else + { + WebhookFlags = (byte) (WebhookFlags & ~(1 << 0)); + } + } + } + public const byte HttpForwardConst = 0x01; + /// + /// Indicates whether to send AuthCookie of actor in the HTTP request to web service or not. + /// + public bool SendAuthCookie + { + get { return (WebhookFlags & SendAuthCookieConst) != 0; } + set { + if (value) + { + WebhookFlags |= SendAuthCookieConst; + } + else + { + WebhookFlags = (byte)(WebhookFlags & ~(1 << 1)); + } + } + } + public const byte SendAuthCookieConst = 0x02; + /// + /// Indicates whether to send HTTP request synchronously or asynchronously to web service. + /// + public bool SendSync + { + get { return (WebhookFlags & SendSyncConst) != 0; } + set { + if (value) + { + WebhookFlags |= SendSyncConst; + } + else + { + WebhookFlags = (byte)(WebhookFlags & ~(1 << 2)); + } + } + } + public const byte SendSyncConst = 0x04; + /// + /// Indicates whether to send serialized game state in HTTP request to web service or not. + /// + public bool SendState + { + get { return (WebhookFlags & SendStateConst) != 0; } + set { + if (value) + { + WebhookFlags |= SendStateConst; + } + else + { + WebhookFlags = (byte)(WebhookFlags & ~(1 << 3)); + } + } + } + public const byte SendStateConst = 0x08; + + public WebFlags(byte webhookFlags) + { + WebhookFlags = webhookFlags; + } + } + +} diff --git a/Assets/Runtime/Photon/PhotonLoadbalancingApi/WebRpc.cs.meta b/Assets/Runtime/Photon/PhotonLoadbalancingApi/WebRpc.cs.meta new file mode 100644 index 0000000..f9a8285 --- /dev/null +++ b/Assets/Runtime/Photon/PhotonLoadbalancingApi/WebRpc.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e82402aea03f000428f5ca11fec7ecfc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Photon/Plugins.meta b/Assets/Runtime/Photon/Plugins.meta new file mode 100644 index 0000000..b7d2fe3 --- /dev/null +++ b/Assets/Runtime/Photon/Plugins.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 64d9415f03804bb40b359b53619181be +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Photon/Plugins/Photon3Unity3D.dll b/Assets/Runtime/Photon/Plugins/Photon3Unity3D.dll new file mode 100644 index 0000000000000000000000000000000000000000..f9409a8e789607ae156f672e924954d2a586b0af GIT binary patch literal 151040 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ΜA|>rKl4@$4iM6w>UL%M0y+bp)evWI!Z~-S- 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 HDDeLRD3Z{j literal 0 HcmV?d00001 diff --git a/Assets/Runtime/Photon/Plugins/Photon3Unity3D.dll.meta b/Assets/Runtime/Photon/Plugins/Photon3Unity3D.dll.meta new file mode 100644 index 0000000..428e194 --- /dev/null +++ b/Assets/Runtime/Photon/Plugins/Photon3Unity3D.dll.meta @@ -0,0 +1,113 @@ +fileFormatVersion: 2 +guid: 3124388131362e24dbb79c27c2fc0a89 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + isPreloaded: 0 + isOverridable: 0 + platformData: + - first: + '': Any + second: + enabled: 0 + settings: + Exclude Android: 0 + Exclude Editor: 0 + Exclude Linux: 0 + Exclude Linux64: 0 + Exclude LinuxUniversal: 0 + Exclude OSXUniversal: 0 + Exclude WebGL: 0 + Exclude Win: 0 + Exclude Win64: 0 + Exclude iOS: 0 + - first: + Android: Android + second: + enabled: 1 + settings: + CPU: ARMv7 + - first: + Any: + second: + enabled: 1 + settings: {} + - first: + Editor: Editor + second: + enabled: 1 + settings: + CPU: AnyCPU + DefaultValueInitialized: true + OS: AnyOS + - first: + Facebook: Win + second: + enabled: 0 + settings: + CPU: AnyCPU + - first: + Facebook: Win64 + second: + enabled: 0 + settings: + CPU: AnyCPU + - first: + Standalone: Linux + second: + enabled: 1 + settings: + CPU: x86 + - first: + Standalone: Linux64 + second: + enabled: 1 + settings: + CPU: x86_64 + - first: + Standalone: LinuxUniversal + second: + enabled: 1 + settings: {} + - first: + Standalone: OSXUniversal + second: + enabled: 1 + settings: + CPU: AnyCPU + - first: + Standalone: Win + second: + enabled: 1 + settings: + CPU: AnyCPU + - first: + Standalone: Win64 + second: + enabled: 1 + settings: + CPU: AnyCPU + - first: + WebGL: WebGL + second: + enabled: 1 + settings: {} + - first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + - first: + iPhone: iOS + second: + enabled: 1 + settings: + AddToEmbeddedBinaries: false + CompileFlags: + FrameworkDependencies: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Photon/Plugins/Photon3Unity3D.xml b/Assets/Runtime/Photon/Plugins/Photon3Unity3D.xml new file mode 100644 index 0000000..7313808 --- /dev/null +++ b/Assets/Runtime/Photon/Plugins/Photon3Unity3D.xml @@ -0,0 +1,2109 @@ + + + + Photon3Unity3D + + + + + This is a substitute for the Hashtable class, missing in: Win8RT and Windows Phone. It uses a Dictionary<object,object> as base. + + + Please be aware that this class might act differently than the Hashtable equivalent. + As far as Photon is concerned, the substitution is sufficiently precise. + + + + + Creates a shallow copy of the Hashtable. + + + A shallow copy of a collection copies only the elements of the collection, whether they are + reference types or value types, but it does not copy the objects that the references refer + to. The references in the new collection point to the same objects that the references in + the original collection point to. + + Shallow copy of the Hashtable. + + + + Contains several (more or less) useful static methods, mostly used for debugging. + + + + + Gets the local machine's "milliseconds since start" value (precision is described in remarks). + + + This method uses Environment.TickCount (cheap but with only 16ms precision). + PhotonPeer.LocalMsTimestampDelegate is available to set the delegate (unless already connected). + + Fraction of the current time in Milliseconds (this is not a proper datetime timestamp). + + + + Creates a background thread that calls the passed function in 100ms intervals, as long as that returns true. + + + + + Creates a background thread that calls the passed function in intervals, as long as that returns true. + + The function to call. Must return true, if it should be called again. + Milliseconds to sleep between calls of myThread. Default: 100ms. + An optional name for the task for eased debugging. + + + + Ends the thread with the given id (= index of the thread list). + + The unique ID of the thread. + True if the thread is canceled and false otherwise, e.g. if the thread with the given ID does not exist. + + + + Ends the thread with the given id (= index of the thread list). + + The unique ID of the thread. + True if the thread is canceled and false otherwise, e.g. if the thread with the given ID does not exist. + + + + Writes the exception's stack trace to the received stream. + + Exception to obtain information from. + Output sream used to write to. + + + + Writes the exception's stack trace to the received stream. Writes to: System.Diagnostics.Debug. + + Exception to obtain information from. + + + + This method returns a string, representing the content of the given IDictionary. + Returns "null" if parameter is null. + + + IDictionary to return as string. + + + The string representation of keys and values in IDictionary. + + + + + This method returns a string, representing the content of the given IDictionary. + Returns "null" if parameter is null. + + IDictionary to return as string. + + + + + Converts a byte-array to string (useful as debugging output). + Uses BitConverter.ToString(list) internally after a null-check of list. + + Byte-array to convert to string. + + List of bytes as string. + + + + + Class to wrap static access to the random.Next() call in a thread safe manner. + + + + + Defines block size for encryption/decryption algorithm + + + + + Defines IV size for encryption/decryption algorithm + + + + + Defines HMAC size for packet authentication algorithm + + + + + Enumeration of situations that change the peers internal status. + Used in calls to OnStatusChanged to inform your application of various situations that might happen. + + + Most of these codes are referenced somewhere else in the documentation when they are relevant to methods. + + + + the PhotonPeer is connected.
See {@link PhotonListener#OnStatusChanged}*
+
+ + the PhotonPeer just disconnected.
See {@link PhotonListener#OnStatusChanged}*
+
+ + the PhotonPeer encountered an exception and will disconnect, too.
See {@link PhotonListener#OnStatusChanged}*
+
+ + the PhotonPeer encountered an exception while opening the incoming connection to the server. The server could be down / not running or the client has no network or a misconfigured DNS.
See {@link PhotonListener#OnStatusChanged}*
+
+ + Used on platforms that throw a security exception on connect. Unity3d does this, e.g., if a webplayer build could not fetch a policy-file from a remote server. + + + PhotonPeer outgoing queue is filling up. send more often. + + + PhotonPeer outgoing queue is filling up. send more often. + + + Sending command failed. Either not connected, or the requested channel is bigger than the number of initialized channels. + + + PhotonPeer outgoing queue is filling up. send more often. + + + PhotonPeer incoming queue is filling up. Dispatch more often. + + + PhotonPeer incoming queue is filling up. Dispatch more often. + + + PhotonPeer incoming queue is filling up. Dispatch more often. + + + Exception, if a server cannot be connected. Most likely, the server is not responding. Ask user to try again later. + + + Disconnection due to a timeout (client did no longer receive ACKs from server). + + + Disconnect by server due to timeout (received a disconnect command, cause server misses ACKs of client). + + + Disconnect by server due to concurrent user limit reached (received a disconnect command). + + + Disconnect by server due to server's logic (received a disconnect command). + + + (1048) Value for OnStatusChanged()-call, when the encryption-setup for secure communication finished successfully. + + + (1049) Value for OnStatusChanged()-call, when the encryption-setup failed for some reason. Check debug logs. + + + + Callback interface for the Photon client side. Must be provided to a new PhotonPeer in its constructor. + + + These methods are used by your PhotonPeer instance to keep your app updated. Read each method's + description and check out the samples to see how to use them. + + + + + Provides textual descriptions for various error conditions and noteworthy situations. + In cases where the application needs to react, a call to OnStatusChanged is used. + OnStatusChanged gives "feedback" to the game, DebugReturn provies human readable messages + on the background. + + + All debug output of the library will be reported through this method. Print it or put it in a + buffer to use it on-screen. Use PhotonPeer.DebugOut to select how verbose the output is. + + DebugLevel (severity) of the message. + Debug text. Print to System.Console or screen. + + + + Callback method which gives you (async) responses for called operations. + + + Similar to method-calling, operations can have a result. + Because operation-calls are non-blocking and executed on the server, responses are provided + after a roundtrip as call to this method. + + Example: Trying to create a room usually succeeds but can fail if the room's name is already + in use (room names are their IDs). + + This method is used as general callback for all operations. Each response corresponds to a certain + "type" of operation by its OperationCode. + + + + When you join a room, the server will assign a consecutive number to each client: the + "actorNr" or "player number". This is sent back in the OperationResult's + Parameters as value of key . + + Fetch your actorNr of a Join response like this: + int actorNr = (int)operationResponse[(byte)OperationCode.ActorNr]; + + The response to an operation\-call. + + + + OnStatusChanged is called to let the game know when asyncronous actions finished or when errors happen. + + + Not all of the many StatusCode values will apply to your game. Example: If you don't use encryption, + the respective status changes are never made. + + The values are all part of the StatusCode enumeration and described value-by-value. + + A code to identify the situation. + + + + Called whenever an event from the Photon Server is dispatched. + + + Events are used for communication between clients and allow the server to update clients over time. + The creation of an event is often triggered by an operation (called by this client or an other). + + Each event carries its specific content in its Parameters. Your application knows which content to + expect by checking the event's 'type', given by the event's Code. + + Events can be defined and extended server-side. + + If you use the LoadBalancing application as base, several events like EvJoin and EvLeave are already defined. + For these events and their Parameters, the library provides constants, so check the EventCode and ParameterCode + classes. + + Photon also allows you to come up with custom events on the fly, purely client-side. To do so, use + OpRaiseEvent. + + Events are buffered on the client side and must be Dispatched. This way, OnEvent is always taking + place in the same thread as a call. + + The event currently being dispatched. + + + + The bytes between Position and Length are copied to the beginning of the buffer. Length decreased by Position. Position set to 0. + + + + + Brings StreamBuffer to the state as after writing of 'length' bytes. Returned buffer and offset can be used to actually fill "written" segment with data. + + + + + Sets stream length. If current position is greater than specified value, it's set to the value. + + + SetLength(0) resets the stream to initial state but preserves underlying byte[] buffer. + + + + + Guarantees that the buffer is at least neededSize bytes. + + + + + Value range for a Peer's connection and initialization state, as returned by the PeerState property. + + + While this is not the same as the StatusCode of IPhotonPeerListener.OnStatusChanged(), it directly relates to it. + In most cases, it makes more sense to build a game's state on top of the OnStatusChanged() as you get changes. + + + + The peer is disconnected and can't call Operations. Call Connect(). + + + The peer is establishing the connection: opening a socket, exchanging packages with Photon. + + + The connection is established and now sends the application name to Photon. + You set the "application name" by calling PhotonPeer.Connect(). + + + The peer is connected and initialized (selected an application). You can now use operations. + + + The peer is disconnecting. It sent a disconnect to the server, which will acknowledge closing the connection. + + + + These are the options that can be used as underlying transport protocol. + + + + Use UDP to connect to Photon, which allows you to send operations reliable or unreliable on demand. + + + Use TCP to connect to Photon. + + + A TCP-based protocol commonly supported by browsers.For WebGL games mostly. Note: No WebSocket IPhotonSocket implementation is in this Assembly. + This protocol is only available in Unity exports to WebGL. + + + A TCP-based, encrypted protocol commonly supported by browsers. For WebGL games mostly. Note: No WebSocket IPhotonSocket implementation is in this Assembly. + This protocol is only available in Unity exports to WebGL. + + + + Level / amount of DebugReturn callbacks. Each debug level includes output for lower ones: OFF, ERROR, WARNING, INFO, ALL. + + + + No debug out. + + + Only error descriptions. + + + Warnings and errors. + + + Information about internal workflows, warnings and errors. + + + Most complete workflow description (but lots of debug output), info, warnings and errors. + + + + Instances of the PhotonPeer class are used to connect to a Photon server and communicate with it. + + + A PhotonPeer instance allows communication with the Photon Server, which in turn distributes messages + to other PhotonPeer clients. + An application can use more than one PhotonPeer instance, which are treated as separate users on the + server. Each should have its own listener instance, to separate the operations, callbacks and events. + + + + False if this library build contains C# Socket code. If true, you must set some type as SocketImplementation before connecting. + + + True, if this library needs a native Photon "Encryptor" plugin library for "Datagram Encryption". If false, this dll attempts to use managed encryption. + + + True if the library was compiled with DEBUG setting. + + + A simplified identifier for client SDKs. Photon's APIs might modify this (as a dll can be used in more than one product). Helps debugging. + + + For the Init-request, we shift the ClientId by one and the last bit signals a "debug" (0) or "release" build (1). + + + Defines if Key Exchange for Encryption is done asynchronously in another thread. + + + Version of this library as string. + + + Enables selection of a (Photon-)serialization protocol. Used in Connect methods. + Defaults to SerializationProtocol.GpBinaryV16; + + + Defines which IPhotonSocket class to use per ConnectionProtocol. + + Several platforms have special Socket implementations and slightly different APIs. + To accomodate this, switching the socket implementation for a network protocol was made available. + By default, UDP and TCP have socket implementations assigned. + + You only need to set the SocketImplementationConfig once, after creating a PhotonPeer + and before connecting. If you switch the TransportProtocol, the correct implementation is being used. + + + + + Can be used to read the IPhotonSocket implementation at runtime (before connecting). + + + Use the SocketImplementationConfig to define which IPhotonSocket is used per ConnectionProtocol. + + + + + Sets the level (and amount) of debug output provided by the library. + + + This affects the callbacks to IPhotonPeerListener.DebugReturn. + Default Level: Error. + + + + + Gets the IPhotonPeerListener of this instance (set in constructor). + Can be used in derived classes for Listener.DebugReturn(). + + + + + Gets count of all bytes coming in (including headers, excluding UDP/TCP overhead) + + + + + Gets count of all bytes going out (including headers, excluding UDP/TCP overhead) + + + + + Gets the size of the dispatched event or operation-result in bytes. + This value is set before OnEvent() or OnOperationResponse() is called (within DispatchIncomingCommands()). + + + Get this value directly in OnEvent() or OnOperationResponse(). Example: + void OnEvent(...) { + int eventSizeInBytes = this.peer.ByteCountCurrentDispatch; + //... + + void OnOperationResponse(...) { + int resultSizeInBytes = this.peer.ByteCountCurrentDispatch; + //... + + + + Returns the debug string of the event or operation-response currently being dispatched or string. Empty if none. + In a release build of the lib, this will always be empty. + + + + Gets the size of the last serialized operation call in bytes. + The value includes all headers for this single operation but excludes those of UDP, Enet Package Headers and TCP. + + + Get this value immediately after calling an operation. + Example: + + this.loadbalancingClient.OpJoinRoom("myroom"); + int opjoinByteCount = this.loadbalancingClient.ByteCountLastOperation; + + + + + Gets the byte-count of incoming "low level" messages, which are either Enet Commands or Tcp Messages. + These include all headers, except those of the underlying internet protocol Udp or Tcp. + + + + + Gets the byte-count of outgoing "low level" messages, which are either Enet Commands or Tcp Messages. + These include all headers, except those of the underlying internet protocol Udp or Tcp. + + + + + Gets a statistic of incoming and outgoing traffic, split by operation, operation-result and event. + + + Operations are outgoing traffic, results and events are incoming. + Includes the per-command header sizes (Udp: Enet Command Header or Tcp: Message Header). + + + + + Returns the count of milliseconds the stats are enabled for tracking. + + + + + Enables or disables collection of statistics in TrafficStatsIncoming, TrafficStatsOutgoing and TrafficstatsGameLevel. + + + Setting this to true, also starts the stopwatch to measure the timespan the stats are collected. + Enables the traffic statistics of a peer: TrafficStatsIncoming, TrafficStatsOutgoing and TrafficstatsGameLevel (nothing else). + Default value: false (disabled). + + + + + Creates new instances of TrafficStats and starts a new timer for those. + + + + Size of CommandLog. Default is 0, no logging. + + A bigger log is better for debugging but uses more memory. + Get the log as string via CommandLogToString. + + + + Converts the CommandLog into a readable table-like string with summary. + + Sent reliable commands begin with SND. Their acknowledgements with ACK. + ACKs list the reliable sequence number of the command they acknowledge (not their own). + Careful: This method should not be called frequently, as it's time- and memory-consuming to create the log. + + + + + Debugging option to tell the Photon Server to log all datagrams. + + + + + Up to 4 resend attempts for a reliable command can be done in quick succession (after RTT+4*Variance). + + + By default 0. Any later resend attempt will then double the time before the next resend. + Max value = 4; + Make sure to adjust SentCountAllowance to a slightly higher value, as more repeats will get done. + + + + + This is the (low level) state of the connection to the server of a PhotonPeer. Managed internally and read-only. + + + Don't mix this up with the StatusCode provided in IPhotonListener.OnStatusChanged(). + Applications should use the StatusCode of OnStatusChanged() to track their state, as + it also covers the higher level initialization between a client and Photon. + + + + + This peer's ID as assigned by the server or 0 if not using UDP. Will be 0xFFFF before the client connects. + + Used for debugging only. This value is not useful in everyday Photon usage. + + + + Initial size internal lists for incoming/outgoing commands (reliable and unreliable). + + + This sets only the initial size. All lists simply grow in size as needed. This means that + incoming or outgoing commands can pile up and consume heap size if Service is not called + often enough to handle the messages in either direction. + + Configure the WarningSize, to get callbacks when the lists reach a certain size. + + UDP: Incoming and outgoing commands each have separate buffers for reliable and unreliable sending. + There are additional buffers for "sent commands" and "ACKs". + TCP: Only two buffers exist: incoming and outgoing commands. + + + + (default=2) minimum number of open connections + + + (default=6) maximum number of open connections, should be > RhttpMinConnections + + + + Limits the queue of received unreliable commands within DispatchIncomingCommands before dispatching them. + This works only in UDP. + This limit is applied when you call DispatchIncomingCommands. If this client (already) received more than + LimitOfUnreliableCommands, it will throw away the older ones instead of dispatching them. This can produce + bigger gaps for unreliable commands but your client catches up faster. + + + This can be useful when the client couldn't dispatch anything for some time (cause it was in a room but + loading a level). + If set to 20, the incoming unreliable queues are truncated to 20. + If 0, all received unreliable commands will be dispatched. + This is a "per channel" value, so each channel can hold up to LimitOfUnreliableCommands commands. + This value interacts with DispatchIncomingCommands: If that is called less often, more commands get skipped. + + + + + Count of all currently received but not-yet-Dispatched reliable commands + (events and operation results) from all channels. + + + + + Count of all commands currently queued as outgoing, including all channels and reliable, unreliable. + + + + + Gets / sets the number of channels available in UDP connections with Photon. + Photon Channels are only supported for UDP. + The default ChannelCount is 2. Channel IDs start with 0 and 255 is a internal channel. + + + + + While not connected, this controls if the next connection(s) should use a per-package CRC checksum. + + + While turned on, the client and server will add a CRC checksum to every sent package. + The checksum enables both sides to detect and ignore packages that were corrupted during transfer. + Corrupted packages have the same impact as lost packages: They require a re-send, adding a delay + and could lead to timeouts. + + Building the checksum has a low processing overhead but increases integrity of sent and received data. + Packages discarded due to failed CRC cecks are counted in PhotonPeer.PacketLossByCrc. + + + + + Count of packages dropped due to failed CRC checks for this connection. + + + + + + Count of packages dropped due to wrong challenge for this connection. + + + + + Count of commands that got repeated (due to local repeat-timing before an ACK was received). + + + + + The WarningSize was used test all message queues for congestion. + + + + + Number of send retries before a peer is considered lost/disconnected. Default: 7. + The initial timeout countdown of a command is calculated by the current roundTripTime + 4 * roundTripTimeVariance. + Please note that the timeout span until a command will be resent is not constant, but based on + the roundtrip time at the initial sending, which will be doubled with every failed retry. + + DisconnectTimeout and SentCountAllowance are competing settings: either might trigger a disconnect on the + client first, depending on the values and Roundtrip Time. + + + + + Sets the milliseconds without reliable command before a ping command (reliable) will be sent (Default: 1000ms). + The ping command is used to keep track of the connection in case the client does not send reliable commands + by itself. + A ping (or reliable commands) will update the RoundTripTime calculation. + + + + + Milliseconds before an individual command must be ACKed by server - after this a timeout-disconnect is triggered. + DisconnectTimeout is not an exact value for a timeout. The exact timing of the timeout depends on the frequency + of Service() calls and commands that are sent with long roundtrip-times and variance are checked less often for + re-sending! + + DisconnectTimeout and SentCountAllowance are competing settings: either might trigger a disconnect on the + client first, depending on the values and Roundtrip Time. + Default: 10000 ms. + + + + + Approximated Environment.TickCount value of server (while connected). + + + UDP: The server's timestamp is automatically fetched after connecting (once). This is done + internally by a command which is acknowledged immediately by the server. + TCP: The server's timestamp fetched with each ping but set only after connecting (once). + + The approximation will be off by +/- 10ms in most cases. Per peer/client and connection, the + offset will be constant (unless FetchServerTimestamp() is used). A constant offset should be + better to adjust for. Unfortunately there is no way to find out how much the local value + differs from the original. + + The approximation adds RoundtripTime / 2 and uses this.LocalTimeInMilliSeconds to calculate + in-between values (this property returns a new value per tick). + + The value sent by Photon equals Environment.TickCount in the logic layer. + + + 0 until connected. + While connected, the value is an approximation of the server's current timestamp. + + + + The internally used "per connection" time value, which is updated infrequently, when the library executes some connectio-related tasks. + + This integer value is an infrequently updated value by design. + The lib internally sets the value when it sends outgoing commands or reads incoming packages. + This is based on SupportClass.GetTickCount() and an initial time value per (server) connection. + This value is also used in low level Enet commands as sent time and optional logging. + + + + The last ConnectionTime value, when some ACKs were sent out by this client. + Only applicable to UDP connections. + + + The last ConnectionTime value, when SendOutgoingCommands actually checked outgoing queues to send them. Must be connected. + Available for UDP and TCP connections. + + + + Gets a local timestamp in milliseconds by calling SupportClass.GetTickCount(). + See LocalMsTimestampDelegate. + + + + + This setter for the (local-) timestamp delegate replaces the default Environment.TickCount with any equal function. + + + About Environment.TickCount: + The value of this property is derived from the system timer and is stored as a 32-bit signed integer. + Consequently, if the system runs continuously, TickCount will increment from zero to Int32..::.MaxValue + for approximately 24.9 days, then jump to Int32..::.MinValue, which is a negative number, then increment + back to zero during the next 24.9 days. + + Exception is thrown peer.PeerState is not PS_DISCONNECTED. + + + + Time until a reliable command is acknowledged by the server. + + The value measures network latency and for UDP it includes the server's ACK-delay (setting in config). + In TCP, there is no ACK-delay, so the value is slightly lower (if you use default settings for Photon). + + RoundTripTime is updated constantly. Every reliable command will contribute a fraction to this value. + + This is also the approximate time until a raised event reaches another client or until an operation + result is available. + + + + + Changes of the roundtriptime as variance value. Gives a hint about how much the time is changing. + + + + + + The server address which was used in PhotonPeer.Connect() or null (before Connect() was called). + + + The ServerAddress can only be changed for HTTP connections (to replace one that goes through a Loadbalancer with a direct URL). + + + + The protocol this peer is currently connected/connecting with (or 0). + + + This is the transport protocol to be used for next connect (see remarks). + The TransportProtocol can be changed anytime but it will not change the + currently active connection. Instead, TransportProtocol will be applied on next Connect. + + + + + Gets or sets the network simulation "enabled" setting. + Changing this value also locks this peer's sending and when setting false, + the internally used queues are executed (so setting to false can take some cycles). + + + + + Gets the settings for built-in Network Simulation for this peer instance + while IsSimulationEnabled will enable or disable them. + Once obtained, the settings can be modified by changing the properties. + + + + + Defines the initial size of an internally used StreamBuffer for Tcp. + The StreamBuffer is used to aggregate operation into (less) send calls, + which uses less resoures. + + + The size is not restricing the buffer and does not affect when poutgoing data is actually sent. + + + + + The Maximum Trasfer Unit (MTU) defines the (network-level) packet-content size that is + guaranteed to arrive at the server in one piece. The Photon Protocol uses this + size to split larger data into packets and for receive-buffers of packets. + + + This value affects the Packet-content. The resulting UDP packages will have additional + headers that also count against the package size (so it's bigger than this limit in the end) + Setting this value while being connected is not allowed and will throw an Exception. + Minimum is 576. Huge values won't speed up connections in most cases! + + + + + This property is set internally, when OpExchangeKeysForEncryption successfully finished. + While it's true, encryption can be used for operations. + + + + + While true, the peer will not send any other commands except ACKs (used in UDP connections). + + + + Implements the message-protocol, based on the underlying network protocol (udp, tcp, http). + + + + Creates a new PhotonPeer instance to communicate with Photon and selects the transport protocol. We recommend UDP. + + a IPhotonPeerListener implementation + Protocol to use to connect to Photon. + + + + Connects to a Photon server. This wraps up DNS name resolution, sending the AppId and establishing encryption. + + + This method does a DNS lookup (if necessary) and connects to the given serverAddress. + + The return value gives you feedback if the address has the correct format. If so, this + starts the process to establish the connection itself, which might take a few seconds. + + When the connection is established, a callback to IPhotonPeerListener.OnStatusChanged + will be done. If the connection can't be established, despite having a valid address, + the OnStatusChanged is called with an error-value. + + The applicationName defines the application logic to use server-side and it should match the name of + one of the apps in your server's config. + + By default, the applicationName is "LoadBalancing" but there is also the "MmoDemo". + You can setup your own application and name it any way you like. + + + Address of the Photon server. Format: ip:port (e.g. 127.0.0.1:5055) or hostname:port (e.g. localhost:5055) + + + The name of the application to use within Photon or the appId of PhotonCloud. + Should match a "Name" for an application, as setup in your PhotonServer.config. + + + true if IP is available (DNS name is resolved) and server is being connected. false on error. + + + + + Connects to a Photon server. This wraps up DNS name resolution, sending the AppId and establishing encryption. + + + This method does a DNS lookup (if necessary) and connects to the given serverAddress. + + The return value gives you feedback if the address has the correct format. If so, this + starts the process to establish the connection itself, which might take a few seconds. + + When the connection is established, a callback to IPhotonPeerListener.OnStatusChanged + will be done. If the connection can't be established, despite having a valid address, + the OnStatusChanged is called with an error-value. + + The applicationName defines the application logic to use server-side and it should match the name of + one of the apps in your server's config. + + By default, the applicationName is "LoadBalancing" but there is also the "MmoDemo". + You can setup your own application and name it any way you like. + + + Address of the Photon server. Format: ip:port (e.g. 127.0.0.1:5055) or hostname:port (e.g. localhost:5055) + + + The name of the application to use within Photon or the appId of PhotonCloud. + Should match a "Name" for an application, as setup in your PhotonServer.config. + + + Allows you to send some data, which may be used by server during peer creation + (e.g. as additional authentication info). + You can use any serializable data type of Photon. + Helpful for self-hosted solutions. Server will read this info on peer creation stage, + and may reject client without creating of peer if auth info is invalid. + + + true if IP is available (DNS name is resolved) and server is being connected. false on error. + + + + + This method initiates a mutual disconnect between this client and the server. + + + Calling this method does not immediately close a connection. Disconnect lets the server + know that this client is no longer listening. For the server, this is a much faster way + to detect that the client is gone but it requires the client to send a few final messages. + + On completion, OnStatusChanged is called with the StatusCode.Disconnect. + + If the client is disconnected already or the connection thread is stopped, then there is no callback. + + The default server logic will leave any joined game and trigger the respective event + () for the remaining players. + + + + + This method immediately closes a connection (pure client side) and ends related listening Threads. + + + Unlike Disconnect, this method will simply stop to listen to the server. Udp connections will timeout. + If the connections was open, this will trigger a callback to OnStatusChanged with code StatusCode.Disconnect. + + + + + This will fetch the server's timestamp and update the approximation for property ServerTimeInMilliseconds. + + + The server time approximation will NOT become more accurate by repeated calls. Accuracy currently depends + on a single roundtrip which is done as fast as possible. + + The command used for this is immediately acknowledged by the server. This makes sure the roundtrip time is + low and the timestamp + rountriptime / 2 is close to the original value. + + + + + This method creates a public key for this client and exchanges it with the server. + + + Encryption is not instantly available but calls OnStatusChanged when it finishes. + Check for StatusCode EncryptionEstablished and EncryptionFailedToEstablish. + + Calling this method sets IsEncryptionAvailable to false. + This method must be called before the "encrypt" parameter of OpCustom can be used. + + If operation could be enqueued for sending + + + PayloadEncryption Secret. Message payloads get encrypted with it individually and on demand. + + + + Initializes Datagram Encryption. + + secret used to chipher udp packets + secret used for authentication of udp packets + + + + This method excutes DispatchIncomingCommands and SendOutgoingCommands in your application Thread-context. + + + The Photon client libraries are designed to fit easily into a game or application. The application + is in control of the context (thread) in which incoming events and responses are executed and has + full control of the creation of UDP/TCP packages. + + Sending packages and dispatching received messages are two separate tasks. Service combines them + into one method at the cost of control. It calls DispatchIncomingCommands and SendOutgoingCommands. + + Call this method regularly (2..20 times a second). + + This will Dispatch ANY remaining buffered responses and events AND will send queued outgoing commands. + Fewer calls might be more effective if a device cannot send many packets per second, as multiple + operations might be combined into one package. + + + You could replace Service by: + + while (DispatchIncomingCommands()); //Dispatch until everything is Dispatched... + SendOutgoingCommands(); //Send a UDP/TCP package with outgoing messages + + + + + + + Creates and sends a UDP/TCP package with outgoing commands (operations and acknowledgements). Also called by Service(). + + + As the Photon library does not create any UDP/TCP packages by itself. Instead, the application + fully controls how many packages are sent and when. A tradeoff, an application will + lose connection, if it is no longer calling SendOutgoingCommands or Service. + + If multiple operations and ACKs are waiting to be sent, they will be aggregated into one + package. The package fills in this order: + ACKs for received commands + A "Ping" - only if no reliable data was sent for a while + Starting with the lowest Channel-Nr: + Reliable Commands in channel + Unreliable Commands in channel + + This gives a higher priority to lower channels. + + A longer interval between sends will lower the overhead per sent operation but + increase the internal delay (which adds "lag"). + + Call this 2..20 times per second (depending on your target platform). + + The if commands are not yet sent. Udp limits it's package size, Tcp doesnt. + + + + Dispatching received messages (commands), causes callbacks for events, responses and state changes within a IPhotonPeerListener. + + + DispatchIncomingCommands only executes a single received + command per call. If a command was dispatched, the return value is true and the method + should be called again. + + This method is called by Service() until currently available commands are dispatched. + In general, this method should be called until it returns false. In a few cases, it might + make sense to pause dispatching (if a certain state is reached and the app needs to load + data, before it should handle new events). + + The callbacks to the peer's IPhotonPeerListener are executed in the same thread that is + calling DispatchIncomingCommands. This makes things easier in a game loop: Event execution + won't clash with painting objects or the game logic. + + + + + Returns a string of the most interesting connection statistics. + When you have issues on the client side, these might contain hints about the issue's cause. + + If true, Incoming and Outgoing low-level stats are included in the string. + Stats as string. + + + + Channel-less wrapper for OpCustom(). + + Operations are handled by their byte\-typed code. + The codes of the "LoadBalancong" application are in the class . + Containing parameters as key\-value pair. The key is byte\-typed, while the value is any serializable datatype. + Selects if the operation must be acknowledged or not. If false, the + operation is not guaranteed to reach the server. + If operation could be enqueued for sending + + + + Allows the client to send any operation to the Photon Server by setting any opCode and the operation's parameters. + + + Photon can be extended with new operations which are identified by a single + byte, defined server side and known as operation code (opCode). Similarly, the operation's parameters + are defined server side as byte keys of values, which a client sends as customOpParameters + accordingly. + Operations are handled by their byte\-typed code. The codes of the + "LoadBalancing" application are in the class . + Containing parameters as key\-value pair. The key is byte\-typed, while the value is any serializable datatype. + Selects if the operation must be acknowledged or not. If false, the + operation is not guaranteed to reach the server. + The channel in which this operation should be sent. + If operation could be enqueued for sending + + + + Allows the client to send any operation to the Photon Server by setting any opCode and the operation's parameters. + + + Variant with encryption parameter. + + Use this only after encryption was established by EstablishEncryption and waiting for the OnStateChanged callback. + + Operations are handled by their byte\-typed code. The codes of the + "LoadBalancing" application are in the class . + Containing parameters as key\-value pair. The key is byte\-typed, while the value is any serializable datatype. + Selects if the operation must be acknowledged or not. If false, the + operation is not guaranteed to reach the server. + The channel in which this operation should be sent. + Can only be true, while IsEncryptionAvailable is true, too. + If operation could be enqueued for sending + + + + Allows the client to send any operation to the Photon Server by setting any opCode and the operation's parameters. + + + Variant with an OperationRequest object. + + This variant offers an alternative way to describe a operation request. Operation code and it's parameters + are wrapped up in a object. Still, the parameters are a Dictionary. + + The operation to call on Photon. + Use unreliable (false) if the call might get lost (when it's content is soon outdated). + Defines the sequence of requests this operation belongs to. + Encrypt request before sending. Depends on IsEncryptionAvailable. + If operation could be enqueued for sending + + + + Registers new types/classes for de/serialization and the fitting methods to call for this type. + + + SerializeMethod and DeserializeMethod are complementary: Feed the product of serializeMethod to + the constructor, to get a comparable instance of the object. + + After registering a Type, it can be used in events and operations and will be serialized like + built-in types. + + Type (class) to register. + A byte-code used as shortcut during transfer of this Type. + Method delegate to create a byte[] from a customType instance. + Method delegate to create instances of customType's from byte[]. + If the Type was registered successfully. + + + Param code. Used in internal op: InitEncryption. + + + Encryption-Mode code. Used in internal op: InitEncryption. + + + Param code. Used in internal op: InitEncryption. + + + Code of internal op: InitEncryption. + + + TODO: Code of internal op: Ping (used in PUN binary websockets). + + + Result code for any (internal) operation. + + + The server's address, as set by a Connect() call, including any protocol, ports and or path. + If rHTTP is used, this can be set directly. + + + Byte count of last sent operation (set during serialization). + + + Byte count of last dispatched message (set during dispatch/deserialization). + + + The command that's currently being dispatched. + + + EnetPeer will set this value, so trafficstats can use it. TCP has 0 bytes per package extra + + + See PhotonPeer value. + + + See PhotonPeer value. + + + See PhotonPeer value. + + + See PhotonPeer value. + + + This ID is assigned by the Realtime Server upon connection. + The application does not have to care about this, but it is useful in debugging. + + + + This is the (low level) connection state of the peer. It's internal and based on eNet's states. + + Applications can read the "high level" state as PhotonPeer.PeerState, which uses a different enum. + + + + The serverTimeOffset is serverTimestamp - localTime. Used to approximate the serverTimestamp with help of localTime + + + + + Gets the currently used settings for the built-in network simulation. + Please check the description of NetworkSimulationSet for more details. + + + + Size of CommandLog. Default is 0, no logging. + + + Log of sent reliable commands and incoming ACKs. + + + Log of incoming reliable commands, used to track which commands from the server this client got. Part of the PhotonPeer.CommandLogToString() result. + + + Reduce CommandLog to CommandLogSize. Oldest entries get discarded. + + + Initializes the CommandLog and InReliableLog according to CommandLogSize. A value of 0 will set both logs to 0. + + + Converts the CommandLog into a readable table-like string with summary. + + + + Count of all bytes going out (including headers) + + + + + Count of all bytes coming in (including headers) + + + + Set via Connect(..., customObject) and sent in Init-Request. + + + Temporary cache of AppId. Used in Connect() to keep the AppId until we send the Init-Request (after the network-level (and Enet) connect). + + + + This is the replacement for the const values used in eNet like: PS_DISCONNECTED, PS_CONNECTED, etc. + + + + No connection is available. Use connect. + + + Establishing a connection already. The app should wait for a status callback. + + + + The low level connection with Photon is established. On connect, the library will automatically + send an Init package to select the application it connects to (see also PhotonPeer.Connect()). + When the Init is done, IPhotonPeerListener.OnStatusChanged() is called with connect. + + Please note that calling operations is only possible after the OnStatusChanged() with StatusCode.Connect. + + + Connection going to be ended. Wait for status callback. + + + Acknowledging a disconnect from Photon. Wait for status callback. + + + Connection not properly disconnected. + + + Set to timeInt, whenever SendOutgoingCommands actually checks outgoing queues to send them. Must be connected. + + + Connect to server and send Init (which inlcudes the appId). + If customData is not null, the new init will be used (http-based). + + + If IPhotonSocket.Connected is true, this value shows if the server's address resolved as IPv6 address. + + You must check the socket's IsConnected state. Otherwise, this value is not initialized. + Sent to server in Init-Request. + + + + Must be called by a IPhotonSocket when it connected to set IsIpv6. + The new value of IsIpv6. + + + + + + + + + + + + Checks the incoming queue and Dispatches received data if possible. + + If a Dispatch happened or not, which shows if more Dispatches might be needed. + + + + Checks outgoing queues for commands to send and puts them on their way. + This creates one package per go in UDP. + + If commands are not sent, cause they didn't fit into the package that's sent. + + + Returns the UDP Payload starting with Magic Number for binary protocol + + + Maximum Transfer Unit to be used for UDP+TCP + + + (default=2) Rhttp: minimum number of open connections + + + (default=6) Rhttp: maximum number of open connections, should be > rhttpMinConnections + + + + Internally uses an operation to exchange encryption keys with the server. + + If the op could be sent. + + + + Core of the Network Simulation, which is available in Debug builds. + Called by a timer in intervals. + + + + One list for all channels keeps sent commands (for re-sending). + + + One pool of ACK byte arrays ( 20 bytes each) for all channels to keep acknowledgements. + + + Gets enabled by "request" from server (not by client). + + + Initial PeerId as used in Connect command. If EnableServerTracing is false. + + + Initial PeerId to enable Photon Tracing, as used in Connect command. See: EnableServerTracing. + + + + Checks the incoming queue and Dispatches received data if possible. + + If a Dispatch happened or not, which shows if more Dispatches might be needed. + + + + gathers acks until udp-packet is full and sends it! + + + + + gathers commands from all (out)queues until udp-packet is full and sends it! + + + + + Checks if any channel has a outgoing reliable command. + + True if any channel has a outgoing reliable command. False otherwise. + + + + Checks connected state and channel before operation is serialized and enqueued for sending. + + operation parameters + code of operation + send as reliable command + channel (sequence) for command + encrypt or not + usually EgMessageType.Operation + if operation could be enqueued + + + reliable-udp-level function to send some byte[] to the server via un/reliable command + only called when a custom operation should be send + (enet) command type + data to carry (operation) + channel in which to send + the invocation ID for this operation (the payload) + + + Serializes an operation into our binary messages (magic number, msg-type byte and message). Optionally encrypts. + This method is mostly the same in EnetPeer, TPeer and HttpPeerBase. Also, for raw messages, we have another variant. + + + reads incoming udp-packages to create and queue incoming commands* + + + queues incoming commands in the correct order as either unreliable, reliable or unsequenced. return value determines if the command is queued / done. + + + removes commands which are acknowledged* + + + Internal class for "commands" - the package in which operations are sent. + + + this variant does only create outgoing commands and increments . incoming ones are created from a DataInputStream + + + + ACKs should never be created as NCommand. use CreateACK to wrtie the serialized ACK right away... + + + + + + + + + reads the command values (commandHeader and command-values) from incoming bytestream and populates the incoming command* + + + TCP "Package" header: 7 bytes + + + TCP "Message" header: 2 bytes + + + TCP header combined: 9 bytes + + + Defines if the (TCP) socket implementation needs to do "framing". + The WebSocket protocol (e.g.) includes framing, so when that is used, we set DoFraming to false. + + + + Checks the incoming queue and Dispatches received data if possible. Returns if a Dispatch happened or + not, which shows if more Dispatches might be needed. + + + + + gathers commands from all (out)queues until udp-packet is full and sends it! + + + + Sends a ping in intervals to keep connection alive (server will timeout connection if nothing is sent). + Always false in this case (local queues are ignored. true would be: "call again to send remaining data"). + + + Serializes an operation into our binary messages (magic number, msg-type byte and message). Optionally encrypts. + This method is mostly the same in EnetPeer, TPeer and HttpPeerBase. Also, for raw messages, we have another variant. + + + enqueues serialized operations to be sent as tcp stream / package + + + Sends a ping and modifies this.lastPingResult to avoid another ping for a while. + + + reads incoming tcp-packages to create and queue incoming commands* + + + + Serialize creates a byte-array from the given object and returns it. + + The object to serialize + The serialized byte-array + + + + Deserialize returns an object reassembled from the given StreamBuffer. + + The buffer to be Deserialized + The Deserialized object + + + + Deserialize returns an object reassembled from the given byte-array. + + The byte-array to be Deserialized + The Deserialized object + + + + Container for an Operation request, which is a code and parameters. + + + On the lowest level, Photon only allows byte-typed keys for operation parameters. + The values of each such parameter can be any serializable datatype: byte, int, hashtable and many more. + + + + Byte-typed code for an operation - the short identifier for the server's method to call. + + + The parameters of the operation - each identified by a byte-typed code in Photon. + + + + Contains the server's response for an operation called by this peer. + The indexer of this class actually provides access to the Parameters Dictionary. + + + The OperationCode defines the type of operation called on Photon and in turn also the Parameters that + are set in the request. Those are provided as Dictionary with byte-keys. + There are pre-defined constants for various codes defined in the LoadBalancing application. + Check: OperationCode, ParameterCode, etc. + + An operation's request is summarized by the ReturnCode: a short typed code for "Ok" or + some different result. The code's meaning is specific per operation. An optional DebugMessage can be + provided to simplify debugging. + + Each call of an operation gets an ID, called the "invocID". This can be matched to the IDs + returned with any operation calls. This way, an application could track if a certain OpRaiseEvent + call was successful. + + + + The code for the operation called initially (by this peer). + Use enums or constants to be able to handle those codes, like OperationCode does. + + + A code that "summarizes" the operation's success or failure. Specific per operation. 0 usually means "ok". + + + An optional string sent by the server to provide readable feedback in error-cases. Might be null. + + + A Dictionary of values returned by an operation, using byte-typed keys per value. + + + + Alternative access to the Parameters, which wraps up a TryGetValue() call on the Parameters Dictionary. + + The byte-code of a returned value. + The value returned by the server, or null if the key does not exist in Parameters. + + + ToString() override. + Relatively short output of OpCode and returnCode. + + + Extensive output of operation results. + To be used in debug situations only, as it returns a string for each value. + + + + Contains all components of a Photon Event. + Event Parameters, like OperationRequests and OperationResults, consist of a Dictionary with byte-typed keys per value. + + + The indexer of this class provides access to the Parameters Dictionary. + + The operation RaiseEvent allows you to provide custom event content. Defined in LoadBalancing, this + CustomContent will be made the value of key ParameterCode.CustomEventContent. + + + + The event code identifies the type of event. + + + The Parameters of an event is a Dictionary<byte, object>. + + + + Alternative access to the Parameters. + + The key byte-code of a event value. + The Parameters value, or null if the key does not exist in Parameters. + + + ToString() override. + Short output of "Event" and it's Code. + + + Extensive output of the event content. + To be used in debug situations only, as it returns a string for each value. + + + + Type of serialization methods to add custom type support. + Use PhotonPeer.ReisterType() to register new types with serialization and deserialization methods. + + The method will get objects passed that were registered with it in RegisterType(). + Return a byte[] that resembles the object passed in. The framework will surround it with length and type info, so don't include it. + + + + Type of deserialization methods to add custom type support. + Use PhotonPeer.RegisterType() to register new types with serialization and deserialization methods. + + The framwork passes in the data it got by the associated SerializeMethod. The type code and length are stripped and applied before a DeserializeMethod is called. + Return a object of the type that was associated with this method through RegisterType(). + + + + Provides tools for the Exit Games Protocol + + + + + Serialize creates a byte-array from the given object and returns it. + + The object to serialize + The serialized byte-array + + + + Deserialize returns an object reassembled from the given byte-array. + + The byte-array to be Deserialized + The Deserialized object + + + + Serializes a short typed value into a byte-array (target) starting at the also given targetOffset. + The altered offset is known to the caller, because it is given via a referenced parameter. + + The short value to be serialized + The byte-array to serialize the short to + The offset in the byte-array + + + + Serializes an int typed value into a byte-array (target) starting at the also given targetOffset. + The altered offset is known to the caller, because it is given via a referenced parameter. + + The int value to be serialized + The byte-array to serialize the short to + The offset in the byte-array + + + + Serializes an float typed value into a byte-array (target) starting at the also given targetOffset. + The altered offset is known to the caller, because it is given via a referenced parameter. + + The float value to be serialized + The byte-array to serialize the short to + The offset in the byte-array + + + + Deserialize fills the given int typed value with the given byte-array (source) starting at the also given offset. + The result is placed in a variable (value). There is no need to return a value because the parameter value is given by reference. + The altered offset is this way also known to the caller. + + The int value to deserialize into + The byte-array to deserialize from + The offset in the byte-array + + + + Deserialize fills the given short typed value with the given byte-array (source) starting at the also given offset. + The result is placed in a variable (value). There is no need to return a value because the parameter value is given by reference. + The altered offset is this way also known to the caller. + + The short value to deserialized into + The byte-array to deserialize from + The offset in the byte-array + + + + Deserialize fills the given float typed value with the given byte-array (source) starting at the also given offset. + The result is placed in a variable (value). There is no need to return a value because the parameter value is given by reference. + The altered offset is this way also known to the caller. + + The float value to deserialize + The byte-array to deserialize from + The offset in the byte-array + + + + Exit Games GpBinaryV16 protocol implementation + + + + + The gp type. + + + + + Unkown type. + + + + + An array of objects. + + + This type is new in version 1.5. + + + + + A boolean Value. + + + + + A byte value. + + + + + An array of bytes. + + + + + An array of objects. + + + + + A 16-bit integer value. + + + + + A 32-bit floating-point value. + + + This type is new in version 1.5. + + + + + A dictionary + + + This type is new in version 1.6. + + + + + A 64-bit floating-point value. + + + This type is new in version 1.5. + + + + + A Hashtable. + + + + + A 32-bit integer value. + + + + + An array of 32-bit integer values. + + + + + A 64-bit integer value. + + + + + A string value. + + + + + An array of string values. + + + + + A custom type. 0x63 + + + + + Null value don't have types. + + + + + Calls the correct serialization method for the passed object. + + + + + DeserializeInteger returns an Integer typed value from the given stream. + + + + Uses C# Socket class from System.Net.Sockets (as Unity usually does). + Incompatible with Windows 8 Store/Phone API. + + + + Sends a "Photon Ping" to a server. + + Address in IPv4 or IPv6 format. An address containing a '.' will be interpretet as IPv4. + True if the Photon Ping could be sent. + + + The protocol for this socket, defined in constructor. + + + Address, as defined via a Connect() call. Including protocol, port and or path. + + + Contains only the server's hostname (stripped protocol, port and or path). Set in IphotonSocket.Connect(). + + + Contains only the server's port address (as string). Set in IphotonSocket.Connect(). + + + Where available, this exposes if the server's address was resolved into an IPv6 address or not. + + + + Separates the given address into address (host name or IP) and port. Port must be included after colon! + + + This method expects any address to include a port. The final ':' in addressAndPort has to separate it. + IPv6 addresses have multiple colons and must use brackets to separate address from port. + + Examples: + ns.exitgames.com:5058 + http://[2001:db8:1f70::999:de8:7648:6e8]:100/ + [2001:db8:1f70::999:de8:7648:6e8]:100 + See: + http://serverfault.com/questions/205793/how-can-one-distinguish-the-host-and-the-port-in-an-ipv6-url + + + + Implements a (very) simple test if a (valid) IPAddress is IPv6 by testing for colons (:). + The reason we use this, is that some DotNet platforms don't provide (or allow usage of) the System.Net namespace. + A valid IPAddress or null. + If the IPAddress.ToString() contains a colon (which means it's IPv6). + + + + Returns null or the IPAddress representing the address, doing Dns resolution if needed. + + Only returns IPv4 or IPv6 adresses, no others. + The string address of a server (hostname or IP). + IPAddress for the string address or null, if the address is neither IPv4, IPv6 or some hostname that could be resolved. + + + Internal class to encapsulate the network i/o functionality for the realtime libary. + + + used by PhotonPeer* + + + Endless loop, run in Receive Thread. + + + + Internal class to encapsulate the network i/o functionality for the realtime libary. + + + + + used by TPeer* + + + + + A simulation item is an action that can be queued to simulate network lag. + + + + With this, the actual delay can be measured, compared to the intended lag. + + + Timestamp after which this item must be executed. + + + Action to execute when the lag-time passed. + + + Starts a new Stopwatch + + + + A set of network simulation settings, enabled (and disabled) by PhotonPeer.IsSimulationEnabled. + + + For performance reasons, the lag and jitter settings can't be produced exactly. + In some cases, the resulting lag will be up to 20ms bigger than the lag settings. + Even if all settings are 0, simulation will be used. Set PhotonPeer.IsSimulationEnabled + to false to disable it if no longer needed. + + All lag, jitter and loss is additional to the current, real network conditions. + If the network is slow in reality, this will add even more lag. + The jitter values will affect the lag positive and negative, so the lag settings + describe the medium lag even with jitter. The jitter influence is: [-jitter..+jitter]. + Packets "lost" due to OutgoingLossPercentage count for BytesOut and LostPackagesOut. + Packets "lost" due to IncomingLossPercentage count for BytesIn and LostPackagesIn. + + + + internal + + + internal + + + internal + + + internal + + + internal + + + internal + + + internal + + + This setting overrides all other settings and turns simulation on/off. Default: false. + + + Outgoing packages delay in ms. Default: 100. + + + Randomizes OutgoingLag by [-OutgoingJitter..+OutgoingJitter]. Default: 0. + + + Percentage of outgoing packets that should be lost. Between 0..100. Default: 1. TCP ignores this setting. + + + Incoming packages delay in ms. Default: 100. + + + Randomizes IncomingLag by [-IncomingJitter..+IncomingJitter]. Default: 0. + + + Percentage of incoming packets that should be lost. Between 0..100. Default: 1. TCP ignores this setting. + + + Counts how many outgoing packages actually got lost. TCP connections ignore loss and this stays 0. + + + Counts how many incoming packages actually got lost. TCP connections ignore loss and this stays 0. + + + + Only in use as long as PhotonPeer.TrafficStatsEnabled = true; + + + + Gets sum of outgoing operations in bytes. + + + Gets count of outgoing operations. + + + Gets sum of byte-cost of incoming operation-results. + + + Gets count of incoming operation-results. + + + Gets sum of byte-cost of incoming events. + + + Gets count of incoming events. + + + + Gets longest time it took to complete a call to OnOperationResponse (in your code). + If such a callback takes long, it will lower the network performance and might lead to timeouts. + + + + Gets OperationCode that causes the LongestOpResponseCallback. See that description. + + + + Gets longest time a call to OnEvent (in your code) took. + If such a callback takes long, it will lower the network performance and might lead to timeouts. + + + + Gets EventCode that caused the LongestEventCallback. See that description. + + + + Gets longest time between subsequent calls to DispatchIncomgingCommands in milliseconds. + Note: This is not a crucial timing for the networking. Long gaps just add "local lag" to events that are available already. + + + + + Gets longest time between subsequent calls to SendOutgoingCommands in milliseconds. + Note: This is a crucial value for network stability. Without calling SendOutgoingCommands, + nothing will be sent to the server, who might time out this client. + + + + + Gets number of calls of DispatchIncomingCommands. + + + + + Gets number of calls of DispatchIncomingCommands. + + + + + Gets number of calls of SendOutgoingCommands. + + + + Gets sum of byte-cost of all "logic level" messages. + + + Gets sum of counted "logic level" messages. + + + Gets sum of byte-cost of all incoming "logic level" messages. + + + Gets sum of counted incoming "logic level" messages. + + + Gets sum of byte-cost of all outgoing "logic level" messages (= OperationByteCount). + + + Gets sum of counted outgoing "logic level" messages (= OperationCount). + + + + Resets the values that can be maxed out, like LongestDeltaBetweenDispatching. See remarks. + + + Set to 0: LongestDeltaBetweenDispatching, LongestDeltaBetweenSending, LongestEventCallback, LongestEventCallbackCode, LongestOpResponseCallback, LongestOpResponseCallbackOpCode. + Also resets internal values: timeOfLastDispatchCall and timeOfLastSendCall (so intervals are tracked correctly). + + + + Gets the byte-size of per-package headers. + + + + Counts commands created/received by this client, ignoring repeats (out command count can be higher due to repeats). + + + + Gets count of bytes as traffic, excluding UDP/TCP headers (42 bytes / x bytes). + + + Timestamp of the last incoming ACK that has been read (every PhotonPeer.TimePingInterval milliseconds this client sends a PING which must be ACKd). + + + Timestamp of last incoming reliable command (every second we expect a PING). + + + + Provides classical Diffie-Hellman Modular Exponentiation Groups defined by the + OAKLEY Key Determination Protocol (RFC 2412). + + + + + Gets the genrator (N) used by the the well known groups 1,2 and 5. + + + + + Gets the 768 bit prime for the well known group 1. + + + + + Gets the 1024 bit prime for the well known group 2. + + + + + Gets the 1536 bit prime for the well known group 5. + + + + + Initializes a new instance of the class. + + + + + Gets the public key that can be used by another DiffieHellmanCryptoProvider object + to generate a shared secret agreement. + + + + + Derives the shared key is generated from the secret agreement between two parties, + given a byte array that contains the second party's public key. + + + The second party's public key. + + +
+
diff --git a/Assets/Runtime/Photon/Plugins/Photon3Unity3D.xml.meta b/Assets/Runtime/Photon/Plugins/Photon3Unity3D.xml.meta new file mode 100644 index 0000000..cc186c8 --- /dev/null +++ b/Assets/Runtime/Photon/Plugins/Photon3Unity3D.xml.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 16bfdffb4c4f75240b7dd4720c995413 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Photon/Plugins/PhotonWebSocket.meta b/Assets/Runtime/Photon/Plugins/PhotonWebSocket.meta new file mode 100644 index 0000000..726d304 --- /dev/null +++ b/Assets/Runtime/Photon/Plugins/PhotonWebSocket.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2139bd8d6da096942a30073374f7f0ab +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Photon/Plugins/PhotonWebSocket/PingHttp.cs b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/PingHttp.cs new file mode 100644 index 0000000..f7fe610 --- /dev/null +++ b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/PingHttp.cs @@ -0,0 +1,37 @@ +#if UNITY_WEBGL + +namespace ExitGames.Client.Photon +{ + using UnityEngine; + + + public class PingHttp : PhotonPing + { + private WWW webRequest; + + public override bool StartPing(string address) + { + address = "https://" + address + "/photon/m/?ping&r=" + UnityEngine.Random.Range(0, 10000); + Debug.Log("StartPing: " + address); + this.webRequest = new WWW(address); + return true; + } + + public override bool Done() + { + if (this.webRequest.isDone) + { + Successful = true; + return true; + } + + return false; + } + + public override void Dispose() + { + this.webRequest.Dispose(); + } + } +} +#endif diff --git a/Assets/Runtime/Photon/Plugins/PhotonWebSocket/PingHttp.cs.meta b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/PingHttp.cs.meta new file mode 100644 index 0000000..53a73eb --- /dev/null +++ b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/PingHttp.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 88e56dcdd2ef1f942b8f19c74c4cf805 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Photon/Plugins/PhotonWebSocket/Readme-Photon-WebSocket.txt b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/Readme-Photon-WebSocket.txt new file mode 100644 index 0000000..10f9527 --- /dev/null +++ b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/Readme-Photon-WebSocket.txt @@ -0,0 +1,22 @@ +To use WebSockets with the Photon C# library, you need to import this folder into your project. + + SocketWebTcpThread can be used in all cases where the Thread class is available + SocketWebTcpCoroutine must be used for WebGL exports and when the Thread class is unavailable + + WebSocket.cs is used in all exports + websocket-sharp.dll is used when not exporting to a browser (and in Unity Editor) + WebSocket.jslib is used for WebGL exports by Unity (and must be setup accordingly) + + +A WebGL export from Unity will find and use these files internally. +Any other project will have to setup a few things in code: + + Define "WEBSOCKET" for your project to make the SocketWebTcp classes available. + To make a connection by WebSocket, setup the PhotonPeer (LoadBalancingPeer, ChatPeer, etc) similar to this: + + Debug.Log("WSS Setup"); + PhotonPeer.TransportProtocol = ConnectionProtocol.WebSocket; // or WebSocketSecure for a release + PhotonPeer.SocketImplementationConfig[ConnectionProtocol.WebSocket] = typeof(SocketWebTcpThread); + PhotonPeer.SocketImplementationConfig[ConnectionProtocol.WebSocketSecure] = typeof(SocketWebTcpThread); + + //PhotonPeer.DebugOut = DebugLevel.INFO; // this would show some logs from the SocketWebTcp implementation \ No newline at end of file diff --git a/Assets/Runtime/Photon/Plugins/PhotonWebSocket/Readme-Photon-WebSocket.txt.meta b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/Readme-Photon-WebSocket.txt.meta new file mode 100644 index 0000000..8bec7bf --- /dev/null +++ b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/Readme-Photon-WebSocket.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 81339ade5de3e2d419b360cfde8630c1 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Photon/Plugins/PhotonWebSocket/SocketWebTcpCoroutine.cs b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/SocketWebTcpCoroutine.cs new file mode 100644 index 0000000..f176b8e --- /dev/null +++ b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/SocketWebTcpCoroutine.cs @@ -0,0 +1,312 @@ +#if UNITY_WEBGL || UNITY_XBOXONE || WEBSOCKET +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Exit Games GmbH. All rights reserved. +// +// +// Internal class to encapsulate the network i/o functionality for the realtime libary. +// +// developer@exitgames.com +// -------------------------------------------------------------------------------------------------------------------- + +using System; +using System.Collections; +using UnityEngine; +using SupportClassPun = ExitGames.Client.Photon.SupportClass; + + +namespace ExitGames.Client.Photon +{ + #if UNITY_5_3 || UNITY_5_3_OR_NEWER + /// + /// Yield Instruction to Wait for real seconds. Very important to keep connection working if Time.TimeScale is altered, we still want accurate network events + /// + public sealed class WaitForRealSeconds : CustomYieldInstruction + { + private readonly float _endTime; + + public override bool keepWaiting + { + get { return _endTime > Time.realtimeSinceStartup; } + } + + public WaitForRealSeconds(float seconds) + { + _endTime = Time.realtimeSinceStartup + seconds; + } + } + #endif + + /// + /// Internal class to encapsulate the network i/o functionality for the realtime libary. + /// + public class SocketWebTcpCoroutine : IPhotonSocket, IDisposable + { + private WebSocket sock; + + private GameObject websocketConnectionObject; + + /// Constructor. Checks if "expected" protocol matches. + public SocketWebTcpCoroutine(PeerBase npeer) : base(npeer) + { + if (this.ReportDebugOfLevel(DebugLevel.INFO)) + { + this.Listener.DebugReturn(DebugLevel.INFO, "new SocketWebTcpCoroutine(). Server: " + this.ConnectAddress + " protocol: " + this.Protocol); + } + + switch (this.Protocol) + { + case ConnectionProtocol.WebSocket: + break; + case ConnectionProtocol.WebSocketSecure: + break; + default: + throw new Exception("Protocol '" + this.Protocol + "' not supported by WebSocket"); + } + + this.PollReceive = false; + } + + /// Connect the websocket (base checks if this was already connected). + public override bool Connect() + { + bool baseOk = base.Connect(); + if (!baseOk) + { + return false; + } + + this.State = PhotonSocketState.Connecting; + + + if (this.websocketConnectionObject != null) + { + UnityEngine.Object.Destroy(this.websocketConnectionObject); + } + + this.websocketConnectionObject = new GameObject("websocketConnectionObject"); + MonoBehaviour mb = this.websocketConnectionObject.AddComponent(); + this.websocketConnectionObject.hideFlags = HideFlags.HideInHierarchy; + UnityEngine.Object.DontDestroyOnLoad(this.websocketConnectionObject); + + + this.sock = new WebSocket(new Uri(this.ConnectAddress)); + // connecting the socket is off-loaded into the coroutine which we start now + + mb.StartCoroutine(this.ReceiveLoop()); + return true; + } + + + /// Disconnect the websocket (no matter what it does right now). + public override bool Disconnect() + { + if (this.State == PhotonSocketState.Disconnecting || this.State == PhotonSocketState.Disconnected) + { + return false; + } + + if (this.ReportDebugOfLevel(DebugLevel.INFO)) + { + this.Listener.DebugReturn(DebugLevel.INFO, "SocketWebTcpCoroutine.Disconnect()"); + } + + this.State = PhotonSocketState.Disconnecting; + if (this.sock != null) + { + try + { + this.sock.Close(); + } + catch + { + } + this.sock = null; + } + + if (this.websocketConnectionObject != null) + { + UnityEngine.Object.Destroy(this.websocketConnectionObject); + } + + this.State = PhotonSocketState.Disconnected; + return true; + } + + /// Calls Disconnect. + public void Dispose() + { + this.Disconnect(); + } + + + /// Used by TPeer to send. + public override PhotonSocketError Send(byte[] data, int length) + { + if (this.State != PhotonSocketState.Connected) + { + return PhotonSocketError.Skipped; + } + + try + { + if (this.ReportDebugOfLevel(DebugLevel.ALL)) + { + this.Listener.DebugReturn(DebugLevel.ALL, "Sending: " + SupportClassPun.ByteArrayToString(data)); + } + + this.sock.Send(data); + } + catch (Exception e) + { + this.Listener.DebugReturn(DebugLevel.ERROR, "Cannot send to: " + this.ConnectAddress + ". " + e.Message); + + if (this.State == PhotonSocketState.Connected) + { + this.HandleException(StatusCode.Exception); + } + return PhotonSocketError.Exception; + } + + return PhotonSocketError.Success; + } + + + /// Not used currently. + public override PhotonSocketError Receive(out byte[] data) + { + data = null; + return PhotonSocketError.NoData; + } + + /// Used by TPeer to receive. + public IEnumerator ReceiveLoop() + { + try + { + this.sock.Connect(); + } + catch (Exception e) + { + if (this.State != PhotonSocketState.Disconnecting && this.State != PhotonSocketState.Disconnected) + { + if (this.ReportDebugOfLevel(DebugLevel.ERROR)) + { + this.EnqueueDebugReturn(DebugLevel.ERROR, "Receive issue. State: " + this.State + ". Server: '" + this.ConnectAddress + "' Exception: " + e); + } + + this.HandleException(StatusCode.ExceptionOnReceive); + } + } + + while (this.State == PhotonSocketState.Connecting && this.sock != null && !this.sock.Connected && this.sock.Error == null) + { + #if UNITY_5_3 || UNITY_5_3_OR_NEWER + yield return new WaitForRealSeconds(0.02f); + #else + float waittime = Time.realtimeSinceStartup + 0.2f; + while (Time.realtimeSinceStartup < waittime) yield return 0; + #endif + } + + if (this.sock == null || this.sock.Error != null) + { + if (this.State != PhotonSocketState.Disconnecting && this.State != PhotonSocketState.Disconnected) + { + this.Listener.DebugReturn(DebugLevel.ERROR, "Exiting receive thread. Server: " + this.ConnectAddress + " Error: " + ((this.sock!=null)?this.sock.Error:"socket==null")); + this.HandleException(StatusCode.ExceptionOnConnect); + } + yield break; + } + + // connected + this.State = PhotonSocketState.Connected; + this.peerBase.OnConnect(); + + + byte[] inBuff = null; + + // receiving + while (this.State == PhotonSocketState.Connected) + { + try + { + if (this.sock.Error != null) + { + if (this.State != PhotonSocketState.Disconnecting && this.State != PhotonSocketState.Disconnected) + { + this.Listener.DebugReturn(DebugLevel.ERROR, "Exiting receive thread (inside loop). Server: " + this.ConnectAddress + " Error: " + this.sock.Error); + this.HandleException(StatusCode.ExceptionOnReceive); + } + break; + } + + inBuff = this.sock.Recv(); + } + catch (Exception e) + { + if (this.State != PhotonSocketState.Disconnecting && this.State != PhotonSocketState.Disconnected) + { + if (this.ReportDebugOfLevel(DebugLevel.ERROR)) + { + this.EnqueueDebugReturn(DebugLevel.ERROR, "Receive issue. State: " + this.State + ". Server: '" + this.ConnectAddress + "' Exception: " + e); + } + + this.HandleException(StatusCode.ExceptionOnReceive); + } + } + + + if (inBuff == null || inBuff.Length == 0) + { + // nothing received. wait a bit, try again + #if UNITY_5_3 || UNITY_5_3_OR_NEWER + yield return new WaitForRealSeconds(0.02f); + #else + float waittime = Time.realtimeSinceStartup + 0.02f; + while (Time.realtimeSinceStartup < waittime) yield return 0; + #endif + continue; + } + if (inBuff.Length < 0) + { + // got disconnected (from remote or net) + if (this.State != PhotonSocketState.Disconnecting && this.State != PhotonSocketState.Disconnected) + { + this.HandleException(StatusCode.DisconnectByServer); + } + break; + } + + try + { + if (this.ReportDebugOfLevel(DebugLevel.ALL)) + { + this.Listener.DebugReturn(DebugLevel.ALL, "TCP << " + inBuff.Length + " = " + SupportClassPun.ByteArrayToString(inBuff)); + } + + this.HandleReceivedDatagram(inBuff, inBuff.Length, false); + } + catch (Exception e) + { + if (this.State != PhotonSocketState.Disconnecting && this.State != PhotonSocketState.Disconnected) + { + if (this.ReportDebugOfLevel(DebugLevel.ERROR)) + { + this.EnqueueDebugReturn(DebugLevel.ERROR, "Receive issue. State: " + this.State + ". Server: '" + this.ConnectAddress + "' Exception: " + e); + } + + this.HandleException(StatusCode.ExceptionOnReceive); + } + } + } + + + this.Disconnect(); + } + } + + internal class MonoBehaviourExt : MonoBehaviour { } +} + +#endif diff --git a/Assets/Runtime/Photon/Plugins/PhotonWebSocket/SocketWebTcpCoroutine.cs.meta b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/SocketWebTcpCoroutine.cs.meta new file mode 100644 index 0000000..0ad1a17 --- /dev/null +++ b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/SocketWebTcpCoroutine.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 02e6576dec44f344eb5d58b9dfe5bdc0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Photon/Plugins/PhotonWebSocket/SocketWebTcpThread.cs b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/SocketWebTcpThread.cs new file mode 100644 index 0000000..b3d75b4 --- /dev/null +++ b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/SocketWebTcpThread.cs @@ -0,0 +1,268 @@ +#if WEBSOCKET +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Exit Games GmbH. All rights reserved. +// +// +// Internal class to encapsulate the network i/o functionality for the realtime libary. +// +// developer@photonengine.com +// -------------------------------------------------------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Net; +using System.Net.Sockets; +using System.Security; +using System.Threading; + + +namespace ExitGames.Client.Photon +{ + /// + /// Internal class to encapsulate the network i/o functionality for the realtime libary. + /// + public class SocketWebTcpThread : IPhotonSocket, IDisposable + { + private WebSocket sock; + + + /// Constructor. Checks if "expected" protocol matches. + public SocketWebTcpThread(PeerBase npeer) : base(npeer) + { + if (this.ReportDebugOfLevel(DebugLevel.INFO)) + { + this.EnqueueDebugReturn(DebugLevel.INFO, "new SocketWebTcpThread(). Server: " + this.ConnectAddress + " protocol: " + this.Protocol+ " State: " + this.State); + } + + switch (this.Protocol) + { + case ConnectionProtocol.WebSocket: + break; + case ConnectionProtocol.WebSocketSecure: + break; + default: + throw new Exception("Protocol '" + this.Protocol + "' not supported by WebSocket"); + } + + this.PollReceive = false; + } + + + /// Connect the websocket (base checks if this was already connected). + public override bool Connect() + { + bool baseOk = base.Connect(); + if (!baseOk) + { + return false; + } + + this.State = PhotonSocketState.Connecting; + + Thread dns = new Thread(this.DnsAndConnect); + dns.Name = "photon dns thread"; + dns.IsBackground = true; + dns.Start(); + + return true; + } + + /// Internally used by this class to resolve the hostname to IP. + internal void DnsAndConnect() + { + try + { + IPAddress ipAddress = IPhotonSocket.GetIpAddress(this.ServerAddress); + if (ipAddress == null) + { + throw new ArgumentException("DNS failed to resolve for address: " + this.ServerAddress); + } + + this.AddressResolvedAsIpv6 = ipAddress.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6; + + + if (this.State != PhotonSocketState.Connecting) + { + return; + } + this.sock = new WebSocket(new Uri(this.ConnectAddress)); + this.sock.Connect(); + + while (this.sock != null && !this.sock.Connected && this.sock.Error == null) + { + Thread.Sleep(0); + } + + if (this.sock.Error != null) + { + this.EnqueueDebugReturn(DebugLevel.ERROR, "Exiting receive thread. Server: " + this.ConnectAddress + " Error: " + this.sock.Error); + this.HandleException(StatusCode.ExceptionOnConnect); + return; + } + + this.State = PhotonSocketState.Connected; + this.peerBase.OnConnect(); + } + catch (SecurityException se) + { + if (this.ReportDebugOfLevel(DebugLevel.ERROR)) + { + this.Listener.DebugReturn(DebugLevel.ERROR, "Connect() to '" + this.ConnectAddress + "' failed: " + se.ToString()); + } + + this.HandleException(StatusCode.SecurityExceptionOnConnect); + return; + } + catch (Exception se) + { + if (this.ReportDebugOfLevel(DebugLevel.ERROR)) + { + this.Listener.DebugReturn(DebugLevel.ERROR, "Connect() to '" + this.ConnectAddress + "' failed: " + se.ToString()); + } + + this.HandleException(StatusCode.ExceptionOnConnect); + return; + } + + Thread run = new Thread(new ThreadStart(this.ReceiveLoop)); + run.Name = "photon receive thread"; + run.IsBackground = true; + run.Start(); + } + + + /// Disconnect the websocket (no matter what it does right now). + public override bool Disconnect() + { + if (this.State == PhotonSocketState.Disconnecting || this.State == PhotonSocketState.Disconnected) + { + return false; + } + + if (this.ReportDebugOfLevel(DebugLevel.INFO)) + { + this.Listener.DebugReturn(DebugLevel.INFO, "SocketWebTcpThread.Disconnect()"); + } + + this.State = PhotonSocketState.Disconnecting; + if (this.sock != null) + { + try + { + this.sock.Close(); + } + catch + { + } + this.sock = null; + } + + this.State = PhotonSocketState.Disconnected; + return true; + } + + /// Calls Disconnect. + public void Dispose() + { + this.Disconnect(); + } + + + /// Used by TPeer to send. + public override PhotonSocketError Send(byte[] data, int length) + { + if (this.State != PhotonSocketState.Connected) + { + return PhotonSocketError.Skipped; + } + + try + { + if (this.ReportDebugOfLevel(DebugLevel.ALL)) + { + this.Listener.DebugReturn(DebugLevel.ALL, "Sending: " + SupportClass.ByteArrayToString(data)); + } + + this.sock.Send(data); + } + catch (Exception e) + { + this.Listener.DebugReturn(DebugLevel.ERROR, "Cannot send to: " + this.ConnectAddress + ". " + e.Message); + + if (this.State == PhotonSocketState.Connected) + { + this.HandleException(StatusCode.Exception); + } + return PhotonSocketError.Exception; + } + + return PhotonSocketError.Success; + } + + + /// Not used currently. + public override PhotonSocketError Receive(out byte[] data) + { + data = null; + return PhotonSocketError.NoData; + } + + /// Used by TPeer to receive. + public void ReceiveLoop() + { + try + { + while (this.State == PhotonSocketState.Connected) + { + if (this.sock.Error != null) + { + this.Listener.DebugReturn(DebugLevel.ERROR, "Exiting receive thread (inside loop). Server: " + this.ConnectAddress + " Error: " + this.sock.Error); + this.HandleException(StatusCode.ExceptionOnReceive); + break; + } + + + byte[] inBuff = this.sock.Recv(); + if (inBuff == null || inBuff.Length == 0) + { + Thread.Sleep(0); + continue; + } + if (inBuff.Length < 0) + { + // got disconnected (from remote or net) + if (this.State != PhotonSocketState.Disconnecting && this.State != PhotonSocketState.Disconnected) + { + this.HandleException(StatusCode.DisconnectByServer); + } + break; + } + + if (this.ReportDebugOfLevel(DebugLevel.ALL)) + { + this.Listener.DebugReturn(DebugLevel.ALL, "TCP << " + inBuff.Length + " = " + SupportClass.ByteArrayToString(inBuff)); + } + + this.HandleReceivedDatagram(inBuff, inBuff.Length, false); + } + } + catch (Exception e) + { + if (this.State != PhotonSocketState.Disconnecting && this.State != PhotonSocketState.Disconnected) + { + if (this.ReportDebugOfLevel(DebugLevel.ERROR)) + { + this.EnqueueDebugReturn(DebugLevel.ERROR, "Receive issue. State: " + this.State + ". Server: '" + this.ConnectAddress + "' Exception: " + e); + } + + this.HandleException(StatusCode.ExceptionOnReceive); + } + } + + this.Disconnect(); + } + } +} + +#endif \ No newline at end of file diff --git a/Assets/Runtime/Photon/Plugins/PhotonWebSocket/SocketWebTcpThread.cs.meta b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/SocketWebTcpThread.cs.meta new file mode 100644 index 0000000..d52c5d9 --- /dev/null +++ b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/SocketWebTcpThread.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f621e51e5c8aead49bd766c4b959a859 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket.meta b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket.meta new file mode 100644 index 0000000..a26c138 --- /dev/null +++ b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ff7e276bdb10f1a4f9bdbfeaafadf2f2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket/WebSocket.cs b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket/WebSocket.cs new file mode 100644 index 0000000..2108aaa --- /dev/null +++ b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket/WebSocket.cs @@ -0,0 +1,155 @@ +#if UNITY_WEBGL || UNITY_XBOXONE || WEBSOCKET + +using System; +using System.Text; + +#if UNITY_WEBGL && !UNITY_EDITOR +using System.Runtime.InteropServices; +#else +using System.Collections.Generic; +using System.Security.Authentication; +#endif + + +public class WebSocket +{ + private Uri mUrl; + + public WebSocket(Uri url) + { + mUrl = url; + + string protocol = mUrl.Scheme; + if (!protocol.Equals("ws") && !protocol.Equals("wss")) + throw new ArgumentException("Unsupported protocol: " + protocol); + } + + public void SendString(string str) + { + Send(Encoding.UTF8.GetBytes (str)); + } + + public string RecvString() + { + byte[] retval = Recv(); + if (retval == null) + return null; + return Encoding.UTF8.GetString (retval); + } + +#if UNITY_WEBGL && !UNITY_EDITOR + [DllImport("__Internal")] + private static extern int SocketCreate (string url); + + [DllImport("__Internal")] + private static extern int SocketState (int socketInstance); + + [DllImport("__Internal")] + private static extern void SocketSend (int socketInstance, byte[] ptr, int length); + + [DllImport("__Internal")] + private static extern void SocketRecv (int socketInstance, byte[] ptr, int length); + + [DllImport("__Internal")] + private static extern int SocketRecvLength (int socketInstance); + + [DllImport("__Internal")] + private static extern void SocketClose (int socketInstance); + + [DllImport("__Internal")] + private static extern int SocketError (int socketInstance, byte[] ptr, int length); + + int m_NativeRef = 0; + + public void Send(byte[] buffer) + { + SocketSend (m_NativeRef, buffer, buffer.Length); + } + + public byte[] Recv() + { + int length = SocketRecvLength (m_NativeRef); + if (length == 0) + return null; + byte[] buffer = new byte[length]; + SocketRecv (m_NativeRef, buffer, length); + return buffer; + } + + public void Connect() + { + m_NativeRef = SocketCreate (mUrl.ToString()); + + //while (SocketState(m_NativeRef) == 0) + // yield return 0; + } + + public void Close() + { + SocketClose(m_NativeRef); + } + + public bool Connected + { + get { return SocketState(m_NativeRef) != 0; } + } + + public string Error + { + get { + const int bufsize = 1024; + byte[] buffer = new byte[bufsize]; + int result = SocketError (m_NativeRef, buffer, bufsize); + + if (result == 0) + return null; + + return Encoding.UTF8.GetString (buffer); + } + } +#else + WebSocketSharp.WebSocket m_Socket; + Queue m_Messages = new Queue(); + bool m_IsConnected = false; + string m_Error = null; + + public void Connect() + { + m_Socket = new WebSocketSharp.WebSocket(mUrl.ToString(), new string[] { "GpBinaryV16" });// modified by TS + m_Socket.SslConfiguration.EnabledSslProtocols = m_Socket.SslConfiguration.EnabledSslProtocols | (SslProtocols)(3072| 768); + m_Socket.OnMessage += (sender, e) => m_Messages.Enqueue(e.RawData); + m_Socket.OnOpen += (sender, e) => m_IsConnected = true; + m_Socket.OnError += (sender, e) => m_Error = e.Message + (e.Exception == null ? "" : " / " + e.Exception); + m_Socket.ConnectAsync(); + } + + public bool Connected { get { return m_IsConnected; } }// added by TS + + + public void Send(byte[] buffer) + { + m_Socket.Send(buffer); + } + + public byte[] Recv() + { + if (m_Messages.Count == 0) + return null; + return m_Messages.Dequeue(); + } + + public void Close() + { + m_Socket.Close(); + } + + public string Error + { + get + { + return m_Error; + } + } +#endif +} +#endif \ No newline at end of file diff --git a/Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket/WebSocket.cs.meta b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket/WebSocket.cs.meta new file mode 100644 index 0000000..47f9faa --- /dev/null +++ b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket/WebSocket.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cfe55c94da5ac634db8d4ca3d1891173 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket/WebSocket.jslib b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket/WebSocket.jslib new file mode 100644 index 0000000..3ddb2c5 --- /dev/null +++ b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket/WebSocket.jslib @@ -0,0 +1,116 @@ +var LibraryWebSockets = { +$webSocketInstances: [], + +SocketCreate: function(url) +{ + var str = Pointer_stringify(url); + var socket = { + socket: new WebSocket(str, ['GpBinaryV16']), + buffer: new Uint8Array(0), + error: null, + messages: [] + } + socket.socket.binaryType = 'arraybuffer'; + socket.socket.onmessage = function (e) { +// if (e.data instanceof Blob) +// { +// var reader = new FileReader(); +// reader.addEventListener("loadend", function() { +// var array = new Uint8Array(reader.result); +// socket.messages.push(array); +// }); +// reader.readAsArrayBuffer(e.data); +// } + if (e.data instanceof ArrayBuffer) + { + var array = new Uint8Array(e.data); + socket.messages.push(array); + } + }; + socket.socket.onclose = function (e) { + if (e.code != 1000) + { + if (e.reason != null && e.reason.length > 0) + socket.error = e.reason; + else + { + switch (e.code) + { + case 1001: + socket.error = "Endpoint going away."; + break; + case 1002: + socket.error = "Protocol error."; + break; + case 1003: + socket.error = "Unsupported message."; + break; + case 1005: + socket.error = "No status."; + break; + case 1006: + socket.error = "Abnormal disconnection."; + break; + case 1009: + socket.error = "Data frame too large."; + break; + default: + socket.error = "Error "+e.code; + } + } + } + } + var instance = webSocketInstances.push(socket) - 1; + return instance; +}, + +SocketState: function (socketInstance) +{ + var socket = webSocketInstances[socketInstance]; + return socket.socket.readyState; +}, + +SocketError: function (socketInstance, ptr, bufsize) +{ + var socket = webSocketInstances[socketInstance]; + if (socket.error == null) + return 0; + var str = socket.error.slice(0, Math.max(0, bufsize - 1)); + writeStringToMemory(str, ptr, false); + return 1; +}, + +SocketSend: function (socketInstance, ptr, length) +{ + var socket = webSocketInstances[socketInstance]; + socket.socket.send (HEAPU8.buffer.slice(ptr, ptr+length)); +}, + +SocketRecvLength: function(socketInstance) +{ + var socket = webSocketInstances[socketInstance]; + if (socket.messages.length == 0) + return 0; + return socket.messages[0].length; +}, + +SocketRecv: function (socketInstance, ptr, length) +{ + var socket = webSocketInstances[socketInstance]; + if (socket.messages.length == 0) + return 0; + if (socket.messages[0].length > length) + return 0; + HEAPU8.set(socket.messages[0], ptr); + socket.messages = socket.messages.slice(1); +}, + +SocketClose: function (socketInstance) +{ + var socket = webSocketInstances[socketInstance]; + socket.socket.close(); +} +}; + +autoAddDeps(LibraryWebSockets, '$webSocketInstances'); +mergeInto(LibraryManager.library, LibraryWebSockets); diff --git a/Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket/WebSocket.jslib.meta b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket/WebSocket.jslib.meta new file mode 100644 index 0000000..4dd8ac6 --- /dev/null +++ b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket/WebSocket.jslib.meta @@ -0,0 +1,34 @@ +fileFormatVersion: 2 +guid: 21d8d0d3e9f68ae43975534c13f23964 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + isPreloaded: 0 + isOverridable: 0 + platformData: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + - first: + Facebook: WebGL + second: + enabled: 1 + settings: {} + - first: + WebGL: WebGL + second: + enabled: 1 + settings: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket/websocket-sharp.README b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket/websocket-sharp.README new file mode 100644 index 0000000..2f14990 --- /dev/null +++ b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket/websocket-sharp.README @@ -0,0 +1,3 @@ +websocket-sharp.dll built from https://github.com/sta/websocket-sharp.git, commit 869dfb09778de51081b0ae64bd2c3217cffe0699 on Aug 24, 2016. + +websocket-sharp is provided under The MIT License as mentioned here: https://github.com/sta/websocket-sharp#license \ No newline at end of file diff --git a/Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket/websocket-sharp.README.meta b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket/websocket-sharp.README.meta new file mode 100644 index 0000000..d9d4cd5 --- /dev/null +++ b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket/websocket-sharp.README.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 004b47cc08839884586e6b91d620b418 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket/websocket-sharp.dll b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket/websocket-sharp.dll new file mode 100644 index 0000000000000000000000000000000000000000..6eabebefcc8c58440553bed36cd058189ef543e8 GIT binary patch literal 244736 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%0D`z5;!?^v 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 literal 0 HcmV?d00001 diff --git a/Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket/websocket-sharp.dll.meta b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket/websocket-sharp.dll.meta new file mode 100644 index 0000000..3b96e9c --- /dev/null +++ b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket/websocket-sharp.dll.meta @@ -0,0 +1,30 @@ +fileFormatVersion: 2 +guid: 0fb606431450aa343839e85c42dd9660 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + isPreloaded: 0 + isOverridable: 0 + platformData: + - first: + Any: + second: + enabled: 1 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + - first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Photon/Plugins/Wsa.meta b/Assets/Runtime/Photon/Plugins/Wsa.meta new file mode 100644 index 0000000..9969505 --- /dev/null +++ b/Assets/Runtime/Photon/Plugins/Wsa.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 83aeb43ebfe53ea4285ca591ed80efe4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Photon/Plugins/Wsa/Photon3Unity3D.dll b/Assets/Runtime/Photon/Plugins/Wsa/Photon3Unity3D.dll new file mode 100644 index 0000000000000000000000000000000000000000..e2b0736db83c9e76fa240ac7c1d9f5307c7b0c39 GIT binary patch literal 112640 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