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..ce8f1a5 --- /dev/null +++ b/Torch.Mod/Messages/MessageBase.cs @@ -0,0 +1,50 @@ +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))] + #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/ModCommunication.cs b/Torch.Mod/ModCommunication.cs new file mode 100644 index 0000000..0295e26 --- /dev/null +++ b/Torch.Mod/ModCommunication.cs @@ -0,0 +1,198 @@ +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); + _closing = true; + ReleaseLock(); + _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"); + 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"); + 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"); + message.TargetType = MessageTarget.AllExcept; + message.Ignore = ignoredUsers; + _outgoing.Enqueue(message); + ReleaseLock(); + } + + public static void SendMessageToServer(MessageBase message) + { + message.TargetType = MessageTarget.Server; + _outgoing.Enqueue(message); + ReleaseLock(); + } + + private static void ReleaseLock() + { + while(!_lock.TryAcquireExclusive()) + _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..a6b330f --- /dev/null +++ b/Torch.Mod/Torch.Mod.projitems @@ -0,0 +1,18 @@ + + + + $(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..b058636 --- /dev/null +++ b/Torch.Mod/TorchModCore.cs @@ -0,0 +1,30 @@ +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() + { + ModCommunication.Unregister(); + } + } +} 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.sln b/Torch.sln index e78e377..9f2f29b 100644 --- a/Torch.sln +++ b/Torch.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.27004.2010 +VisualStudioVersion = 15.0.26430.14 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Torch", "Torch\Torch.csproj", "{7E01635C-3B67-472E-BCD6-C5539564F214}" EndProject @@ -27,41 +27,60 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Versioning", "Versioning", Versioning\AssemblyVersion.cs = Versioning\AssemblyVersion.cs EndProjectSection EndProject +Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Torch.Mod", "Torch.Mod\Torch.Mod.shproj", "{3CE4D2E9-B461-4F19-8233-F87E0DFDDD74}" +EndProject Global - GlobalSection(Performance) = preSolution - HasPerformanceSessions = true + GlobalSection(SharedMSBuildProjectFiles) = preSolution + Torch.Mod\Torch.Mod.projitems*{3ce4d2e9-b461-4f19-8233-f87e0dfddd74}*SharedItemsImports = 13 + Torch.Mod\Torch.Mod.projitems*{7e01635c-3b67-472e-bcd6-c5539564f214}*SharedItemsImports = 4 EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 + Release|Any CPU = Release|Any CPU Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7E01635C-3B67-472E-BCD6-C5539564F214}.Debug|Any CPU.ActiveCfg = Debug|x64 {7E01635C-3B67-472E-BCD6-C5539564F214}.Debug|x64.ActiveCfg = Debug|x64 {7E01635C-3B67-472E-BCD6-C5539564F214}.Debug|x64.Build.0 = Debug|x64 + {7E01635C-3B67-472E-BCD6-C5539564F214}.Release|Any CPU.ActiveCfg = Release|x64 {7E01635C-3B67-472E-BCD6-C5539564F214}.Release|x64.ActiveCfg = Release|x64 {7E01635C-3B67-472E-BCD6-C5539564F214}.Release|x64.Build.0 = Release|x64 + {FBA5D932-6254-4A1E-BAF4-E229FA94E3C2}.Debug|Any CPU.ActiveCfg = Debug|x64 {FBA5D932-6254-4A1E-BAF4-E229FA94E3C2}.Debug|x64.ActiveCfg = Debug|x64 {FBA5D932-6254-4A1E-BAF4-E229FA94E3C2}.Debug|x64.Build.0 = Debug|x64 + {FBA5D932-6254-4A1E-BAF4-E229FA94E3C2}.Release|Any CPU.ActiveCfg = Release|x64 {FBA5D932-6254-4A1E-BAF4-E229FA94E3C2}.Release|x64.ActiveCfg = Release|x64 {FBA5D932-6254-4A1E-BAF4-E229FA94E3C2}.Release|x64.Build.0 = Release|x64 + {E36DF745-260B-4956-A2E8-09F08B2E7161}.Debug|Any CPU.ActiveCfg = Debug|x64 {E36DF745-260B-4956-A2E8-09F08B2E7161}.Debug|x64.ActiveCfg = Debug|x64 {E36DF745-260B-4956-A2E8-09F08B2E7161}.Debug|x64.Build.0 = Debug|x64 + {E36DF745-260B-4956-A2E8-09F08B2E7161}.Release|Any CPU.ActiveCfg = Release|x64 {E36DF745-260B-4956-A2E8-09F08B2E7161}.Release|x64.ActiveCfg = Release|x64 {E36DF745-260B-4956-A2E8-09F08B2E7161}.Release|x64.Build.0 = Release|x64 + {CA50886B-7B22-4CD8-93A0-C06F38D4F77D}.Debug|Any CPU.ActiveCfg = Debug|x64 {CA50886B-7B22-4CD8-93A0-C06F38D4F77D}.Debug|x64.ActiveCfg = Debug|x64 {CA50886B-7B22-4CD8-93A0-C06F38D4F77D}.Debug|x64.Build.0 = Debug|x64 + {CA50886B-7B22-4CD8-93A0-C06F38D4F77D}.Release|Any CPU.ActiveCfg = Release|x64 {CA50886B-7B22-4CD8-93A0-C06F38D4F77D}.Release|x64.ActiveCfg = Release|x64 {CA50886B-7B22-4CD8-93A0-C06F38D4F77D}.Release|x64.Build.0 = Release|x64 + {C3C8B671-6AD1-44AA-A8DA-E0C0DC0FEDF5}.Debug|Any CPU.ActiveCfg = Debug|x64 {C3C8B671-6AD1-44AA-A8DA-E0C0DC0FEDF5}.Debug|x64.ActiveCfg = Debug|x64 {C3C8B671-6AD1-44AA-A8DA-E0C0DC0FEDF5}.Debug|x64.Build.0 = Debug|x64 + {C3C8B671-6AD1-44AA-A8DA-E0C0DC0FEDF5}.Release|Any CPU.ActiveCfg = Release|x64 {C3C8B671-6AD1-44AA-A8DA-E0C0DC0FEDF5}.Release|x64.ActiveCfg = Release|x64 {C3C8B671-6AD1-44AA-A8DA-E0C0DC0FEDF5}.Release|x64.Build.0 = Release|x64 + {9EFD1D91-2FA2-47ED-B537-D8BC3B0E543E}.Debug|Any CPU.ActiveCfg = Debug|x64 {9EFD1D91-2FA2-47ED-B537-D8BC3B0E543E}.Debug|x64.ActiveCfg = Debug|x64 {9EFD1D91-2FA2-47ED-B537-D8BC3B0E543E}.Debug|x64.Build.0 = Debug|x64 + {9EFD1D91-2FA2-47ED-B537-D8BC3B0E543E}.Release|Any CPU.ActiveCfg = Release|x64 {9EFD1D91-2FA2-47ED-B537-D8BC3B0E543E}.Release|x64.ActiveCfg = Release|x64 {9EFD1D91-2FA2-47ED-B537-D8BC3B0E543E}.Release|x64.Build.0 = Release|x64 + {632E78C0-0DAC-4B71-B411-2F1B333CC310}.Debug|Any CPU.ActiveCfg = Debug|x64 {632E78C0-0DAC-4B71-B411-2F1B333CC310}.Debug|x64.ActiveCfg = Debug|x64 {632E78C0-0DAC-4B71-B411-2F1B333CC310}.Debug|x64.Build.0 = Debug|x64 + {632E78C0-0DAC-4B71-B411-2F1B333CC310}.Release|Any CPU.ActiveCfg = Release|x64 {632E78C0-0DAC-4B71-B411-2F1B333CC310}.Release|x64.ActiveCfg = Release|x64 {632E78C0-0DAC-4B71-B411-2F1B333CC310}.Release|x64.Build.0 = Release|x64 EndGlobalSection @@ -74,4 +93,7 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BB51D91F-958D-4B63-A897-3C40642ACD3E} EndGlobalSection + GlobalSection(Performance) = preSolution + HasPerformanceSessions = true + EndGlobalSection EndGlobal diff --git a/Torch/Commands/TorchCommands.cs b/Torch/Commands/TorchCommands.cs index e3affc8..1879bae 100644 --- a/Torch/Commands/TorchCommands.cs +++ b/Torch/Commands/TorchCommands.cs @@ -17,6 +17,8 @@ using Torch.API.Managers; using Torch.API.Session; using Torch.Commands.Permissions; using Torch.Managers; +using Torch.Mod; +using Torch.Mod.Messages; using VRage.Game.ModAPI; namespace Torch.Commands @@ -75,7 +77,7 @@ namespace Torch.Commands } } - [Command("longhelp", "Get verbose help. Will send a long message, check the Comms tab.")] + [Command("longhelp", "Get verbose help. Will send a long message in a dialog window.")] [Permission(MyPromoteLevel.None)] public void LongHelp() { @@ -107,13 +109,20 @@ namespace Torch.Commands } else { - var sb = new StringBuilder("Available commands:\n"); + var sb = new StringBuilder(); foreach (var command in commandManager.Commands.WalkTree()) { if (command.IsCommand) sb.AppendLine($"{command.Command.SyntaxHelp}\n {command.Command.HelpText}"); } - Context.Respond(sb.ToString()); + + if (!Context.SentBySelf) + { + var m = new DialogMessage("Torch Help", subtitle: "Available commands:", content: sb.ToString()); + ModCommunication.SendMessageTo(m, Context.Player.SteamUserId); + } + else + Context.Respond($"Available commands: {sb}"); } } @@ -169,6 +178,13 @@ namespace Torch.Commands }); } + [Command("notify", "Shows a message as a notification in the middle of all players' screens.")] + [Permission(MyPromoteLevel.Admin)] + public void Notify(string message, int disappearTimeMs = 2000, string font = "White") + { + ModCommunication.SendMessageToClients(new NotificationMessage(message, disappearTimeMs, font)); + } + [Command("restart cancel", "Cancel a pending restart.")] public void CancelRestart() { diff --git a/Torch/Patches/SessionDownloadPatch.cs b/Torch/Patches/SessionDownloadPatch.cs new file mode 100644 index 0000000..fc81ad5 --- /dev/null +++ b/Torch/Patches/SessionDownloadPatch.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Sandbox.Game.World; +using Torch.Managers.PatchManager; +using Torch.Mod; +using VRage.Game; + +namespace Torch.Patches +{ + [PatchShim] + internal class SessionDownloadPatch + { + internal static void Patch(PatchContext context) + { + context.GetPattern(typeof(MySession).GetMethod(nameof(MySession.GetWorld))).Suffixes.Add(typeof(SessionDownloadPatch).GetMethod(nameof(SuffixGetWorld), BindingFlags.Static | BindingFlags.NonPublic)); + } + + // ReSharper disable once InconsistentNaming + private static void SuffixGetWorld(ref MyObjectBuilder_World __result) + { + if (!__result.Checkpoint.Mods.Any(m => m.PublishedFileId == TorchModCore.MOD_ID)) + __result.Checkpoint.Mods.Add(new MyObjectBuilder_Checkpoint.ModItem(TorchModCore.MOD_ID)); + } + } +} diff --git a/Torch/Torch.csproj b/Torch/Torch.csproj index e9791ac..b10ede3 100644 --- a/Torch/Torch.csproj +++ b/Torch/Torch.csproj @@ -209,6 +209,7 @@ + @@ -318,6 +319,7 @@ + \ No newline at end of file