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 @@
-
-
+
diff --git a/Torch.Server/Views/PluginsControl.xaml b/Torch.Server/Views/PluginsControl.xaml
index a7c1119..2dfbec0 100644
--- a/Torch.Server/Views/PluginsControl.xaml
+++ b/Torch.Server/Views/PluginsControl.xaml
@@ -28,7 +28,7 @@
-
+
diff --git a/Torch.Server/Views/PluginsControl.xaml.cs b/Torch.Server/Views/PluginsControl.xaml.cs
index 51f2ebd..3000f9c 100644
--- a/Torch.Server/Views/PluginsControl.xaml.cs
+++ b/Torch.Server/Views/PluginsControl.xaml.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Text;
@@ -19,6 +20,7 @@ using Torch.API;
using Torch.API.Managers;
using Torch.Managers;
using Torch.Server.ViewModels;
+using Torch.Views;
namespace Torch.Server.Views
{
@@ -35,6 +37,17 @@ namespace Torch.Server.Views
InitializeComponent();
}
+ private void PluginManagerOnPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
+ {
+ if (propertyChangedEventArgs.PropertyName == nameof(PluginManagerViewModel.SelectedPlugin))
+ {
+ if (((PluginManagerViewModel)DataContext).SelectedPlugin.Control is PropertyGrid)
+ PScroll.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled;
+ else
+ PScroll.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
+ }
+ }
+
public void BindServer(ITorchServer server)
{
_server = server;
@@ -48,6 +61,7 @@ namespace Torch.Server.Views
_plugins = _server.Managers.GetManager();
var pluginManager = new PluginManagerViewModel(_plugins);
DataContext = pluginManager;
+ pluginManager.PropertyChanged += PluginManagerOnPropertyChanged;
});
}
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 8fb1681..53f68b9 100644
--- a/Torch/Commands/TorchCommands.cs
+++ b/Torch/Commands/TorchCommands.cs
@@ -17,6 +17,9 @@ 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;
using VRage.Game.ModAPI;
namespace Torch.Commands
@@ -78,7 +81,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()
{
@@ -110,13 +113,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}");
}
}
@@ -172,6 +182,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..dbc04f7
--- /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 static 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/Persistent.cs b/Torch/Persistent.cs
index 670d2ba..cddc182 100644
--- a/Torch/Persistent.cs
+++ b/Torch/Persistent.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
@@ -18,7 +18,19 @@ namespace Torch
public sealed class Persistent : IDisposable where T : new()
{
public string Path { get; set; }
- public T Data { get; private set; }
+ private T _data;
+ public T Data
+ {
+ get => _data;
+ private set
+ {
+ if (_data is INotifyPropertyChanged npc1)
+ npc1.PropertyChanged -= OnPropertyChanged;
+ _data = value;
+ if (_data is INotifyPropertyChanged npc2)
+ npc2.PropertyChanged += OnPropertyChanged;
+ }
+ }
~Persistent()
{
@@ -29,13 +41,23 @@ namespace Torch
{
Path = path;
Data = data;
- if (Data is INotifyPropertyChanged npc)
- npc.PropertyChanged += OnPropertyChanged;
+ }
+
+ private Timer _saveConfigTimer;
+
+ private void SaveAsync()
+ {
+ if (_saveConfigTimer == null)
+ {
+ _saveConfigTimer = new Timer((x) => Save());
+ }
+
+ _saveConfigTimer.Change(1000, -1);
}
private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
- Save();
+ SaveAsync();
}
public void Save(string path = null)
@@ -52,20 +74,20 @@ namespace Torch
public static Persistent Load(string path, bool saveIfNew = true)
{
- var config = new Persistent(path, new T());
+ Persistent config = null;
if (File.Exists(path))
{
var ser = new XmlSerializer(typeof(T));
using (var f = File.OpenText(path))
{
- config.Data = (T)ser.Deserialize(f);
+ config = new Persistent(path, (T)ser.Deserialize(f));
}
}
- else if (saveIfNew)
- {
- config.Save(path);
- }
+ if (config == null)
+ config = new Persistent(path, new T());
+ if (!File.Exists(path) && saveIfNew)
+ config.Save();
return config;
}
@@ -76,6 +98,7 @@ namespace Torch
{
if (Data is INotifyPropertyChanged npc)
npc.PropertyChanged -= OnPropertyChanged;
+ _saveConfigTimer?.Dispose();
Save();
}
catch
diff --git a/Torch/Plugins/PluginManager.cs b/Torch/Plugins/PluginManager.cs
index e34852c..b2531ea 100644
--- a/Torch/Plugins/PluginManager.cs
+++ b/Torch/Plugins/PluginManager.cs
@@ -377,6 +377,7 @@ namespace Torch.Managers
private void InstantiatePlugin(PluginManifest manifest, IEnumerable assemblies)
{
Type pluginType = null;
+ bool mult = false;
foreach (var asm in assemblies)
{
foreach (var type in asm.GetExportedTypes())
@@ -384,16 +385,26 @@ namespace Torch.Managers
if (!type.GetInterfaces().Contains(typeof(ITorchPlugin)))
continue;
+ _log.Info($"Loading plugin at {type.FullName}");
+
if (pluginType != null)
{
- _log.Error($"The plugin '{manifest.Name}' has multiple implementations of {nameof(ITorchPlugin)}, not loading.");
- return;
+ //_log.Error($"The plugin '{manifest.Name}' has multiple implementations of {nameof(ITorchPlugin)}, not loading.");
+ //return;
+ mult = true;
+ continue;
}
pluginType = type;
}
}
+ if (mult)
+ {
+ _log.Error($"The plugin '{manifest.Name}' has multiple implementations of {nameof(ITorchPlugin)}, not loading.");
+ return;
+ }
+
if (pluginType == null)
{
_log.Error($"The plugin '{manifest.Name}' does not have an implementation of {nameof(ITorchPlugin)}, not loading.");
diff --git a/Torch/Torch.csproj b/Torch/Torch.csproj
index 6af5960..9c803a5 100644
--- a/Torch/Torch.csproj
+++ b/Torch/Torch.csproj
@@ -208,6 +208,7 @@
+
@@ -269,6 +270,9 @@
DictionaryEditor.xaml
+
+ EmbeddedCollectionEditor.xaml
+
ObjectCollectionEditor.xaml
@@ -299,6 +303,10 @@
MSBuild:Compile
Designer
+
+ Designer
+ MSBuild:Compile
+
Designer
MSBuild:Compile
@@ -315,6 +323,7 @@
+
\ No newline at end of file
diff --git a/Torch/Views/DisplayAttribute.cs b/Torch/Views/DisplayAttribute.cs
index 9dda943..ba9c043 100644
--- a/Torch/Views/DisplayAttribute.cs
+++ b/Torch/Views/DisplayAttribute.cs
@@ -16,6 +16,7 @@ namespace Torch.Views
public bool Enabled = true;
public bool Visible = true;
public bool ReadOnly = false;
+ public Type EditorType = null;
public DisplayAttribute()
{ }
diff --git a/Torch/Views/EmbeddedCollectionEditor.xaml b/Torch/Views/EmbeddedCollectionEditor.xaml
new file mode 100644
index 0000000..fac4d11
--- /dev/null
+++ b/Torch/Views/EmbeddedCollectionEditor.xaml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Torch/Views/EmbeddedCollectionEditor.xaml.cs b/Torch/Views/EmbeddedCollectionEditor.xaml.cs
new file mode 100644
index 0000000..ff5a859
--- /dev/null
+++ b/Torch/Views/EmbeddedCollectionEditor.xaml.cs
@@ -0,0 +1,126 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Navigation;
+using System.Windows.Shapes;
+using NLog;
+using NLog.Fluent;
+
+namespace Torch.Views
+{
+ ///
+ /// Interaction logic for EmbeddedCollectionEditor.xaml
+ ///
+ public partial class EmbeddedCollectionEditor : UserControl
+ {
+ public EmbeddedCollectionEditor()
+ {
+ InitializeComponent();
+ DataContextChanged += OnDataContextChanged;
+ }
+
+ private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
+ {
+ var c = dependencyPropertyChangedEventArgs.NewValue as ICollection;
+ //var c = DataContext as ICollection;
+ if (c != null)
+ Edit(c);
+ }
+
+ private static readonly Dictionary MethodCache = new Dictionary();
+ private static readonly MethodInfo EditMethod;
+
+
+ static EmbeddedCollectionEditor()
+ {
+ var m = typeof(EmbeddedCollectionEditor).GetMethods();
+ EditMethod = m.First(mt => mt.Name == "Edit" && mt.GetGenericArguments().Length == 1);
+ }
+
+ public void Edit(ICollection collection)
+ {
+ if (collection == null)
+ {
+ MessageBox.Show("Cannot load null collection.", "Edit Error");
+ return;
+ }
+
+ var gt = collection.GetType().GenericTypeArguments[0];
+
+ //substitute for 'where T : new()'
+ if (gt.GetConstructor(Type.EmptyTypes) == null)
+ {
+ MessageBox.Show("Unsupported collection type. Type must have paramaterless ctor.", "Edit Error");
+ return;
+ }
+
+ if (!MethodCache.TryGetValue(gt, out MethodInfo gm))
+ {
+ gm = EditMethod.MakeGenericMethod(gt);
+ MethodCache.Add(gt, gm);
+ }
+
+ gm.Invoke(this, new object[] {collection});
+ }
+
+ public void Edit(ICollection collection) where T : new()
+ {
+ var oc = collection as ObservableCollection ?? new ObservableCollection(collection);
+
+ AddButton.Click += (sender, args) =>
+ {
+ var t = new T();
+ oc.Add(t);
+ ElementList.SelectedItem = t;
+ };
+
+ RemoveButton.Click += RemoveButton_OnClick;
+ ElementList.SelectionChanged += ElementsList_OnSelected;
+
+ ElementList.ItemsSource = oc;
+ oc.CollectionChanged += (sender, args) => RefreshList();
+
+ if (!(collection is ObservableCollection))
+ {
+ collection.Clear();
+ foreach (var o in oc)
+ collection.Add(o);
+ }
+ }
+
+ private void RemoveButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ //this is kinda shitty, but item count is normally small, and it prevents CollectionModifiedExceptions
+ var l = (ObservableCollection)ElementList.ItemsSource;
+ var r = new List(ElementList.SelectedItems.Cast());
+ foreach (var item in r)
+ l.Remove(item);
+ if (l.Any())
+ ElementList.SelectedIndex = 0;
+ }
+
+ private void ElementsList_OnSelected(object sender, RoutedEventArgs e)
+ {
+ var item = (sender as ListBox)?.SelectedItem;
+ PGrid.DataContext = item;
+ }
+
+ private void RefreshList()
+ {
+ ElementList.Items.Refresh();
+ }
+ }
+}
+
diff --git a/Torch/Views/ObjectCollectionEditor.xaml b/Torch/Views/ObjectCollectionEditor.xaml
index 45f1a68..dfc88d6 100644
--- a/Torch/Views/ObjectCollectionEditor.xaml
+++ b/Torch/Views/ObjectCollectionEditor.xaml
@@ -6,24 +6,5 @@
xmlns:local="clr-namespace:Torch.Views"
mc:Ignorable="d"
Height="370" Width="400" Title="Edit Collection">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/Torch/Views/ObjectCollectionEditor.xaml.cs b/Torch/Views/ObjectCollectionEditor.xaml.cs
index 6d21ec9..f32c315 100644
--- a/Torch/Views/ObjectCollectionEditor.xaml.cs
+++ b/Torch/Views/ObjectCollectionEditor.xaml.cs
@@ -25,90 +25,18 @@ namespace Torch.Views
///
public partial class ObjectCollectionEditor : Window
{
- private static readonly Dictionary MethodCache = new Dictionary();
- private static readonly MethodInfo EditMethod;
-
public ObjectCollectionEditor()
{
InitializeComponent();
}
- static ObjectCollectionEditor()
- {
- var m = typeof(ObjectCollectionEditor).GetMethods();
- EditMethod = m.First(mt => mt.Name == "Edit" && mt.GetGenericArguments().Length == 1);
- }
-
public void Edit(ICollection collection, string title)
{
- if (collection == null)
- {
- MessageBox.Show("Cannot load null collection.", "Edit Error");
- return;
- }
-
- var gt = collection.GetType().GenericTypeArguments[0];
-
- //substitute for 'where T : new()'
- if (gt.GetConstructor(Type.EmptyTypes) == null)
- {
- MessageBox.Show("Unsupported collection type. Type must have paramaterless ctor.", "Edit Error");
- return;
- }
-
- if (!MethodCache.TryGetValue(gt, out MethodInfo gm))
- {
- gm = EditMethod.MakeGenericMethod(gt);
- MethodCache.Add(gt, gm);
- }
-
- gm.Invoke(this, new object[] {collection, title});
- }
-
- public void Edit(ICollection collection, string title) where T : new()
- {
- var oc = collection as ObservableCollection ?? new ObservableCollection(collection);
-
- AddButton.Click += (sender, args) =>
- {
- var t = new T();
- oc.Add(t);
- ElementList.SelectedItem = t;
- };
-
- RemoveButton.Click += RemoveButton_OnClick;
- ElementList.SelectionChanged += ElementsList_OnSelected;
-
- ElementList.ItemsSource = oc;
-
+ Editor.Edit(collection);
Title = title;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
ShowDialog();
-
- if (!(collection is ObservableCollection))
- {
- collection.Clear();
- foreach (var o in oc)
- collection.Add(o);
- }
- }
-
- private void RemoveButton_OnClick(object sender, RoutedEventArgs e)
- {
- //this is kinda shitty, but item count is normally small, and it prevents CollectionModifiedExceptions
- var l = (ObservableCollection)ElementList.ItemsSource;
- var r = new List(ElementList.SelectedItems.Cast());
- foreach (var item in r)
- l.Remove(item);
- if (l.Any())
- ElementList.SelectedIndex = 0;
- }
-
- private void ElementsList_OnSelected(object sender, RoutedEventArgs e)
- {
- var item = (sender as ListBox)?.SelectedItem;
- PGrid.DataContext = item;
}
}
}
diff --git a/Torch/Views/PropertyGrid.xaml b/Torch/Views/PropertyGrid.xaml
index 78b24d5..b3d8e79 100644
--- a/Torch/Views/PropertyGrid.xaml
+++ b/Torch/Views/PropertyGrid.xaml
@@ -20,6 +20,6 @@
-
+
diff --git a/Torch/Views/PropertyGrid.xaml.cs b/Torch/Views/PropertyGrid.xaml.cs
index 9e0bd39..f5e713e 100644
--- a/Torch/Views/PropertyGrid.xaml.cs
+++ b/Torch/Views/PropertyGrid.xaml.cs
@@ -67,8 +67,8 @@ namespace Torch.Views
var properties = t.GetProperties(BindingFlags.Instance | BindingFlags.Public);
var grid = new Grid();
+ grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Auto) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(2, GridUnitType.Star) });
- grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
var categories = new Dictionary>();
var descriptors = new Dictionary(properties.Length);
@@ -145,7 +145,12 @@ namespace Torch.Views
grid.Children.Add(text);
FrameworkElement valueControl;
- if (property.GetSetMethod() == null || descriptor?.ReadOnly == true)
+ if (descriptor?.EditorType != null)
+ {
+ valueControl = (FrameworkElement)Activator.CreateInstance(descriptor.EditorType);
+ valueControl.SetBinding(FrameworkElement.DataContextProperty, property.Name);
+ }
+ else if (property.GetSetMethod() == null && !(propertyType.IsGenericType && typeof(ICollection).IsAssignableFrom(propertyType.GetGenericTypeDefinition()))|| descriptor?.ReadOnly == true)
{
valueControl = new TextBlock();
var binding = new Binding(property.Name)
@@ -211,11 +216,21 @@ namespace Torch.Views
valueControl = button;
}
- else if (propertyType.IsPrimitive || propertyType == typeof(string))
+ else if (propertyType.IsPrimitive)
{
valueControl = new TextBox();
valueControl.SetBinding(TextBox.TextProperty, property.Name);
}
+ else if (propertyType == typeof(string))
+ {
+ var tb = new TextBox();
+ tb.TextWrapping = TextWrapping.Wrap;
+ tb.AcceptsReturn = true;
+ tb.AcceptsTab = true;
+ tb.SpellCheck.IsEnabled = true;
+ tb.SetBinding(TextBox.TextProperty, property.Name);
+ valueControl = tb;
+ }
else
{
var button = new Button