diff --git a/Jenkinsfile b/Jenkinsfile index 88aadf7..009bf16 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -50,7 +50,7 @@ node { packageAndArchive(buildMode, "torch-server", "Torch.Client*") - packageAndArchive(buildMode, "torch-client", "Torch.Server*") + /*packageAndArchive(buildMode, "torch-client", "Torch.Server*")*/ } /* Disabled because they fail builds more often than they detect actual problems @@ -73,4 +73,4 @@ node { ]) } */ -} \ No newline at end of file +} diff --git a/Torch.Mod/Messages/DialogMessage.cs b/Torch.Mod/Messages/DialogMessage.cs new file mode 100644 index 0000000..ae54317 --- /dev/null +++ b/Torch.Mod/Messages/DialogMessage.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using ProtoBuf; +using Sandbox.ModAPI; + +namespace Torch.Mod.Messages +{ + /// Dialogs are structured as follows + /// + /// _____________________________________ + /// | Title | + /// -------------------------------------- + /// | Prefix Subtitle | + /// -------------------------------------- + /// | ________________________________ | + /// | | Content | | + /// | --------------------------------- | + /// | ____________ | + /// | | ButtonText | | + /// | -------------- | + /// -------------------------------------- + /// + /// Button has a callback on click option, + /// but can't serialize that, so ¯\_(ツ)_/¯ + [ProtoContract] + public class DialogMessage : MessageBase + { + [ProtoMember(201)] + public string Title; + [ProtoMember(202)] + public string Subtitle; + [ProtoMember(203)] + public string Prefix; + [ProtoMember(204)] + public string Content; + [ProtoMember(205)] + public string ButtonText; + + public DialogMessage() + { } + + public DialogMessage(string title, string subtitle, string content) + { + Title = title; + Subtitle = subtitle; + Content = content; + Prefix = String.Empty; + } + + public DialogMessage(string title = null, string prefix = null, string subtitle = null, string content = null, string buttonText = null) + { + Title = title; + Subtitle = subtitle; + Prefix = prefix ?? String.Empty; + Content = content; + ButtonText = buttonText; + } + + public override void ProcessClient() + { + MyAPIGateway.Utilities.ShowMissionScreen(Title, Prefix, Subtitle, Content, null, ButtonText); + } + + public override void ProcessServer() + { + throw new Exception(); + } + } +} diff --git a/Torch.Mod/Messages/MessageBase.cs b/Torch.Mod/Messages/MessageBase.cs new file mode 100644 index 0000000..3a00738 --- /dev/null +++ b/Torch.Mod/Messages/MessageBase.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using ProtoBuf; + +namespace Torch.Mod.Messages +{ + #region Includes + [ProtoInclude(1, typeof(DialogMessage))] + [ProtoInclude(2, typeof(NotificationMessage))] + [ProtoInclude(3, typeof(VoxelResetMessage))] + #endregion + + [ProtoContract] + public abstract class MessageBase + { + [ProtoMember(101)] + public ulong SenderId; + + public abstract void ProcessClient(); + public abstract void ProcessServer(); + + //members below not serialized, they're just metadata about the intended target(s) of this message + internal MessageTarget TargetType; + internal ulong Target; + internal ulong[] Ignore; + internal byte[] CompressedData; + } + + public enum MessageTarget + { + /// + /// Send to Target + /// + Single, + /// + /// Send to Server + /// + Server, + /// + /// Send to all Clients (only valid from server) + /// + AllClients, + /// + /// Send to all except those steam ID listed in Ignore + /// + AllExcept, + } +} diff --git a/Torch.Mod/Messages/NotificationMessage.cs b/Torch.Mod/Messages/NotificationMessage.cs new file mode 100644 index 0000000..951f053 --- /dev/null +++ b/Torch.Mod/Messages/NotificationMessage.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Text; +using ProtoBuf; +using Sandbox.ModAPI; + +namespace Torch.Mod.Messages +{ + [ProtoContract] + public class NotificationMessage : MessageBase + { + [ProtoMember(201)] + public string Message; + [ProtoMember(202)] + public string Font; + [ProtoMember(203)] + public int DisappearTimeMs; + + public NotificationMessage() + { } + + public NotificationMessage(string message, int disappearTimeMs, string font) + { + Message = message; + DisappearTimeMs = disappearTimeMs; + Font = font; + } + + public override void ProcessClient() + { + MyAPIGateway.Utilities.ShowNotification(Message, DisappearTimeMs, Font); + } + + public override void ProcessServer() + { + throw new Exception(); + } + } +} diff --git a/Torch.Mod/Messages/VoxelResetMessage.cs b/Torch.Mod/Messages/VoxelResetMessage.cs new file mode 100644 index 0000000..e5cd66b --- /dev/null +++ b/Torch.Mod/Messages/VoxelResetMessage.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Text; +using ProtoBuf; +using Sandbox.ModAPI; +using VRage.ModAPI; +using VRage.Voxels; + +namespace Torch.Mod.Messages +{ + [ProtoContract] + public class VoxelResetMessage : MessageBase + { + [ProtoMember(201)] + public long[] EntityId; + + public VoxelResetMessage() + { } + + public VoxelResetMessage(long[] entityId) + { + EntityId = entityId; + } + + public override void ProcessClient() + { + MyAPIGateway.Parallel.ForEach(EntityId, id => + { + IMyEntity e; + if (!MyAPIGateway.Entities.TryGetEntityById(id, out e)) + return; + + var v = e as IMyVoxelBase; + if (v == null) + return; + + v.Storage.Reset(MyStorageDataTypeFlags.All); + }); + } + + public override void ProcessServer() + { + throw new Exception(); + } + } +} diff --git a/Torch.Mod/ModCommunication.cs b/Torch.Mod/ModCommunication.cs new file mode 100644 index 0000000..6c1c546 --- /dev/null +++ b/Torch.Mod/ModCommunication.cs @@ -0,0 +1,213 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Sandbox.ModAPI; +using Torch.Mod.Messages; +using VRage; +using VRage.Game.ModAPI; +using VRage.Utils; +using Task = ParallelTasks.Task; + +namespace Torch.Mod +{ + public static class ModCommunication + { + public const ushort NET_ID = 4352; + private static bool _closing; + private static ConcurrentQueue _outgoing; + private static ConcurrentQueue _incoming; + private static List _playerCache; + private static FastResourceLock _lock; + private static Task _task; + + public static void Register() + { + MyLog.Default.WriteLineAndConsole("TORCH MOD: Registering mod communication."); + _outgoing = new ConcurrentQueue(); + _incoming = new ConcurrentQueue(); + _playerCache = new List(); + _lock = new FastResourceLock(); + + + MyAPIGateway.Multiplayer.RegisterMessageHandler(NET_ID, MessageHandler); + //background thread to handle de/compression and processing + _task = MyAPIGateway.Parallel.StartBackground(DoProcessing); + MyLog.Default.WriteLineAndConsole("TORCH MOD: Mod communication registered successfully."); + } + + public static void Unregister() + { + MyLog.Default.WriteLineAndConsole("TORCH MOD: Unregistering mod communication."); + MyAPIGateway.Multiplayer?.UnregisterMessageHandler(NET_ID, MessageHandler); + ReleaseLock(); + _closing = true; + //_task.Wait(); + } + + private static void MessageHandler(byte[] bytes) + { + _incoming.Enqueue(bytes); + ReleaseLock(); + } + + public static void DoProcessing() + { + while (!_closing) + { + try + { + byte[] incoming; + while (_incoming.TryDequeue(out incoming)) + { + MessageBase m; + try + { + var o = MyCompression.Decompress(incoming); + m = MyAPIGateway.Utilities.SerializeFromBinary(o); + } + catch (Exception ex) + { + MyLog.Default.WriteLineAndConsole($"TORCH MOD: Failed to deserialize message! {ex}"); + continue; + } + if (MyAPIGateway.Multiplayer.IsServer) + m.ProcessServer(); + else + m.ProcessClient(); + } + + if (!_outgoing.IsEmpty) + { + List tosend = new List(_outgoing.Count); + MessageBase outMessage; + while (_outgoing.TryDequeue(out outMessage)) + { + var b = MyAPIGateway.Utilities.SerializeToBinary(outMessage); + outMessage.CompressedData = MyCompression.Compress(b); + tosend.Add(outMessage); + } + + MyAPIGateway.Utilities.InvokeOnGameThread(() => + { + MyAPIGateway.Players.GetPlayers(_playerCache); + foreach (var outgoing in tosend) + { + switch (outgoing.TargetType) + { + case MessageTarget.Single: + MyAPIGateway.Multiplayer.SendMessageTo(NET_ID, outgoing.CompressedData, outgoing.Target); + break; + case MessageTarget.Server: + MyAPIGateway.Multiplayer.SendMessageToServer(NET_ID, outgoing.CompressedData); + break; + case MessageTarget.AllClients: + foreach (var p in _playerCache) + { + if (p.SteamUserId == MyAPIGateway.Multiplayer.MyId) + continue; + MyAPIGateway.Multiplayer.SendMessageTo(NET_ID, outgoing.CompressedData, p.SteamUserId); + } + break; + case MessageTarget.AllExcept: + foreach (var p in _playerCache) + { + if (p.SteamUserId == MyAPIGateway.Multiplayer.MyId || outgoing.Ignore.Contains(p.SteamUserId)) + continue; + MyAPIGateway.Multiplayer.SendMessageTo(NET_ID, outgoing.CompressedData, p.SteamUserId); + } + break; + default: + throw new Exception(); + } + } + _playerCache.Clear(); + }); + } + + AcquireLock(); + } + catch (Exception ex) + { + MyLog.Default.WriteLineAndConsole($"TORCH MOD: Exception occurred in communication thread! {ex}"); + } + } + + MyLog.Default.WriteLineAndConsole("TORCH MOD: COMMUNICATION THREAD: EXIT SIGNAL RECIEVED!"); + //exit signal received. Clean everything and GTFO + _outgoing = null; + _incoming = null; + _playerCache = null; + _lock = null; + } + + public static void SendMessageTo(MessageBase message, ulong target) + { + if (!MyAPIGateway.Multiplayer.IsServer) + throw new Exception("Only server can send targeted messages"); + + if (_closing) + return; + + message.Target = target; + message.TargetType = MessageTarget.Single; + MyLog.Default.WriteLineAndConsole($"Sending message of type {message.GetType().FullName}"); + _outgoing.Enqueue(message); + ReleaseLock(); + } + + public static void SendMessageToClients(MessageBase message) + { + if (!MyAPIGateway.Multiplayer.IsServer) + throw new Exception("Only server can send targeted messages"); + + if (_closing) + return; + + message.TargetType = MessageTarget.AllClients; + _outgoing.Enqueue(message); + ReleaseLock(); + } + + public static void SendMessageExcept(MessageBase message, params ulong[] ignoredUsers) + { + if (!MyAPIGateway.Multiplayer.IsServer) + throw new Exception("Only server can send targeted messages"); + + if (_closing) + return; + + message.TargetType = MessageTarget.AllExcept; + message.Ignore = ignoredUsers; + _outgoing.Enqueue(message); + ReleaseLock(); + } + + public static void SendMessageToServer(MessageBase message) + { + if (_closing) + return; + + message.TargetType = MessageTarget.Server; + _outgoing.Enqueue(message); + ReleaseLock(); + } + + private static void ReleaseLock() + { + while(_lock?.TryAcquireExclusive() == false) + _lock?.ReleaseExclusive(); + _lock?.ReleaseExclusive(); + } + + private static void AcquireLock() + { + ReleaseLock(); + _lock?.AcquireExclusive(); + _lock?.AcquireExclusive(); + } + } +} diff --git a/Torch.Mod/Torch.Mod.projitems b/Torch.Mod/Torch.Mod.projitems new file mode 100644 index 0000000..f05b7b6 --- /dev/null +++ b/Torch.Mod/Torch.Mod.projitems @@ -0,0 +1,19 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + 3ce4d2e9-b461-4f19-8233-f87e0dfddd74 + + + Torch.Mod + + + + + + + + + + \ No newline at end of file diff --git a/Torch.Mod/Torch.Mod.shproj b/Torch.Mod/Torch.Mod.shproj new file mode 100644 index 0000000..bc7cbf1 --- /dev/null +++ b/Torch.Mod/Torch.Mod.shproj @@ -0,0 +1,13 @@ + + + + 3ce4d2e9-b461-4f19-8233-f87e0dfddd74 + 14.0 + + + + + + + + diff --git a/Torch.Mod/TorchModCore.cs b/Torch.Mod/TorchModCore.cs new file mode 100644 index 0000000..a064fe6 --- /dev/null +++ b/Torch.Mod/TorchModCore.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using VRage.Game.Components; + +namespace Torch.Mod +{ + [MySessionComponentDescriptor(MyUpdateOrder.AfterSimulation)] + public class TorchModCore : MySessionComponentBase + { + public const long MOD_ID = 1406994352; + private static bool _init; + + public override void UpdateAfterSimulation() + { + if (_init) + return; + + _init = true; + ModCommunication.Register(); + } + + protected override void UnloadData() + { + try + { + ModCommunication.Unregister(); + } + catch + { + //session unloading, don't care + } + } + } +} diff --git a/Torch.Server/TorchServer.cs b/Torch.Server/TorchServer.cs index 21aa84e..42d1aa0 100644 --- a/Torch.Server/TorchServer.cs +++ b/Torch.Server/TorchServer.cs @@ -18,6 +18,7 @@ using Torch.API; using Torch.API.Managers; using Torch.API.Session; using Torch.Commands; +using Torch.Mod; using Torch.Server.Commands; using Torch.Server.Managers; using Torch.Utils; @@ -45,7 +46,7 @@ namespace Torch.Server private Timer _watchdog; /// - public TorchServer(TorchConfig config = null) + public TorchServer(TorchConfig config = null) { DedicatedInstance = new InstanceManager(this); AddManager(DedicatedInstance); @@ -174,10 +175,14 @@ namespace Torch.Server { _watchdog?.Dispose(); _watchdog = null; + ModCommunication.Unregister(); } if (newState == TorchSessionState.Loaded) + { CurrentSession.Managers.GetManager().RegisterCommandModule(typeof(WhitelistCommands)); + ModCommunication.Register(); + } } /// diff --git a/Torch.Server/Views/ConfigControl.xaml b/Torch.Server/Views/ConfigControl.xaml index 0f4462e..43ac44e 100644 --- a/Torch.Server/Views/ConfigControl.xaml +++ b/Torch.Server/Views/ConfigControl.xaml @@ -26,8 +26,8 @@