updated NLog to v5
fixed most of issues with world creating/loading fixed log window lags fixed some compiler warnings fixed empty log files creating fixed logging performance added better logging of load process commands autocomplete in torch GUI chat in torch GUI now has entered history (like console, use up/down arrows) abstraction of instance manager session name now correctly displaying in torchSessionManager messages TorchSession now has loaded world property (as torch now has more control about world load process) now only dedicated config locks after session start
This commit is contained in:
11
NLog.config
11
NLog.config
@@ -4,14 +4,17 @@
|
||||
<variable name="logStamp" value="${time} ${pad:padding=-8:inner=[${level:uppercase=true}]}" />
|
||||
<variable name="logContent" value="${message:withException=true}"/>
|
||||
|
||||
<targets async="true">
|
||||
<targets>
|
||||
<default-wrapper xsi:type="AsyncWrapper" overflowAction="Block" optimizeBufferReuse="true" />
|
||||
<target xsi:type="Null" name="null" formatMessage="false" />
|
||||
<target xsi:type="File" name="keen" layout="${var:logStamp} ${logger}: ${var:logContent}" fileName="Logs\Keen-${shortdate}.log" />
|
||||
<target xsi:type="File" name="main" layout="${var:logStamp} ${logger}: ${var:logContent}" fileName="Logs\Torch-${shortdate}.log" />
|
||||
<target xsi:type="File" keepFileOpen="true" concurrentWrites="false" name="keen" layout="${var:logStamp} ${logger}: ${var:logContent}"
|
||||
fileName="Logs\Keen-${shortdate}.log" />
|
||||
<target xsi:type="File" keepFileOpen="true" concurrentWrites="false" name="main" layout="${var:logStamp} ${logger}: ${var:logContent}"
|
||||
fileName="Logs\Torch-${shortdate}.log" />
|
||||
<target xsi:type="File" name="chat" layout="${longdate} ${message}" fileName="Logs\Chat.log" />
|
||||
<target xsi:type="ColoredConsole" name="console" layout="${var:logStamp} ${logger:shortName=true}: ${var:logContent}" />
|
||||
<target xsi:type="File" name="patch" layout="${var:logContent}" fileName="Logs\patch.log"/>
|
||||
<target xsi:type="FlowDocument" name="wpf" layout="${var:logStamp} ${logger:shortName=true}: ${var:logContent}" />
|
||||
<target xsi:type="LogViewerTarget" name="wpf" layout="[${level:uppercase=true}] ${logger:shortName=true}: ${var:logContent}" />
|
||||
</targets>
|
||||
|
||||
<rules>
|
||||
|
@@ -29,7 +29,7 @@ namespace Torch
|
||||
int WindowHeight { get; set; }
|
||||
int FontSize { get; set; }
|
||||
UGCServiceType UgcServiceType { get; set; }
|
||||
|
||||
bool EntityManagerEnabled { get; set; }
|
||||
void Save(string path = null);
|
||||
}
|
||||
}
|
21
Torch.API/Managers/IInstanceManager.cs
Normal file
21
Torch.API/Managers/IInstanceManager.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using VRage.Game;
|
||||
|
||||
namespace Torch.API.Managers;
|
||||
|
||||
public interface IInstanceManager : IManager
|
||||
{
|
||||
IWorld SelectedWorld { get; }
|
||||
void LoadInstance(string path, bool validate = true);
|
||||
void SelectCreatedWorld(string worldPath);
|
||||
void SelectWorld(string worldPath, bool modsOnly = true);
|
||||
void ImportSelectedWorldConfig();
|
||||
void SaveConfig();
|
||||
}
|
||||
|
||||
public interface IWorld
|
||||
{
|
||||
string FolderName { get; }
|
||||
string WorldPath { get; }
|
||||
MyObjectBuilder_SessionSettings KeenSessionSettings { get; }
|
||||
MyObjectBuilder_Checkpoint KeenCheckpoint { get; }
|
||||
}
|
@@ -22,6 +22,11 @@ namespace Torch.API.Session
|
||||
/// The Space Engineers game session this session is bound to.
|
||||
/// </summary>
|
||||
MySession KeenSession { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Currently running world
|
||||
/// </summary>
|
||||
IWorld World { get; }
|
||||
|
||||
/// <inheritdoc cref="IDependencyManager"/>
|
||||
IDependencyManager Managers { get; }
|
||||
|
@@ -17,7 +17,7 @@
|
||||
</PropertyGroup>
|
||||
<!-- <Import Project="$(SolutionDir)\TransformOnBuild.targets" /> -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NLog" Version="4.7.13" />
|
||||
<PackageReference Include="NLog" Version="5.0.0-rc2" />
|
||||
<PackageReference Include="SemanticVersioning" Version="2.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
@@ -18,7 +18,7 @@
|
||||
<!-- <Import Project="$(SolutionDir)\TransformOnBuild.targets" /> -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NLog" Version="4.7.13" />
|
||||
<PackageReference Include="NLog" Version="5.0.0-rc2" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
@@ -1,54 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Media;
|
||||
using NLog;
|
||||
using NLog.Targets;
|
||||
|
||||
namespace Torch.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// NLog target that writes to a <see cref="FlowDocument"/>.
|
||||
/// </summary>
|
||||
[Target("flowDocument")]
|
||||
public sealed class FlowDocumentTarget : TargetWithLayout
|
||||
{
|
||||
private FlowDocument _document = new FlowDocument { Background = new SolidColorBrush(Colors.Black) };
|
||||
private readonly Paragraph _paragraph = new Paragraph();
|
||||
private readonly int _maxLines = 500;
|
||||
|
||||
public FlowDocument Document => _document;
|
||||
|
||||
public FlowDocumentTarget()
|
||||
{
|
||||
_document.Blocks.Add(_paragraph);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Write(LogEventInfo logEvent)
|
||||
{
|
||||
_document.Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
var message = $"{Layout.Render(logEvent)}\n";
|
||||
_paragraph.Inlines.Add(new Run(message) {Foreground = LogLevelColors[logEvent.Level]});
|
||||
|
||||
// A massive paragraph slows the UI down
|
||||
if (_paragraph.Inlines.Count > _maxLines)
|
||||
_paragraph.Inlines.Remove(_paragraph.Inlines.FirstInline);
|
||||
});
|
||||
}
|
||||
|
||||
private static readonly Dictionary<LogLevel, SolidColorBrush> LogLevelColors = new Dictionary<LogLevel, SolidColorBrush>
|
||||
{
|
||||
[LogLevel.Trace] = new SolidColorBrush(Colors.DimGray),
|
||||
[LogLevel.Debug] = new SolidColorBrush(Colors.DarkGray),
|
||||
[LogLevel.Info] = new SolidColorBrush(Colors.White),
|
||||
[LogLevel.Warn] = new SolidColorBrush(Colors.Magenta),
|
||||
[LogLevel.Error] = new SolidColorBrush(Colors.Yellow),
|
||||
[LogLevel.Fatal] = new SolidColorBrush(Colors.Red),
|
||||
};
|
||||
}
|
||||
}
|
@@ -5,6 +5,7 @@ using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
@@ -156,6 +157,10 @@ quit";
|
||||
gameThread.Start();
|
||||
|
||||
var ui = new TorchUI(_server);
|
||||
|
||||
SynchronizationContext.SetSynchronizationContext(
|
||||
new DispatcherSynchronizationContext(Dispatcher.CurrentDispatcher));
|
||||
|
||||
ui.ShowDialog();
|
||||
}
|
||||
}
|
||||
@@ -192,8 +197,9 @@ quit";
|
||||
try
|
||||
{
|
||||
log.Info("Downloading SteamCMD.");
|
||||
using (var client = new WebClient())
|
||||
client.DownloadFile("https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip", STEAMCMD_ZIP);
|
||||
using (var client = new HttpClient())
|
||||
using (var file = File.Create(STEAMCMD_ZIP))
|
||||
client.GetStreamAsync("https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip").Result.CopyTo(file);
|
||||
|
||||
ZipFile.ExtractToDirectory(STEAMCMD_ZIP, STEAMCMD_DIR);
|
||||
File.Delete(STEAMCMD_ZIP);
|
||||
|
51
Torch.Server/LogViewerTarget.cs
Normal file
51
Torch.Server/LogViewerTarget.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using NLog;
|
||||
using NLog.Targets;
|
||||
using Torch.Server.ViewModels;
|
||||
using Torch.Server.Views;
|
||||
|
||||
namespace Torch.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// NLog target that writes to a <see cref="LogViewerControl"/>.
|
||||
/// </summary>
|
||||
[Target("logViewer")]
|
||||
public sealed class LogViewerTarget : TargetWithLayout
|
||||
{
|
||||
public IList<LogEntry> LogEntries { get; set; }
|
||||
public SynchronizationContext TargetContext { get; set; }
|
||||
private readonly int _maxLines = 1000;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Write(LogEventInfo logEvent)
|
||||
{
|
||||
TargetContext?.Post(_sendOrPostCallback, logEvent);
|
||||
}
|
||||
|
||||
private void WriteCallback(object state)
|
||||
{
|
||||
var logEvent = (LogEventInfo) state;
|
||||
LogEntries?.Add(new(logEvent.TimeStamp, Layout.Render(logEvent), LogLevelColors[logEvent.Level]));
|
||||
}
|
||||
|
||||
private static readonly Dictionary<LogLevel, SolidColorBrush> LogLevelColors = new()
|
||||
{
|
||||
[LogLevel.Trace] = new SolidColorBrush(Colors.DimGray),
|
||||
[LogLevel.Debug] = new SolidColorBrush(Colors.DarkGray),
|
||||
[LogLevel.Info] = new SolidColorBrush(Colors.White),
|
||||
[LogLevel.Warn] = new SolidColorBrush(Colors.Magenta),
|
||||
[LogLevel.Error] = new SolidColorBrush(Colors.Yellow),
|
||||
[LogLevel.Fatal] = new SolidColorBrush(Colors.Red),
|
||||
};
|
||||
|
||||
private readonly SendOrPostCallback _sendOrPostCallback;
|
||||
|
||||
public LogViewerTarget()
|
||||
{
|
||||
_sendOrPostCallback = WriteCallback;
|
||||
}
|
||||
}
|
||||
}
|
@@ -5,6 +5,7 @@ using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Havok;
|
||||
@@ -30,7 +31,7 @@ using VRage.Plugins;
|
||||
|
||||
namespace Torch.Server.Managers
|
||||
{
|
||||
public class InstanceManager : Manager
|
||||
public class InstanceManager : Manager, IInstanceManager
|
||||
{
|
||||
private const string CONFIG_NAME = "SpaceEngineers-Dedicated.cfg";
|
||||
|
||||
@@ -44,7 +45,9 @@ namespace Torch.Server.Managers
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
public IWorld SelectedWorld => DedicatedConfig.SelectedWorld;
|
||||
|
||||
public void LoadInstance(string path, bool validate = true)
|
||||
{
|
||||
Log.Info($"Loading instance {path}");
|
||||
@@ -221,14 +224,11 @@ namespace Torch.Server.Managers
|
||||
|
||||
public void SaveConfig()
|
||||
{
|
||||
if (((TorchServer)Torch).HasRun)
|
||||
if (!((TorchServer)Torch).HasRun)
|
||||
{
|
||||
Log.Warn("Checkpoint cache is stale, not saving dedicated config.");
|
||||
return;
|
||||
DedicatedConfig.Save(Path.Combine(Torch.Config.InstancePath, CONFIG_NAME));
|
||||
Log.Info("Saved dedicated config.");
|
||||
}
|
||||
|
||||
DedicatedConfig.Save(Path.Combine(Torch.Config.InstancePath, CONFIG_NAME));
|
||||
Log.Info("Saved dedicated config.");
|
||||
|
||||
try
|
||||
{
|
||||
@@ -255,7 +255,7 @@ namespace Torch.Server.Managers
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error("Failed to write sandbox config, changes will not appear on server");
|
||||
Log.Error("Failed to write sandbox config");
|
||||
Log.Error(e);
|
||||
}
|
||||
}
|
||||
@@ -276,12 +276,14 @@ namespace Torch.Server.Managers
|
||||
}
|
||||
}
|
||||
|
||||
public class WorldViewModel : ViewModel
|
||||
public class WorldViewModel : ViewModel, IWorld
|
||||
{
|
||||
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
|
||||
|
||||
public string FolderName { get; set; }
|
||||
public string WorldPath { get; }
|
||||
public MyObjectBuilder_SessionSettings KeenSessionSettings => WorldConfiguration.Settings;
|
||||
public MyObjectBuilder_Checkpoint KeenCheckpoint => Checkpoint;
|
||||
public long WorldSizeKB { get; }
|
||||
private string _checkpointPath;
|
||||
private string _worldConfigPath;
|
||||
@@ -329,13 +331,15 @@ namespace Torch.Server.Managers
|
||||
|
||||
public void LoadSandbox()
|
||||
{
|
||||
MyObjectBuilderSerializer.DeserializeXML(_checkpointPath, out MyObjectBuilder_Checkpoint checkpoint);
|
||||
if (!MyObjectBuilderSerializer.DeserializeXML(_checkpointPath, out MyObjectBuilder_Checkpoint checkpoint))
|
||||
throw new SerializationException("Error reading checkpoint, see keen log for details");
|
||||
Checkpoint = new CheckpointViewModel(checkpoint);
|
||||
|
||||
// migrate old saves
|
||||
if (File.Exists(_worldConfigPath))
|
||||
{
|
||||
MyObjectBuilderSerializer.DeserializeXML(_worldConfigPath, out MyObjectBuilder_WorldConfiguration worldConfig);
|
||||
if (!MyObjectBuilderSerializer.DeserializeXML(_worldConfigPath, out MyObjectBuilder_WorldConfiguration worldConfig))
|
||||
throw new SerializationException("Error reading settings, see keen log for details");
|
||||
WorldConfiguration = new WorldConfigurationViewModel(worldConfig);
|
||||
}
|
||||
else
|
||||
|
@@ -25,7 +25,7 @@ namespace Torch.Server
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
Target.Register<FlowDocumentTarget>("FlowDocument");
|
||||
Target.Register<LogViewerTarget>(nameof(LogViewerTarget));
|
||||
//Ensures that all the files are downloaded in the Torch directory.
|
||||
var workingDir = new FileInfo(typeof(Program).Assembly.Location).Directory!.FullName;
|
||||
var binDir = Path.Combine(workingDir, "DedicatedServer64");
|
||||
|
@@ -3,7 +3,7 @@
|
||||
"profiles": {
|
||||
"Torch.Server": {
|
||||
"commandName": "Project",
|
||||
|
||||
"commandLineArgs": "-noupdate",
|
||||
"use64Bit": true,
|
||||
"hotReloadEnabled": false
|
||||
}
|
||||
|
@@ -39,12 +39,13 @@
|
||||
</PropertyGroup>
|
||||
<!-- <Import Project="$(SolutionDir)\TransformOnBuild.targets" /> -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoCompleteTextBox" Version="1.3.0" />
|
||||
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
|
||||
<PackageReference Include="ControlzEx" Version="5.0.1" />
|
||||
<PackageReference Include="MahApps.Metro" Version="2.4.9" />
|
||||
<PackageReference Include="MdXaml" Version="1.12.0" />
|
||||
<PackageReference Include="Microsoft.Diagnostics.Runtime" Version="2.0.226801" />
|
||||
<PackageReference Include="NLog" Version="4.7.13" />
|
||||
<PackageReference Include="NLog" Version="5.0.0-rc2" />
|
||||
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" PrivateAssets="all" />
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||
<PackageReference Include="System.Management" Version="6.0.0" />
|
||||
|
@@ -10,6 +10,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Diagnostics.Runtime;
|
||||
using NLog;
|
||||
using PropertyChanged;
|
||||
using Sandbox;
|
||||
using Sandbox.Engine.Multiplayer;
|
||||
using Sandbox.Game.Multiplayer;
|
||||
@@ -212,6 +213,7 @@ namespace Torch.Server
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
[SuppressPropertyChangedWarnings]
|
||||
private void OnSessionStateChanged(ITorchSession session, TorchSessionState newState)
|
||||
{
|
||||
if (newState == TorchSessionState.Unloading || newState == TorchSessionState.Unloaded)
|
||||
|
@@ -18,11 +18,6 @@ namespace Torch.Server.ViewModels
|
||||
private MyConfigDedicated<MyObjectBuilder_SessionSettings> _config;
|
||||
public MyConfigDedicated<MyObjectBuilder_SessionSettings> Model => _config;
|
||||
|
||||
public ConfigDedicatedViewModel() : this(new MyConfigDedicated<MyObjectBuilder_SessionSettings>(""))
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public ConfigDedicatedViewModel(MyConfigDedicated<MyObjectBuilder_SessionSettings> configDedicated)
|
||||
{
|
||||
_config = configDedicated;
|
||||
@@ -36,8 +31,7 @@ namespace Torch.Server.ViewModels
|
||||
Validate();
|
||||
|
||||
_config.SessionSettings = SessionSettings;
|
||||
// Never ever
|
||||
//_config.IgnoreLastSession = true;
|
||||
_config.IgnoreLastSession = true;
|
||||
_config.Save(path);
|
||||
}
|
||||
|
||||
@@ -73,8 +67,9 @@ namespace Torch.Server.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateAllModInfosAsync()
|
||||
public Task UpdateAllModInfosAsync()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
/*if (!Mods.Any())
|
||||
return;
|
||||
List<MyWorkshopItem> modInfos;
|
||||
|
13
Torch.Server/ViewModels/LogViewerViewModel.cs
Normal file
13
Torch.Server/ViewModels/LogViewerViewModel.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Torch.Server.ViewModels;
|
||||
|
||||
public class LogViewerViewModel : ViewModel
|
||||
{
|
||||
public ObservableCollection<LogEntry> LogEntries { get; set; } = new();
|
||||
}
|
||||
|
||||
|
||||
public record LogEntry(DateTime Timestamp, string Message, SolidColorBrush Color);
|
@@ -85,8 +85,9 @@ namespace Torch.Server.ViewModels
|
||||
/// via the Steam web API.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> UpdateModInfoAsync()
|
||||
public Task<bool> UpdateModInfoAsync()
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
/*if (UgcService.ToLower() == "mod.io")
|
||||
return true;
|
||||
|
||||
@@ -104,7 +105,6 @@ namespace Torch.Server.ViewModels
|
||||
Log.Info("Mod Info successfully retrieved!");
|
||||
FriendlyName = modInfo.Title;
|
||||
Description = modInfo.Description;*/
|
||||
return true;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
|
@@ -2,7 +2,8 @@
|
||||
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:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:editors="http://wpfcontrols.com/"
|
||||
mc:Ignorable="d">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
@@ -18,7 +19,7 @@
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button Grid.Column="1" Content="Send" DockPanel.Dock="Right" Width="50" Margin="5" Click="SendButton_Click"></Button>
|
||||
<TextBox Grid.Column="0" x:Name="Message" Margin="5" KeyDown="Message_OnKeyDown"></TextBox>
|
||||
<editors:AutoCompleteTextBox Grid.Column="0" Margin="5" KeyDown="Message_OnKeyDown" x:Name="MessageBox" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
@@ -28,7 +28,9 @@ using Torch.API.Managers;
|
||||
using Torch.API.Session;
|
||||
using Torch.Managers;
|
||||
using Torch.Server.Managers;
|
||||
using Torch.Server.Views;
|
||||
using VRage.Game;
|
||||
using Color = VRageMath.Color;
|
||||
|
||||
namespace Torch.Server
|
||||
{
|
||||
@@ -38,12 +40,17 @@ namespace Torch.Server
|
||||
public partial class ChatControl : UserControl
|
||||
{
|
||||
private static Logger _log = LogManager.GetCurrentClassLogger();
|
||||
private ITorchServer _server;
|
||||
#pragma warning disable CS0618
|
||||
private ITorchServer _server = (ITorchServer) TorchBase.Instance;
|
||||
#pragma warning restore CS0618
|
||||
private readonly LinkedList<string> _lastMessages = new();
|
||||
private LinkedListNode<string> _currentLastMessageNode;
|
||||
|
||||
public ChatControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.IsVisibleChanged += OnIsVisibleChanged;
|
||||
MessageBox.Provider = new CommandSuggestionsProvider(_server);
|
||||
}
|
||||
|
||||
private void OnIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||
@@ -57,8 +64,8 @@ namespace Torch.Server
|
||||
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
Message.Focus();
|
||||
Keyboard.Focus(Message);
|
||||
MessageBox.Focus();
|
||||
Keyboard.Focus(MessageBox);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -160,35 +167,50 @@ namespace Torch.Server
|
||||
|
||||
private void Message_OnKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Enter)
|
||||
OnMessageEntered();
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.Enter:
|
||||
OnMessageEntered();
|
||||
break;
|
||||
case Key.Up:
|
||||
_currentLastMessageNode = _currentLastMessageNode?.Previous ?? _lastMessages.Last;
|
||||
MessageBox.Text = _currentLastMessageNode?.Value ?? string.Empty;
|
||||
break;
|
||||
case Key.Down:
|
||||
_currentLastMessageNode = _currentLastMessageNode?.Next ?? _lastMessages.First;
|
||||
MessageBox.Text = _currentLastMessageNode?.Value ?? string.Empty;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMessageEntered()
|
||||
{
|
||||
//Can't use Message.Text directly because of object ownership in WPF.
|
||||
var text = Message.Text;
|
||||
var text = MessageBox.Text;
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return;
|
||||
|
||||
var commands = _server.CurrentSession?.Managers.GetManager<Torch.Commands.CommandManager>();
|
||||
if (commands != null && commands.IsCommand(text))
|
||||
{
|
||||
InsertMessage(new TorchChatMessage(TorchBase.Instance.Config.ChatName, text, TorchBase.Instance.Config.ChatColor));
|
||||
InsertMessage(new(_server.Config.ChatName, text, Color.Red, _server.Config.ChatColor));
|
||||
_server.Invoke(() =>
|
||||
{
|
||||
if (!commands.HandleCommandFromServer(text, InsertMessage))
|
||||
{
|
||||
InsertMessage(new TorchChatMessage(TorchBase.Instance.Config.ChatName, "Invalid command.", TorchBase.Instance.Config.ChatColor));
|
||||
return;
|
||||
}
|
||||
if (commands.HandleCommandFromServer(text, InsertMessage)) return;
|
||||
InsertMessage(new(_server.Config.ChatName, "Invalid command.", Color.Red, _server.Config.ChatColor));
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
_server.CurrentSession?.Managers.GetManager<IChatManagerClient>().SendMessageAsSelf(text);
|
||||
}
|
||||
Message.Text = "";
|
||||
if (_currentLastMessageNode is { } && _currentLastMessageNode.Value == text)
|
||||
{
|
||||
_lastMessages.Remove(_currentLastMessageNode);
|
||||
}
|
||||
_lastMessages.AddLast(text);
|
||||
_currentLastMessageNode = null;
|
||||
MessageBox.Text = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
48
Torch.Server/Views/CommandSuggestionsProvider.cs
Normal file
48
Torch.Server/Views/CommandSuggestionsProvider.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
using AutoCompleteTextBox.Editors;
|
||||
using Sandbox;
|
||||
using Torch.API;
|
||||
using Torch.API.Managers;
|
||||
using Torch.Commands;
|
||||
|
||||
namespace Torch.Server.Views;
|
||||
|
||||
public class CommandSuggestionsProvider : ISuggestionProvider
|
||||
{
|
||||
private readonly ITorchServer _server;
|
||||
private CommandManager _commandManager;
|
||||
|
||||
public CommandSuggestionsProvider(ITorchServer server)
|
||||
{
|
||||
_server = server;
|
||||
if (_server.CurrentSession is null)
|
||||
_server.GameStateChanged += ServerOnGameStateChanged;
|
||||
else
|
||||
_commandManager = _server.CurrentSession.Managers.GetManager<CommandManager>();
|
||||
}
|
||||
|
||||
private void ServerOnGameStateChanged(MySandboxGame game, TorchGameState newState)
|
||||
{
|
||||
if (_server.CurrentSession is { })
|
||||
_commandManager = _server.CurrentSession.Managers.GetManager<CommandManager>();
|
||||
}
|
||||
|
||||
public IEnumerable GetSuggestions(string filter)
|
||||
{
|
||||
if (_commandManager is null || !_commandManager.IsCommand(filter))
|
||||
yield break;
|
||||
var args = filter[1..].Split(' ').ToList();
|
||||
var skip = _commandManager.Commands.GetNode(args, out var node);
|
||||
if (skip == -1)
|
||||
yield break;
|
||||
var lastArg = args.Last();
|
||||
|
||||
foreach (var subcommandsKey in node.Subcommands.Keys)
|
||||
{
|
||||
if (lastArg != node.Name && !subcommandsKey.Contains(lastArg))
|
||||
continue;
|
||||
yield return $"!{string.Join(' ', node.GetPath())} {subcommandsKey}";
|
||||
}
|
||||
}
|
||||
}
|
@@ -16,9 +16,6 @@
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
<UserControl.DataContext>
|
||||
<viewModels:ConfigDedicatedViewModel />
|
||||
</UserControl.DataContext>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
@@ -58,7 +55,7 @@
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" x:Name="DediConfigScrollViewer">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
@@ -133,7 +130,7 @@
|
||||
</Grid>
|
||||
<TabControl Grid.Column="1" Margin="3">
|
||||
<TabItem Header="World">
|
||||
<views:PropertyGrid DataContext="{Binding SessionSettings}" IgnoreDisplay ="True" />
|
||||
<views:PropertyGrid DataContext="{Binding SessionSettings}" />
|
||||
</TabItem>
|
||||
<TabItem Header="Torch">
|
||||
<views:PropertyGrid x:Name="TorchSettings" />
|
||||
|
@@ -8,6 +8,8 @@ using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using Sandbox;
|
||||
using Torch.API;
|
||||
using Torch.API.Managers;
|
||||
using Torch.Server.Annotations;
|
||||
using Torch.Server.Managers;
|
||||
@@ -32,15 +34,26 @@ namespace Torch.Server.Views
|
||||
|
||||
public ConfigControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
_instanceManager = TorchBase.Instance.Managers.GetManager<InstanceManager>();
|
||||
#pragma warning disable CS0618
|
||||
var instance = TorchBase.Instance;
|
||||
#pragma warning restore CS0618
|
||||
instance.GameStateChanged += InstanceOnGameStateChanged;
|
||||
|
||||
_instanceManager = instance.Managers.GetManager<InstanceManager>();
|
||||
_instanceManager.InstanceLoaded += _instanceManager_InstanceLoaded;
|
||||
DataContext = _instanceManager.DedicatedConfig;
|
||||
TorchSettings.DataContext = (TorchConfig)TorchBase.Instance.Config;
|
||||
InitializeComponent();
|
||||
TorchSettings.DataContext = (TorchConfig)instance.Config;
|
||||
// Gets called once all children are loaded
|
||||
Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(ApplyStyles));
|
||||
}
|
||||
|
||||
private void InstanceOnGameStateChanged(MySandboxGame game, TorchGameState newState)
|
||||
{
|
||||
if (newState > TorchGameState.Creating)
|
||||
Dispatcher.InvokeAsync(() => DediConfigScrollViewer.IsEnabled = false);
|
||||
}
|
||||
|
||||
private void CheckValid()
|
||||
{
|
||||
ConfigValid = !_bindingExpressions.Any(x => x.HasError);
|
||||
|
56
Torch.Server/Views/LogViewerControl.xaml
Normal file
56
Torch.Server/Views/LogViewerControl.xaml
Normal file
@@ -0,0 +1,56 @@
|
||||
<UserControl x:Class="Torch.Server.Views.LogViewerControl"
|
||||
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.Server.Views"
|
||||
xmlns:viewModels="clr-namespace:Torch.Server.ViewModels"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="300" d:DesignWidth="300">
|
||||
<UserControl.DataContext>
|
||||
<viewModels:LogViewerViewModel />
|
||||
</UserControl.DataContext>
|
||||
<UserControl.Resources>
|
||||
<Style TargetType="ItemsControl" x:Key="LogViewerStyle">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate>
|
||||
<ScrollViewer CanContentScroll="True" ScrollChanged="ScrollViewer_OnScrollChanged" Loaded="ScrollViewer_OnLoaded" Unloaded="ScrollViewer_OnUnloaded">
|
||||
<ItemsPresenter />
|
||||
</ScrollViewer>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
|
||||
<Setter Property="ItemsPanel">
|
||||
<Setter.Value>
|
||||
<ItemsPanelTemplate>
|
||||
<VirtualizingStackPanel IsItemsHost="True" />
|
||||
</ItemsPanelTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="{x:Type TextBlock}" x:Key="TextBlockBaseStyle">
|
||||
<Setter Property="FontFamily" Value="Consolas" />
|
||||
</Style>
|
||||
|
||||
<DataTemplate DataType="{x:Type viewModels:LogEntry}">
|
||||
<Grid IsSharedSizeScope="True">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition SharedSizeGroup="Date" Width="Auto" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock Text="{Binding Timestamp, StringFormat=HH:mm:ss.fff}" Grid.Column="0"
|
||||
FontWeight="Bold" Style="{StaticResource TextBlockBaseStyle}" Foreground="{Binding Color}" Margin="5,0,5,0" />
|
||||
|
||||
<TextBlock Text="{Binding Message}" Grid.Column="1"
|
||||
TextWrapping="Wrap" Style="{StaticResource TextBlockBaseStyle}" Foreground="{Binding Color}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</UserControl.Resources>
|
||||
<Grid Background="#0c0c0c">
|
||||
<ItemsControl ItemsSource="{Binding LogEntries}" Style="{StaticResource LogViewerStyle}" />
|
||||
</Grid>
|
||||
</UserControl>
|
50
Torch.Server/Views/LogViewerControl.xaml.cs
Normal file
50
Torch.Server/Views/LogViewerControl.xaml.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using Torch.Collections;
|
||||
using Torch.Server.ViewModels;
|
||||
|
||||
namespace Torch.Server.Views;
|
||||
|
||||
public partial class LogViewerControl : UserControl
|
||||
{
|
||||
private bool _isAutoscrollEnabled = true;
|
||||
private readonly List<ScrollViewer> _viewers = new();
|
||||
public LogViewerControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
((LogViewerViewModel)DataContext).LogEntries.CollectionChanged += LogEntriesOnCollectionChanged;
|
||||
}
|
||||
|
||||
private void LogEntriesOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
if (e.Action != NotifyCollectionChangedAction.Add || !_isAutoscrollEnabled)
|
||||
return;
|
||||
foreach (var scrollViewer in _viewers)
|
||||
{
|
||||
scrollViewer.ScrollToEnd();
|
||||
}
|
||||
}
|
||||
|
||||
private void ScrollViewer_OnScrollChanged(object sender, ScrollChangedEventArgs e)
|
||||
{
|
||||
var scrollViewer = (ScrollViewer) sender;
|
||||
if (e.ExtentHeightChange == 0)
|
||||
// ReSharper disable once CompareOfFloatsByEqualityOperator
|
||||
_isAutoscrollEnabled = scrollViewer.VerticalOffset == scrollViewer.ScrollableHeight;
|
||||
}
|
||||
|
||||
private void ScrollViewer_OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_viewers.Add((ScrollViewer) sender);
|
||||
}
|
||||
|
||||
private void ScrollViewer_OnUnloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_viewers.Remove((ScrollViewer) sender);
|
||||
}
|
||||
}
|
@@ -25,9 +25,6 @@
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
<UserControl.DataContext>
|
||||
<viewModels:ConfigDedicatedViewModel />
|
||||
</UserControl.DataContext>
|
||||
<Grid Style="{StaticResource RootGridStyle}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="500px"/>
|
||||
|
@@ -17,11 +17,6 @@
|
||||
<converters:InverseBooleanConverter x:Key="InverseBool"/>
|
||||
</ResourceDictionary>
|
||||
</Window.Resources>
|
||||
<!--
|
||||
<Window.DataContext>
|
||||
<local:TorchServer/>
|
||||
</Window.DataContext>
|
||||
-->
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"></RowDefinition>
|
||||
@@ -63,10 +58,10 @@
|
||||
</StackPanel>
|
||||
<TabControl Grid.Row="2" Height="Auto" x:Name="TabControl" Margin="5,10,5,5">
|
||||
<TabItem Header="Log">
|
||||
<RichTextBox x:Name="ConsoleText" VerticalScrollBarVisibility="Visible" FontFamily="Consolas" IsReadOnly="True" Background="#0c0c0c"/>
|
||||
<views:LogViewerControl x:Name="ConsoleText" Margin="3" />
|
||||
</TabItem>
|
||||
<TabItem Header="Configuration">
|
||||
<Grid IsEnabled="{Binding Path=HasRun, Converter={StaticResource InverseBool}}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition/>
|
||||
@@ -93,7 +88,7 @@
|
||||
<local:PlayerListControl Grid.Column="1" x:Name="PlayerList" DockPanel.Dock="Right"/>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
<TabItem Header="Entity Manager" x:Name="EntityManagerTab">
|
||||
<TabItem Header="Entity Manager" x:Name="EntityManagerTab" IsEnabled="{Binding Config.EntityManagerEnabled}">
|
||||
</TabItem>
|
||||
<TabItem Header="Plugins">
|
||||
<views:PluginsControl x:Name="Plugins" />
|
||||
|
@@ -14,12 +14,14 @@ using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
using System.Windows.Threading;
|
||||
using NLog;
|
||||
using NLog.Targets.Wrappers;
|
||||
using Sandbox;
|
||||
using Torch.API;
|
||||
using Torch.API.Managers;
|
||||
using Torch.Server.Managers;
|
||||
using Torch.Server.ViewModels;
|
||||
using Torch.Server.Views;
|
||||
using MessageBoxResult = System.Windows.MessageBoxResult;
|
||||
|
||||
@@ -30,23 +32,22 @@ namespace Torch.Server
|
||||
/// </summary>
|
||||
public partial class TorchUI : Window
|
||||
{
|
||||
private TorchServer _server;
|
||||
private TorchConfig _config;
|
||||
|
||||
private bool _autoscrollLog = true;
|
||||
private readonly TorchServer _server;
|
||||
private ITorchConfig Config => _server.Config;
|
||||
|
||||
public TorchUI(TorchServer server)
|
||||
{
|
||||
WindowStartupLocation = WindowStartupLocation.Manual;
|
||||
_config = (TorchConfig)server.Config;
|
||||
Width = _config.WindowWidth;
|
||||
Height = _config.WindowHeight;
|
||||
_server = server;
|
||||
//TODO: data binding for whole server
|
||||
DataContext = server;
|
||||
|
||||
WindowStartupLocation = WindowStartupLocation.Manual;
|
||||
Width = Config.WindowWidth;
|
||||
Height = Config.WindowHeight;
|
||||
InitializeComponent();
|
||||
ConsoleText.FontSize = Config.FontSize;
|
||||
|
||||
AttachConsole();
|
||||
Loaded += OnLoaded;
|
||||
|
||||
//Left = _config.WindowPosition.X;
|
||||
//Top = _config.WindowPosition.Y;
|
||||
@@ -56,94 +57,35 @@ namespace Torch.Server
|
||||
Chat.BindServer(server);
|
||||
PlayerList.BindServer(server);
|
||||
Plugins.BindServer(server);
|
||||
LoadConfig((TorchConfig)server.Config);
|
||||
|
||||
if (Config.EntityManagerEnabled)
|
||||
{
|
||||
EntityManagerTab.Content = new EntitiesControl();
|
||||
}
|
||||
|
||||
Themes.uiSource = this;
|
||||
Themes.SetConfig(_config);
|
||||
Title = $"{_config.InstanceName} - Torch {server.TorchVersion}, SE {server.GameVersion}";
|
||||
|
||||
Loaded += TorchUI_Loaded;
|
||||
Themes.SetConfig((TorchConfig) Config);
|
||||
Title = $"{Config.InstanceName} - Torch {server.TorchVersion}, SE {server.GameVersion}";
|
||||
}
|
||||
|
||||
private void TorchUI_Loaded(object sender, RoutedEventArgs e)
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var scrollViewer = FindDescendant<ScrollViewer>(ConsoleText);
|
||||
scrollViewer.ScrollChanged += ConsoleText_OnScrollChanged;
|
||||
AttachConsole();
|
||||
}
|
||||
|
||||
private void AttachConsole()
|
||||
{
|
||||
const string target = "wpf";
|
||||
var doc = LogManager.Configuration.FindTargetByName<FlowDocumentTarget>(target)?.Document;
|
||||
if (doc == null)
|
||||
const string targetName = "wpf";
|
||||
var target = LogManager.Configuration.FindTargetByName<LogViewerTarget>(targetName);
|
||||
if (target == null)
|
||||
{
|
||||
var wrapped = LogManager.Configuration.FindTargetByName<WrapperTargetBase>(target);
|
||||
doc = (wrapped?.WrappedTarget as FlowDocumentTarget)?.Document;
|
||||
var wrapped = LogManager.Configuration.FindTargetByName<WrapperTargetBase>(targetName);
|
||||
target = wrapped?.WrappedTarget as LogViewerTarget;
|
||||
}
|
||||
ConsoleText.FontSize = _config.FontSize;
|
||||
ConsoleText.Document = doc ?? new FlowDocument(new Paragraph(new Run("No target!")));
|
||||
ConsoleText.TextChanged += ConsoleText_OnTextChanged;
|
||||
}
|
||||
|
||||
public static T FindDescendant<T>(DependencyObject obj) where T : DependencyObject
|
||||
{
|
||||
if (obj == null) return default(T);
|
||||
int numberChildren = VisualTreeHelper.GetChildrenCount(obj);
|
||||
if (numberChildren == 0) return default(T);
|
||||
|
||||
for (int i = 0; i < numberChildren; i++)
|
||||
{
|
||||
DependencyObject child = VisualTreeHelper.GetChild(obj, i);
|
||||
if (child is T)
|
||||
{
|
||||
return (T)child;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < numberChildren; i++)
|
||||
{
|
||||
DependencyObject child = VisualTreeHelper.GetChild(obj, i);
|
||||
var potentialMatch = FindDescendant<T>(child);
|
||||
if (potentialMatch != default(T))
|
||||
{
|
||||
return potentialMatch;
|
||||
}
|
||||
}
|
||||
|
||||
return default(T);
|
||||
}
|
||||
|
||||
private void ConsoleText_OnTextChanged(object sender, TextChangedEventArgs args)
|
||||
{
|
||||
var textBox = (RichTextBox) sender;
|
||||
if (_autoscrollLog)
|
||||
ConsoleText.ScrollToEnd();
|
||||
}
|
||||
|
||||
private void ConsoleText_OnScrollChanged(object sender, ScrollChangedEventArgs e)
|
||||
{
|
||||
var scrollViewer = (ScrollViewer) sender;
|
||||
if (e.ExtentHeightChange == 0)
|
||||
{
|
||||
// User change.
|
||||
_autoscrollLog = scrollViewer.VerticalOffset == scrollViewer.ScrollableHeight;
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadConfig(TorchConfig config)
|
||||
{
|
||||
if (!Directory.Exists(config.InstancePath))
|
||||
return;
|
||||
|
||||
_config = config;
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
EntityManagerTab.IsEnabled = _config.EntityManagerEnabled;
|
||||
if (_config.EntityManagerEnabled)
|
||||
{
|
||||
EntityManagerTab.Content = new EntitiesControl();
|
||||
}
|
||||
});
|
||||
if (target is null) return;
|
||||
var viewModel = (LogViewerViewModel)ConsoleText.DataContext;
|
||||
target.LogEntries = viewModel.LogEntries;
|
||||
target.TargetContext = SynchronizationContext.Current;
|
||||
}
|
||||
|
||||
private void BtnStart_Click(object sender, RoutedEventArgs e)
|
||||
@@ -176,21 +118,5 @@ namespace Torch.Server
|
||||
|
||||
Process.GetCurrentProcess().Kill();
|
||||
}
|
||||
|
||||
private void BtnRestart_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
//MySandboxGame.Static.Invoke(MySandboxGame.ReloadDedicatedServerSession); use i
|
||||
}
|
||||
|
||||
private void InstancePathBox_OnLostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
|
||||
{
|
||||
var name = ((TextBox)sender).Text;
|
||||
|
||||
if (!Directory.Exists(name))
|
||||
return;
|
||||
|
||||
_config.InstancePath = name;
|
||||
_server.Managers.GetManager<InstanceManager>().LoadInstance(_config.InstancePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -19,7 +19,7 @@
|
||||
<!-- <Import Project="$(SolutionDir)\TransformOnBuild.targets" /> -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NLog" Version="4.7.13" />
|
||||
<PackageReference Include="NLog" Version="5.0.0-rc2" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
@@ -49,12 +49,13 @@ namespace Torch.Commands
|
||||
{
|
||||
return !string.IsNullOrEmpty(command) && command[0] == Prefix;
|
||||
}
|
||||
|
||||
public void RegisterCommandModule(Type moduleType, ITorchPlugin plugin = null)
|
||||
|
||||
public int RegisterCommandModule(Type moduleType, ITorchPlugin plugin = null)
|
||||
{
|
||||
if (!moduleType.IsSubclassOf(typeof(CommandModule)))
|
||||
return;
|
||||
return 0;
|
||||
|
||||
var i = 0;
|
||||
foreach (var method in moduleType.GetMethods())
|
||||
{
|
||||
var commandAttrib = method.GetCustomAttribute<CommandAttribute>();
|
||||
@@ -63,11 +64,14 @@ namespace Torch.Commands
|
||||
|
||||
var command = new Command(plugin, method);
|
||||
var cmdPath = string.Join(".", command.Path);
|
||||
_log.Info($"Registering command '{cmdPath}'");
|
||||
_log.Debug($"Registering command '{cmdPath}'");
|
||||
i++;
|
||||
|
||||
if (!Commands.AddCommand(command))
|
||||
_log.Error($"Command path {cmdPath} is already registered.");
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
public void UnregisterPluginCommands(ITorchPlugin plugin)
|
||||
@@ -78,10 +82,9 @@ namespace Torch.Commands
|
||||
public void RegisterPluginCommands(ITorchPlugin plugin)
|
||||
{
|
||||
var assembly = plugin.GetType().Assembly;
|
||||
foreach (var type in assembly.ExportedTypes)
|
||||
{
|
||||
RegisterCommandModule(type, plugin);
|
||||
}
|
||||
var count = assembly.ExportedTypes.Sum(type => RegisterCommandModule(type, plugin));
|
||||
if (count > 0)
|
||||
_log.Info($"Registered {count} commands from {plugin.Name}");
|
||||
}
|
||||
|
||||
private List<TorchChatMessage> HandleCommandFromServerInternal(string message, Action<TorchChatMessage> subscriber = null)
|
||||
|
@@ -23,6 +23,12 @@ namespace Torch.Managers.PatchManager
|
||||
{
|
||||
private static Action<ILHook, bool> IsAppliedSetter;
|
||||
|
||||
[ReflectedMethodInfo(typeof(MethodBase), nameof(MethodBase.GetMethodFromHandle), Parameters = new[] {typeof(RuntimeMethodHandle)})]
|
||||
private static MethodInfo _getMethodFromHandle = null!;
|
||||
|
||||
[ReflectedMethodInfo(typeof(MethodBase), nameof(MethodBase.GetMethodFromHandle), Parameters = new[] {typeof(RuntimeMethodHandle), typeof(RuntimeTypeHandle)})]
|
||||
private static MethodInfo _getMethodFromHandleGeneric = null!;
|
||||
|
||||
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
|
||||
private readonly MethodBase _method;
|
||||
|
||||
@@ -103,6 +109,7 @@ namespace Torch.Managers.PatchManager
|
||||
public const string INSTANCE_PARAMETER = "__instance";
|
||||
public const string RESULT_PARAMETER = "__result";
|
||||
public const string PREFIX_SKIPPED_PARAMETER = "__prefixSkipped";
|
||||
public const string ORIGINAL_PARAMETER = "__original";
|
||||
public const string LOCAL_PARAMETER = "__local";
|
||||
|
||||
private void SavePatchedMethod(string target)
|
||||
@@ -320,6 +327,24 @@ namespace Torch.Managers.PatchManager
|
||||
yield return new MsilInstruction(OpCodes.Ldarg_0);
|
||||
break;
|
||||
}
|
||||
case ORIGINAL_PARAMETER:
|
||||
{
|
||||
if (!typeof(MethodBase).IsAssignableFrom(param.ParameterType))
|
||||
throw new PatchException($"Original parameter should be assignable to {nameof(MethodBase)}",
|
||||
_method);
|
||||
yield return new MsilInstruction(OpCodes.Ldtoken).InlineValue(_method);
|
||||
if (_method.DeclaringType!.ContainsGenericParameters)
|
||||
{
|
||||
yield return new MsilInstruction(OpCodes.Ldtoken).InlineValue(_method.DeclaringType);
|
||||
yield return new MsilInstruction(OpCodes.Call).InlineValue(_getMethodFromHandleGeneric);
|
||||
}
|
||||
else
|
||||
yield return new MsilInstruction(OpCodes.Call).InlineValue(_getMethodFromHandle);
|
||||
|
||||
if (param.ParameterType != typeof(MethodBase))
|
||||
yield return new MsilInstruction(OpCodes.Castclass).InlineValue(param.ParameterType);
|
||||
break;
|
||||
}
|
||||
case PREFIX_SKIPPED_PARAMETER:
|
||||
{
|
||||
if (param.ParameterType != typeof(bool))
|
||||
|
39
Torch/Patches/EntityIdentifierPatch.cs
Normal file
39
Torch/Patches/EntityIdentifierPatch.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Torch.Managers.PatchManager;
|
||||
using Torch.Utils;
|
||||
using VRage;
|
||||
using VRage.ModAPI;
|
||||
|
||||
namespace Torch.Patches;
|
||||
|
||||
internal static class EntityIdentifierPatch
|
||||
{
|
||||
[ReflectedGetter(Type = typeof(MyEntityIdentifier), Name = "m_perThreadData")]
|
||||
private static Func<object> _perThreadGetter = null!;
|
||||
|
||||
[ReflectedGetter(TypeName = "VRage.MyEntityIdentifier+PerThreadData, VRage.Game", Name = "EntityList")]
|
||||
private static Func<object, Dictionary<long, IMyEntity>> _entityDataGetter = null;
|
||||
|
||||
[ReflectedMethodInfo(typeof(MyEntityIdentifier), "GetPerThreadEntities")]
|
||||
private static MethodInfo _getPerThreadMethod = null!;
|
||||
|
||||
public static void Patch(PatchContext context)
|
||||
{
|
||||
context.GetPattern(_getPerThreadMethod).AddPrefix(nameof(GetPerThreadPrefix));
|
||||
}
|
||||
|
||||
// Rider DPA
|
||||
// Large Object Heap: Allocated 286,3 MB (300156688 B) of type VRage.ModAPI.IMyEntity[] by
|
||||
// List<__Canon>.AddWithResize() -> MyEntityIdentifier.GetPerThreadEntities(List)
|
||||
private static bool GetPerThreadPrefix(List<IMyEntity> result)
|
||||
{
|
||||
/*
|
||||
* This is better than 100500 calls of .Add because .Values returns ICollection<>
|
||||
* .AddRange will work with it without additional enumerations
|
||||
*/
|
||||
result.AddRange(_entityDataGetter(_perThreadGetter()).Values);
|
||||
return false;
|
||||
}
|
||||
}
|
@@ -9,6 +9,7 @@ using NLog;
|
||||
using Torch.API;
|
||||
using Torch.Managers.PatchManager;
|
||||
using Torch.Utils;
|
||||
using VRage;
|
||||
using VRage.Utils;
|
||||
|
||||
namespace Torch.Patches
|
||||
@@ -42,71 +43,81 @@ namespace Torch.Patches
|
||||
|
||||
[ReflectedMethodInfo(typeof(MyLog), nameof(MyLog.WriteLineAndConsole), Parameters = new[] { typeof(string) })]
|
||||
private static MethodInfo _logWriteLineAndConsole;
|
||||
|
||||
[ReflectedMethodInfo(typeof(MyLog), nameof(MyLog.Init))]
|
||||
private static MethodInfo _logInit;
|
||||
#pragma warning restore 649
|
||||
|
||||
|
||||
public static void Patch(PatchContext context)
|
||||
{
|
||||
context.GetPattern(_logStringBuilder).Prefixes.Add(Method(nameof(PrefixLogStringBuilder)));
|
||||
context.GetPattern(_logFormatted).Prefixes.Add(Method(nameof(PrefixLogFormatted)));
|
||||
context.GetPattern(_logStringBuilder).AddPrefix(nameof(PrefixLogStringBuilder));
|
||||
context.GetPattern(_logFormatted).AddPrefix(nameof(PrefixLogFormatted));
|
||||
|
||||
context.GetPattern(_logWriteLine).Prefixes.Add(Method(nameof(PrefixWriteLine)));
|
||||
context.GetPattern(_logAppendToClosedLog).Prefixes.Add(Method(nameof(PrefixAppendToClosedLog)));
|
||||
context.GetPattern(_logWriteLineAndConsole).Prefixes.Add(Method(nameof(PrefixWriteLineConsole)));
|
||||
context.GetPattern(_logWriteLine).AddPrefix(nameof(PrefixWriteLine));
|
||||
context.GetPattern(_logAppendToClosedLog).AddPrefix(nameof(PrefixAppendToClosedLog));
|
||||
context.GetPattern(_logWriteLineAndConsole).AddPrefix(nameof(PrefixWriteLineConsole));
|
||||
|
||||
context.GetPattern(_logWriteLineException).Prefixes.Add(Method(nameof(PrefixWriteLineException)));
|
||||
context.GetPattern(_logAppendToClosedLogException).Prefixes.Add(Method(nameof(PrefixAppendToClosedLogException)));
|
||||
context.GetPattern(_logWriteLineException).AddPrefix(nameof(PrefixWriteLineException));
|
||||
context.GetPattern(_logAppendToClosedLogException).AddPrefix(nameof(PrefixAppendToClosedLogException));
|
||||
|
||||
context.GetPattern(_logWriteLineOptions).Prefixes.Add(Method(nameof(PrefixWriteLineOptions)));
|
||||
context.GetPattern(_logWriteLineOptions).AddPrefix(nameof(PrefixWriteLineOptions));
|
||||
|
||||
}
|
||||
|
||||
private static MethodInfo Method(string name)
|
||||
{
|
||||
return typeof(KeenLogPatch).GetMethod(name, BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
context.GetPattern(_logInit).AddPrefix(nameof(PrefixInit));
|
||||
}
|
||||
|
||||
[ReflectedMethod(Name = "GetIdentByThread")]
|
||||
private static Func<MyLog, int, int> GetIndentByThread = null!;
|
||||
private static Func<MyLog, int, int> _getIndentByThread = null!;
|
||||
|
||||
[ThreadStatic]
|
||||
private static StringBuilder _tmpStringBuilder;
|
||||
|
||||
private static StringBuilder PrepareLog(MyLog log)
|
||||
[ReflectedGetter(Name = "m_lock")]
|
||||
private static Func<MyLog, FastResourceLock> _lockGetter = null!;
|
||||
|
||||
[ReflectedSetter(Name = "m_enabled")]
|
||||
private static Action<MyLog, bool> _enabledSetter = null!;
|
||||
|
||||
private static int GetIndentByCurrentThread()
|
||||
{
|
||||
_tmpStringBuilder ??= new();
|
||||
using var l = _lockGetter(MyLog.Default).AcquireExclusiveUsing();
|
||||
return _getIndentByThread(MyLog.Default, Environment.CurrentManagedThreadId);
|
||||
}
|
||||
|
||||
private static bool PrefixInit(MyLog __instance, StringBuilder appVersionString)
|
||||
{
|
||||
__instance.WriteLine("Log Started");
|
||||
var byThreadField =
|
||||
typeof(MyLog).GetField("m_indentsByThread", BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||
var indentsField = typeof(MyLog).GetField("m_indents", BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||
|
||||
_tmpStringBuilder.Clear();
|
||||
var t = GetIndentByThread(log, Environment.CurrentManagedThreadId);
|
||||
|
||||
_tmpStringBuilder.Append(' ', t * 3);
|
||||
return _tmpStringBuilder;
|
||||
byThreadField.SetValue(__instance, Activator.CreateInstance(byThreadField.FieldType));
|
||||
indentsField.SetValue(__instance, Activator.CreateInstance(indentsField.FieldType));
|
||||
_enabledSetter(__instance, true);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool PrefixWriteLine(MyLog __instance, string msg)
|
||||
{
|
||||
if (__instance.LogEnabled)
|
||||
_log.Debug(PrepareLog(__instance).Append(msg));
|
||||
if (__instance.LogEnabled && _log.IsDebugEnabled)
|
||||
_log.Debug($"{" ".PadRight(3 * GetIndentByCurrentThread())}{msg}");
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool PrefixWriteLineConsole(MyLog __instance, string msg)
|
||||
{
|
||||
if (__instance.LogEnabled)
|
||||
_log.Info(PrepareLog(__instance).Append(msg));
|
||||
if (__instance.LogEnabled && _log.IsInfoEnabled)
|
||||
_log.Info($"{" ".PadRight(3 * GetIndentByCurrentThread())}{msg}");
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool PrefixAppendToClosedLog(MyLog __instance, string text)
|
||||
{
|
||||
if (__instance.LogEnabled)
|
||||
_log.Info(PrepareLog(__instance).Append(text));
|
||||
if (__instance.LogEnabled && _log.IsDebugEnabled)
|
||||
_log.Debug($"{" ".PadRight(3 * GetIndentByCurrentThread())}{text}");
|
||||
return false;
|
||||
}
|
||||
private static bool PrefixWriteLineOptions(MyLog __instance, string message, LoggingOptions option)
|
||||
{
|
||||
if (__instance.LogEnabled && __instance.LogFlag(option))
|
||||
_log.Info(PrepareLog(__instance).Append(message));
|
||||
if (__instance.LogEnabled && __instance.LogFlag(option) && _log.IsDebugEnabled)
|
||||
_log.Info($"{" ".PadRight(3 * GetIndentByCurrentThread())}{message}");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -126,22 +137,22 @@ namespace Torch.Patches
|
||||
{
|
||||
if (__instance.LogEnabled)
|
||||
return false;
|
||||
// Sometimes this is called with a pre-formatted string and no args
|
||||
// and causes a crash when the format string contains braces
|
||||
var sb = PrepareLog(__instance);
|
||||
if (args is {Length: > 0})
|
||||
sb.AppendFormat(format, args);
|
||||
else
|
||||
sb.Append(format);
|
||||
|
||||
_log.Log(LogLevelFor(severity), sb);
|
||||
// ReSharper disable once TemplateIsNotCompileTimeConstantProblem
|
||||
_log.Log(new(LogLevelFor(severity), _log.Name, $"{" ".PadRight(3 * GetIndentByCurrentThread())}{string.Format(format, args)}"));
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool PrefixLogStringBuilder(MyLog __instance, MyLogSeverity severity, StringBuilder builder)
|
||||
{
|
||||
if (__instance.LogEnabled)
|
||||
_log.Log(LogLevelFor(severity), PrepareLog(__instance).Append(builder));
|
||||
if (!__instance.LogEnabled) return false;
|
||||
var indent = GetIndentByCurrentThread() * 3;
|
||||
|
||||
// because append resizes every char
|
||||
builder.EnsureCapacity(indent);
|
||||
builder.Append(' ', indent);
|
||||
|
||||
_log.Log(LogLevelFor(severity), builder);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
68
Torch/Patches/LoaderHook.cs
Normal file
68
Torch/Patches/LoaderHook.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System.Collections;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using NLog;
|
||||
using Sandbox.Definitions;
|
||||
using Sandbox.Game.World;
|
||||
using Torch.Managers.PatchManager;
|
||||
using Torch.Utils;
|
||||
|
||||
namespace Torch.Patches;
|
||||
|
||||
[PatchShim]
|
||||
internal static class LoaderHook
|
||||
{
|
||||
private static readonly ILogger Log = LogManager.GetCurrentClassLogger();
|
||||
private static Stopwatch _stopwatch;
|
||||
|
||||
[ReflectedMethodInfo(typeof(MyScriptManager), nameof(MyScriptManager.LoadData))]
|
||||
private static MethodInfo _compilerLoadData = null!;
|
||||
|
||||
public static void Patch(PatchContext context)
|
||||
{
|
||||
var pattern = context.GetPattern(_compilerLoadData);
|
||||
pattern.AddPrefix(nameof(CompilePrefix));
|
||||
pattern.AddSuffix(nameof(CompileSuffix));
|
||||
|
||||
var methods = typeof(MyDefinitionManager)
|
||||
.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
|
||||
pattern = context.GetPattern(methods.First(b =>
|
||||
b.Name == "LoadDefinitions" && b.GetParameters()[0].ParameterType.Name.Contains("List")));
|
||||
pattern.AddPrefix(nameof(LoadDefinitionsPrefix));
|
||||
pattern.AddSuffix(nameof(LoadDefinitionsSuffix));
|
||||
}
|
||||
|
||||
private static void CompilePrefix()
|
||||
{
|
||||
_stopwatch?.Reset();
|
||||
_stopwatch = Stopwatch.StartNew();
|
||||
Log.Info("Mod scripts compilation started");
|
||||
}
|
||||
|
||||
private static void CompileSuffix()
|
||||
{
|
||||
_stopwatch.Stop();
|
||||
Log.Info($"Compilation finished. Took {_stopwatch.Elapsed:g}");
|
||||
_stopwatch = null;
|
||||
}
|
||||
|
||||
private static void LoadDefinitionsPrefix(MyDefinitionManager __instance)
|
||||
{
|
||||
if (!__instance.Loading)
|
||||
return;
|
||||
_stopwatch?.Reset();
|
||||
_stopwatch = Stopwatch.StartNew();
|
||||
Log.Info("Definitions loading started");
|
||||
}
|
||||
|
||||
private static void LoadDefinitionsSuffix(MyDefinitionManager __instance)
|
||||
{
|
||||
if (!__instance.Loading)
|
||||
return;
|
||||
_stopwatch.Stop();
|
||||
Log.Info($"Definitions load finished. Took {_stopwatch.Elapsed:g}");
|
||||
_stopwatch = null;
|
||||
}
|
||||
}
|
@@ -26,13 +26,16 @@ namespace Torch.Session
|
||||
/// </summary>
|
||||
public MySession KeenSession { get; }
|
||||
|
||||
public IWorld World { get; }
|
||||
|
||||
/// <inheritdoc cref="IDependencyManager"/>
|
||||
public IDependencyManager Managers { get; }
|
||||
|
||||
public TorchSession(ITorchBase torch, MySession keenSession)
|
||||
public TorchSession(ITorchBase torch, MySession keenSession, IWorld world)
|
||||
{
|
||||
Torch = torch;
|
||||
KeenSession = keenSession;
|
||||
World = world;
|
||||
Managers = new DependencyManager(torch.Managers);
|
||||
}
|
||||
|
||||
|
@@ -28,6 +28,9 @@ namespace Torch.Session
|
||||
|
||||
private readonly Dictionary<ulong, MyObjectBuilder_Checkpoint.ModItem> _overrideMods;
|
||||
|
||||
[Dependency]
|
||||
private IInstanceManager _instanceManager = null!;
|
||||
|
||||
public event Action<CollectionChangeEventArgs> OverrideModsChanged;
|
||||
|
||||
/// <summary>
|
||||
@@ -101,15 +104,18 @@ namespace Torch.Session
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_instanceManager.SelectedWorld is null)
|
||||
throw new InvalidOperationException("No valid worlds selected! Please select world first.");
|
||||
|
||||
if (_currentSession != null)
|
||||
{
|
||||
_log.Warn($"Override old torch session {_currentSession.KeenSession.Name}");
|
||||
_currentSession.Detach();
|
||||
}
|
||||
|
||||
_log.Info($"Starting new torch session for {MySession.Static.Name}");
|
||||
_log.Info($"Starting new torch session for {_instanceManager.SelectedWorld.FolderName}");
|
||||
|
||||
_currentSession = new TorchSession(Torch, MySession.Static);
|
||||
_currentSession = new TorchSession(Torch, MySession.Static, _instanceManager.SelectedWorld);
|
||||
SetState(TorchSessionState.Loading);
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -123,11 +129,9 @@ namespace Torch.Session
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_currentSession == null)
|
||||
{
|
||||
_log.Warn("Session loaded event occurred when we don't have a session.");
|
||||
return;
|
||||
}
|
||||
if (_currentSession is null)
|
||||
throw new InvalidOperationException("Session loaded event occurred when we don't have a session.");
|
||||
|
||||
foreach (SessionManagerFactoryDel factory in _factories)
|
||||
{
|
||||
IManager manager = factory(CurrentSession);
|
||||
@@ -135,7 +139,7 @@ namespace Torch.Session
|
||||
CurrentSession.Managers.AddManager(manager);
|
||||
}
|
||||
(CurrentSession as TorchSession)?.Attach();
|
||||
_log.Info($"Loaded torch session for {MySession.Static.Name}");
|
||||
_log.Info($"Loaded torch session for {CurrentSession.World.FolderName}");
|
||||
SetState(TorchSessionState.Loaded);
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -149,12 +153,10 @@ namespace Torch.Session
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_currentSession == null)
|
||||
{
|
||||
_log.Warn("Session unloading event occurred when we don't have a session.");
|
||||
return;
|
||||
}
|
||||
_log.Info($"Unloading torch session for {_currentSession.KeenSession.Name}");
|
||||
if (_currentSession is null)
|
||||
throw new InvalidOperationException("Session loaded event occurred when we don't have a session.");
|
||||
|
||||
_log.Info($"Unloading torch session for {_currentSession.World.FolderName}");
|
||||
SetState(TorchSessionState.Unloading);
|
||||
_currentSession.Detach();
|
||||
}
|
||||
@@ -169,12 +171,10 @@ namespace Torch.Session
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_currentSession == null)
|
||||
{
|
||||
_log.Warn("Session unloading event occurred when we don't have a session.");
|
||||
return;
|
||||
}
|
||||
_log.Info($"Unloaded torch session for {_currentSession.KeenSession.Name}");
|
||||
if (_currentSession is null)
|
||||
throw new InvalidOperationException("Session loaded event occurred when we don't have a session.");
|
||||
|
||||
_log.Info($"Unloaded torch session for {_currentSession.World.FolderName}");
|
||||
SetState(TorchSessionState.Unloaded);
|
||||
_currentSession = null;
|
||||
}
|
||||
|
@@ -20,10 +20,10 @@
|
||||
<!-- <Import Project="$(SolutionDir)\TransformOnBuild.targets" /> -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ControlzEx" Version="5.0.1" />
|
||||
<PackageReference Include="InfoOf.Fody" Version="2.1.0" />
|
||||
<PackageReference Include="InfoOf.Fody" Version="2.1.0" PrivateAssets="all" />
|
||||
<PackageReference Include="MahApps.Metro" Version="2.4.9" />
|
||||
<PackageReference Include="MonoMod.RuntimeDetour" Version="22.1.4.3" />
|
||||
<PackageReference Include="NLog" Version="4.7.13" />
|
||||
<PackageReference Include="NLog" Version="5.0.0-rc2" />
|
||||
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" PrivateAssets="all" />
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||
<PackageReference Include="Torch.SixLabors.ImageSharp" Version="1.0.0-beta6" />
|
||||
|
Reference in New Issue
Block a user