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 }