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:
z__
2022-02-02 14:09:08 +07:00
parent ab61674b47
commit 1c92f69bd4
36 changed files with 599 additions and 298 deletions

View File

@@ -4,14 +4,17 @@
<variable name="logStamp" value="${time} ${pad:padding=-8:inner=[${level:uppercase=true}]}" /> <variable name="logStamp" value="${time} ${pad:padding=-8:inner=[${level:uppercase=true}]}" />
<variable name="logContent" value="${message:withException=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="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" keepFileOpen="true" concurrentWrites="false" name="keen" layout="${var:logStamp} ${logger}: ${var:logContent}"
<target xsi:type="File" name="main" layout="${var:logStamp} ${logger}: ${var:logContent}" fileName="Logs\Torch-${shortdate}.log" /> 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="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="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="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> </targets>
<rules> <rules>

View File

@@ -29,7 +29,7 @@ namespace Torch
int WindowHeight { get; set; } int WindowHeight { get; set; }
int FontSize { get; set; } int FontSize { get; set; }
UGCServiceType UgcServiceType { get; set; } UGCServiceType UgcServiceType { get; set; }
bool EntityManagerEnabled { get; set; }
void Save(string path = null); void Save(string path = null);
} }
} }

View 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; }
}

View File

@@ -23,6 +23,11 @@ namespace Torch.API.Session
/// </summary> /// </summary>
MySession KeenSession { get; } MySession KeenSession { get; }
/// <summary>
/// Currently running world
/// </summary>
IWorld World { get; }
/// <inheritdoc cref="IDependencyManager"/> /// <inheritdoc cref="IDependencyManager"/>
IDependencyManager Managers { get; } IDependencyManager Managers { get; }

View File

@@ -17,7 +17,7 @@
</PropertyGroup> </PropertyGroup>
<!-- <Import Project="$(SolutionDir)\TransformOnBuild.targets" /> --> <!-- <Import Project="$(SolutionDir)\TransformOnBuild.targets" /> -->
<ItemGroup> <ItemGroup>
<PackageReference Include="NLog" Version="4.7.13" /> <PackageReference Include="NLog" Version="5.0.0-rc2" />
<PackageReference Include="SemanticVersioning" Version="2.0.0" /> <PackageReference Include="SemanticVersioning" Version="2.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -18,7 +18,7 @@
<!-- <Import Project="$(SolutionDir)\TransformOnBuild.targets" /> --> <!-- <Import Project="$(SolutionDir)\TransformOnBuild.targets" /> -->
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <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" /> <PackageReference Include="xunit" Version="2.4.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

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

View File

@@ -5,6 +5,7 @@ using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
@@ -156,6 +157,10 @@ quit";
gameThread.Start(); gameThread.Start();
var ui = new TorchUI(_server); var ui = new TorchUI(_server);
SynchronizationContext.SetSynchronizationContext(
new DispatcherSynchronizationContext(Dispatcher.CurrentDispatcher));
ui.ShowDialog(); ui.ShowDialog();
} }
} }
@@ -192,8 +197,9 @@ quit";
try try
{ {
log.Info("Downloading SteamCMD."); log.Info("Downloading SteamCMD.");
using (var client = new WebClient()) using (var client = new HttpClient())
client.DownloadFile("https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip", STEAMCMD_ZIP); 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); ZipFile.ExtractToDirectory(STEAMCMD_ZIP, STEAMCMD_DIR);
File.Delete(STEAMCMD_ZIP); File.Delete(STEAMCMD_ZIP);

View 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;
}
}
}

View File

@@ -5,6 +5,7 @@ using System.ComponentModel;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Runtime.Serialization;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Havok; using Havok;
@@ -30,7 +31,7 @@ using VRage.Plugins;
namespace Torch.Server.Managers namespace Torch.Server.Managers
{ {
public class InstanceManager : Manager public class InstanceManager : Manager, IInstanceManager
{ {
private const string CONFIG_NAME = "SpaceEngineers-Dedicated.cfg"; private const string CONFIG_NAME = "SpaceEngineers-Dedicated.cfg";
@@ -45,6 +46,8 @@ namespace Torch.Server.Managers
} }
public IWorld SelectedWorld => DedicatedConfig.SelectedWorld;
public void LoadInstance(string path, bool validate = true) public void LoadInstance(string path, bool validate = true)
{ {
Log.Info($"Loading instance {path}"); Log.Info($"Loading instance {path}");
@@ -221,14 +224,11 @@ namespace Torch.Server.Managers
public void SaveConfig() 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)); DedicatedConfig.Save(Path.Combine(Torch.Config.InstancePath, CONFIG_NAME));
Log.Info("Saved dedicated config."); Log.Info("Saved dedicated config.");
}
try try
{ {
@@ -255,7 +255,7 @@ namespace Torch.Server.Managers
} }
catch (Exception e) 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); 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(); private static readonly Logger Log = LogManager.GetCurrentClassLogger();
public string FolderName { get; set; } public string FolderName { get; set; }
public string WorldPath { get; } public string WorldPath { get; }
public MyObjectBuilder_SessionSettings KeenSessionSettings => WorldConfiguration.Settings;
public MyObjectBuilder_Checkpoint KeenCheckpoint => Checkpoint;
public long WorldSizeKB { get; } public long WorldSizeKB { get; }
private string _checkpointPath; private string _checkpointPath;
private string _worldConfigPath; private string _worldConfigPath;
@@ -329,13 +331,15 @@ namespace Torch.Server.Managers
public void LoadSandbox() 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); Checkpoint = new CheckpointViewModel(checkpoint);
// migrate old saves // migrate old saves
if (File.Exists(_worldConfigPath)) 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); WorldConfiguration = new WorldConfigurationViewModel(worldConfig);
} }
else else

View File

@@ -25,7 +25,7 @@ namespace Torch.Server
[STAThread] [STAThread]
public static void Main(string[] args) 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. //Ensures that all the files are downloaded in the Torch directory.
var workingDir = new FileInfo(typeof(Program).Assembly.Location).Directory!.FullName; var workingDir = new FileInfo(typeof(Program).Assembly.Location).Directory!.FullName;
var binDir = Path.Combine(workingDir, "DedicatedServer64"); var binDir = Path.Combine(workingDir, "DedicatedServer64");

View File

@@ -3,7 +3,7 @@
"profiles": { "profiles": {
"Torch.Server": { "Torch.Server": {
"commandName": "Project", "commandName": "Project",
"commandLineArgs": "-noupdate",
"use64Bit": true, "use64Bit": true,
"hotReloadEnabled": false "hotReloadEnabled": false
} }

View File

@@ -39,12 +39,13 @@
</PropertyGroup> </PropertyGroup>
<!-- <Import Project="$(SolutionDir)\TransformOnBuild.targets" /> --> <!-- <Import Project="$(SolutionDir)\TransformOnBuild.targets" /> -->
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoCompleteTextBox" Version="1.3.0" />
<PackageReference Include="Ben.Demystifier" Version="0.4.1" /> <PackageReference Include="Ben.Demystifier" Version="0.4.1" />
<PackageReference Include="ControlzEx" Version="5.0.1" /> <PackageReference Include="ControlzEx" Version="5.0.1" />
<PackageReference Include="MahApps.Metro" Version="2.4.9" /> <PackageReference Include="MahApps.Metro" Version="2.4.9" />
<PackageReference Include="MdXaml" Version="1.12.0" /> <PackageReference Include="MdXaml" Version="1.12.0" />
<PackageReference Include="Microsoft.Diagnostics.Runtime" Version="2.0.226801" /> <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="PropertyChanged.Fody" Version="3.4.0" PrivateAssets="all" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="System.Management" Version="6.0.0" /> <PackageReference Include="System.Management" Version="6.0.0" />

View File

@@ -10,6 +10,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Diagnostics.Runtime; using Microsoft.Diagnostics.Runtime;
using NLog; using NLog;
using PropertyChanged;
using Sandbox; using Sandbox;
using Sandbox.Engine.Multiplayer; using Sandbox.Engine.Multiplayer;
using Sandbox.Game.Multiplayer; using Sandbox.Game.Multiplayer;
@@ -212,6 +213,7 @@ namespace Torch.Server
Environment.Exit(0); Environment.Exit(0);
} }
[SuppressPropertyChangedWarnings]
private void OnSessionStateChanged(ITorchSession session, TorchSessionState newState) private void OnSessionStateChanged(ITorchSession session, TorchSessionState newState)
{ {
if (newState == TorchSessionState.Unloading || newState == TorchSessionState.Unloaded) if (newState == TorchSessionState.Unloading || newState == TorchSessionState.Unloaded)

View File

@@ -18,11 +18,6 @@ namespace Torch.Server.ViewModels
private MyConfigDedicated<MyObjectBuilder_SessionSettings> _config; private MyConfigDedicated<MyObjectBuilder_SessionSettings> _config;
public MyConfigDedicated<MyObjectBuilder_SessionSettings> Model => _config; public MyConfigDedicated<MyObjectBuilder_SessionSettings> Model => _config;
public ConfigDedicatedViewModel() : this(new MyConfigDedicated<MyObjectBuilder_SessionSettings>(""))
{
}
public ConfigDedicatedViewModel(MyConfigDedicated<MyObjectBuilder_SessionSettings> configDedicated) public ConfigDedicatedViewModel(MyConfigDedicated<MyObjectBuilder_SessionSettings> configDedicated)
{ {
_config = configDedicated; _config = configDedicated;
@@ -36,8 +31,7 @@ namespace Torch.Server.ViewModels
Validate(); Validate();
_config.SessionSettings = SessionSettings; _config.SessionSettings = SessionSettings;
// Never ever _config.IgnoreLastSession = true;
//_config.IgnoreLastSession = true;
_config.Save(path); _config.Save(path);
} }
@@ -73,8 +67,9 @@ namespace Torch.Server.ViewModels
} }
} }
public async Task UpdateAllModInfosAsync() public Task UpdateAllModInfosAsync()
{ {
return Task.CompletedTask;
/*if (!Mods.Any()) /*if (!Mods.Any())
return; return;
List<MyWorkshopItem> modInfos; List<MyWorkshopItem> modInfos;

View 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);

View File

@@ -85,8 +85,9 @@ namespace Torch.Server.ViewModels
/// via the Steam web API. /// via the Steam web API.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public async Task<bool> UpdateModInfoAsync() public Task<bool> UpdateModInfoAsync()
{ {
return Task.FromResult(true);
/*if (UgcService.ToLower() == "mod.io") /*if (UgcService.ToLower() == "mod.io")
return true; return true;
@@ -104,7 +105,6 @@ namespace Torch.Server.ViewModels
Log.Info("Mod Info successfully retrieved!"); Log.Info("Mod Info successfully retrieved!");
FriendlyName = modInfo.Title; FriendlyName = modInfo.Title;
Description = modInfo.Description;*/ Description = modInfo.Description;*/
return true;
} }
public override string ToString() public override string ToString()

View File

@@ -3,6 +3,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 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"> mc:Ignorable="d">
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
@@ -18,7 +19,7 @@
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Button Grid.Column="1" Content="Send" DockPanel.Dock="Right" Width="50" Margin="5" Click="SendButton_Click"></Button> <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>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@@ -28,7 +28,9 @@ using Torch.API.Managers;
using Torch.API.Session; using Torch.API.Session;
using Torch.Managers; using Torch.Managers;
using Torch.Server.Managers; using Torch.Server.Managers;
using Torch.Server.Views;
using VRage.Game; using VRage.Game;
using Color = VRageMath.Color;
namespace Torch.Server namespace Torch.Server
{ {
@@ -38,12 +40,17 @@ namespace Torch.Server
public partial class ChatControl : UserControl public partial class ChatControl : UserControl
{ {
private static Logger _log = LogManager.GetCurrentClassLogger(); 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() public ChatControl()
{ {
InitializeComponent(); InitializeComponent();
this.IsVisibleChanged += OnIsVisibleChanged; this.IsVisibleChanged += OnIsVisibleChanged;
MessageBox.Provider = new CommandSuggestionsProvider(_server);
} }
private void OnIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e) private void OnIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
@@ -57,8 +64,8 @@ namespace Torch.Server
Dispatcher.Invoke(() => Dispatcher.Invoke(() =>
{ {
Message.Focus(); MessageBox.Focus();
Keyboard.Focus(Message); Keyboard.Focus(MessageBox);
}); });
}); });
} }
@@ -160,35 +167,50 @@ namespace Torch.Server
private void Message_OnKeyDown(object sender, KeyEventArgs e) private void Message_OnKeyDown(object sender, KeyEventArgs e)
{ {
if (e.Key == Key.Enter) switch (e.Key)
{
case Key.Enter:
OnMessageEntered(); 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() private void OnMessageEntered()
{ {
//Can't use Message.Text directly because of object ownership in WPF. //Can't use Message.Text directly because of object ownership in WPF.
var text = Message.Text; var text = MessageBox.Text;
if (string.IsNullOrEmpty(text)) if (string.IsNullOrEmpty(text))
return; return;
var commands = _server.CurrentSession?.Managers.GetManager<Torch.Commands.CommandManager>(); var commands = _server.CurrentSession?.Managers.GetManager<Torch.Commands.CommandManager>();
if (commands != null && commands.IsCommand(text)) 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(() => _server.Invoke(() =>
{ {
if (!commands.HandleCommandFromServer(text, InsertMessage)) if (commands.HandleCommandFromServer(text, InsertMessage)) return;
{ InsertMessage(new(_server.Config.ChatName, "Invalid command.", Color.Red, _server.Config.ChatColor));
InsertMessage(new TorchChatMessage(TorchBase.Instance.Config.ChatName, "Invalid command.", TorchBase.Instance.Config.ChatColor));
return;
}
}); });
} }
else else
{ {
_server.CurrentSession?.Managers.GetManager<IChatManagerClient>().SendMessageAsSelf(text); _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 = "";
} }
} }
} }

View 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}";
}
}
}

View File

@@ -16,9 +16,6 @@
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
</ResourceDictionary> </ResourceDictionary>
</UserControl.Resources> </UserControl.Resources>
<UserControl.DataContext>
<viewModels:ConfigDedicatedViewModel />
</UserControl.DataContext>
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
@@ -58,7 +55,7 @@
<RowDefinition /> <RowDefinition />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<ScrollViewer VerticalScrollBarVisibility="Auto"> <ScrollViewer VerticalScrollBarVisibility="Auto" x:Name="DediConfigScrollViewer">
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
@@ -133,7 +130,7 @@
</Grid> </Grid>
<TabControl Grid.Column="1" Margin="3"> <TabControl Grid.Column="1" Margin="3">
<TabItem Header="World"> <TabItem Header="World">
<views:PropertyGrid DataContext="{Binding SessionSettings}" IgnoreDisplay ="True" /> <views:PropertyGrid DataContext="{Binding SessionSettings}" />
</TabItem> </TabItem>
<TabItem Header="Torch"> <TabItem Header="Torch">
<views:PropertyGrid x:Name="TorchSettings" /> <views:PropertyGrid x:Name="TorchSettings" />

View File

@@ -8,6 +8,8 @@ using System.Windows.Controls;
using System.Windows.Data; using System.Windows.Data;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Threading; using System.Windows.Threading;
using Sandbox;
using Torch.API;
using Torch.API.Managers; using Torch.API.Managers;
using Torch.Server.Annotations; using Torch.Server.Annotations;
using Torch.Server.Managers; using Torch.Server.Managers;
@@ -32,15 +34,26 @@ namespace Torch.Server.Views
public ConfigControl() public ConfigControl()
{ {
InitializeComponent(); #pragma warning disable CS0618
_instanceManager = TorchBase.Instance.Managers.GetManager<InstanceManager>(); var instance = TorchBase.Instance;
#pragma warning restore CS0618
instance.GameStateChanged += InstanceOnGameStateChanged;
_instanceManager = instance.Managers.GetManager<InstanceManager>();
_instanceManager.InstanceLoaded += _instanceManager_InstanceLoaded; _instanceManager.InstanceLoaded += _instanceManager_InstanceLoaded;
DataContext = _instanceManager.DedicatedConfig; DataContext = _instanceManager.DedicatedConfig;
TorchSettings.DataContext = (TorchConfig)TorchBase.Instance.Config; InitializeComponent();
TorchSettings.DataContext = (TorchConfig)instance.Config;
// Gets called once all children are loaded // Gets called once all children are loaded
Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(ApplyStyles)); 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() private void CheckValid()
{ {
ConfigValid = !_bindingExpressions.Any(x => x.HasError); ConfigValid = !_bindingExpressions.Any(x => x.HasError);

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

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

View File

@@ -25,9 +25,6 @@
</Style> </Style>
</ResourceDictionary> </ResourceDictionary>
</UserControl.Resources> </UserControl.Resources>
<UserControl.DataContext>
<viewModels:ConfigDedicatedViewModel />
</UserControl.DataContext>
<Grid Style="{StaticResource RootGridStyle}"> <Grid Style="{StaticResource RootGridStyle}">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="500px"/> <ColumnDefinition Width="500px"/>

View File

@@ -17,11 +17,6 @@
<converters:InverseBooleanConverter x:Key="InverseBool"/> <converters:InverseBooleanConverter x:Key="InverseBool"/>
</ResourceDictionary> </ResourceDictionary>
</Window.Resources> </Window.Resources>
<!--
<Window.DataContext>
<local:TorchServer/>
</Window.DataContext>
-->
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition>
@@ -63,10 +58,10 @@
</StackPanel> </StackPanel>
<TabControl Grid.Row="2" Height="Auto" x:Name="TabControl" Margin="5,10,5,5"> <TabControl Grid.Row="2" Height="Auto" x:Name="TabControl" Margin="5,10,5,5">
<TabItem Header="Log"> <TabItem Header="Log">
<RichTextBox x:Name="ConsoleText" VerticalScrollBarVisibility="Visible" FontFamily="Consolas" IsReadOnly="True" Background="#0c0c0c"/> <views:LogViewerControl x:Name="ConsoleText" Margin="3" />
</TabItem> </TabItem>
<TabItem Header="Configuration"> <TabItem Header="Configuration">
<Grid IsEnabled="{Binding Path=HasRun, Converter={StaticResource InverseBool}}"> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition/> <RowDefinition/>
@@ -93,7 +88,7 @@
<local:PlayerListControl Grid.Column="1" x:Name="PlayerList" DockPanel.Dock="Right"/> <local:PlayerListControl Grid.Column="1" x:Name="PlayerList" DockPanel.Dock="Right"/>
</Grid> </Grid>
</TabItem> </TabItem>
<TabItem Header="Entity Manager" x:Name="EntityManagerTab"> <TabItem Header="Entity Manager" x:Name="EntityManagerTab" IsEnabled="{Binding Config.EntityManagerEnabled}">
</TabItem> </TabItem>
<TabItem Header="Plugins"> <TabItem Header="Plugins">
<views:PluginsControl x:Name="Plugins" /> <views:PluginsControl x:Name="Plugins" />

View File

@@ -14,12 +14,14 @@ using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using System.Windows.Navigation; using System.Windows.Navigation;
using System.Windows.Shapes; using System.Windows.Shapes;
using System.Windows.Threading;
using NLog; using NLog;
using NLog.Targets.Wrappers; using NLog.Targets.Wrappers;
using Sandbox; using Sandbox;
using Torch.API; using Torch.API;
using Torch.API.Managers; using Torch.API.Managers;
using Torch.Server.Managers; using Torch.Server.Managers;
using Torch.Server.ViewModels;
using Torch.Server.Views; using Torch.Server.Views;
using MessageBoxResult = System.Windows.MessageBoxResult; using MessageBoxResult = System.Windows.MessageBoxResult;
@@ -30,23 +32,22 @@ namespace Torch.Server
/// </summary> /// </summary>
public partial class TorchUI : Window public partial class TorchUI : Window
{ {
private TorchServer _server; private readonly TorchServer _server;
private TorchConfig _config; private ITorchConfig Config => _server.Config;
private bool _autoscrollLog = true;
public TorchUI(TorchServer server) public TorchUI(TorchServer server)
{ {
WindowStartupLocation = WindowStartupLocation.Manual;
_config = (TorchConfig)server.Config;
Width = _config.WindowWidth;
Height = _config.WindowHeight;
_server = server; _server = server;
//TODO: data binding for whole server //TODO: data binding for whole server
DataContext = server; DataContext = server;
InitializeComponent();
AttachConsole(); WindowStartupLocation = WindowStartupLocation.Manual;
Width = Config.WindowWidth;
Height = Config.WindowHeight;
InitializeComponent();
ConsoleText.FontSize = Config.FontSize;
Loaded += OnLoaded;
//Left = _config.WindowPosition.X; //Left = _config.WindowPosition.X;
//Top = _config.WindowPosition.Y; //Top = _config.WindowPosition.Y;
@@ -56,94 +57,35 @@ namespace Torch.Server
Chat.BindServer(server); Chat.BindServer(server);
PlayerList.BindServer(server); PlayerList.BindServer(server);
Plugins.BindServer(server); Plugins.BindServer(server);
LoadConfig((TorchConfig)server.Config);
Themes.uiSource = this; if (Config.EntityManagerEnabled)
Themes.SetConfig(_config); {
Title = $"{_config.InstanceName} - Torch {server.TorchVersion}, SE {server.GameVersion}"; EntityManagerTab.Content = new EntitiesControl();
Loaded += TorchUI_Loaded;
} }
private void TorchUI_Loaded(object sender, RoutedEventArgs e) Themes.uiSource = this;
Themes.SetConfig((TorchConfig) Config);
Title = $"{Config.InstanceName} - Torch {server.TorchVersion}, SE {server.GameVersion}";
}
private void OnLoaded(object sender, RoutedEventArgs e)
{ {
var scrollViewer = FindDescendant<ScrollViewer>(ConsoleText); AttachConsole();
scrollViewer.ScrollChanged += ConsoleText_OnScrollChanged;
} }
private void AttachConsole() private void AttachConsole()
{ {
const string target = "wpf"; const string targetName = "wpf";
var doc = LogManager.Configuration.FindTargetByName<FlowDocumentTarget>(target)?.Document; var target = LogManager.Configuration.FindTargetByName<LogViewerTarget>(targetName);
if (doc == null) if (target == null)
{ {
var wrapped = LogManager.Configuration.FindTargetByName<WrapperTargetBase>(target); var wrapped = LogManager.Configuration.FindTargetByName<WrapperTargetBase>(targetName);
doc = (wrapped?.WrappedTarget as FlowDocumentTarget)?.Document; target = wrapped?.WrappedTarget as LogViewerTarget;
} }
ConsoleText.FontSize = _config.FontSize; if (target is null) return;
ConsoleText.Document = doc ?? new FlowDocument(new Paragraph(new Run("No target!"))); var viewModel = (LogViewerViewModel)ConsoleText.DataContext;
ConsoleText.TextChanged += ConsoleText_OnTextChanged; target.LogEntries = viewModel.LogEntries;
} target.TargetContext = SynchronizationContext.Current;
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();
}
});
} }
private void BtnStart_Click(object sender, RoutedEventArgs e) private void BtnStart_Click(object sender, RoutedEventArgs e)
@@ -176,21 +118,5 @@ namespace Torch.Server
Process.GetCurrentProcess().Kill(); 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);
}
} }
} }

View File

@@ -19,7 +19,7 @@
<!-- <Import Project="$(SolutionDir)\TransformOnBuild.targets" /> --> <!-- <Import Project="$(SolutionDir)\TransformOnBuild.targets" /> -->
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <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" /> <PackageReference Include="xunit" Version="2.4.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -50,11 +50,12 @@ namespace Torch.Commands
return !string.IsNullOrEmpty(command) && command[0] == Prefix; 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))) if (!moduleType.IsSubclassOf(typeof(CommandModule)))
return; return 0;
var i = 0;
foreach (var method in moduleType.GetMethods()) foreach (var method in moduleType.GetMethods())
{ {
var commandAttrib = method.GetCustomAttribute<CommandAttribute>(); var commandAttrib = method.GetCustomAttribute<CommandAttribute>();
@@ -63,11 +64,14 @@ namespace Torch.Commands
var command = new Command(plugin, method); var command = new Command(plugin, method);
var cmdPath = string.Join(".", command.Path); var cmdPath = string.Join(".", command.Path);
_log.Info($"Registering command '{cmdPath}'"); _log.Debug($"Registering command '{cmdPath}'");
i++;
if (!Commands.AddCommand(command)) if (!Commands.AddCommand(command))
_log.Error($"Command path {cmdPath} is already registered."); _log.Error($"Command path {cmdPath} is already registered.");
} }
return i;
} }
public void UnregisterPluginCommands(ITorchPlugin plugin) public void UnregisterPluginCommands(ITorchPlugin plugin)
@@ -78,10 +82,9 @@ namespace Torch.Commands
public void RegisterPluginCommands(ITorchPlugin plugin) public void RegisterPluginCommands(ITorchPlugin plugin)
{ {
var assembly = plugin.GetType().Assembly; var assembly = plugin.GetType().Assembly;
foreach (var type in assembly.ExportedTypes) var count = assembly.ExportedTypes.Sum(type => RegisterCommandModule(type, plugin));
{ if (count > 0)
RegisterCommandModule(type, plugin); _log.Info($"Registered {count} commands from {plugin.Name}");
}
} }
private List<TorchChatMessage> HandleCommandFromServerInternal(string message, Action<TorchChatMessage> subscriber = null) private List<TorchChatMessage> HandleCommandFromServerInternal(string message, Action<TorchChatMessage> subscriber = null)

View File

@@ -23,6 +23,12 @@ namespace Torch.Managers.PatchManager
{ {
private static Action<ILHook, bool> IsAppliedSetter; 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 static readonly Logger _log = LogManager.GetCurrentClassLogger();
private readonly MethodBase _method; private readonly MethodBase _method;
@@ -103,6 +109,7 @@ namespace Torch.Managers.PatchManager
public const string INSTANCE_PARAMETER = "__instance"; public const string INSTANCE_PARAMETER = "__instance";
public const string RESULT_PARAMETER = "__result"; public const string RESULT_PARAMETER = "__result";
public const string PREFIX_SKIPPED_PARAMETER = "__prefixSkipped"; public const string PREFIX_SKIPPED_PARAMETER = "__prefixSkipped";
public const string ORIGINAL_PARAMETER = "__original";
public const string LOCAL_PARAMETER = "__local"; public const string LOCAL_PARAMETER = "__local";
private void SavePatchedMethod(string target) private void SavePatchedMethod(string target)
@@ -320,6 +327,24 @@ namespace Torch.Managers.PatchManager
yield return new MsilInstruction(OpCodes.Ldarg_0); yield return new MsilInstruction(OpCodes.Ldarg_0);
break; 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: case PREFIX_SKIPPED_PARAMETER:
{ {
if (param.ParameterType != typeof(bool)) if (param.ParameterType != typeof(bool))

View 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;
}
}

View File

@@ -9,6 +9,7 @@ using NLog;
using Torch.API; using Torch.API;
using Torch.Managers.PatchManager; using Torch.Managers.PatchManager;
using Torch.Utils; using Torch.Utils;
using VRage;
using VRage.Utils; using VRage.Utils;
namespace Torch.Patches namespace Torch.Patches
@@ -42,71 +43,81 @@ namespace Torch.Patches
[ReflectedMethodInfo(typeof(MyLog), nameof(MyLog.WriteLineAndConsole), Parameters = new[] { typeof(string) })] [ReflectedMethodInfo(typeof(MyLog), nameof(MyLog.WriteLineAndConsole), Parameters = new[] { typeof(string) })]
private static MethodInfo _logWriteLineAndConsole; private static MethodInfo _logWriteLineAndConsole;
[ReflectedMethodInfo(typeof(MyLog), nameof(MyLog.Init))]
private static MethodInfo _logInit;
#pragma warning restore 649 #pragma warning restore 649
public static void Patch(PatchContext context) public static void Patch(PatchContext context)
{ {
context.GetPattern(_logStringBuilder).Prefixes.Add(Method(nameof(PrefixLogStringBuilder))); context.GetPattern(_logStringBuilder).AddPrefix(nameof(PrefixLogStringBuilder));
context.GetPattern(_logFormatted).Prefixes.Add(Method(nameof(PrefixLogFormatted))); context.GetPattern(_logFormatted).AddPrefix(nameof(PrefixLogFormatted));
context.GetPattern(_logWriteLine).Prefixes.Add(Method(nameof(PrefixWriteLine))); context.GetPattern(_logWriteLine).AddPrefix(nameof(PrefixWriteLine));
context.GetPattern(_logAppendToClosedLog).Prefixes.Add(Method(nameof(PrefixAppendToClosedLog))); context.GetPattern(_logAppendToClosedLog).AddPrefix(nameof(PrefixAppendToClosedLog));
context.GetPattern(_logWriteLineAndConsole).Prefixes.Add(Method(nameof(PrefixWriteLineConsole))); context.GetPattern(_logWriteLineAndConsole).AddPrefix(nameof(PrefixWriteLineConsole));
context.GetPattern(_logWriteLineException).Prefixes.Add(Method(nameof(PrefixWriteLineException))); context.GetPattern(_logWriteLineException).AddPrefix(nameof(PrefixWriteLineException));
context.GetPattern(_logAppendToClosedLogException).Prefixes.Add(Method(nameof(PrefixAppendToClosedLogException))); context.GetPattern(_logAppendToClosedLogException).AddPrefix(nameof(PrefixAppendToClosedLogException));
context.GetPattern(_logWriteLineOptions).Prefixes.Add(Method(nameof(PrefixWriteLineOptions))); context.GetPattern(_logWriteLineOptions).AddPrefix(nameof(PrefixWriteLineOptions));
} context.GetPattern(_logInit).AddPrefix(nameof(PrefixInit));
private static MethodInfo Method(string name)
{
return typeof(KeenLogPatch).GetMethod(name, BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
} }
[ReflectedMethod(Name = "GetIdentByThread")] [ReflectedMethod(Name = "GetIdentByThread")]
private static Func<MyLog, int, int> GetIndentByThread = null!; private static Func<MyLog, int, int> _getIndentByThread = null!;
[ThreadStatic] [ReflectedGetter(Name = "m_lock")]
private static StringBuilder _tmpStringBuilder; private static Func<MyLog, FastResourceLock> _lockGetter = null!;
private static StringBuilder PrepareLog(MyLog log) [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);
}
_tmpStringBuilder.Clear(); private static bool PrefixInit(MyLog __instance, StringBuilder appVersionString)
var t = GetIndentByThread(log, Environment.CurrentManagedThreadId); {
__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.Append(' ', t * 3); byThreadField.SetValue(__instance, Activator.CreateInstance(byThreadField.FieldType));
return _tmpStringBuilder; indentsField.SetValue(__instance, Activator.CreateInstance(indentsField.FieldType));
_enabledSetter(__instance, true);
return false;
} }
private static bool PrefixWriteLine(MyLog __instance, string msg) private static bool PrefixWriteLine(MyLog __instance, string msg)
{ {
if (__instance.LogEnabled) if (__instance.LogEnabled && _log.IsDebugEnabled)
_log.Debug(PrepareLog(__instance).Append(msg)); _log.Debug($"{" ".PadRight(3 * GetIndentByCurrentThread())}{msg}");
return false; return false;
} }
private static bool PrefixWriteLineConsole(MyLog __instance, string msg) private static bool PrefixWriteLineConsole(MyLog __instance, string msg)
{ {
if (__instance.LogEnabled) if (__instance.LogEnabled && _log.IsInfoEnabled)
_log.Info(PrepareLog(__instance).Append(msg)); _log.Info($"{" ".PadRight(3 * GetIndentByCurrentThread())}{msg}");
return false; return false;
} }
private static bool PrefixAppendToClosedLog(MyLog __instance, string text) private static bool PrefixAppendToClosedLog(MyLog __instance, string text)
{ {
if (__instance.LogEnabled) if (__instance.LogEnabled && _log.IsDebugEnabled)
_log.Info(PrepareLog(__instance).Append(text)); _log.Debug($"{" ".PadRight(3 * GetIndentByCurrentThread())}{text}");
return false; return false;
} }
private static bool PrefixWriteLineOptions(MyLog __instance, string message, LoggingOptions option) private static bool PrefixWriteLineOptions(MyLog __instance, string message, LoggingOptions option)
{ {
if (__instance.LogEnabled && __instance.LogFlag(option)) if (__instance.LogEnabled && __instance.LogFlag(option) && _log.IsDebugEnabled)
_log.Info(PrepareLog(__instance).Append(message)); _log.Info($"{" ".PadRight(3 * GetIndentByCurrentThread())}{message}");
return false; return false;
} }
@@ -126,22 +137,22 @@ namespace Torch.Patches
{ {
if (__instance.LogEnabled) if (__instance.LogEnabled)
return false; 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; return false;
} }
private static bool PrefixLogStringBuilder(MyLog __instance, MyLogSeverity severity, StringBuilder builder) private static bool PrefixLogStringBuilder(MyLog __instance, MyLogSeverity severity, StringBuilder builder)
{ {
if (__instance.LogEnabled) if (!__instance.LogEnabled) return false;
_log.Log(LogLevelFor(severity), PrepareLog(__instance).Append(builder)); var indent = GetIndentByCurrentThread() * 3;
// because append resizes every char
builder.EnsureCapacity(indent);
builder.Append(' ', indent);
_log.Log(LogLevelFor(severity), builder);
return false; return false;
} }

View 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;
}
}

View File

@@ -26,13 +26,16 @@ namespace Torch.Session
/// </summary> /// </summary>
public MySession KeenSession { get; } public MySession KeenSession { get; }
public IWorld World { get; }
/// <inheritdoc cref="IDependencyManager"/> /// <inheritdoc cref="IDependencyManager"/>
public IDependencyManager Managers { get; } public IDependencyManager Managers { get; }
public TorchSession(ITorchBase torch, MySession keenSession) public TorchSession(ITorchBase torch, MySession keenSession, IWorld world)
{ {
Torch = torch; Torch = torch;
KeenSession = keenSession; KeenSession = keenSession;
World = world;
Managers = new DependencyManager(torch.Managers); Managers = new DependencyManager(torch.Managers);
} }

View File

@@ -28,6 +28,9 @@ namespace Torch.Session
private readonly Dictionary<ulong, MyObjectBuilder_Checkpoint.ModItem> _overrideMods; private readonly Dictionary<ulong, MyObjectBuilder_Checkpoint.ModItem> _overrideMods;
[Dependency]
private IInstanceManager _instanceManager = null!;
public event Action<CollectionChangeEventArgs> OverrideModsChanged; public event Action<CollectionChangeEventArgs> OverrideModsChanged;
/// <summary> /// <summary>
@@ -101,15 +104,18 @@ namespace Torch.Session
{ {
try try
{ {
if (_instanceManager.SelectedWorld is null)
throw new InvalidOperationException("No valid worlds selected! Please select world first.");
if (_currentSession != null) if (_currentSession != null)
{ {
_log.Warn($"Override old torch session {_currentSession.KeenSession.Name}"); _log.Warn($"Override old torch session {_currentSession.KeenSession.Name}");
_currentSession.Detach(); _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); SetState(TorchSessionState.Loading);
} }
catch (Exception e) catch (Exception e)
@@ -123,11 +129,9 @@ namespace Torch.Session
{ {
try try
{ {
if (_currentSession == null) if (_currentSession is null)
{ throw new InvalidOperationException("Session loaded event occurred when we don't have a session.");
_log.Warn("Session loaded event occurred when we don't have a session.");
return;
}
foreach (SessionManagerFactoryDel factory in _factories) foreach (SessionManagerFactoryDel factory in _factories)
{ {
IManager manager = factory(CurrentSession); IManager manager = factory(CurrentSession);
@@ -135,7 +139,7 @@ namespace Torch.Session
CurrentSession.Managers.AddManager(manager); CurrentSession.Managers.AddManager(manager);
} }
(CurrentSession as TorchSession)?.Attach(); (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); SetState(TorchSessionState.Loaded);
} }
catch (Exception e) catch (Exception e)
@@ -149,12 +153,10 @@ namespace Torch.Session
{ {
try try
{ {
if (_currentSession == null) if (_currentSession is null)
{ throw new InvalidOperationException("Session loaded event occurred when we don't have a session.");
_log.Warn("Session unloading event occurred when we don't have a session.");
return; _log.Info($"Unloading torch session for {_currentSession.World.FolderName}");
}
_log.Info($"Unloading torch session for {_currentSession.KeenSession.Name}");
SetState(TorchSessionState.Unloading); SetState(TorchSessionState.Unloading);
_currentSession.Detach(); _currentSession.Detach();
} }
@@ -169,12 +171,10 @@ namespace Torch.Session
{ {
try try
{ {
if (_currentSession == null) if (_currentSession is null)
{ throw new InvalidOperationException("Session loaded event occurred when we don't have a session.");
_log.Warn("Session unloading event occurred when we don't have a session.");
return; _log.Info($"Unloaded torch session for {_currentSession.World.FolderName}");
}
_log.Info($"Unloaded torch session for {_currentSession.KeenSession.Name}");
SetState(TorchSessionState.Unloaded); SetState(TorchSessionState.Unloaded);
_currentSession = null; _currentSession = null;
} }

View File

@@ -20,10 +20,10 @@
<!-- <Import Project="$(SolutionDir)\TransformOnBuild.targets" /> --> <!-- <Import Project="$(SolutionDir)\TransformOnBuild.targets" /> -->
<ItemGroup> <ItemGroup>
<PackageReference Include="ControlzEx" Version="5.0.1" /> <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="MahApps.Metro" Version="2.4.9" />
<PackageReference Include="MonoMod.RuntimeDetour" Version="22.1.4.3" /> <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="PropertyChanged.Fody" Version="3.4.0" PrivateAssets="all" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="Torch.SixLabors.ImageSharp" Version="1.0.0-beta6" /> <PackageReference Include="Torch.SixLabors.ImageSharp" Version="1.0.0-beta6" />