Merge branch 'experiment' of https://github.com/TorchAPI/Torch into experiment

This commit is contained in:
Brant Martin
2018-07-18 22:35:23 -04:00
26 changed files with 825 additions and 122 deletions

4
Jenkinsfile vendored
View File

@@ -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 {
])
}
*/
}
}

View File

@@ -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();
}
}
}

View File

@@ -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
{
/// <summary>
/// Send to Target
/// </summary>
Single,
/// <summary>
/// Send to Server
/// </summary>
Server,
/// <summary>
/// Send to all Clients (only valid from server)
/// </summary>
AllClients,
/// <summary>
/// Send to all except those steam ID listed in Ignore
/// </summary>
AllExcept,
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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<MessageBase> _outgoing;
private static ConcurrentQueue<byte[]> _incoming;
private static List<IMyPlayer> _playerCache;
private static FastResourceLock _lock;
private static Task _task;
public static void Register()
{
MyLog.Default.WriteLineAndConsole("TORCH MOD: Registering mod communication.");
_outgoing = new ConcurrentQueue<MessageBase>();
_incoming = new ConcurrentQueue<byte[]>();
_playerCache = new List<IMyPlayer>();
_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<MessageBase>(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<MessageBase> tosend = new List<MessageBase>(_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();
}
}
}

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
<HasSharedItems>true</HasSharedItems>
<SharedGUID>3ce4d2e9-b461-4f19-8233-f87e0dfddd74</SharedGUID>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<Import_RootNamespace>Torch.Mod</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)Messages\NotificationMessage.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Messages\DialogMessage.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Messages\MessageBase.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Messages\VoxelResetMessage.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ModCommunication.cs" />
<Compile Include="$(MSBuildThisFileDirectory)TorchModCore.cs" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Globals">
<ProjectGuid>3ce4d2e9-b461-4f19-8233-f87e0dfddd74</ProjectGuid>
<MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
<PropertyGroup />
<Import Project="Torch.Mod.projitems" Label="Shared" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
</Project>

37
Torch.Mod/TorchModCore.cs Normal file
View File

@@ -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
}
}
}
}

View File

@@ -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;
/// <inheritdoc />
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<CommandManager>().RegisterCommandModule(typeof(WhitelistCommands));
ModCommunication.Register();
}
}
/// <inheritdoc />

View File

@@ -26,8 +26,8 @@
</Grid.RowDefinitions>
<DockPanel Grid.Row="0">
<Label Content="World:" DockPanel.Dock="Left" />
<Button Content="Import World Config" Margin="3" DockPanel.Dock="Right" Click="ImportConfig_OnClick" ToolTip="Override the DS config with the one from the selected world."/>
<ComboBox ItemsSource="{Binding Worlds}" SelectedItem="{Binding SelectedWorld}" Margin="3"
<Button Content="Import World Config" Margin="3" DockPanel.Dock="Right" Click="ImportConfig_OnClick" ToolTip="Override the DS config with the one from the selected world." IsEnabled="{Binding ElementName=WorldList, Path=Items.Count, Mode=OneWay}"/>
<ComboBox x:Name="WorldList" ItemsSource="{Binding Worlds}" SelectedItem="{Binding SelectedWorld}" Margin="3"
SelectionChanged="Selector_OnSelectionChanged">
<ComboBox.ItemTemplate>
<DataTemplate DataType="managers:WorldViewModel">

View File

@@ -28,7 +28,7 @@
</ListView>
<Button Grid.Row="1" Content="Open Folder" Margin="3" DockPanel.Dock="Bottom" Click="OpenFolder_OnClick"/>
</Grid>
<ScrollViewer Grid.Column="1" VerticalScrollBarVisibility="Auto" Margin="3">
<ScrollViewer Name="PScroll" Grid.Column="1" Margin="3">
<Frame NavigationUIVisibility="Hidden" Content="{Binding SelectedPlugin.Control}"/>
</ScrollViewer>
</Grid>

View File

@@ -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<PluginManager>();
var pluginManager = new PluginManagerViewModel(_plugins);
DataContext = pluginManager;
pluginManager.PropertyChanged += PluginManagerOnPropertyChanged;
});
}

View File

@@ -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

View File

@@ -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()
{

View File

@@ -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));
}
}
}

View File

@@ -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<T> : 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<T> Load(string path, bool saveIfNew = true)
{
var config = new Persistent<T>(path, new T());
Persistent<T> 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<T>(path, (T)ser.Deserialize(f));
}
}
else if (saveIfNew)
{
config.Save(path);
}
if (config == null)
config = new Persistent<T>(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

View File

@@ -377,6 +377,7 @@ namespace Torch.Managers
private void InstantiatePlugin(PluginManifest manifest, IEnumerable<Assembly> 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.");

View File

@@ -208,6 +208,7 @@
<Compile Include="Patches\GameAnalyticsPatch.cs" />
<Compile Include="Patches\GameStatePatchShim.cs" />
<Compile Include="Patches\ObjectFactoryInitPatch.cs" />
<Compile Include="Patches\SessionDownloadPatch.cs" />
<Compile Include="Patches\TorchAsyncSaving.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Collections\KeyTree.cs" />
@@ -269,6 +270,9 @@
<DependentUpon>DictionaryEditor.xaml</DependentUpon>
</Compile>
<Compile Include="Views\DisplayAttribute.cs" />
<Compile Include="Views\EmbeddedCollectionEditor.xaml.cs">
<DependentUpon>EmbeddedCollectionEditor.xaml</DependentUpon>
</Compile>
<Compile Include="Views\ObjectCollectionEditor.xaml.cs">
<DependentUpon>ObjectCollectionEditor.xaml</DependentUpon>
</Compile>
@@ -299,6 +303,10 @@
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Views\EmbeddedCollectionEditor.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\ObjectCollectionEditor.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
@@ -315,6 +323,7 @@
<ItemGroup>
<Service Include="{508349B6-6B84-4DF5-91F0-309BEEBAD82D}" />
</ItemGroup>
<Import Project="..\Torch.Mod\Torch.Mod.projitems" Label="Shared" />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\TransformOnBuild.targets" />
</Project>

View File

@@ -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()
{ }

View File

@@ -0,0 +1,32 @@
<UserControl x:Class="Torch.Views.EmbeddedCollectionEditor"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Torch.Views"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid Width="Auto" Height="Auto">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="25" />
</Grid.RowDefinitions>
<ListBox Grid.Row="0" Grid.Column="0" x:Name="ElementList"
HorizontalContentAlignment="Stretch" Margin="0" VerticalContentAlignment="Stretch" />
<GridSplitter Grid.Column="1" Grid.Row="0" Width="2" HorizontalAlignment="Left" VerticalAlignment="Stretch"
Background="Gray" ShowsPreview="True" VerticalContentAlignment="Stretch" />
<local:PropertyGrid Grid.Row="0" Grid.Column="1" x:Name="PGrid" Margin="4,0,0,0" />
<Button Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" x:Name="AddButton" Content="Add"
HorizontalAlignment="Left" Margin="0" VerticalAlignment="Top"
Width="90" />
<Button Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" x:Name="RemoveButton" Content="Remove"
HorizontalAlignment="Left" Margin="100,0,0,0"
VerticalAlignment="Top" Width="90" />
</Grid>
</UserControl>

View File

@@ -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
{
/// <summary>
/// Interaction logic for EmbeddedCollectionEditor.xaml
/// </summary>
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<Type, MethodInfo> MethodCache = new Dictionary<Type, MethodInfo>();
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<T>(ICollection<T> collection) where T : new()
{
var oc = collection as ObservableCollection<T> ?? new ObservableCollection<T>(collection);
AddButton.Click += (sender, args) =>
{
var t = new T();
oc.Add(t);
ElementList.SelectedItem = t;
};
RemoveButton.Click += RemoveButton_OnClick<T>;
ElementList.SelectionChanged += ElementsList_OnSelected;
ElementList.ItemsSource = oc;
oc.CollectionChanged += (sender, args) => RefreshList();
if (!(collection is ObservableCollection<T>))
{
collection.Clear();
foreach (var o in oc)
collection.Add(o);
}
}
private void RemoveButton_OnClick<T>(object sender, RoutedEventArgs e)
{
//this is kinda shitty, but item count is normally small, and it prevents CollectionModifiedExceptions
var l = (ObservableCollection<T>)ElementList.ItemsSource;
var r = new List<T>(ElementList.SelectedItems.Cast<T>());
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();
}
}
}

View File

@@ -6,24 +6,5 @@
xmlns:local="clr-namespace:Torch.Views"
mc:Ignorable="d"
Height="370" Width="400" Title="Edit Collection">
<Grid Width="Auto" Height="Auto">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="25"/>
</Grid.RowDefinitions>
<ListBox Grid.Row="0" Grid.Column="0" x:Name="ElementList"
HorizontalContentAlignment="Stretch" Margin="0" VerticalContentAlignment="Stretch" />
<GridSplitter Grid.Column="1" Grid.Row="0" Width="2" HorizontalAlignment="Left" VerticalAlignment="Stretch" Background="Gray" ShowsPreview="True" VerticalContentAlignment="Stretch"/>
<local:PropertyGrid Grid.Row="0" Grid.Column="1" x:Name="PGrid" Margin="4,0,0,0" />
<Button Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" x:Name="AddButton" Content="Add" HorizontalAlignment="Left" Margin="0" VerticalAlignment="Top"
Width="90" />
<Button Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" x:Name="RemoveButton" Content="Remove" HorizontalAlignment="Left" Margin="100,0,0,0"
VerticalAlignment="Top" Width="90" />
</Grid>
<local:EmbeddedCollectionEditor x:Name="Editor"/>
</Window>

View File

@@ -25,90 +25,18 @@ namespace Torch.Views
/// </summary>
public partial class ObjectCollectionEditor : Window
{
private static readonly Dictionary<Type, MethodInfo> MethodCache = new Dictionary<Type, MethodInfo>();
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<T>(ICollection<T> collection, string title) where T : new()
{
var oc = collection as ObservableCollection<T> ?? new ObservableCollection<T>(collection);
AddButton.Click += (sender, args) =>
{
var t = new T();
oc.Add(t);
ElementList.SelectedItem = t;
};
RemoveButton.Click += RemoveButton_OnClick<T>;
ElementList.SelectionChanged += ElementsList_OnSelected;
ElementList.ItemsSource = oc;
Editor.Edit(collection);
Title = title;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
ShowDialog();
if (!(collection is ObservableCollection<T>))
{
collection.Clear();
foreach (var o in oc)
collection.Add(o);
}
}
private void RemoveButton_OnClick<T>(object sender, RoutedEventArgs e)
{
//this is kinda shitty, but item count is normally small, and it prevents CollectionModifiedExceptions
var l = (ObservableCollection<T>)ElementList.ItemsSource;
var r = new List<T>(ElementList.SelectedItems.Cast<T>());
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;
}
}
}

View File

@@ -20,6 +20,6 @@
<TextBox Name="TbFilter" Grid.Column="1" Margin="3" TextChanged="UpdateFilter" IsEnabled="False"/>
</Grid>
<ScrollViewer Grid.Row="1" x:Name="ScrollViewer"/>
<TextBlock x:Name="TbDescription" Grid.Row="2" MinHeight="18"/>
<TextBlock x:Name="TbDescription" Grid.Row="2" MinHeight="18" TextWrapping="Wrap"/>
</Grid>
</UserControl>

View File

@@ -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<string, List<PropertyInfo>>();
var descriptors = new Dictionary<PropertyInfo, DisplayAttribute>(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