using System; using System.Collections.Generic; using ProtoBuf; using Sandbox.Game.Entities; using Sandbox.ModAPI; using VRage.Game; using VRage.Game.Components; using VRage.Game.Entity; using VRage.ModAPI; using VRage.Utils; namespace SENetworkAPI { public enum TransferType { ServerToClient, ClientToServer, Both } public enum SyncType { Post, Fetch, Broadcast, None } [ProtoContract] internal class SyncData { [ProtoMember(3)] public byte[] Data; [ProtoMember(2)] public long EntityId; [ProtoMember(1)] public long Id; [ProtoMember(4)] public SyncType SyncType; } public abstract class NetSync { internal static Dictionary> PropertiesByEntity = new Dictionary>(); internal static Dictionary PropertyById = new Dictionary(); internal static object locker = new object(); internal static long generatorId = 1; /// /// Triggers after recieving a fetch request from clients /// and allows you to modify this property before it is sent. /// public Action BeforeFetchRequestResponse; /// /// The allowed network communication direction /// public TransferType TransferType { get; internal set; } /// /// The identity of this property /// public long Id { get; internal set; } /// /// Enables/Disables network traffic out when setting a value /// public bool SyncOnLoad { get; internal set; } /// /// Limits sync updates to within sync distance /// public bool LimitToSyncDistance { get; internal set; } /// /// the last recorded network traffic /// public long LastMessageTimestamp { get; internal set; } internal static long GeneratePropertyId() { return generatorId++; } /// /// Request the lastest value from the server /// public abstract void Fetch(); internal abstract void Push(SyncType type, ulong sendTo); internal abstract void SetNetworkValue(byte[] data, ulong sender); } public class NetSync : NetSync { private readonly string sessionName; private T _value; private MyEntity Entity; /// /// Fires each time the value is changed /// Provides the old value and the new value /// public Action ValueChanged; /// /// Fires only when the a network call is made /// Provides the old value and the new value /// also provides the steamId /// public Action ValueChangedByNetwork; /// IMyEntity object this property is attached to /// /// Sets an initial value /// automatically syncs data to clients when the class initializes /// marking this true only sends data to clients within sync distance public NetSync(IMyEntity entity, TransferType transferType, T startingValue = default, bool syncOnLoad = true, bool limitToSyncDistance = true) { if (entity == null) throw new Exception("[NetworkAPI] Attemped to create a NetSync property. MyEntity was null."); Init(entity as MyEntity, transferType, startingValue, syncOnLoad, limitToSyncDistance); } /// MyEntity object this property is attached to /// /// Sets an initial value /// automatically syncs data to clients when the class initializes /// marking this true only sends data to clients within sync distance public NetSync(MyEntity entity, TransferType transferType, T startingValue = default, bool syncOnLoad = true, bool limitToSyncDistance = true) { if (entity == null) throw new Exception("[NetworkAPI] Attemped to create a NetSync property. MyEntity was null."); Init(entity, transferType, startingValue, syncOnLoad, limitToSyncDistance); } /// MyGameLogicComponent object this property is attached to /// /// Sets an initial value /// automatically syncs data to clients when the class initializes /// marking this true only sends data to clients within sync distance public NetSync(MyGameLogicComponent logic, TransferType transferType, T startingValue = default, bool syncOnLoad = true, bool limitToSyncDistance = true) { if (logic?.Entity == null) throw new Exception( "[NetworkAPI] Attemped to create a NetSync property. MyGameLogicComponent was null."); Init(logic.Entity as MyEntity, transferType, startingValue, syncOnLoad, limitToSyncDistance); } /// MySessionComponentBase object this property is attached to /// /// Sets an initial value /// automatically syncs data to clients when the class initializes /// marking this true only sends data to clients within sync distance public NetSync(MySessionComponentBase logic, TransferType transferType, T startingValue = default, bool syncOnLoad = true, bool limitToSyncDistance = true) { if (logic == null) throw new Exception( "[NetworkAPI] Attemped to create a NetSync property. MySessionComponentBase was null."); sessionName = logic.GetType().Name; Init(null, transferType, startingValue, syncOnLoad, limitToSyncDistance); } /// /// this property syncs across the network when changed /// public T Value { get => _value; set => SetValue(value, SyncType.Broadcast); } /// /// This funtion is called by the constructer /// /// /// Sets an initial value /// automatically syncs data to clients when the class initializes /// marking this true only sends data to clients within sync distance private void Init(MyEntity entity, TransferType transferType, T startingValue = default, bool syncOnLoad = true, bool limitToSyncDistance = true) { TransferType = transferType; _value = startingValue; SyncOnLoad = syncOnLoad; LimitToSyncDistance = limitToSyncDistance; if (entity != null) { Entity = entity; Entity.OnClose += Entity_OnClose; if (PropertiesByEntity.ContainsKey(Entity)) { PropertiesByEntity[Entity].Add(this); Id = PropertiesByEntity[Entity].Count - 1; } else { PropertiesByEntity.Add(Entity, new List { this }); Id = 0; } } else { lock (locker) { Id = GeneratePropertyId(); PropertyById.Add(Id, this); } } if (SyncOnLoad) { if (Entity != null) Entity.AddedToScene += SyncOnAddedToScene; else Fetch(); } if (NetworkAPI.LogNetworkTraffic) MyLog.Default.Info( $"[NetworkAPI] Property Created: {Descriptor()}, Transfer: {transferType}, SyncOnLoad: {SyncOnLoad}"); } private void SyncOnAddedToScene(MyEntity e) { if (Entity != e) return; Fetch(); Entity.AddedToScene -= SyncOnAddedToScene; } private void Entity_OnClose(MyEntity entity) { PropertyById.Remove(Id); } /// /// Allows you to change how syncing works when setting the value this way /// public void SetValue(T val, SyncType syncType = SyncType.None) { var oldval = _value; lock (_value) { _value = val; } SendValue(syncType); ValueChanged?.Invoke(oldval, val); } /// /// Sets the data received over the network /// internal override void SetNetworkValue(byte[] data, ulong sender) { try { var oldval = _value; lock (_value) { _value = MyAPIGateway.Utilities.SerializeFromBinary(data); if (NetworkAPI.LogNetworkTraffic) MyLog.Default.Info($"[NetworkAPI] {Descriptor()} New value: {oldval} --- Old value: {_value}"); } if (MyAPIGateway.Multiplayer.IsServer) SendValue(); ValueChanged?.Invoke(oldval, _value); ValueChangedByNetwork?.Invoke(oldval, _value, sender); } catch (Exception e) { MyLog.Default.Error($"[NetworkAPI] Failed to deserialize network property data\n{e}"); } } /// /// sends the value across the network /// private void SendValue(SyncType syncType = SyncType.Broadcast, ulong sendTo = ulong.MinValue) { try { if (!NetworkAPI.IsInitialized) { MyLog.Default.Error( "[NetworkAPI] _ERROR_ The NetworkAPI has not been initialized. Use NetworkAPI.Init() to initialize it."); return; } if (syncType == SyncType.None) { if (NetworkAPI.LogNetworkTraffic) MyLog.Default.Info($"[NetworkAPI] _INTERNAL_ {Descriptor()} Wont send value: {Value}"); return; } if ((syncType != SyncType.Fetch && TransferType == TransferType.ServerToClient && !MyAPIGateway.Multiplayer.IsServer) || (TransferType == TransferType.ClientToServer && MyAPIGateway.Multiplayer.IsServer)) { if (NetworkAPI.LogNetworkTraffic) MyLog.Default.Info( $"[NetworkAPI] {Descriptor()} Bad send direction transfer type is {TransferType}"); return; } if (MyAPIGateway.Session.OnlineMode == MyOnlineModeEnum.OFFLINE) { if (NetworkAPI.LogNetworkTraffic) MyLog.Default.Info($"[NetworkAPI] _OFFLINE_ {Descriptor()} Wont send value: {Value}"); return; } if (Value == null) { if (NetworkAPI.LogNetworkTraffic) MyLog.Default.Error( $"[NetworkAPI] _ERROR_ {Descriptor()} Value is null. Cannot transmit null value."); return; } var data = new SyncData { Id = Id, EntityId = Entity != null ? Entity.EntityId : 0, Data = MyAPIGateway.Utilities.SerializeToBinary(_value), SyncType = syncType }; var id = ulong.MinValue; if (MyAPIGateway.Session?.LocalHumanPlayer != null) id = MyAPIGateway.Session.LocalHumanPlayer.SteamUserId; if (id == sendTo && id != ulong.MinValue) MyLog.Default.Error( $"[NetworkAPI] _ERROR_ {Descriptor()} The sender id is the same as the recievers id. data will not be sent."); if (NetworkAPI.LogNetworkTraffic) MyLog.Default.Info( $"[NetworkAPI] _TRANSMITTING_ {Descriptor()} - Id:{data.Id}, EId:{data.EntityId}, {data.SyncType}, {(data.SyncType == SyncType.Fetch ? "" : $"Val:{_value}")}"); if (LimitToSyncDistance && Entity != null) NetworkAPI.Instance.SendCommand( new Command { IsProperty = true, Data = MyAPIGateway.Utilities.SerializeToBinary(data), SteamId = id }, Entity.PositionComp.GetPosition(), steamId: sendTo); else NetworkAPI.Instance.SendCommand( new Command { IsProperty = true, Data = MyAPIGateway.Utilities.SerializeToBinary(data), SteamId = id }, sendTo); } catch (Exception e) { MyLog.Default.Error($"[NetworkAPI] _ERROR_ SendValue(): Problem syncing value: {e}"); } } /// /// Receives and redirects all property traffic /// /// this hold the path to the property and the data to sync internal static void RouteMessage(SyncData pack, ulong sender, long timestamp) { if (pack == null) { MyLog.Default.Error("[NetworkAPI] Property data is null"); return; } if (NetworkAPI.LogNetworkTraffic) MyLog.Default.Info($"[NetworkAPI] Id:{pack.Id}, EId:{pack.EntityId}, {pack.SyncType}"); NetSync property; if (pack.EntityId == 0) { if (!PropertyById.ContainsKey(pack.Id)) { MyLog.Default.Info("[NetworkAPI] id not registered in dictionary 'PropertyById'"); return; } property = PropertyById[pack.Id]; } else { var entity = (MyEntity)MyAPIGateway.Entities.GetEntityById(pack.EntityId); if (entity == null) { MyLog.Default.Info("[NetworkAPI] Failed to get entity by id"); return; } if (!PropertiesByEntity.ContainsKey(entity)) { MyLog.Default.Info("[NetworkAPI] Entity not registered in dictionary 'PropertiesByEntity'"); return; } var properties = PropertiesByEntity[entity]; if (pack.Id >= properties.Count) { MyLog.Default.Info("[NetworkAPI] property index out of range"); return; } property = properties[(int)pack.Id]; } property.LastMessageTimestamp = timestamp; if (pack.SyncType == SyncType.Fetch) { property.BeforeFetchRequestResponse?.Invoke(sender); property.Push(SyncType.Post, sender); } else { property.SetNetworkValue(pack.Data, sender); } } /// /// Request the lastest value from the server /// Servers are not allowed to fetch from clients /// public override void Fetch() { if (!MyAPIGateway.Multiplayer.IsServer) SendValue(SyncType.Fetch); } /// /// Send data now /// public void Push() { SendValue(); } /// /// Send data to single user /// public void Push(ulong sendTo) { SendValue(SyncType.Post, sendTo); } /// /// Send data across the network now /// internal override void Push(SyncType type, ulong sendTo = ulong.MinValue) { SendValue(type, sendTo); } /// /// Identifier for logging readability /// internal string Descriptor() { if (Entity != null) { if (Entity is MyCubeBlock) return $"<{(Entity as MyCubeBlock).CubeGrid.DisplayName}_{(Entity.DefinitionId?.SubtypeId == null ? Entity.GetType().Name : Entity.DefinitionId?.SubtypeId.ToString())}.{Entity.EntityId}_{typeof(T).Name}.{Id}>"; return $"<{(Entity.DefinitionId?.SubtypeId == null ? Entity.GetType().Name : Entity.DefinitionId?.SubtypeId.ToString())}.{Entity.EntityId}_{typeof(T).Name}.{Id}>"; } return $"<{sessionName}_{typeof(T).Name}.{Id}>"; } } }