Merge branch 'master' into experiment

This commit is contained in:
Brant Martin
2018-07-18 20:24:15 -04:00
committed by GitHub
26 changed files with 825 additions and 122 deletions

2
Jenkinsfile vendored
View File

@@ -50,7 +50,7 @@ node {
packageAndArchive(buildMode, "torch-server", "Torch.Client*") 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 /* Disabled because they fail builds more often than they detect actual problems

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.Managers;
using Torch.API.Session; using Torch.API.Session;
using Torch.Commands; using Torch.Commands;
using Torch.Mod;
using Torch.Server.Commands; using Torch.Server.Commands;
using Torch.Server.Managers; using Torch.Server.Managers;
using Torch.Utils; using Torch.Utils;
@@ -174,10 +175,14 @@ namespace Torch.Server
{ {
_watchdog?.Dispose(); _watchdog?.Dispose();
_watchdog = null; _watchdog = null;
ModCommunication.Unregister();
} }
if (newState == TorchSessionState.Loaded) if (newState == TorchSessionState.Loaded)
{
CurrentSession.Managers.GetManager<CommandManager>().RegisterCommandModule(typeof(WhitelistCommands)); CurrentSession.Managers.GetManager<CommandManager>().RegisterCommandModule(typeof(WhitelistCommands));
ModCommunication.Register();
}
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@@ -26,8 +26,8 @@
</Grid.RowDefinitions> </Grid.RowDefinitions>
<DockPanel Grid.Row="0"> <DockPanel Grid.Row="0">
<Label Content="World:" DockPanel.Dock="Left" /> <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."/> <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 ItemsSource="{Binding Worlds}" SelectedItem="{Binding SelectedWorld}" Margin="3" <ComboBox x:Name="WorldList" ItemsSource="{Binding Worlds}" SelectedItem="{Binding SelectedWorld}" Margin="3"
SelectionChanged="Selector_OnSelectionChanged"> SelectionChanged="Selector_OnSelectionChanged">
<ComboBox.ItemTemplate> <ComboBox.ItemTemplate>
<DataTemplate DataType="managers:WorldViewModel"> <DataTemplate DataType="managers:WorldViewModel">

View File

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

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@@ -19,6 +20,7 @@ using Torch.API;
using Torch.API.Managers; using Torch.API.Managers;
using Torch.Managers; using Torch.Managers;
using Torch.Server.ViewModels; using Torch.Server.ViewModels;
using Torch.Views;
namespace Torch.Server.Views namespace Torch.Server.Views
{ {
@@ -35,6 +37,17 @@ namespace Torch.Server.Views
InitializeComponent(); 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) public void BindServer(ITorchServer server)
{ {
_server = server; _server = server;
@@ -48,6 +61,7 @@ namespace Torch.Server.Views
_plugins = _server.Managers.GetManager<PluginManager>(); _plugins = _server.Managers.GetManager<PluginManager>();
var pluginManager = new PluginManagerViewModel(_plugins); var pluginManager = new PluginManagerViewModel(_plugins);
DataContext = pluginManager; DataContext = pluginManager;
pluginManager.PropertyChanged += PluginManagerOnPropertyChanged;
}); });
} }

View File

@@ -1,7 +1,7 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15 # Visual Studio 15
VisualStudioVersion = 15.0.27004.2010 VisualStudioVersion = 15.0.26430.14
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Torch", "Torch\Torch.csproj", "{7E01635C-3B67-472E-BCD6-C5539564F214}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Torch", "Torch\Torch.csproj", "{7E01635C-3B67-472E-BCD6-C5539564F214}"
EndProject EndProject
@@ -27,41 +27,60 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Versioning", "Versioning",
Versioning\AssemblyVersion.cs = Versioning\AssemblyVersion.cs Versioning\AssemblyVersion.cs = Versioning\AssemblyVersion.cs
EndProjectSection EndProjectSection
EndProject EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Torch.Mod", "Torch.Mod\Torch.Mod.shproj", "{3CE4D2E9-B461-4F19-8233-F87E0DFDDD74}"
EndProject
Global Global
GlobalSection(Performance) = preSolution GlobalSection(SharedMSBuildProjectFiles) = preSolution
HasPerformanceSessions = true 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 EndGlobalSection
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64 Debug|x64 = Debug|x64
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64 Release|x64 = Release|x64
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution 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.ActiveCfg = Debug|x64
{7E01635C-3B67-472E-BCD6-C5539564F214}.Debug|x64.Build.0 = 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.ActiveCfg = Release|x64
{7E01635C-3B67-472E-BCD6-C5539564F214}.Release|x64.Build.0 = 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.ActiveCfg = Debug|x64
{FBA5D932-6254-4A1E-BAF4-E229FA94E3C2}.Debug|x64.Build.0 = 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.ActiveCfg = Release|x64
{FBA5D932-6254-4A1E-BAF4-E229FA94E3C2}.Release|x64.Build.0 = 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.ActiveCfg = Debug|x64
{E36DF745-260B-4956-A2E8-09F08B2E7161}.Debug|x64.Build.0 = 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.ActiveCfg = Release|x64
{E36DF745-260B-4956-A2E8-09F08B2E7161}.Release|x64.Build.0 = 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.ActiveCfg = Debug|x64
{CA50886B-7B22-4CD8-93A0-C06F38D4F77D}.Debug|x64.Build.0 = 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.ActiveCfg = Release|x64
{CA50886B-7B22-4CD8-93A0-C06F38D4F77D}.Release|x64.Build.0 = 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.ActiveCfg = Debug|x64
{C3C8B671-6AD1-44AA-A8DA-E0C0DC0FEDF5}.Debug|x64.Build.0 = 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.ActiveCfg = Release|x64
{C3C8B671-6AD1-44AA-A8DA-E0C0DC0FEDF5}.Release|x64.Build.0 = 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.ActiveCfg = Debug|x64
{9EFD1D91-2FA2-47ED-B537-D8BC3B0E543E}.Debug|x64.Build.0 = 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.ActiveCfg = Release|x64
{9EFD1D91-2FA2-47ED-B537-D8BC3B0E543E}.Release|x64.Build.0 = 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.ActiveCfg = Debug|x64
{632E78C0-0DAC-4B71-B411-2F1B333CC310}.Debug|x64.Build.0 = 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.ActiveCfg = Release|x64
{632E78C0-0DAC-4B71-B411-2F1B333CC310}.Release|x64.Build.0 = Release|x64 {632E78C0-0DAC-4B71-B411-2F1B333CC310}.Release|x64.Build.0 = Release|x64
EndGlobalSection EndGlobalSection
@@ -74,4 +93,7 @@ Global
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BB51D91F-958D-4B63-A897-3C40642ACD3E} SolutionGuid = {BB51D91F-958D-4B63-A897-3C40642ACD3E}
EndGlobalSection EndGlobalSection
GlobalSection(Performance) = preSolution
HasPerformanceSessions = true
EndGlobalSection
EndGlobal EndGlobal

View File

@@ -17,6 +17,9 @@ using Torch.API.Managers;
using Torch.API.Session; using Torch.API.Session;
using Torch.Commands.Permissions; using Torch.Commands.Permissions;
using Torch.Managers; using Torch.Managers;
using Torch.Mod;
using Torch.Mod.Messages;
using VRage.Game;
using VRage.Game.ModAPI; using VRage.Game.ModAPI;
namespace Torch.Commands namespace Torch.Commands
@@ -75,7 +78,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)] [Permission(MyPromoteLevel.None)]
public void LongHelp() public void LongHelp()
{ {
@@ -107,13 +110,20 @@ namespace Torch.Commands
} }
else else
{ {
var sb = new StringBuilder("Available commands:\n"); var sb = new StringBuilder();
foreach (var command in commandManager.Commands.WalkTree()) foreach (var command in commandManager.Commands.WalkTree())
{ {
if (command.IsCommand) if (command.IsCommand)
sb.AppendLine($"{command.Command.SyntaxHelp}\n {command.Command.HelpText}"); 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 +179,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.")] [Command("restart cancel", "Cancel a pending restart.")]
public void CancelRestart() 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.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.IO; using System.IO;
@@ -18,7 +18,19 @@ namespace Torch
public sealed class Persistent<T> : IDisposable where T : new() public sealed class Persistent<T> : IDisposable where T : new()
{ {
public string Path { get; set; } 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() ~Persistent()
{ {
@@ -29,13 +41,23 @@ namespace Torch
{ {
Path = path; Path = path;
Data = data; 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) private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{ {
Save(); SaveAsync();
} }
public void Save(string path = null) public void Save(string path = null)
@@ -52,20 +74,20 @@ namespace Torch
public static Persistent<T> Load(string path, bool saveIfNew = true) 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)) if (File.Exists(path))
{ {
var ser = new XmlSerializer(typeof(T)); var ser = new XmlSerializer(typeof(T));
using (var f = File.OpenText(path)) using (var f = File.OpenText(path))
{ {
config.Data = (T)ser.Deserialize(f); config = new Persistent<T>(path, (T)ser.Deserialize(f));
} }
} }
else if (saveIfNew) if (config == null)
{ config = new Persistent<T>(path, new T());
config.Save(path); if (!File.Exists(path) && saveIfNew)
} config.Save();
return config; return config;
} }
@@ -76,6 +98,7 @@ namespace Torch
{ {
if (Data is INotifyPropertyChanged npc) if (Data is INotifyPropertyChanged npc)
npc.PropertyChanged -= OnPropertyChanged; npc.PropertyChanged -= OnPropertyChanged;
_saveConfigTimer?.Dispose();
Save(); Save();
} }
catch catch

View File

@@ -377,6 +377,7 @@ namespace Torch.Managers
private void InstantiatePlugin(PluginManifest manifest, IEnumerable<Assembly> assemblies) private void InstantiatePlugin(PluginManifest manifest, IEnumerable<Assembly> assemblies)
{ {
Type pluginType = null; Type pluginType = null;
bool mult = false;
foreach (var asm in assemblies) foreach (var asm in assemblies)
{ {
foreach (var type in asm.GetExportedTypes()) foreach (var type in asm.GetExportedTypes())
@@ -384,16 +385,26 @@ namespace Torch.Managers
if (!type.GetInterfaces().Contains(typeof(ITorchPlugin))) if (!type.GetInterfaces().Contains(typeof(ITorchPlugin)))
continue; continue;
_log.Info($"Loading plugin at {type.FullName}");
if (pluginType != null) if (pluginType != null)
{ {
_log.Error($"The plugin '{manifest.Name}' has multiple implementations of {nameof(ITorchPlugin)}, not loading."); //_log.Error($"The plugin '{manifest.Name}' has multiple implementations of {nameof(ITorchPlugin)}, not loading.");
return; //return;
mult = true;
continue;
} }
pluginType = type; pluginType = type;
} }
} }
if (mult)
{
_log.Error($"The plugin '{manifest.Name}' has multiple implementations of {nameof(ITorchPlugin)}, not loading.");
return;
}
if (pluginType == null) if (pluginType == null)
{ {
_log.Error($"The plugin '{manifest.Name}' does not have an implementation of {nameof(ITorchPlugin)}, not loading."); _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\GameAnalyticsPatch.cs" />
<Compile Include="Patches\GameStatePatchShim.cs" /> <Compile Include="Patches\GameStatePatchShim.cs" />
<Compile Include="Patches\ObjectFactoryInitPatch.cs" /> <Compile Include="Patches\ObjectFactoryInitPatch.cs" />
<Compile Include="Patches\SessionDownloadPatch.cs" />
<Compile Include="Patches\TorchAsyncSaving.cs" /> <Compile Include="Patches\TorchAsyncSaving.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Collections\KeyTree.cs" /> <Compile Include="Collections\KeyTree.cs" />
@@ -269,6 +270,9 @@
<DependentUpon>DictionaryEditor.xaml</DependentUpon> <DependentUpon>DictionaryEditor.xaml</DependentUpon>
</Compile> </Compile>
<Compile Include="Views\DisplayAttribute.cs" /> <Compile Include="Views\DisplayAttribute.cs" />
<Compile Include="Views\EmbeddedCollectionEditor.xaml.cs">
<DependentUpon>EmbeddedCollectionEditor.xaml</DependentUpon>
</Compile>
<Compile Include="Views\ObjectCollectionEditor.xaml.cs"> <Compile Include="Views\ObjectCollectionEditor.xaml.cs">
<DependentUpon>ObjectCollectionEditor.xaml</DependentUpon> <DependentUpon>ObjectCollectionEditor.xaml</DependentUpon>
</Compile> </Compile>
@@ -299,6 +303,10 @@
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType> <SubType>Designer</SubType>
</Page> </Page>
<Page Include="Views\EmbeddedCollectionEditor.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\ObjectCollectionEditor.xaml"> <Page Include="Views\ObjectCollectionEditor.xaml">
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
@@ -315,6 +323,7 @@
<ItemGroup> <ItemGroup>
<Service Include="{508349B6-6B84-4DF5-91F0-309BEEBAD82D}" /> <Service Include="{508349B6-6B84-4DF5-91F0-309BEEBAD82D}" />
</ItemGroup> </ItemGroup>
<Import Project="..\Torch.Mod\Torch.Mod.projitems" Label="Shared" />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\TransformOnBuild.targets" /> <Import Project="$(SolutionDir)\TransformOnBuild.targets" />
</Project> </Project>

View File

@@ -16,6 +16,7 @@ namespace Torch.Views
public bool Enabled = true; public bool Enabled = true;
public bool Visible = true; public bool Visible = true;
public bool ReadOnly = false; public bool ReadOnly = false;
public Type EditorType = null;
public DisplayAttribute() 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" xmlns:local="clr-namespace:Torch.Views"
mc:Ignorable="d" mc:Ignorable="d"
Height="370" Width="400" Title="Edit Collection"> Height="370" Width="400" Title="Edit Collection">
<Grid Width="Auto" Height="Auto"> <local:EmbeddedCollectionEditor x:Name="Editor"/>
<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>
</Window> </Window>

View File

@@ -25,90 +25,18 @@ namespace Torch.Views
/// </summary> /// </summary>
public partial class ObjectCollectionEditor : Window public partial class ObjectCollectionEditor : Window
{ {
private static readonly Dictionary<Type, MethodInfo> MethodCache = new Dictionary<Type, MethodInfo>();
private static readonly MethodInfo EditMethod;
public ObjectCollectionEditor() public ObjectCollectionEditor()
{ {
InitializeComponent(); 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) public void Edit(ICollection collection, string title)
{ {
if (collection == null) Editor.Edit(collection);
{
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;
Title = title; Title = title;
WindowStartupLocation = WindowStartupLocation.CenterOwner; WindowStartupLocation = WindowStartupLocation.CenterOwner;
ShowDialog(); 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"/> <TextBox Name="TbFilter" Grid.Column="1" Margin="3" TextChanged="UpdateFilter" IsEnabled="False"/>
</Grid> </Grid>
<ScrollViewer Grid.Row="1" x:Name="ScrollViewer"/> <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> </Grid>
</UserControl> </UserControl>

View File

@@ -67,8 +67,8 @@ namespace Torch.Views
var properties = t.GetProperties(BindingFlags.Instance | BindingFlags.Public); var properties = t.GetProperties(BindingFlags.Instance | BindingFlags.Public);
var grid = new Grid(); 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(2, GridUnitType.Star) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
var categories = new Dictionary<string, List<PropertyInfo>>(); var categories = new Dictionary<string, List<PropertyInfo>>();
var descriptors = new Dictionary<PropertyInfo, DisplayAttribute>(properties.Length); var descriptors = new Dictionary<PropertyInfo, DisplayAttribute>(properties.Length);
@@ -145,7 +145,12 @@ namespace Torch.Views
grid.Children.Add(text); grid.Children.Add(text);
FrameworkElement valueControl; 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(); valueControl = new TextBlock();
var binding = new Binding(property.Name) var binding = new Binding(property.Name)
@@ -211,11 +216,21 @@ namespace Torch.Views
valueControl = button; valueControl = button;
} }
else if (propertyType.IsPrimitive || propertyType == typeof(string)) else if (propertyType.IsPrimitive)
{ {
valueControl = new TextBox(); valueControl = new TextBox();
valueControl.SetBinding(TextBox.TextProperty, property.Name); 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 else
{ {
var button = new Button var button = new Button