using System; using System.Collections.Generic; using Sandbox.ModAPI; using VRage; using VRage.Utils; using VRageMath; namespace SENetworkAPI { public enum NetworkTypes { Dedicated, Server, Client } public abstract class NetworkAPI { public const int CompressionThreshold = 100000; public static NetworkAPI Instance; public static bool LogNetworkTraffic = false; /// /// Gets the diffrence between now and a given timestamp in frames (60 fps) /// /// /// private static readonly double frames = 1000d / 60d; public readonly ushort ComId; public readonly string Keyword; public readonly string ModName; internal Dictionary> ChatCommands = new Dictionary>(); internal Dictionary> NetworkCommands = new Dictionary>(); /// /// Event driven client, server syncing API. /// /// The communication channel this mod will listen on /// The title use for displaying chat messages /// The string identifying a chat command public NetworkAPI(ushort comId, string modName, string keyword = null) { ComId = comId; ModName = modName == null ? string.Empty : modName; Keyword = keyword != null ? keyword.ToLower() : null; if (UsingTextCommands) { MyAPIGateway.Utilities.MessageEntered -= HandleChatInput; MyAPIGateway.Utilities.MessageEntered += HandleChatInput; } MyAPIGateway.Multiplayer.UnregisterMessageHandler(ComId, HandleIncomingPacket); MyAPIGateway.Multiplayer.RegisterMessageHandler(ComId, HandleIncomingPacket); MyLog.Default.Info( $"[NetworkAPI] Initialized. Type: {GetType().Name} ComId: {ComId} Name: {ModName} Keyword: {Keyword}"); } public static bool IsInitialized => Instance != null; internal bool UsingTextCommands => Keyword != null; /// /// Event triggers apon reciveing data over the network /// steamId, command, data /// public event Action OnCommandRecived; /// /// Invokes chat command events /// /// Chat message string /// should be shown normally in global chat private void HandleChatInput(string messageText, ref bool sendToOthers) { var args = messageText.ToLower().Split(' '); if (args[0] != Keyword) return; sendToOthers = false; var arguments = messageText.Substring(Keyword.Length).Trim(' '); // Meh... this is kinda yucky if (args.Length == 1 && ChatCommands.ContainsKey(string.Empty)) { ChatCommands[string.Empty]?.Invoke(string.Empty); } else if (args.Length > 1 && ChatCommands.ContainsKey(args[1])) { ChatCommands[args[1]]?.Invoke(arguments.Substring(args[1].Length).Trim(' ')); } else { if (!MyAPIGateway.Utilities.IsDedicated) MyAPIGateway.Utilities.ShowMessage(ModName, "Command not recognized."); } } /// /// Unpacks commands and handles arguments /// /// Data chunck recived from the network private void HandleIncomingPacket(byte[] msg) { try { var cmd = MyAPIGateway.Utilities.SerializeFromBinary(msg); if (LogNetworkTraffic) { MyLog.Default.Info("[NetworkAPI] ----- TRANSMISSION RECIEVED -----"); MyLog.Default.Info( $"[NetworkAPI] Type: {(cmd.IsProperty ? "Property" : $"Command ID: {cmd.CommandString}")}, {(cmd.IsCompressed ? "Compressed, " : "")}From: {cmd.SteamId} "); } if (cmd.IsCompressed) { cmd.Data = MyCompression.Decompress(cmd.Data); cmd.IsCompressed = false; } if (cmd.IsProperty) { NetSync.RouteMessage(MyAPIGateway.Utilities.SerializeFromBinary(cmd.Data), cmd.SteamId, cmd.Timestamp); } else { if (!string.IsNullOrWhiteSpace(cmd.Message)) { if (!MyAPIGateway.Utilities.IsDedicated) if (MyAPIGateway.Session != null) MyAPIGateway.Utilities.ShowMessage(ModName, cmd.Message); if (MyAPIGateway.Multiplayer.IsServer) SendCommand(null, cmd.Message); } if (cmd.CommandString != null) { OnCommandRecived?.Invoke(cmd.SteamId, cmd.CommandString, cmd.Data, new DateTime(cmd.Timestamp)); var command = cmd.CommandString.Split(' ')[0]; if (NetworkCommands.ContainsKey(command)) NetworkCommands[command]?.Invoke(cmd.SteamId, cmd.CommandString, cmd.Data, new DateTime(cmd.Timestamp)); } } if (LogNetworkTraffic) MyLog.Default.Info("[NetworkAPI] ----- END -----"); } catch (Exception e) { MyLog.Default.Error($"[NetworkAPI] Failure in message processing:\n{e}"); } } /// /// Registers a callback that will fire when the command string is sent /// /// The command that triggers the callback /// The function that runs when a command is recived public void RegisterNetworkCommand(string command, Action callback) { if (command == null) throw new Exception( "[NetworkAPI] Cannot register a command using null. null is reserved for chat messages."); command = command.ToLower(); if (NetworkCommands.ContainsKey(command)) throw new Exception( $"[NetworkAPI] Failed to add the network command callback '{command}'. A command with the same name was already added."); NetworkCommands.Add(command, callback); } /// /// Unregisters a command /// /// public void UnregisterNetworkCommand(string command) { if (NetworkCommands.ContainsKey(command)) NetworkCommands.Remove(command); } /// /// will trigger when you type /// /// /// /// this is the text command that will be typed into chat /// this is the function that will be called when the keyword is typed public void RegisterChatCommand(string command, Action callback) { if (command == null) command = string.Empty; command = command.ToLower(); if (ChatCommands.ContainsKey(command)) throw new Exception( $"[NetworkAPI] Failed to add the network command callback '{command}'. A command with the same name was already added."); ChatCommands.Add(command, callback); } /// /// Unregisters a chat command /// /// the chat command to unregister public void UnregisterChatCommand(string command) { if (ChatCommands.ContainsKey(command)) ChatCommands.Remove(command); } /// /// Sends a command packet across the network /// /// The command word and any arguments delimidated with spaces /// Text to be writen in chat /// A serialized object used to send game information /// The date timestamp this command was sent /// A players steam id /// Makes sure the data gets to the target public abstract void SendCommand(string commandString, string message = null, byte[] data = null, DateTime? sent = null, ulong steamId = ulong.MinValue, bool isReliable = true); /// /// Sends a command packet across the network /// /// The command word and any arguments delimidated with spaces /// /// /// Text to be writen in chat /// A serialized object used to send game information /// The date timestamp this command was sent /// A players steam id /// Makes sure the data gets to the target public abstract void SendCommand(string commandString, Vector3D point, double radius = 0, string message = null, byte[] data = null, DateTime? sent = null, ulong steamId = ulong.MinValue, bool isReliable = true); /// /// Sends a command packet to the server / client /// /// The object to be sent across the network /// the id of the user this is being sent to. 0 sends it to all users in range /// make sure the packet reaches its destination internal abstract void SendCommand(Command cmd, ulong steamId = ulong.MinValue, bool isReliable = true); /// /// Sends a command packet to the server / client if in range /// /// The object to be sent across the network /// the center of the sending sphere /// the radius of the sending sphere /// the id of the user this is being sent to. 0 sends it to all users in range /// make sure the packet reaches its destination internal abstract void SendCommand(Command cmd, Vector3D point, double range = 0, ulong steamId = ulong.MinValue, bool isReliable = true); /// /// Posts text into the ingame chat. /// /// public abstract void Say(string message); /// /// Unregisters listeners /// [ObsoleteAttribute("This property is obsolete. Close is no longer required", false)] public void Close() { MyLog.Default.Info($"[NetworkAPI] Unregistering communication stream: {ComId}"); if (UsingTextCommands) MyAPIGateway.Utilities.MessageEntered -= HandleChatInput; MyAPIGateway.Multiplayer.UnregisterMessageHandler(ComId, HandleIncomingPacket); } /// /// Calls Instance.Close() /// [ObsoleteAttribute("This property is obsolete. Dispose is no longer required", false)] public static void Dispose() { if (IsInitialized) Instance.Close(); Instance = null; } /// /// Initializes the default instance of the NetworkAPI /// public static void Init(ushort comId, string modName, string keyword = null) { if (IsInitialized) return; if (!MyAPIGateway.Multiplayer.IsServer) Instance = new Client(comId, modName, keyword); else Instance = new Server(comId, modName, keyword); } /// /// Gets the diffrence between now and a given timestamp in milliseconds /// /// public static float GetDeltaMilliseconds(long timestamp) { return (DateTime.UtcNow.Ticks - timestamp) / TimeSpan.TicksPerMillisecond; } public static int GetDeltaFrames(long timestamp) { return (int)Math.Ceiling(GetDeltaMilliseconds(timestamp) / frames); } } }