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 0000000..f9409a8 Binary files /dev/null and b/Assets/Runtime/Photon/Plugins/Photon3Unity3D.dll differ 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 0000000..6eabebe Binary files /dev/null and b/Assets/Runtime/Photon/Plugins/PhotonWebSocket/WebSocket/websocket-sharp.dll differ 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 0000000..e2b0736 Binary files /dev/null and b/Assets/Runtime/Photon/Plugins/Wsa/Photon3Unity3D.dll differ diff --git a/Assets/Runtime/Photon/Plugins/Wsa/Photon3Unity3D.dll.meta b/Assets/Runtime/Photon/Plugins/Wsa/Photon3Unity3D.dll.meta new file mode 100644 index 0000000..1618eaa --- /dev/null +++ b/Assets/Runtime/Photon/Plugins/Wsa/Photon3Unity3D.dll.meta @@ -0,0 +1,114 @@ +fileFormatVersion: 2 +guid: 8b3cc63923895354ca37992f6c87ab63 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + isPreloaded: 0 + isOverridable: 0 + platformData: + - first: + '': Any + second: + enabled: 0 + settings: + Exclude Android: 1 + Exclude Editor: 1 + Exclude Linux: 1 + Exclude Linux64: 1 + Exclude LinuxUniversal: 1 + Exclude OSXUniversal: 1 + Exclude WebGL: 1 + Exclude Win: 1 + Exclude Win64: 1 + Exclude iOS: 1 + - first: + Android: Android + second: + enabled: 0 + settings: + CPU: ARMv7 + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + 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: 0 + settings: + CPU: x86 + - first: + Standalone: Linux64 + second: + enabled: 0 + settings: + CPU: x86_64 + - first: + Standalone: LinuxUniversal + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: OSXUniversal + second: + enabled: 0 + settings: + CPU: AnyCPU + - first: + Standalone: Win + second: + enabled: 0 + settings: + CPU: AnyCPU + - first: + Standalone: Win64 + second: + enabled: 0 + settings: + CPU: AnyCPU + - first: + WebGL: WebGL + second: + enabled: 0 + settings: {} + - first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + - first: + iPhone: iOS + second: + enabled: 0 + settings: + AddToEmbeddedBinaries: false + CompileFlags: + FrameworkDependencies: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/manifest.json b/Packages/manifest.json new file mode 100644 index 0000000..8e51b88 --- /dev/null +++ b/Packages/manifest.json @@ -0,0 +1,49 @@ +{ + "dependencies": { + "com.unity.2d.animation": "3.2.16", + "com.unity.2d.pixel-perfect": "2.1.0", + "com.unity.2d.psdimporter": "2.1.10", + "com.unity.2d.sprite": "1.0.0", + "com.unity.2d.spriteshape": "3.0.18", + "com.unity.2d.tilemap": "1.0.0", + "com.unity.collab-proxy": "1.14.12", + "com.unity.ide.rider": "1.2.1", + "com.unity.ide.visualstudio": "2.0.14", + "com.unity.ide.vscode": "1.2.4", + "com.unity.test-framework": "1.1.30", + "com.unity.textmeshpro": "2.1.6", + "com.unity.timeline": "1.2.18", + "com.unity.ugui": "1.0.0", + "com.unity.modules.ai": "1.0.0", + "com.unity.modules.androidjni": "1.0.0", + "com.unity.modules.animation": "1.0.0", + "com.unity.modules.assetbundle": "1.0.0", + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.cloth": "1.0.0", + "com.unity.modules.director": "1.0.0", + "com.unity.modules.imageconversion": "1.0.0", + "com.unity.modules.imgui": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0", + "com.unity.modules.particlesystem": "1.0.0", + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.physics2d": "1.0.0", + "com.unity.modules.screencapture": "1.0.0", + "com.unity.modules.terrain": "1.0.0", + "com.unity.modules.terrainphysics": "1.0.0", + "com.unity.modules.tilemap": "1.0.0", + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.uielements": "1.0.0", + "com.unity.modules.umbra": "1.0.0", + "com.unity.modules.unityanalytics": "1.0.0", + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.unitywebrequestassetbundle": "1.0.0", + "com.unity.modules.unitywebrequestaudio": "1.0.0", + "com.unity.modules.unitywebrequesttexture": "1.0.0", + "com.unity.modules.unitywebrequestwww": "1.0.0", + "com.unity.modules.vehicles": "1.0.0", + "com.unity.modules.video": "1.0.0", + "com.unity.modules.vr": "1.0.0", + "com.unity.modules.wind": "1.0.0", + "com.unity.modules.xr": "1.0.0" + } +} diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json new file mode 100644 index 0000000..68f44a3 --- /dev/null +++ b/Packages/packages-lock.json @@ -0,0 +1,404 @@ +{ + "dependencies": { + "com.unity.2d.animation": { + "version": "3.2.16", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.2d.common": "2.1.1", + "com.unity.mathematics": "1.1.0", + "com.unity.2d.sprite": "1.0.0", + "com.unity.modules.animation": "1.0.0", + "com.unity.modules.uielements": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.2d.common": { + "version": "2.1.1", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.2d.sprite": "1.0.0", + "com.unity.modules.uielements": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.2d.path": { + "version": "2.1.1", + "depth": 1, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.2d.pixel-perfect": { + "version": "2.1.0", + "depth": 0, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.2d.psdimporter": { + "version": "2.1.10", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.2d.common": "2.1.0", + "com.unity.2d.animation": "3.2.14", + "com.unity.2d.sprite": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.2d.sprite": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.2d.spriteshape": { + "version": "3.0.18", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.mathematics": "1.1.0", + "com.unity.2d.common": "2.1.0", + "com.unity.2d.path": "2.1.1" + }, + "url": "https://packages.unity.com" + }, + "com.unity.2d.tilemap": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.collab-proxy": { + "version": "1.14.12", + "depth": 0, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.ext.nunit": { + "version": "1.0.6", + "depth": 1, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.ide.rider": { + "version": "1.2.1", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.test-framework": "1.1.1" + }, + "url": "https://packages.unity.com" + }, + "com.unity.ide.visualstudio": { + "version": "2.0.14", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.test-framework": "1.1.9" + }, + "url": "https://packages.unity.com" + }, + "com.unity.ide.vscode": { + "version": "1.2.4", + "depth": 0, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.mathematics": { + "version": "1.1.0", + "depth": 1, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.test-framework": { + "version": "1.1.30", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.ext.nunit": "1.0.6", + "com.unity.modules.imgui": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.textmeshpro": { + "version": "2.1.6", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.ugui": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.timeline": { + "version": "1.2.18", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.modules.director": "1.0.0", + "com.unity.modules.animation": "1.0.0", + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.particlesystem": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.ugui": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.imgui": "1.0.0" + } + }, + "com.unity.modules.ai": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.androidjni": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.animation": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.assetbundle": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.audio": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.cloth": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics": "1.0.0" + } + }, + "com.unity.modules.director": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.animation": "1.0.0" + } + }, + "com.unity.modules.imageconversion": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.imgui": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.jsonserialize": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.particlesystem": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.physics": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.physics2d": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.screencapture": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.imageconversion": "1.0.0" + } + }, + "com.unity.modules.subsystems": { + "version": "1.0.0", + "depth": 1, + "source": "builtin", + "dependencies": { + "com.unity.modules.jsonserialize": "1.0.0" + } + }, + "com.unity.modules.terrain": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.terrainphysics": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.terrain": "1.0.0" + } + }, + "com.unity.modules.tilemap": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics2d": "1.0.0" + } + }, + "com.unity.modules.ui": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.uielements": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.imgui": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0" + } + }, + "com.unity.modules.umbra": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.unityanalytics": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0" + } + }, + "com.unity.modules.unitywebrequest": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.unitywebrequestassetbundle": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.assetbundle": "1.0.0", + "com.unity.modules.unitywebrequest": "1.0.0" + } + }, + "com.unity.modules.unitywebrequestaudio": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.audio": "1.0.0" + } + }, + "com.unity.modules.unitywebrequesttexture": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.imageconversion": "1.0.0" + } + }, + "com.unity.modules.unitywebrequestwww": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.unitywebrequestassetbundle": "1.0.0", + "com.unity.modules.unitywebrequestaudio": "1.0.0", + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.assetbundle": "1.0.0", + "com.unity.modules.imageconversion": "1.0.0" + } + }, + "com.unity.modules.vehicles": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics": "1.0.0" + } + }, + "com.unity.modules.video": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.unitywebrequest": "1.0.0" + } + }, + "com.unity.modules.vr": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.jsonserialize": "1.0.0", + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.xr": "1.0.0" + } + }, + "com.unity.modules.wind": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.xr": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0", + "com.unity.modules.subsystems": "1.0.0" + } + } + } +} diff --git a/ProjectSettings/AudioManager.asset b/ProjectSettings/AudioManager.asset new file mode 100644 index 0000000..07ebfb0 --- /dev/null +++ b/ProjectSettings/AudioManager.asset @@ -0,0 +1,19 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!11 &1 +AudioManager: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Volume: 1 + Rolloff Scale: 1 + Doppler Factor: 1 + Default Speaker Mode: 2 + m_SampleRate: 0 + m_DSPBufferSize: 1024 + m_VirtualVoiceCount: 512 + m_RealVoiceCount: 32 + m_SpatializerPlugin: + m_AmbisonicDecoderPlugin: + m_DisableAudio: 0 + m_VirtualizeEffects: 1 + m_RequestedDSPBufferSize: 1024 diff --git a/ProjectSettings/ClusterInputManager.asset b/ProjectSettings/ClusterInputManager.asset new file mode 100644 index 0000000..e7886b2 --- /dev/null +++ b/ProjectSettings/ClusterInputManager.asset @@ -0,0 +1,6 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!236 &1 +ClusterInputManager: + m_ObjectHideFlags: 0 + m_Inputs: [] diff --git a/ProjectSettings/DynamicsManager.asset b/ProjectSettings/DynamicsManager.asset new file mode 100644 index 0000000..cdc1f3e --- /dev/null +++ b/ProjectSettings/DynamicsManager.asset @@ -0,0 +1,34 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!55 &1 +PhysicsManager: + m_ObjectHideFlags: 0 + serializedVersion: 11 + m_Gravity: {x: 0, y: -9.81, z: 0} + m_DefaultMaterial: {fileID: 0} + m_BounceThreshold: 2 + m_SleepThreshold: 0.005 + m_DefaultContactOffset: 0.01 + m_DefaultSolverIterations: 6 + m_DefaultSolverVelocityIterations: 1 + m_QueriesHitBackfaces: 0 + m_QueriesHitTriggers: 1 + m_EnableAdaptiveForce: 0 + m_ClothInterCollisionDistance: 0 + m_ClothInterCollisionStiffness: 0 + m_ContactsGeneration: 1 + m_LayerCollisionMatrix: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff + m_AutoSimulation: 1 + m_AutoSyncTransforms: 0 + m_ReuseCollisionCallbacks: 1 + m_ClothInterCollisionSettingsToggle: 0 + m_ContactPairsMode: 0 + m_BroadphaseType: 0 + m_WorldBounds: + m_Center: {x: 0, y: 0, z: 0} + m_Extent: {x: 250, y: 250, z: 250} + m_WorldSubdivisions: 8 + m_FrictionType: 0 + m_EnableEnhancedDeterminism: 0 + m_EnableUnifiedHeightmaps: 1 + m_DefaultMaxAngluarSpeed: 7 diff --git a/ProjectSettings/EditorBuildSettings.asset b/ProjectSettings/EditorBuildSettings.asset new file mode 100644 index 0000000..0147887 --- /dev/null +++ b/ProjectSettings/EditorBuildSettings.asset @@ -0,0 +1,8 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1045 &1 +EditorBuildSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Scenes: [] + m_configObjects: {} diff --git a/ProjectSettings/EditorSettings.asset b/ProjectSettings/EditorSettings.asset new file mode 100644 index 0000000..5a7387f --- /dev/null +++ b/ProjectSettings/EditorSettings.asset @@ -0,0 +1,35 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!159 &1 +EditorSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_ExternalVersionControlSupport: Visible Meta Files + m_SerializationMode: 2 + m_LineEndingsForNewScripts: 0 + m_DefaultBehaviorMode: 1 + m_PrefabRegularEnvironment: {fileID: 0} + m_PrefabUIEnvironment: {fileID: 0} + m_SpritePackerMode: 4 + m_SpritePackerPaddingPower: 1 + m_EtcTextureCompressorBehavior: 1 + m_EtcTextureFastCompressor: 1 + m_EtcTextureNormalCompressor: 2 + m_EtcTextureBestCompressor: 4 + m_ProjectGenerationIncludedExtensions: txt;xml;fnt;cd;asmdef;rsp;asmref + m_ProjectGenerationRootNamespace: + m_CollabEditorSettings: + inProgressEnabled: 1 + m_EnableTextureStreamingInEditMode: 1 + m_EnableTextureStreamingInPlayMode: 1 + m_AsyncShaderCompilation: 1 + m_EnterPlayModeOptionsEnabled: 0 + m_EnterPlayModeOptions: 3 + m_ShowLightmapResolutionOverlay: 1 + m_UseLegacyProbeSampleCount: 1 + m_AssetPipelineMode: 1 + m_CacheServerMode: 0 + m_CacheServerEndpoint: + m_CacheServerNamespacePrefix: default + m_CacheServerEnableDownload: 1 + m_CacheServerEnableUpload: 1 diff --git a/ProjectSettings/GraphicsSettings.asset b/ProjectSettings/GraphicsSettings.asset new file mode 100644 index 0000000..6c2632a --- /dev/null +++ b/ProjectSettings/GraphicsSettings.asset @@ -0,0 +1,57 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!30 &1 +GraphicsSettings: + m_ObjectHideFlags: 0 + serializedVersion: 12 + m_Deferred: + m_Mode: 1 + m_Shader: {fileID: 69, guid: 0000000000000000f000000000000000, type: 0} + m_DeferredReflections: + m_Mode: 1 + m_Shader: {fileID: 74, guid: 0000000000000000f000000000000000, type: 0} + m_ScreenSpaceShadows: + m_Mode: 1 + m_Shader: {fileID: 64, guid: 0000000000000000f000000000000000, type: 0} + m_LegacyDeferred: + m_Mode: 1 + m_Shader: {fileID: 63, guid: 0000000000000000f000000000000000, type: 0} + m_DepthNormals: + m_Mode: 1 + m_Shader: {fileID: 62, guid: 0000000000000000f000000000000000, type: 0} + m_MotionVectors: + m_Mode: 1 + m_Shader: {fileID: 75, guid: 0000000000000000f000000000000000, type: 0} + m_LightHalo: + m_Mode: 1 + m_Shader: {fileID: 105, guid: 0000000000000000f000000000000000, type: 0} + m_LensFlare: + m_Mode: 1 + m_Shader: {fileID: 102, guid: 0000000000000000f000000000000000, type: 0} + m_AlwaysIncludedShaders: + - {fileID: 10753, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 10770, guid: 0000000000000000f000000000000000, type: 0} + m_PreloadedShaders: [] + m_SpritesDefaultMaterial: {fileID: 10754, guid: 0000000000000000f000000000000000, + type: 0} + m_CustomRenderPipeline: {fileID: 0} + m_TransparencySortMode: 0 + m_TransparencySortAxis: {x: 0, y: 0, z: 1} + m_DefaultRenderingPath: 1 + m_DefaultMobileRenderingPath: 1 + m_TierSettings: [] + m_LightmapStripping: 0 + m_FogStripping: 0 + m_InstancingStripping: 0 + m_LightmapKeepPlain: 1 + m_LightmapKeepDirCombined: 1 + m_LightmapKeepDynamicPlain: 1 + m_LightmapKeepDynamicDirCombined: 1 + m_LightmapKeepShadowMask: 1 + m_LightmapKeepSubtractive: 1 + m_FogKeepLinear: 1 + m_FogKeepExp: 1 + m_FogKeepExp2: 1 + m_AlbedoSwatchInfos: [] + m_LightsUseLinearIntensity: 0 + m_LightsUseColorTemperature: 0 diff --git a/ProjectSettings/InputManager.asset b/ProjectSettings/InputManager.asset new file mode 100644 index 0000000..17c8f53 --- /dev/null +++ b/ProjectSettings/InputManager.asset @@ -0,0 +1,295 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!13 &1 +InputManager: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Axes: + - serializedVersion: 3 + m_Name: Horizontal + descriptiveName: + descriptiveNegativeName: + negativeButton: left + positiveButton: right + altNegativeButton: a + altPositiveButton: d + gravity: 3 + dead: 0.001 + sensitivity: 3 + snap: 1 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Vertical + descriptiveName: + descriptiveNegativeName: + negativeButton: down + positiveButton: up + altNegativeButton: s + altPositiveButton: w + gravity: 3 + dead: 0.001 + sensitivity: 3 + snap: 1 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire1 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left ctrl + altNegativeButton: + altPositiveButton: mouse 0 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire2 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left alt + altNegativeButton: + altPositiveButton: mouse 1 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire3 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left shift + altNegativeButton: + altPositiveButton: mouse 2 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Jump + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: space + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Mouse X + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0 + sensitivity: 0.1 + snap: 0 + invert: 0 + type: 1 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Mouse Y + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0 + sensitivity: 0.1 + snap: 0 + invert: 0 + type: 1 + axis: 1 + joyNum: 0 + - serializedVersion: 3 + m_Name: Mouse ScrollWheel + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0 + sensitivity: 0.1 + snap: 0 + invert: 0 + type: 1 + axis: 2 + joyNum: 0 + - serializedVersion: 3 + m_Name: Horizontal + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0.19 + sensitivity: 1 + snap: 0 + invert: 0 + type: 2 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Vertical + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0.19 + sensitivity: 1 + snap: 0 + invert: 1 + type: 2 + axis: 1 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire1 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: joystick button 0 + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire2 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: joystick button 1 + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire3 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: joystick button 2 + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Jump + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: joystick button 3 + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Submit + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: return + altNegativeButton: + altPositiveButton: joystick button 0 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Submit + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: enter + altNegativeButton: + altPositiveButton: space + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Cancel + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: escape + altNegativeButton: + altPositiveButton: joystick button 1 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 diff --git a/ProjectSettings/NavMeshAreas.asset b/ProjectSettings/NavMeshAreas.asset new file mode 100644 index 0000000..3b0b7c3 --- /dev/null +++ b/ProjectSettings/NavMeshAreas.asset @@ -0,0 +1,91 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!126 &1 +NavMeshProjectSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + areas: + - name: Walkable + cost: 1 + - name: Not Walkable + cost: 1 + - name: Jump + cost: 2 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + m_LastAgentTypeID: -887442657 + m_Settings: + - serializedVersion: 2 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.75 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + accuratePlacement: 0 + debug: + m_Flags: 0 + m_SettingNames: + - Humanoid diff --git a/ProjectSettings/NetworkManager.asset b/ProjectSettings/NetworkManager.asset new file mode 100644 index 0000000..5dc6a83 --- /dev/null +++ b/ProjectSettings/NetworkManager.asset @@ -0,0 +1,8 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!149 &1 +NetworkManager: + m_ObjectHideFlags: 0 + m_DebugLevel: 0 + m_Sendrate: 15 + m_AssetToPrefab: {} diff --git a/ProjectSettings/PackageManagerSettings.asset b/ProjectSettings/PackageManagerSettings.asset new file mode 100644 index 0000000..6920e3a --- /dev/null +++ b/ProjectSettings/PackageManagerSettings.asset @@ -0,0 +1,38 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &1 +MonoBehaviour: + m_ObjectHideFlags: 61 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 13964, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_ScopedRegistriesSettingsExpanded: 1 + oneTimeWarningShown: 0 + m_Registries: + - m_Id: main + m_Name: + m_Url: https://packages.unity.com + m_Scopes: [] + m_IsDefault: 1 + m_UserSelectedRegistryName: + m_UserAddingNewScopedRegistry: 0 + m_RegistryInfoDraft: + m_ErrorMessage: + m_Original: + m_Id: + m_Name: + m_Url: + m_Scopes: [] + m_IsDefault: 0 + m_Modified: 0 + m_Name: + m_Url: + m_Scopes: + - + m_SelectedScopeIndex: 0 diff --git a/ProjectSettings/Physics2DSettings.asset b/ProjectSettings/Physics2DSettings.asset new file mode 100644 index 0000000..47880b1 --- /dev/null +++ b/ProjectSettings/Physics2DSettings.asset @@ -0,0 +1,56 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!19 &1 +Physics2DSettings: + m_ObjectHideFlags: 0 + serializedVersion: 4 + m_Gravity: {x: 0, y: -9.81} + m_DefaultMaterial: {fileID: 0} + m_VelocityIterations: 8 + m_PositionIterations: 3 + m_VelocityThreshold: 1 + m_MaxLinearCorrection: 0.2 + m_MaxAngularCorrection: 8 + m_MaxTranslationSpeed: 100 + m_MaxRotationSpeed: 360 + m_BaumgarteScale: 0.2 + m_BaumgarteTimeOfImpactScale: 0.75 + m_TimeToSleep: 0.5 + m_LinearSleepTolerance: 0.01 + m_AngularSleepTolerance: 2 + m_DefaultContactOffset: 0.01 + m_JobOptions: + serializedVersion: 2 + useMultithreading: 0 + useConsistencySorting: 0 + m_InterpolationPosesPerJob: 100 + m_NewContactsPerJob: 30 + m_CollideContactsPerJob: 100 + m_ClearFlagsPerJob: 200 + m_ClearBodyForcesPerJob: 200 + m_SyncDiscreteFixturesPerJob: 50 + m_SyncContinuousFixturesPerJob: 50 + m_FindNearestContactsPerJob: 100 + m_UpdateTriggerContactsPerJob: 100 + m_IslandSolverCostThreshold: 100 + m_IslandSolverBodyCostScale: 1 + m_IslandSolverContactCostScale: 10 + m_IslandSolverJointCostScale: 10 + m_IslandSolverBodiesPerJob: 50 + m_IslandSolverContactsPerJob: 50 + m_AutoSimulation: 1 + m_QueriesHitTriggers: 1 + m_QueriesStartInColliders: 1 + m_CallbacksOnDisable: 1 + m_ReuseCollisionCallbacks: 1 + m_AutoSyncTransforms: 0 + m_AlwaysShowColliders: 0 + m_ShowColliderSleep: 1 + m_ShowColliderContacts: 0 + m_ShowColliderAABB: 0 + m_ContactArrowScale: 0.2 + m_ColliderAwakeColor: {r: 0.5686275, g: 0.95686275, b: 0.54509807, a: 0.7529412} + m_ColliderAsleepColor: {r: 0.5686275, g: 0.95686275, b: 0.54509807, a: 0.36078432} + m_ColliderContactColor: {r: 1, g: 0, b: 1, a: 0.6862745} + m_ColliderAABBColor: {r: 1, g: 1, b: 0, a: 0.2509804} + m_LayerCollisionMatrix: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff diff --git a/ProjectSettings/PresetManager.asset b/ProjectSettings/PresetManager.asset new file mode 100644 index 0000000..67a94da --- /dev/null +++ b/ProjectSettings/PresetManager.asset @@ -0,0 +1,7 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1386491679 &1 +PresetManager: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_DefaultPresets: {} diff --git a/ProjectSettings/ProjectSettings.asset b/ProjectSettings/ProjectSettings.asset new file mode 100644 index 0000000..e9ef2a8 --- /dev/null +++ b/ProjectSettings/ProjectSettings.asset @@ -0,0 +1,684 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!129 &1 +PlayerSettings: + m_ObjectHideFlags: 0 + serializedVersion: 20 + productGUID: 2f13c77eefcc98049994aaeb206abc2e + AndroidProfiler: 0 + AndroidFilterTouchesWhenObscured: 0 + AndroidEnableSustainedPerformanceMode: 0 + defaultScreenOrientation: 4 + targetDevice: 2 + useOnDemandResources: 0 + accelerometerFrequency: 60 + companyName: nitori.itch.io + productName: icedLemonJam + defaultCursor: {fileID: 0} + cursorHotspot: {x: 0, y: 0} + m_SplashScreenBackgroundColor: {r: 0.13725491, g: 0.12156863, b: 0.1254902, a: 1} + m_ShowUnitySplashScreen: 1 + m_ShowUnitySplashLogo: 1 + m_SplashScreenOverlayOpacity: 1 + m_SplashScreenAnimation: 1 + m_SplashScreenLogoStyle: 1 + m_SplashScreenDrawMode: 0 + m_SplashScreenBackgroundAnimationZoom: 1 + m_SplashScreenLogoAnimationZoom: 1 + m_SplashScreenBackgroundLandscapeAspect: 1 + m_SplashScreenBackgroundPortraitAspect: 1 + m_SplashScreenBackgroundLandscapeUvs: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + m_SplashScreenBackgroundPortraitUvs: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + m_SplashScreenLogos: [] + m_VirtualRealitySplashScreen: {fileID: 0} + m_HolographicTrackingLossScreen: {fileID: 0} + defaultScreenWidth: 1024 + defaultScreenHeight: 768 + defaultScreenWidthWeb: 960 + defaultScreenHeightWeb: 600 + m_StereoRenderingPath: 0 + m_ActiveColorSpace: 0 + m_MTRendering: 1 + m_StackTraceTypes: 010000000100000001000000010000000100000001000000 + iosShowActivityIndicatorOnLoading: -1 + androidShowActivityIndicatorOnLoading: -1 + iosUseCustomAppBackgroundBehavior: 0 + iosAllowHTTPDownload: 1 + allowedAutorotateToPortrait: 1 + allowedAutorotateToPortraitUpsideDown: 1 + allowedAutorotateToLandscapeRight: 1 + allowedAutorotateToLandscapeLeft: 1 + useOSAutorotation: 1 + use32BitDisplayBuffer: 1 + preserveFramebufferAlpha: 0 + disableDepthAndStencilBuffers: 0 + androidStartInFullscreen: 1 + androidRenderOutsideSafeArea: 1 + androidUseSwappy: 0 + androidBlitType: 0 + androidResizableWindow: 0 + androidDefaultWindowWidth: 1920 + androidDefaultWindowHeight: 1080 + androidMinimumWindowWidth: 400 + androidMinimumWindowHeight: 300 + androidFullscreenMode: 1 + defaultIsNativeResolution: 1 + macRetinaSupport: 1 + runInBackground: 1 + captureSingleScreen: 0 + muteOtherAudioSources: 0 + Prepare IOS For Recording: 0 + Force IOS Speakers When Recording: 0 + deferSystemGesturesMode: 0 + hideHomeButton: 0 + submitAnalytics: 1 + usePlayerLog: 1 + bakeCollisionMeshes: 0 + forceSingleInstance: 0 + useFlipModelSwapchain: 1 + resizableWindow: 0 + useMacAppStoreValidation: 0 + macAppStoreCategory: public.app-category.games + gpuSkinning: 0 + xboxPIXTextureCapture: 0 + xboxEnableAvatar: 0 + xboxEnableKinect: 0 + xboxEnableKinectAutoTracking: 0 + xboxEnableFitness: 0 + visibleInBackground: 1 + allowFullscreenSwitch: 1 + fullscreenMode: 1 + xboxSpeechDB: 0 + xboxEnableHeadOrientation: 0 + xboxEnableGuest: 0 + xboxEnablePIXSampling: 0 + metalFramebufferOnly: 0 + xboxOneResolution: 0 + xboxOneSResolution: 0 + xboxOneXResolution: 3 + xboxOneMonoLoggingLevel: 0 + xboxOneLoggingLevel: 1 + xboxOneDisableEsram: 0 + xboxOneEnableTypeOptimization: 0 + xboxOnePresentImmediateThreshold: 0 + switchQueueCommandMemory: 0 + switchQueueControlMemory: 16384 + switchQueueComputeMemory: 262144 + switchNVNShaderPoolsGranularity: 33554432 + switchNVNDefaultPoolsGranularity: 16777216 + switchNVNOtherPoolsGranularity: 16777216 + switchNVNMaxPublicTextureIDCount: 0 + switchNVNMaxPublicSamplerIDCount: 0 + stadiaPresentMode: 0 + stadiaTargetFramerate: 0 + vulkanNumSwapchainBuffers: 3 + vulkanEnableSetSRGBWrite: 0 + vulkanEnableLateAcquireNextImage: 0 + m_SupportedAspectRatios: + 4:3: 1 + 5:4: 1 + 16:10: 1 + 16:9: 1 + Others: 1 + bundleVersion: 0.1 + preloadedAssets: [] + metroInputSource: 0 + wsaTransparentSwapchain: 0 + m_HolographicPauseOnTrackingLoss: 1 + xboxOneDisableKinectGpuReservation: 1 + xboxOneEnable7thCore: 1 + vrSettings: + cardboard: + depthFormat: 0 + enableTransitionView: 0 + daydream: + depthFormat: 0 + useSustainedPerformanceMode: 0 + enableVideoLayer: 0 + useProtectedVideoMemory: 0 + minimumSupportedHeadTracking: 0 + maximumSupportedHeadTracking: 1 + hololens: + depthFormat: 1 + depthBufferSharingEnabled: 1 + lumin: + depthFormat: 0 + frameTiming: 2 + enableGLCache: 0 + glCacheMaxBlobSize: 524288 + glCacheMaxFileSize: 8388608 + oculus: + sharedDepthBuffer: 1 + dashSupport: 1 + lowOverheadMode: 0 + protectedContext: 0 + v2Signing: 1 + enable360StereoCapture: 0 + isWsaHolographicRemotingEnabled: 0 + enableFrameTimingStats: 0 + useHDRDisplay: 0 + D3DHDRBitDepth: 0 + m_ColorGamuts: 00000000 + targetPixelDensity: 30 + resolutionScalingMode: 0 + androidSupportedAspectRatio: 1 + androidMaxAspectRatio: 2.1 + applicationIdentifier: {} + buildNumber: {} + AndroidBundleVersionCode: 1 + AndroidMinSdkVersion: 19 + AndroidTargetSdkVersion: 0 + AndroidPreferredInstallLocation: 1 + aotOptions: + stripEngineCode: 1 + iPhoneStrippingLevel: 0 + iPhoneScriptCallOptimization: 0 + ForceInternetPermission: 0 + ForceSDCardPermission: 0 + CreateWallpaper: 0 + APKExpansionFiles: 0 + keepLoadedShadersAlive: 0 + StripUnusedMeshComponents: 1 + VertexChannelCompressionMask: 4054 + iPhoneSdkVersion: 988 + iOSTargetOSVersionString: 10.0 + tvOSSdkVersion: 0 + tvOSRequireExtendedGameController: 0 + tvOSTargetOSVersionString: 10.0 + uIPrerenderedIcon: 0 + uIRequiresPersistentWiFi: 0 + uIRequiresFullScreen: 1 + uIStatusBarHidden: 1 + uIExitOnSuspend: 0 + uIStatusBarStyle: 0 + appleTVSplashScreen: {fileID: 0} + appleTVSplashScreen2x: {fileID: 0} + tvOSSmallIconLayers: [] + tvOSSmallIconLayers2x: [] + tvOSLargeIconLayers: [] + tvOSLargeIconLayers2x: [] + tvOSTopShelfImageLayers: [] + tvOSTopShelfImageLayers2x: [] + tvOSTopShelfImageWideLayers: [] + tvOSTopShelfImageWideLayers2x: [] + iOSLaunchScreenType: 0 + iOSLaunchScreenPortrait: {fileID: 0} + iOSLaunchScreenLandscape: {fileID: 0} + iOSLaunchScreenBackgroundColor: + serializedVersion: 2 + rgba: 0 + iOSLaunchScreenFillPct: 100 + iOSLaunchScreenSize: 100 + iOSLaunchScreenCustomXibPath: + iOSLaunchScreeniPadType: 0 + iOSLaunchScreeniPadImage: {fileID: 0} + iOSLaunchScreeniPadBackgroundColor: + serializedVersion: 2 + rgba: 0 + iOSLaunchScreeniPadFillPct: 100 + iOSLaunchScreeniPadSize: 100 + iOSLaunchScreeniPadCustomXibPath: + iOSUseLaunchScreenStoryboard: 0 + iOSLaunchScreenCustomStoryboardPath: + iOSDeviceRequirements: [] + iOSURLSchemes: [] + iOSBackgroundModes: 0 + iOSMetalForceHardShadows: 0 + metalEditorSupport: 1 + metalAPIValidation: 1 + iOSRenderExtraFrameOnPause: 0 + iosCopyPluginsCodeInsteadOfSymlink: 0 + appleDeveloperTeamID: + iOSManualSigningProvisioningProfileID: + tvOSManualSigningProvisioningProfileID: + iOSManualSigningProvisioningProfileType: 0 + tvOSManualSigningProvisioningProfileType: 0 + appleEnableAutomaticSigning: 0 + iOSRequireARKit: 0 + iOSAutomaticallyDetectAndAddCapabilities: 1 + appleEnableProMotion: 0 + clonedFromGUID: 5f34be1353de5cf4398729fda238591b + templatePackageId: com.unity.template.2d@3.3.2 + templateDefaultScene: Assets/Scenes/SampleScene.unity + AndroidTargetArchitectures: 1 + AndroidTargetDevices: 0 + AndroidSplashScreenScale: 0 + androidSplashScreen: {fileID: 0} + AndroidKeystoreName: + AndroidKeyaliasName: + AndroidBuildApkPerCpuArchitecture: 0 + AndroidTVCompatibility: 0 + AndroidIsGame: 1 + AndroidEnableTango: 0 + androidEnableBanner: 1 + androidUseLowAccuracyLocation: 0 + androidUseCustomKeystore: 0 + m_AndroidBanners: + - width: 320 + height: 180 + banner: {fileID: 0} + androidGamepadSupportLevel: 0 + chromeosInputEmulation: 1 + AndroidValidateAppBundleSize: 1 + AndroidAppBundleSizeToValidate: 150 + m_BuildTargetIcons: [] + m_BuildTargetPlatformIcons: [] + m_BuildTargetBatching: [] + m_BuildTargetGraphicsJobs: + - m_BuildTarget: MacStandaloneSupport + m_GraphicsJobs: 0 + - m_BuildTarget: Switch + m_GraphicsJobs: 0 + - m_BuildTarget: MetroSupport + m_GraphicsJobs: 0 + - m_BuildTarget: AppleTVSupport + m_GraphicsJobs: 0 + - m_BuildTarget: BJMSupport + m_GraphicsJobs: 0 + - m_BuildTarget: LinuxStandaloneSupport + m_GraphicsJobs: 0 + - m_BuildTarget: PS4Player + m_GraphicsJobs: 0 + - m_BuildTarget: iOSSupport + m_GraphicsJobs: 0 + - m_BuildTarget: WindowsStandaloneSupport + m_GraphicsJobs: 0 + - m_BuildTarget: XboxOnePlayer + m_GraphicsJobs: 0 + - m_BuildTarget: LuminSupport + m_GraphicsJobs: 0 + - m_BuildTarget: AndroidPlayer + m_GraphicsJobs: 0 + - m_BuildTarget: WebGLSupport + m_GraphicsJobs: 0 + m_BuildTargetGraphicsJobMode: + - m_BuildTarget: PS4Player + m_GraphicsJobMode: 0 + - m_BuildTarget: XboxOnePlayer + m_GraphicsJobMode: 0 + m_BuildTargetGraphicsAPIs: + - m_BuildTarget: AndroidPlayer + m_APIs: 150000000b000000 + m_Automatic: 0 + m_BuildTargetVRSettings: [] + openGLRequireES31: 0 + openGLRequireES31AEP: 0 + openGLRequireES32: 0 + m_TemplateCustomTags: {} + mobileMTRendering: + Android: 1 + iPhone: 1 + tvOS: 1 + m_BuildTargetGroupLightmapEncodingQuality: [] + m_BuildTargetGroupLightmapSettings: [] + playModeTestRunnerEnabled: 0 + runPlayModeTestAsEditModeTest: 0 + actionOnDotNetUnhandledException: 1 + enableInternalProfiler: 0 + logObjCUncaughtExceptions: 1 + enableCrashReportAPI: 0 + cameraUsageDescription: + locationUsageDescription: + microphoneUsageDescription: + switchNetLibKey: + switchSocketMemoryPoolSize: 6144 + switchSocketAllocatorPoolSize: 128 + switchSocketConcurrencyLimit: 14 + switchScreenResolutionBehavior: 2 + switchUseCPUProfiler: 0 + switchApplicationID: 0x01004b9000490000 + switchNSODependencies: + switchTitleNames_0: + switchTitleNames_1: + switchTitleNames_2: + switchTitleNames_3: + switchTitleNames_4: + switchTitleNames_5: + switchTitleNames_6: + switchTitleNames_7: + switchTitleNames_8: + switchTitleNames_9: + switchTitleNames_10: + switchTitleNames_11: + switchTitleNames_12: + switchTitleNames_13: + switchTitleNames_14: + switchTitleNames_15: + switchPublisherNames_0: + switchPublisherNames_1: + switchPublisherNames_2: + switchPublisherNames_3: + switchPublisherNames_4: + switchPublisherNames_5: + switchPublisherNames_6: + switchPublisherNames_7: + switchPublisherNames_8: + switchPublisherNames_9: + switchPublisherNames_10: + switchPublisherNames_11: + switchPublisherNames_12: + switchPublisherNames_13: + switchPublisherNames_14: + switchPublisherNames_15: + switchIcons_0: {fileID: 0} + switchIcons_1: {fileID: 0} + switchIcons_2: {fileID: 0} + switchIcons_3: {fileID: 0} + switchIcons_4: {fileID: 0} + switchIcons_5: {fileID: 0} + switchIcons_6: {fileID: 0} + switchIcons_7: {fileID: 0} + switchIcons_8: {fileID: 0} + switchIcons_9: {fileID: 0} + switchIcons_10: {fileID: 0} + switchIcons_11: {fileID: 0} + switchIcons_12: {fileID: 0} + switchIcons_13: {fileID: 0} + switchIcons_14: {fileID: 0} + switchIcons_15: {fileID: 0} + switchSmallIcons_0: {fileID: 0} + switchSmallIcons_1: {fileID: 0} + switchSmallIcons_2: {fileID: 0} + switchSmallIcons_3: {fileID: 0} + switchSmallIcons_4: {fileID: 0} + switchSmallIcons_5: {fileID: 0} + switchSmallIcons_6: {fileID: 0} + switchSmallIcons_7: {fileID: 0} + switchSmallIcons_8: {fileID: 0} + switchSmallIcons_9: {fileID: 0} + switchSmallIcons_10: {fileID: 0} + switchSmallIcons_11: {fileID: 0} + switchSmallIcons_12: {fileID: 0} + switchSmallIcons_13: {fileID: 0} + switchSmallIcons_14: {fileID: 0} + switchSmallIcons_15: {fileID: 0} + switchManualHTML: + switchAccessibleURLs: + switchLegalInformation: + switchMainThreadStackSize: 1048576 + switchPresenceGroupId: + switchLogoHandling: 0 + switchReleaseVersion: 0 + switchDisplayVersion: 1.0.0 + switchStartupUserAccount: 0 + switchTouchScreenUsage: 0 + switchSupportedLanguagesMask: 0 + switchLogoType: 0 + switchApplicationErrorCodeCategory: + switchUserAccountSaveDataSize: 0 + switchUserAccountSaveDataJournalSize: 0 + switchApplicationAttribute: 0 + switchCardSpecSize: -1 + switchCardSpecClock: -1 + switchRatingsMask: 0 + switchRatingsInt_0: 0 + switchRatingsInt_1: 0 + switchRatingsInt_2: 0 + switchRatingsInt_3: 0 + switchRatingsInt_4: 0 + switchRatingsInt_5: 0 + switchRatingsInt_6: 0 + switchRatingsInt_7: 0 + switchRatingsInt_8: 0 + switchRatingsInt_9: 0 + switchRatingsInt_10: 0 + switchRatingsInt_11: 0 + switchRatingsInt_12: 0 + switchLocalCommunicationIds_0: + switchLocalCommunicationIds_1: + switchLocalCommunicationIds_2: + switchLocalCommunicationIds_3: + switchLocalCommunicationIds_4: + switchLocalCommunicationIds_5: + switchLocalCommunicationIds_6: + switchLocalCommunicationIds_7: + switchParentalControl: 0 + switchAllowsScreenshot: 1 + switchAllowsVideoCapturing: 1 + switchAllowsRuntimeAddOnContentInstall: 0 + switchDataLossConfirmation: 0 + switchUserAccountLockEnabled: 0 + switchSystemResourceMemory: 16777216 + switchSupportedNpadStyles: 22 + switchNativeFsCacheSize: 32 + switchIsHoldTypeHorizontal: 0 + switchSupportedNpadCount: 8 + switchSocketConfigEnabled: 0 + switchTcpInitialSendBufferSize: 32 + switchTcpInitialReceiveBufferSize: 64 + switchTcpAutoSendBufferSizeMax: 256 + switchTcpAutoReceiveBufferSizeMax: 256 + switchUdpSendBufferSize: 9 + switchUdpReceiveBufferSize: 42 + switchSocketBufferEfficiency: 4 + switchSocketInitializeEnabled: 1 + switchNetworkInterfaceManagerInitializeEnabled: 1 + switchPlayerConnectionEnabled: 1 + switchUseMicroSleepForYield: 1 + switchMicroSleepForYieldTime: 25 + ps4NPAgeRating: 12 + ps4NPTitleSecret: + ps4NPTrophyPackPath: + ps4ParentalLevel: 11 + ps4ContentID: ED1633-NPXX51362_00-0000000000000000 + ps4Category: 0 + ps4MasterVersion: 01.00 + ps4AppVersion: 01.00 + ps4AppType: 0 + ps4ParamSfxPath: + ps4VideoOutPixelFormat: 0 + ps4VideoOutInitialWidth: 1920 + ps4VideoOutBaseModeInitialWidth: 1920 + ps4VideoOutReprojectionRate: 60 + ps4PronunciationXMLPath: + ps4PronunciationSIGPath: + ps4BackgroundImagePath: + ps4StartupImagePath: + ps4StartupImagesFolder: + ps4IconImagesFolder: + ps4SaveDataImagePath: + ps4SdkOverride: + ps4BGMPath: + ps4ShareFilePath: + ps4ShareOverlayImagePath: + ps4PrivacyGuardImagePath: + ps4ExtraSceSysFile: + ps4NPtitleDatPath: + ps4RemotePlayKeyAssignment: -1 + ps4RemotePlayKeyMappingDir: + ps4PlayTogetherPlayerCount: 0 + ps4EnterButtonAssignment: 1 + ps4ApplicationParam1: 0 + ps4ApplicationParam2: 0 + ps4ApplicationParam3: 0 + ps4ApplicationParam4: 0 + ps4DownloadDataSize: 0 + ps4GarlicHeapSize: 2048 + ps4ProGarlicHeapSize: 2560 + playerPrefsMaxSize: 32768 + ps4Passcode: frAQBc8Wsa1xVPfvJcrgRYwTiizs2trQ + ps4pnSessions: 1 + ps4pnPresence: 1 + ps4pnFriends: 1 + ps4pnGameCustomData: 1 + playerPrefsSupport: 0 + enableApplicationExit: 0 + resetTempFolder: 1 + restrictedAudioUsageRights: 0 + ps4UseResolutionFallback: 0 + ps4ReprojectionSupport: 0 + ps4UseAudio3dBackend: 0 + ps4UseLowGarlicFragmentationMode: 1 + ps4SocialScreenEnabled: 0 + ps4ScriptOptimizationLevel: 0 + ps4Audio3dVirtualSpeakerCount: 14 + ps4attribCpuUsage: 0 + ps4PatchPkgPath: + ps4PatchLatestPkgPath: + ps4PatchChangeinfoPath: + ps4PatchDayOne: 0 + ps4attribUserManagement: 0 + ps4attribMoveSupport: 0 + ps4attrib3DSupport: 0 + ps4attribShareSupport: 0 + ps4attribExclusiveVR: 0 + ps4disableAutoHideSplash: 0 + ps4videoRecordingFeaturesUsed: 0 + ps4contentSearchFeaturesUsed: 0 + ps4CompatibilityPS5: 0 + ps4AllowPS5Detection: 0 + ps4GPU800MHz: 1 + ps4attribEyeToEyeDistanceSettingVR: 0 + ps4IncludedModules: [] + ps4attribVROutputEnabled: 0 + ps5ParamFilePath: + ps5VideoOutPixelFormat: 0 + ps5VideoOutInitialWidth: 1920 + ps5VideoOutOutputMode: 1 + ps5BackgroundImagePath: + ps5StartupImagePath: + ps5Pic2Path: + ps5StartupImagesFolder: + ps5IconImagesFolder: + ps5SaveDataImagePath: + ps5SdkOverride: + ps5BGMPath: + ps5ShareOverlayImagePath: + ps5NPConfigZipPath: + ps5Passcode: frAQBc8Wsa1xVPfvJcrgRYwTiizs2trQ + ps5UseResolutionFallback: 0 + ps5UseAudio3dBackend: 0 + ps5ScriptOptimizationLevel: 2 + ps5Audio3dVirtualSpeakerCount: 14 + ps5UpdateReferencePackage: + ps5disableAutoHideSplash: 0 + ps5OperatingSystemCanDisableSplashScreen: 0 + ps5IncludedModules: [] + ps5SharedBinaryContentLabels: [] + ps5SharedBinarySystemFolders: [] + monoEnv: + splashScreenBackgroundSourceLandscape: {fileID: 0} + splashScreenBackgroundSourcePortrait: {fileID: 0} + blurSplashScreenBackground: 1 + spritePackerPolicy: + webGLMemorySize: 16 + webGLExceptionSupport: 1 + webGLNameFilesAsHashes: 0 + webGLDataCaching: 1 + webGLDebugSymbols: 0 + webGLEmscriptenArgs: + webGLModulesDirectory: + webGLTemplate: APPLICATION:Default + webGLAnalyzeBuildSize: 0 + webGLUseEmbeddedResources: 0 + webGLCompressionFormat: 1 + webGLLinkerTarget: 1 + webGLThreadsSupport: 0 + webGLWasmStreaming: 0 + scriptingDefineSymbols: + 1: UNITY + platformArchitecture: {} + scriptingBackend: {} + il2cppCompilerConfiguration: {} + managedStrippingLevel: {} + incrementalIl2cppBuild: {} + suppressCommonWarnings: 1 + allowUnsafeCode: 0 + additionalIl2CppArgs: + scriptingRuntimeVersion: 1 + gcIncremental: 0 + assemblyVersionValidation: 1 + gcWBarrierValidation: 0 + apiCompatibilityLevelPerPlatform: {} + m_RenderingPath: 1 + m_MobileRenderingPath: 1 + metroPackageName: Template_2D + metroPackageVersion: + metroCertificatePath: + metroCertificatePassword: + metroCertificateSubject: + metroCertificateIssuer: + metroCertificateNotAfter: 0000000000000000 + metroApplicationDescription: Template_2D + wsaImages: {} + metroTileShortName: + metroTileShowName: 0 + metroMediumTileShowName: 0 + metroLargeTileShowName: 0 + metroWideTileShowName: 0 + metroSupportStreamingInstall: 0 + metroLastRequiredScene: 0 + metroDefaultTileSize: 1 + metroTileForegroundText: 2 + metroTileBackgroundColor: {r: 0.13333334, g: 0.17254902, b: 0.21568628, a: 0} + metroSplashScreenBackgroundColor: {r: 0.12941177, g: 0.17254902, b: 0.21568628, + a: 1} + metroSplashScreenUseBackgroundColor: 0 + platformCapabilities: {} + metroTargetDeviceFamilies: {} + metroFTAName: + metroFTAFileTypes: [] + metroProtocolName: + XboxOneProductId: + XboxOneUpdateKey: + XboxOneSandboxId: + XboxOneContentId: + XboxOneTitleId: + XboxOneSCId: + XboxOneGameOsOverridePath: + XboxOnePackagingOverridePath: + XboxOneAppManifestOverridePath: + XboxOneVersion: 1.0.0.0 + XboxOnePackageEncryption: 0 + XboxOnePackageUpdateGranularity: 2 + XboxOneDescription: + XboxOneLanguage: + - enus + XboxOneCapability: [] + XboxOneGameRating: {} + XboxOneIsContentPackage: 0 + XboxOneEnhancedXboxCompatibilityMode: 0 + XboxOneEnableGPUVariability: 1 + XboxOneSockets: {} + XboxOneSplashScreen: {fileID: 0} + XboxOneAllowedProductIds: [] + XboxOnePersistentLocalStorageSize: 0 + XboxOneXTitleMemory: 8 + XboxOneOverrideIdentityName: + XboxOneOverrideIdentityPublisher: + vrEditorSettings: + daydream: + daydreamIconForeground: {fileID: 0} + daydreamIconBackground: {fileID: 0} + cloudServicesEnabled: + UNet: 1 + luminIcon: + m_Name: + m_ModelFolderPath: + m_PortalFolderPath: + luminCert: + m_CertPath: + m_SignPackage: 1 + luminIsChannelApp: 0 + luminVersion: + m_VersionCode: 1 + m_VersionName: + apiCompatibilityLevel: 6 + cloudProjectId: + framebufferDepthMemorylessMode: 0 + projectName: + organizationId: + cloudEnabled: 0 + enableNativePlatformBackendsForNewInputSystem: 0 + disableOldInputManagerSupport: 0 + legacyClampBlendShapeWeights: 0 diff --git a/ProjectSettings/ProjectVersion.txt b/ProjectSettings/ProjectVersion.txt new file mode 100644 index 0000000..bf8f7eb --- /dev/null +++ b/ProjectSettings/ProjectVersion.txt @@ -0,0 +1,2 @@ +m_EditorVersion: 2019.4.36f1 +m_EditorVersionWithRevision: 2019.4.36f1 (660c164b2fc5) diff --git a/ProjectSettings/QualitySettings.asset b/ProjectSettings/QualitySettings.asset new file mode 100644 index 0000000..84c1610 --- /dev/null +++ b/ProjectSettings/QualitySettings.asset @@ -0,0 +1,192 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!47 &1 +QualitySettings: + m_ObjectHideFlags: 0 + serializedVersion: 5 + m_CurrentQuality: 3 + m_QualitySettings: + - serializedVersion: 2 + name: Very Low + pixelLightCount: 0 + shadows: 0 + shadowResolution: 0 + shadowProjection: 1 + shadowCascades: 1 + shadowDistance: 15 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 0 + blendWeights: 1 + textureQuality: 1 + anisotropicTextures: 0 + antiAliasing: 0 + softParticles: 0 + softVegetation: 0 + realtimeReflectionProbes: 0 + billboardsFaceCameraPosition: 0 + vSyncCount: 0 + lodBias: 0.3 + maximumLODLevel: 0 + particleRaycastBudget: 4 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + resolutionScalingFixedDPIFactor: 1 + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: Low + pixelLightCount: 0 + shadows: 0 + shadowResolution: 0 + shadowProjection: 1 + shadowCascades: 1 + shadowDistance: 20 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 0 + blendWeights: 2 + textureQuality: 0 + anisotropicTextures: 0 + antiAliasing: 0 + softParticles: 0 + softVegetation: 0 + realtimeReflectionProbes: 0 + billboardsFaceCameraPosition: 0 + vSyncCount: 0 + lodBias: 0.4 + maximumLODLevel: 0 + particleRaycastBudget: 16 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + resolutionScalingFixedDPIFactor: 1 + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: Medium + pixelLightCount: 1 + shadows: 0 + shadowResolution: 0 + shadowProjection: 1 + shadowCascades: 1 + shadowDistance: 20 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 0 + blendWeights: 2 + textureQuality: 0 + anisotropicTextures: 0 + antiAliasing: 0 + softParticles: 0 + softVegetation: 0 + realtimeReflectionProbes: 0 + billboardsFaceCameraPosition: 0 + vSyncCount: 1 + lodBias: 0.7 + maximumLODLevel: 0 + particleRaycastBudget: 64 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + resolutionScalingFixedDPIFactor: 1 + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: High + pixelLightCount: 2 + shadows: 0 + shadowResolution: 1 + shadowProjection: 1 + shadowCascades: 2 + shadowDistance: 40 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 1 + blendWeights: 2 + textureQuality: 0 + anisotropicTextures: 0 + antiAliasing: 0 + softParticles: 0 + softVegetation: 1 + realtimeReflectionProbes: 0 + billboardsFaceCameraPosition: 0 + vSyncCount: 1 + lodBias: 1 + maximumLODLevel: 0 + particleRaycastBudget: 256 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + resolutionScalingFixedDPIFactor: 1 + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: Very High + pixelLightCount: 3 + shadows: 0 + shadowResolution: 2 + shadowProjection: 1 + shadowCascades: 2 + shadowDistance: 70 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 1 + blendWeights: 4 + textureQuality: 0 + anisotropicTextures: 0 + antiAliasing: 0 + softParticles: 0 + softVegetation: 1 + realtimeReflectionProbes: 0 + billboardsFaceCameraPosition: 0 + vSyncCount: 1 + lodBias: 1.5 + maximumLODLevel: 0 + particleRaycastBudget: 1024 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + resolutionScalingFixedDPIFactor: 1 + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: Ultra + pixelLightCount: 4 + shadows: 0 + shadowResolution: 0 + shadowProjection: 1 + shadowCascades: 4 + shadowDistance: 150 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 1 + blendWeights: 4 + textureQuality: 0 + anisotropicTextures: 0 + antiAliasing: 0 + softParticles: 0 + softVegetation: 1 + realtimeReflectionProbes: 0 + billboardsFaceCameraPosition: 0 + vSyncCount: 1 + lodBias: 2 + maximumLODLevel: 0 + particleRaycastBudget: 4096 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + resolutionScalingFixedDPIFactor: 1 + excludedTargetPlatforms: [] + m_PerPlatformDefaultQuality: + Android: 2 + Nintendo 3DS: 5 + Nintendo Switch: 5 + PS4: 5 + PSM: 5 + PSP2: 2 + Stadia: 5 + Standalone: 5 + Tizen: 2 + WebGL: 3 + WiiU: 5 + Windows Store Apps: 5 + XboxOne: 5 + iPhone: 2 + tvOS: 2 diff --git a/ProjectSettings/TagManager.asset b/ProjectSettings/TagManager.asset new file mode 100644 index 0000000..1c92a78 --- /dev/null +++ b/ProjectSettings/TagManager.asset @@ -0,0 +1,43 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!78 &1 +TagManager: + serializedVersion: 2 + tags: [] + layers: + - Default + - TransparentFX + - Ignore Raycast + - + - Water + - UI + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + m_SortingLayers: + - name: Default + uniqueID: 0 + locked: 0 diff --git a/ProjectSettings/TimeManager.asset b/ProjectSettings/TimeManager.asset new file mode 100644 index 0000000..06bcc6d --- /dev/null +++ b/ProjectSettings/TimeManager.asset @@ -0,0 +1,9 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!5 &1 +TimeManager: + m_ObjectHideFlags: 0 + Fixed Timestep: 0.02 + Maximum Allowed Timestep: 0.1 + m_TimeScale: 1 + Maximum Particle Timestep: 0.03 diff --git a/ProjectSettings/UnityConnectSettings.asset b/ProjectSettings/UnityConnectSettings.asset new file mode 100644 index 0000000..fa0b146 --- /dev/null +++ b/ProjectSettings/UnityConnectSettings.asset @@ -0,0 +1,34 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!310 &1 +UnityConnectSettings: + m_ObjectHideFlags: 0 + serializedVersion: 1 + m_Enabled: 0 + m_TestMode: 0 + m_EventOldUrl: https://api.uca.cloud.unity3d.com/v1/events + m_EventUrl: https://cdp.cloud.unity3d.com/v1/events + m_ConfigUrl: https://config.uca.cloud.unity3d.com + m_TestInitMode: 0 + CrashReportingSettings: + m_EventUrl: https://perf-events.cloud.unity3d.com + m_Enabled: 0 + m_LogBufferSize: 10 + m_CaptureEditorExceptions: 1 + UnityPurchasingSettings: + m_Enabled: 0 + m_TestMode: 0 + UnityAnalyticsSettings: + m_Enabled: 0 + m_TestMode: 0 + m_InitializeOnStartup: 1 + UnityAdsSettings: + m_Enabled: 0 + m_InitializeOnStartup: 1 + m_TestMode: 0 + m_IosGameId: + m_AndroidGameId: + m_GameIds: {} + m_GameId: + PerformanceReportingSettings: + m_Enabled: 0 diff --git a/ProjectSettings/VFXManager.asset b/ProjectSettings/VFXManager.asset new file mode 100644 index 0000000..3a95c98 --- /dev/null +++ b/ProjectSettings/VFXManager.asset @@ -0,0 +1,12 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!937362698 &1 +VFXManager: + m_ObjectHideFlags: 0 + m_IndirectShader: {fileID: 0} + m_CopyBufferShader: {fileID: 0} + m_SortShader: {fileID: 0} + m_StripUpdateShader: {fileID: 0} + m_RenderPipeSettingsPath: + m_FixedTimeStep: 0.016666668 + m_MaxDeltaTime: 0.05 diff --git a/ProjectSettings/XRSettings.asset b/ProjectSettings/XRSettings.asset new file mode 100644 index 0000000..482590c --- /dev/null +++ b/ProjectSettings/XRSettings.asset @@ -0,0 +1,10 @@ +{ + "m_SettingKeys": [ + "VR Device Disabled", + "VR Device User Alert" + ], + "m_SettingValues": [ + "False", + "False" + ] +} \ No newline at end of file