From 376685cbfd3416d63b0d8d50e5712d35001db313 Mon Sep 17 00:00:00 2001 From: SentorX <66237290+buttheadbob@users.noreply.github.com> Date: Tue, 27 Jan 2026 00:52:01 +0100 Subject: [PATCH 1/7] Fix command argument parsing bug when token substring matches earlier token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Fix command argument parsing bug when token substring matches earlier token ### Changes - **Bug Fix**: Revised `CommandTree.GetCommand` method to correctly extract arguments from command strings. - **Minor Formatting**: Updated array initializer syntax for consistency. ### Bug Description The previous implementation used `IndexOf(split[skip])` to locate the start of the arguments within the original command text. This approach fails when an earlier command token contains the argument token as a substring. For example, given the command `"qs storegrid ore 10000"`, the token `"ore"` appears inside the preceding token `"storegrid"`. `IndexOf` would then return the position inside `"storegrid"`, causing `argText` to incorrectly start at `"oregrid ore 10000"` rather than at the actual argument `"ore 10000"`. ### Solution The fix tracks the position of each preceding token by iterating over the command tokens up to the argument start. For each token, we locate it in the original string using a case‑sensitive ordinal search, advance the position past the token and any trailing spaces, and finally take the substring from that position. This ensures the argument text begins exactly after the full command name. ### Code Changes 1. **Line 99**: Changed `new []{' '}` to `new[] { ' ' }` (cosmetic). 2. **Lines 106–145**: Replaced the faulty substring logic with a robust token‑position tracking algorithm. Added detailed inline comments explaining the bug and the fix. 3. **Line 147**: Added a blank line for readability. ### Impact - Commands with overlapping token substrings will now parse arguments correctly. - No breaking changes; the method signature and overall behavior remain unchanged. - Improves reliability of command‑line parsing in Torch's command system. --- Torch/Commands/CommandTree.cs | 37 ++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/Torch/Commands/CommandTree.cs b/Torch/Commands/CommandTree.cs index d67a1772..ee483bfb 100644 --- a/Torch/Commands/CommandTree.cs +++ b/Torch/Commands/CommandTree.cs @@ -96,7 +96,7 @@ public Command GetCommand(List path, out List args) public Command GetCommand(string commandText, out string argText) { - var split = commandText.Split(new []{' '}, StringSplitOptions.RemoveEmptyEntries).ToList(); + var split = commandText.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).ToList(); var skip = GetNode(split, out CommandNode node); if (skip == -1) { @@ -106,18 +106,45 @@ public Command GetCommand(string commandText, out string argText) if (split.Count > skip) { - var substringIndex = commandText.IndexOf(split[skip]); - if (substringIndex <= commandText.Length) + // =================================================================== + // BUG (original approach): + // var substringIndex = commandText.IndexOf(split[skip]); + // if (substringIndex <= commandText.Length) + // { + // argText = commandText.Substring(substringIndex); + // return node.Command; + // } + // + // Why it fails: + // IndexOf finds the first occurrence of the token, which may be inside a previous token. + // Example: commandText = "qs storegrid ore 10000", split[skip] = "ore". + // "storegrid" contains "ore", so IndexOf returns the position inside "storegrid". + // This causes argText to start in the middle of the command name, e.g. "oregrid ore 10000". + // =================================================================== + + // FIX: Track the position of each preceding token so we start after the command name. + int startIndex = 0; + for (int i = 0; i < skip; i++) { - argText = commandText.Substring(substringIndex); - return node.Command; + // Match the token at its expected position (case‑sensitive, ordinal search). + int idx = commandText.IndexOf(split[i], startIndex, StringComparison.Ordinal); + if (idx < 0) + break; // Should never happen because we just split the same string. + startIndex = idx + split[i].Length; + // Skip any spaces that follow the token. + while (startIndex < commandText.Length && commandText[startIndex] == ' ') + startIndex++; } + // startIndex now points to the beginning of the first argument token. + argText = commandText.Substring(startIndex).TrimStart(); + return node.Command; } argText = ""; return node.Command; } + public string GetTreeString() { var indent = 0; From dd4322c0815d2aff08b27edb538f06e48d1d0646 Mon Sep 17 00:00:00 2001 From: SentorX <66237290+buttheadbob@users.noreply.github.com> Date: Tue, 27 Jan 2026 07:04:26 +0100 Subject: [PATCH 2/7] Fix duplicate logging on startup --- Torch.Server/Initializer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Torch.Server/Initializer.cs b/Torch.Server/Initializer.cs index f7524005..471f5455 100644 --- a/Torch.Server/Initializer.cs +++ b/Torch.Server/Initializer.cs @@ -64,7 +64,7 @@ public bool Initialize(string[] args) #if !DEBUG AppDomain.CurrentDomain.UnhandledException += HandleException; - LogManager.Configuration.AddRule(LogLevel.Info, LogLevel.Fatal, "console"); + // LogManager.Configuration.AddRule(LogLevel.Info, LogLevel.Fatal, "console"); This is a duplicate rule which already exists in Nlog.conf LogManager.ReconfigExistingLoggers(); #endif From 8ce9654b4172873b020e18f2fcc0d2d4a436e962 Mon Sep 17 00:00:00 2001 From: SentorX <66237290+buttheadbob@users.noreply.github.com> Date: Tue, 27 Jan 2026 08:44:14 +0100 Subject: [PATCH 3/7] refactor: improve SteamCMD update reliability and adjust game directory structure Move dedicated server files to 'game' subdirectory Add retry logic and error handling for SteamCMD updates Remove unused using statements Centralize path calculation with GetDedicatedServer64Path method --- Torch.Server/Initializer.cs | 109 ++++++++++++++++++++++++++---------- Torch.Server/Program.cs | 2 +- 2 files changed, 80 insertions(+), 31 deletions(-) diff --git a/Torch.Server/Initializer.cs b/Torch.Server/Initializer.cs index 471f5455..f0cb3c55 100644 --- a/Torch.Server/Initializer.cs +++ b/Torch.Server/Initializer.cs @@ -8,18 +8,10 @@ using System.Reflection; using System.Text; using System.Threading; -using System.Threading.Tasks; using System.Windows; -using System.Windows.Threading; using NLog; -using NLog.Targets; using Sandbox; -using Sandbox.Engine.Utils; -using Torch.Utils; using VRage; -using VRage.FileSystem; -using VRage.Scripting; -using VRage.Utils; namespace Torch.Server { @@ -35,13 +27,16 @@ public class Initializer private static readonly string STEAMCMD_PATH = $"{STEAMCMD_DIR}\\steamcmd.exe"; private static readonly string RUNSCRIPT_PATH = $"{STEAMCMD_DIR}\\runscript.txt"; - private const string RUNSCRIPT = @"force_install_dir ../ + private const string RUNSCRIPT = @"force_install_dir ../game login anonymous -app_update 298740 +app_update 298740 quit"; private TorchServer _server; private string _basePath; + private static string GetDedicatedServer64Path(string basePath) => Path.Combine(basePath, "game", "DedicatedServer64"); + + internal Persistent ConfigPersistent { get; private set; } public TorchConfig Config => ConfigPersistent?.Data; public TorchServer Server => _server; @@ -83,7 +78,8 @@ public bool Initialize(string[] args) RunSteamCmd(); var basePath = new FileInfo(typeof(Program).Assembly.Location).Directory.ToString(); - var apiSource = Path.Combine(basePath, "DedicatedServer64", "steam_api64.dll"); + var dedicatedServerPath = GetDedicatedServer64Path(basePath); + var apiSource = Path.Combine(dedicatedServerPath, "steam_api64.dll"); var apiTarget = Path.Combine(basePath, "steam_api64.dll"); if (!File.Exists(apiTarget)) @@ -101,37 +97,37 @@ public bool Initialize(string[] args) var requiredVersion = new Version(9, 86, 62, 31); // Steam Client 64-bit DLL - var clientSource64 = Path.Combine(basePath, "DedicatedServer64", "steamclient64.dll"); + var clientSource64 = Path.Combine(dedicatedServerPath, "steamclient64.dll"); var clientTarget64 = Path.Combine(basePath, "steamclient64.dll"); CopyAndVerifyDll(clientSource64, clientTarget64, requiredVersion); // Steam Client 32-bit DLL - var clientSource = Path.Combine(basePath, "DedicatedServer64", "steamclient.dll"); + var clientSource = Path.Combine(dedicatedServerPath, "steamclient.dll"); var clientTarget = Path.Combine(basePath, "steamclient.dll"); CopyAndVerifyDll(clientSource, clientTarget, requiredVersion); // tier0 64-bit DLL - var tier0Source64 = Path.Combine(basePath, "DedicatedServer64", "tier0_s64.dll"); + var tier0Source64 = Path.Combine(dedicatedServerPath, "tier0_s64.dll"); var tier0Target64 = Path.Combine(basePath, "tier0_s64.dll"); CopyAndVerifyDll(tier0Source64, tier0Target64, requiredVersion); // tier0 32-bit DLL - var tier0Source = Path.Combine(basePath, "DedicatedServer64", "tier0_s.dll"); + var tier0Source = Path.Combine(dedicatedServerPath, "tier0_s.dll"); var tier0Target = Path.Combine(basePath, "tier0_s.dll"); CopyAndVerifyDll(tier0Source, tier0Target, requiredVersion); // vstdlib 64-bit DLL - var vstdlibSource64 = Path.Combine(basePath, "DedicatedServer64", "vstdlib_s64.dll"); + var vstdlibSource64 = Path.Combine(dedicatedServerPath, "vstdlib_s64.dll"); var vstdlibTarget64 = Path.Combine(basePath, "vstdlib_s64.dll"); CopyAndVerifyDll(vstdlibSource64, vstdlibTarget64, requiredVersion); // vstdlib 32-bit DLL - var vstdlibSource = Path.Combine(basePath, "DedicatedServer64", "vstdlib_s.dll"); + var vstdlibSource = Path.Combine(dedicatedServerPath, "vstdlib_s.dll"); var vstdlibTarget = Path.Combine(basePath, "vstdlib_s.dll"); CopyAndVerifyDll(vstdlibSource, vstdlibTarget, requiredVersion); - var havokSource = Path.Combine(basePath, "DedicatedServer64", "Havok.dll"); + var havokSource = Path.Combine(dedicatedServerPath, "Havok.dll"); var havokTarget = Path.Combine(basePath, "Havok.dll"); if (!File.Exists(havokTarget)) @@ -257,20 +253,73 @@ public static void RunSteamCmd() } log.Info("Checking for DS updates."); - var steamCmdProc = new ProcessStartInfo(STEAMCMD_PATH, "+runscript runscript.txt") - { - WorkingDirectory = Path.Combine(Directory.GetCurrentDirectory(), STEAMCMD_DIR), - UseShellExecute = false, - RedirectStandardOutput = true, - StandardOutputEncoding = Encoding.ASCII - }; - var cmd = Process.Start(steamCmdProc); - // ReSharper disable once PossibleNullReferenceException - while (!cmd.HasExited) + const int maxAttempts = 2; + for (int attempt = 1; attempt <= maxAttempts; attempt++) { - log.Info(cmd.StandardOutput.ReadLine()); - Thread.Sleep(100); + if (attempt > 1) + { + log.Info($"Retrying SteamCMD update (attempt {attempt})..."); + Thread.Sleep(3000); // brief delay before retry + } + + var steamCmdProc = new ProcessStartInfo(STEAMCMD_PATH, "+runscript runscript.txt") + { + WorkingDirectory = Path.Combine(Directory.GetCurrentDirectory(), STEAMCMD_DIR), + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + StandardOutputEncoding = Encoding.ASCII, + StandardErrorEncoding = Encoding.ASCII + }; + var cmd = Process.Start(steamCmdProc); + if (cmd == null) + { + log.Error("Failed to start SteamCMD process."); + continue; + } + + // Read output and error asynchronously to avoid deadlocks + var output = new StringBuilder(); + var error = new StringBuilder(); + cmd.OutputDataReceived += (_, args) => + { + if (args.Data != null) + { + log.Info(args.Data); + output.AppendLine(args.Data); + } + }; + cmd.ErrorDataReceived += (_, args) => + { + if (args.Data != null) + { + log.Error($"SteamCMD stderr: {args.Data}"); + error.AppendLine(args.Data); + } + }; + cmd.BeginOutputReadLine(); + cmd.BeginErrorReadLine(); + + cmd.WaitForExit(); + int exitCode = cmd.ExitCode; + + // Ensure all events are processed + Thread.Sleep(500); + + if (exitCode == 0) + { + log.Info("SteamCMD update completed successfully."); + return; + } + + log.Warn($"SteamCMD exited with code {exitCode}. Output: {output}"); + if (error.Length > 0) + log.Error($"SteamCMD errors: {error}"); + + // If this was the last attempt, break and let the caller continue (copy will fail later) + if (attempt == maxAttempts) + log.Error("SteamCMD update failed after all attempts. The DS files may be missing."); } } diff --git a/Torch.Server/Program.cs b/Torch.Server/Program.cs index 0f163f3e..9385ed34 100644 --- a/Torch.Server/Program.cs +++ b/Torch.Server/Program.cs @@ -27,7 +27,7 @@ public static void Main(string[] args) Target.Register("FlowDocument"); //Ensures that all the files are downloaded in the Torch directory. var workingDir = new FileInfo(typeof(Program).Assembly.Location).Directory.ToString(); - var binDir = Path.Combine(workingDir, "DedicatedServer64"); + var binDir = Path.Combine(workingDir, "game", "DedicatedServer64"); Directory.SetCurrentDirectory(workingDir); //HACK for block skins update From eb8f4bacb1fb541f0296350cda1efd0213ab797b Mon Sep 17 00:00:00 2001 From: SentorX <66237290+buttheadbob@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:06:57 +0100 Subject: [PATCH 4/7] Add prerequisite detection and DedicatedServer64 symlink for plugin developers - Added EnsureDedicatedServer64Symlink method that creates a directory junction from ./DedicatedServer64 to ./game/DedicatedServer64 for backward compatibility with plugin development workflows. --- Torch.Server/Initializer.cs | 217 +++++++++++++++++++++++++++++++++++- 1 file changed, 215 insertions(+), 2 deletions(-) diff --git a/Torch.Server/Initializer.cs b/Torch.Server/Initializer.cs index f0cb3c55..09b9f051 100644 --- a/Torch.Server/Initializer.cs +++ b/Torch.Server/Initializer.cs @@ -9,6 +9,7 @@ using System.Text; using System.Threading; using System.Windows; +using Microsoft.Win32; using NLog; using Sandbox; using VRage; @@ -35,7 +36,213 @@ app_update 298740 private string _basePath; private static string GetDedicatedServer64Path(string basePath) => Path.Combine(basePath, "game", "DedicatedServer64"); - + + private void EnsureDedicatedServer64Symlink() + { + var targetPath = GetDedicatedServer64Path(_basePath); + var linkPath = Path.Combine(_basePath, "DedicatedServer64"); + + if (!Directory.Exists(targetPath)) + { + Log.Warn($"Target DedicatedServer64 folder does not exist at {targetPath}, skipping symlink creation."); + return; + } + + if (Directory.Exists(linkPath)) + { + // Check if it's already a junction pointing to the correct target + var attr = File.GetAttributes(linkPath); + if ((attr & FileAttributes.ReparsePoint) != 0) + { + // It's a junction/symlink, we assume it's correct (could verify target but skip for simplicity) + Log.Info($"DedicatedServer64 junction already exists at {linkPath}"); + return; + } + else + { + // It's a regular directory - we shouldn't replace it + Log.Warn($"DedicatedServer64 already exists as a regular directory at {linkPath}, not creating junction."); + return; + } + } + + // Create junction using mklink + var startInfo = new ProcessStartInfo + { + FileName = "cmd.exe", + Arguments = $"/c mklink /J \"{linkPath}\" \"{targetPath}\"", + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + try + { + using (var process = Process.Start(startInfo)) + { + process.WaitForExit(); + if (process.ExitCode == 0) + { + Log.Info($"Created DedicatedServer64 junction at {linkPath} pointing to {targetPath}"); + } + else + { + var output = process.StandardOutput.ReadToEnd(); + var error = process.StandardError.ReadToEnd(); + Log.Warn($"Failed to create junction: {output} {error}"); + } + } + } + catch (Exception ex) + { + Log.Warn($"Failed to create DedicatedServer64 junction: {ex.Message}"); + } + } + + private void CheckPrerequisites() + { + if (Environment.OSVersion.Platform != PlatformID.Win32NT) + { + Log.Info("Skipping prerequisite checks on non-Windows platform."); + return; + } + + bool allOk = true; + + // .NET Framework 4.8 + if (!IsNetFramework48Installed()) + { + Log.Error(".NET Framework 4.8 is not installed. Please install it from https://go.microsoft.com/fwlink/?LinkId=2085155"); + Log.Error( + "Please visit Torch's Wiki - Installation page for more information at https://wiki.torchapi.com/en/installing-torch"); + allOk = false; + } + + // VC++ 2013 Redistributable (x64) + if (!IsVcRedist2013Installed()) + { + Log.Error("Visual C++ 2013 Redistributable (x64) is not installed. Please install it from https://aka.ms/highdpimfc2013x64enu"); + Log.Error("Please visit Torch's Wiki - Installation page for more information at https://wiki.torchapi.com/en/installing-torch"); + allOk = false; + } + + // VC++ 2019 Redistributable (x64) + if (!IsVcRedist2019Installed()) + { + Log.Error("Visual C++ 2019 Redistributable (x64) is not installed. Please install it from https://aka.ms/vc14/vc_redist.x64.exe"); + Log.Error("Please visit Torch's Wiki - Installation page for more information at https://wiki.torchapi.com/en/installing-torch"); + allOk = false; + } + + if (allOk) + Log.Info("All prerequisites satisfied."); + else + { + Log.Error("Prerequisites not satisfied. Please install the missing components and try again."); + Log.Error("Press any key to exit..."); + Console.ReadKey(); + Environment.Exit(1); + } + } + + private static bool IsNetFramework48Installed() + { + try + { + using (var ndpKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full")) + { + if (ndpKey?.GetValue("Release") is int release) + return release >= 528040; // .NET 4.8 + } + } + catch + { + // ignore + } + return false; + } + + private static bool IsVcRedist2013Installed() + { + // Check registry keys for x64 and x86 + string[] registryPaths = new[] + { + @"SOFTWARE\Microsoft\VisualStudio\12.0\VC\Runtimes\x64", + @"SOFTWARE\WOW6432Node\Microsoft\VisualStudio\12.0\VC\Runtimes\x64", + @"SOFTWARE\Microsoft\VisualStudio\12.0\VC\Runtimes\x86", + @"SOFTWARE\WOW6432Node\Microsoft\VisualStudio\12.0\VC\Runtimes\x86" + }; + foreach (var path in registryPaths) + { + try + { + using (var vcKey = Registry.LocalMachine.OpenSubKey(path)) + { + if (vcKey?.GetValue("Installed") is int installed && installed == 1) + return true; + } + } + catch + { + // ignore + } + } + + // Fallback: check for presence of msvcp120.dll in system directory + string systemDir = Environment.GetFolderPath(Environment.SpecialFolder.System); + string dllPath = Path.Combine(systemDir, "msvcp120.dll"); + if (File.Exists(dllPath)) + return true; + + // Also check vcruntime120.dll + dllPath = Path.Combine(systemDir, "vcruntime120.dll"); + if (File.Exists(dllPath)) + return true; + + return false; + } + + private static bool IsVcRedist2019Installed() + { + // Check registry keys for x64 and x86 + string[] registryPaths = new[] + { + @"SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64", + @"SOFTWARE\WOW6432Node\Microsoft\VisualStudio\14.0\VC\Runtimes\x64", + @"SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x86", + @"SOFTWARE\WOW6432Node\Microsoft\VisualStudio\14.0\VC\Runtimes\x86", + @"SOFTWARE\Microsoft\VisualStudio\14.2\VC\Runtimes\x64", + @"SOFTWARE\WOW6432Node\Microsoft\VisualStudio\14.2\VC\Runtimes\x64" + }; + foreach (var path in registryPaths) + { + try + { + using (var vcKey = Registry.LocalMachine.OpenSubKey(path)) + { + if (vcKey?.GetValue("Installed") is int installed && installed == 1) + return true; + } + } + catch + { + // ignore + } + } + + // Fallback: check for presence of vcruntime140.dll in system directory + string systemDir = Environment.GetFolderPath(Environment.SpecialFolder.System); + string dllPath = Path.Combine(systemDir, "vcruntime140.dll"); + if (File.Exists(dllPath)) + return true; + + // Also check msvcp140.dll + dllPath = Path.Combine(systemDir, "msvcp140.dll"); + if (File.Exists(dllPath)) + return true; + + return false; + } internal Persistent ConfigPersistent { get; private set; } public TorchConfig Config => ConfigPersistent?.Data; @@ -74,9 +281,14 @@ public bool Initialize(string[] args) #endif // This is what happens when Keen is bad and puts extensions into the System namespace. + CheckPrerequisites(); + if (!Enumerable.Contains(args, "-noupdate")) RunSteamCmd(); + // Legacy/Plugin Dev Support + EnsureDedicatedServer64Symlink(); + var basePath = new FileInfo(typeof(Program).Assembly.Location).Directory.ToString(); var dedicatedServerPath = GetDedicatedServer64Path(basePath); var apiSource = Path.Combine(dedicatedServerPath, "steam_api64.dll"); @@ -254,12 +466,13 @@ public static void RunSteamCmd() log.Info("Checking for DS updates."); - const int maxAttempts = 2; + const int maxAttempts = 3; for (int attempt = 1; attempt <= maxAttempts; attempt++) { if (attempt > 1) { log.Info($"Retrying SteamCMD update (attempt {attempt})..."); + log.Info("This may take awhile depending on your internet connection, please be patient."); Thread.Sleep(3000); // brief delay before retry } From 4031b21fcdadaa6c99162d739a03efc30d212bbb Mon Sep 17 00:00:00 2001 From: SentorX <66237290+buttheadbob@users.noreply.github.com> Date: Tue, 27 Jan 2026 12:17:36 +0100 Subject: [PATCH 5/7] Fix world creation crash and UI refresh issues - Ensure world list refreshes after new world creation by calling RefreshModel in InstanceManager.SelectWorld --- Torch.Server/Managers/InstanceManager.cs | 4 +++- Torch.Server/Views/WorldGeneratorDialog.xaml.cs | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Torch.Server/Managers/InstanceManager.cs b/Torch.Server/Managers/InstanceManager.cs index 9a36675f..a603c644 100644 --- a/Torch.Server/Managers/InstanceManager.cs +++ b/Torch.Server/Managers/InstanceManager.cs @@ -52,7 +52,7 @@ public void LoadInstance(string path, bool validate = true) ValidateInstance(path); MyFileSystem.Reset(); - MyFileSystem.Init("Content", path); + MyFileSystem.Init("game/Content", path); //Initializes saves path. Why this isn't in Init() we may never know. MyFileSystem.InitUserSpecific(null); @@ -106,6 +106,7 @@ public void SelectWorld(string worldPath, bool modsOnly = true) } DedicatedConfig.SelectedWorld = worldInfo; + DedicatedConfig.RefreshModel(); if (DedicatedConfig.SelectedWorld?.Checkpoint != null) { DedicatedConfig.Mods.Clear(); @@ -121,6 +122,7 @@ public void SelectWorld(WorldViewModel world, bool modsOnly = true) { DedicatedConfig.LoadWorld = world.WorldPath; DedicatedConfig.SelectedWorld = world; + DedicatedConfig.RefreshModel(); if (DedicatedConfig.SelectedWorld?.Checkpoint != null) { DedicatedConfig.Mods.Clear(); diff --git a/Torch.Server/Views/WorldGeneratorDialog.xaml.cs b/Torch.Server/Views/WorldGeneratorDialog.xaml.cs index 9854a761..e0a2650d 100644 --- a/Torch.Server/Views/WorldGeneratorDialog.xaml.cs +++ b/Torch.Server/Views/WorldGeneratorDialog.xaml.cs @@ -46,7 +46,7 @@ public WorldGeneratorDialog(InstanceManager instanceManager) InitializeComponent(); _loadLocalization(); - string worldsDir = Path.Combine(MyFileSystem.ContentPath, "CustomWorlds"); + string worldsDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "game", "Content", "CustomWorlds"); var result = new List>(); GetWorldInfo(worldsDir, result); @@ -93,6 +93,9 @@ private void ButtonBase_OnClick(object sender, RoutedEventArgs e) // Trash code to work around inconsistent path formats. var fileRelPath = file.Replace($"{_currentItem.Path.TrimEnd('\\')}\\", ""); var destPath = Path.Combine(worldPath, fileRelPath); + var destDir = Path.GetDirectoryName(destPath); + if (destDir != null && !Directory.Exists(destDir)) + Directory.CreateDirectory(destDir); File.Copy(file, destPath); } @@ -103,6 +106,7 @@ private void ButtonBase_OnClick(object sender, RoutedEventArgs e) _instanceManager.SelectWorld(worldPath, false); _instanceManager.ImportSelectedWorldConfig(); + _instanceManager.DedicatedConfig.SelectedWorld?.SaveSandbox(); Close(); } From 02eb577239caa603b65817af684b3e9e82e5d09e Mon Sep 17 00:00:00 2001 From: SentorX <66237290+buttheadbob@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:16:16 +0100 Subject: [PATCH 6/7] Fix world config import and mods list update --- Torch.Server/Managers/InstanceManager.cs | 67 +++++++++++++++++++----- 1 file changed, 53 insertions(+), 14 deletions(-) diff --git a/Torch.Server/Managers/InstanceManager.cs b/Torch.Server/Managers/InstanceManager.cs index a603c644..3b00f8e2 100644 --- a/Torch.Server/Managers/InstanceManager.cs +++ b/Torch.Server/Managers/InstanceManager.cs @@ -1,33 +1,21 @@ using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.ComponentModel; using System.IO; using System.Linq; -using System.Reflection; -using System.Text; using System.Threading.Tasks; -using Havok; using NLog; using Sandbox; -using Sandbox.Engine.Networking; using Sandbox.Engine.Utils; -using Sandbox.Game; -using Sandbox.Game.Gui; using Torch.API; -using Torch.API.Managers; using Torch.Collections; using Torch.Managers; using Torch.Mod; using Torch.Server.ViewModels; using Torch.Utils; -using VRage; using VRage.FileSystem; using VRage.Game; -using VRage.Game.ObjectBuilder; +using VRage.Game.ModAPI; using VRage.ObjectBuilders; using VRage.ObjectBuilders.Private; -using VRage.Plugins; namespace Torch.Server.Managers { @@ -115,6 +103,7 @@ public void SelectWorld(string worldPath, bool modsOnly = true) foreach (var m in DedicatedConfig.SelectedWorld.WorldConfiguration.Mods) DedicatedConfig.Mods.Add(new ModItemInfo(m)); Task.Run(() => DedicatedConfig.UpdateAllModInfosAsync()); + DedicatedConfig.RefreshModel(); } } @@ -131,6 +120,7 @@ public void SelectWorld(WorldViewModel world, bool modsOnly = true) foreach (var m in DedicatedConfig.SelectedWorld.WorldConfiguration.Mods) DedicatedConfig.Mods.Add(new ModItemInfo(m)); Task.Run(() => DedicatedConfig.UpdateAllModInfosAsync()); + DedicatedConfig.RefreshModel(); } } @@ -146,11 +136,38 @@ private void ImportWorldConfig(WorldViewModel world, bool modsOnly = true) mods.Add(new ModItemInfo(mod)); DedicatedConfig.Mods = mods; - Log.Debug("Loaded mod list from world"); if (!modsOnly) DedicatedConfig.SessionSettings = world.WorldConfiguration.Settings; + + // Update left pane fields from world checkpoint + if (world.Checkpoint != null) + { + DedicatedConfig.WorldName = world.Checkpoint.SessionName; + DedicatedConfig.ServerDescription = world.Checkpoint.Description; + DedicatedConfig.ServerName = world.Checkpoint.SessionName; + if (!string.IsNullOrEmpty(world.Checkpoint.Password)) + DedicatedConfig.Password = world.Checkpoint.Password; + // Update administrators from promoted users + if (world.Checkpoint.PromotedUsers != null) + { + var adminIds = world.Checkpoint.PromotedUsers.Dictionary + .Where(kvp => kvp.Value >= MyPromoteLevel.Admin) + .Select(kvp => kvp.Key.ToString()) + .ToList(); + DedicatedConfig.Administrators = adminIds; + } + } + + // Ensure LoadWorld is set to this world's path + DedicatedConfig.LoadWorld = world.WorldPath; + + // Make sure the config uses LoadWorld instead of last session + if (DedicatedConfig.Model is MyConfigDedicated config) + config.IgnoreLastSession = true; + + DedicatedConfig.RefreshModel(); } private void ImportWorldConfig(bool modsOnly = true) @@ -181,6 +198,28 @@ private void ImportWorldConfig(bool modsOnly = true) if (!modsOnly) DedicatedConfig.SessionSettings = new SessionSettingsViewModel(checkpoint.Settings); + + // Update left pane fields from world checkpoint + DedicatedConfig.WorldName = checkpoint.SessionName; + DedicatedConfig.ServerDescription = checkpoint.Description; + DedicatedConfig.ServerName = checkpoint.SessionName; + if (!string.IsNullOrEmpty(checkpoint.Password)) + DedicatedConfig.Password = checkpoint.Password; + // Update administrators from promoted users + if (checkpoint.PromotedUsers != null) + { + var adminIds = checkpoint.PromotedUsers.Dictionary + .Where(kvp => kvp.Value >= MyPromoteLevel.Admin) + .Select(kvp => kvp.Key.ToString()) + .ToList(); + DedicatedConfig.Administrators = adminIds; + } + + // Make sure the config uses LoadWorld instead of last session + if (DedicatedConfig.Model is MyConfigDedicated config) + config.IgnoreLastSession = true; + + DedicatedConfig.RefreshModel(); } catch (Exception e) { From 2122fc6741d80ab2800eb9d0c6d382371daa2b8b Mon Sep 17 00:00:00 2001 From: SentorX <66237290+buttheadbob@users.noreply.github.com> Date: Tue, 27 Jan 2026 17:16:19 +0100 Subject: [PATCH 7/7] Modernize concurrent collections and UI bindings --- Torch.API/Torch.API.csproj | 5 +- Torch.API/packages.config | 2 +- Torch.Server/Managers/EntityControlManager.cs | 29 +- Torch.Server/Managers/InstanceManager.cs | 7 +- Torch.Server/Torch.Server.csproj | 5 +- .../ViewModels/ConfigDedicatedViewModel.cs | 12 +- .../Entities/Blocks/BlockViewModel.cs | 3 +- .../ViewModels/Entities/EntityViewModel.cs | 4 +- .../ViewModels/Entities/GridViewModel.cs | 11 +- .../ViewModels/Entities/VoxelMapViewModel.cs | 3 +- .../ViewModels/EntityTreeViewModel.cs | 68 ++-- .../ViewModels/PluginManagerViewModel.cs | 10 +- Torch.Server/Views/ChatControl.xaml | 7 +- .../Views/Converters/ModToIdConverter.cs | 3 +- Torch.Server/Views/LogEventViewer.xaml | 13 +- Torch.Server/Views/ModListControl.xaml.cs | 21 +- Torch.Server/Views/ModsControl.xaml | 14 +- Torch.Server/Views/PluginBrowser.xaml.cs | 15 +- Torch.Server/app.config | 11 +- Torch.Server/packages.config | 2 +- Torch.sln | 2 + .../Concurrent/DispatcherObservableEvent.cs | 155 ++++++++ .../ObservableConcurrentDictionary.cs | 366 ++++++++++++++++++ .../ObservableConcurrentDictionaryValues.cs | 232 +++++++++++ .../Concurrent/ObservableConcurrentHashSet.cs | 189 +++++++++ .../Concurrent/ObservableConcurrentList.cs | 284 ++++++++++++++ .../ObservableConcurrentSortedList.cs | 277 +++++++++++++ .../ReadOnlyObservableConcurrentDictionary.cs | 63 +++ Torch/Collections/MtObservableList.cs | 6 +- .../MtObservableSortedDictionary.cs | 7 +- Torch/Collections/SortedView.cs | 5 +- Torch/Managers/MultiplayerManagerBase.cs | 3 +- Torch/Plugins/PluginManager.cs | 3 +- Torch/Torch.csproj | 11 +- Torch/Views/PropertyGrid.xaml.cs | 11 - 35 files changed, 1727 insertions(+), 132 deletions(-) create mode 100644 Torch/Collections/Concurrent/DispatcherObservableEvent.cs create mode 100644 Torch/Collections/Concurrent/ObservableConcurrentDictionary.cs create mode 100644 Torch/Collections/Concurrent/ObservableConcurrentDictionaryValues.cs create mode 100644 Torch/Collections/Concurrent/ObservableConcurrentHashSet.cs create mode 100644 Torch/Collections/Concurrent/ObservableConcurrentList.cs create mode 100644 Torch/Collections/Concurrent/ObservableConcurrentSortedList.cs create mode 100644 Torch/Collections/Concurrent/ReadOnlyObservableConcurrentDictionary.cs diff --git a/Torch.API/Torch.API.csproj b/Torch.API/Torch.API.csproj index 9bc7e421..86656d18 100644 --- a/Torch.API/Torch.API.csproj +++ b/Torch.API/Torch.API.csproj @@ -44,9 +44,8 @@ ..\GameBinaries\HavokWrapper.dll False - - ..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll - True + + ..\packages\Newtonsoft.Json.13.0.4\lib\net45\Newtonsoft.Json.dll ..\packages\NLog.4.4.12\lib\net45\NLog.dll diff --git a/Torch.API/packages.config b/Torch.API/packages.config index fbd3f147..ea61ec50 100644 --- a/Torch.API/packages.config +++ b/Torch.API/packages.config @@ -1,6 +1,6 @@  - + \ No newline at end of file diff --git a/Torch.Server/Managers/EntityControlManager.cs b/Torch.Server/Managers/EntityControlManager.cs index 65d1e8fb..cf22458f 100644 --- a/Torch.Server/Managers/EntityControlManager.cs +++ b/Torch.Server/Managers/EntityControlManager.cs @@ -1,16 +1,11 @@ -using System; +using System; using System.Collections.Generic; -using System.Linq; -using System.Reflection; using System.Runtime.CompilerServices; -using System.Text; -using System.Threading; -using System.Threading.Tasks; using System.Windows.Controls; using NLog; -using NLog.Fluent; using Torch.API; using Torch.Collections; +using Torch.Collections.Concurrent; using Torch.Managers; using Torch.Server.ViewModels.Entities; using Torch.Utils; @@ -87,8 +82,8 @@ protected override EntityControlViewModel Create(EntityViewModel evm) private readonly List _modelFactories = new List(); private readonly List _controlFactories = new List(); - private readonly List> _boundEntityViewModels = new List>(); - private readonly ConditionalWeakTable> _boundViewModels = new ConditionalWeakTable>(); + private readonly ObservableConcurrentList> _boundEntityViewModels = new ObservableConcurrentList>(); + private readonly ConditionalWeakTable> _boundViewModels = new ConditionalWeakTable>(); /// /// This factory will be used to create component models for matching entity models. @@ -109,14 +104,14 @@ public void RegisterModelFactory(Func components)) + _boundViewModels.TryGetValue(target, out ObservableConcurrentList components)) { if (target is TEntityBaseModel tent) UpdateBinding(target, components); i++; } else - _boundEntityViewModels.RemoveAtFast(i); + _boundEntityViewModels.RemoveAt(i); } } } @@ -190,7 +185,7 @@ private void RefreshControls() where TEntityComponentMode while (i < _boundEntityViewModels.Count) { if (_boundEntityViewModels[i].TryGetTarget(out EntityViewModel target) && - _boundViewModels.TryGetValue(target, out MtObservableList components)) + _boundViewModels.TryGetValue(target, out ObservableConcurrentList components)) { foreach (EntityControlViewModel component in components) if (component is TEntityComponentModel) @@ -198,7 +193,7 @@ private void RefreshControls() where TEntityComponentMode i++; } else - _boundEntityViewModels.RemoveAtFast(i); + _boundEntityViewModels.RemoveAt(i); } } @@ -207,7 +202,7 @@ private void RefreshControls() where TEntityComponentMode /// /// view model to query /// - public MtObservableList BoundModels(EntityViewModel entity) + public ObservableConcurrentList BoundModels(EntityViewModel entity) { return _boundViewModels.GetValue(entity, CreateFreshBinding); } @@ -231,9 +226,9 @@ public Control CreateControl(EntityControlViewModel model) return null; } - private MtObservableList CreateFreshBinding(EntityViewModel key) + private ObservableConcurrentList CreateFreshBinding(EntityViewModel key) { - var binding = new MtObservableList(); + var binding = new ObservableConcurrentList(); lock (this) { _boundEntityViewModels.Add(new WeakReference(key)); @@ -246,7 +241,7 @@ private MtObservableList CreateFreshBinding(EntityViewMo return binding; } - private void UpdateBinding(EntityViewModel key, MtObservableList binding) + private void UpdateBinding(EntityViewModel key, ObservableConcurrentList binding) { if (!binding.IsObserved) return; diff --git a/Torch.Server/Managers/InstanceManager.cs b/Torch.Server/Managers/InstanceManager.cs index 3b00f8e2..80c00e61 100644 --- a/Torch.Server/Managers/InstanceManager.cs +++ b/Torch.Server/Managers/InstanceManager.cs @@ -6,7 +6,7 @@ using Sandbox; using Sandbox.Engine.Utils; using Torch.API; -using Torch.Collections; +using Torch.Collections.Concurrent; using Torch.Managers; using Torch.Mod; using Torch.Server.ViewModels; @@ -131,7 +131,8 @@ public void ImportSelectedWorldConfig() private void ImportWorldConfig(WorldViewModel world, bool modsOnly = true) { - var mods = new MtObservableList(); + + var mods = new ObservableConcurrentList(); foreach (var mod in world.WorldConfiguration.Mods) mods.Add(new ModItemInfo(mod)); DedicatedConfig.Mods = mods; @@ -189,7 +190,7 @@ private void ImportWorldConfig(bool modsOnly = true) return; } - var mods = new MtObservableList(); + var mods = new ObservableConcurrentList(); foreach (var mod in checkpoint.Mods) mods.Add(new ModItemInfo(mod)); DedicatedConfig.Mods = mods; diff --git a/Torch.Server/Torch.Server.csproj b/Torch.Server/Torch.Server.csproj index f33fad8e..88d45c05 100644 --- a/Torch.Server/Torch.Server.csproj +++ b/Torch.Server/Torch.Server.csproj @@ -92,9 +92,8 @@ ..\GameBinaries\netstandard.dll - - ..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll - True + + ..\packages\Newtonsoft.Json.13.0.4\lib\net45\Newtonsoft.Json.dll ..\packages\NLog.4.4.12\lib\net45\NLog.dll diff --git a/Torch.Server/ViewModels/ConfigDedicatedViewModel.cs b/Torch.Server/ViewModels/ConfigDedicatedViewModel.cs index a417eef6..55ab235a 100644 --- a/Torch.Server/ViewModels/ConfigDedicatedViewModel.cs +++ b/Torch.Server/ViewModels/ConfigDedicatedViewModel.cs @@ -1,17 +1,13 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Linq; -using System.Text; using System.Threading.Tasks; using NLog; using Sandbox.Engine.Utils; -using Torch.Collections; using Torch.Server.Managers; using VRage.Game; -using VRage.Game.ModAPI; using Torch.Utils.SteamWorkshopTools; -using Torch.Collections; +using Torch.Collections.Concurrent; namespace Torch.Server.ViewModels { @@ -64,7 +60,7 @@ public bool Validate() private SessionSettingsViewModel _sessionSettings; public SessionSettingsViewModel SessionSettings { get => _sessionSettings; set { _sessionSettings = value; OnPropertyChanged(); } } - public MtObservableList Worlds { get; } = new MtObservableList(); + public ObservableConcurrentList Worlds { get; } = new ObservableConcurrentList(); private WorldViewModel _selectedWorld; public WorldViewModel SelectedWorld { @@ -118,8 +114,8 @@ public async Task UpdateAllModInfosAsync(Action messageHandler = null) public List Banned { get => _config.Banned; set => SetValue(x => _config.Banned = x, value); } - private MtObservableList _mods = new MtObservableList(); - public MtObservableList Mods + private ObservableConcurrentList _mods = new ObservableConcurrentList(); + public ObservableConcurrentList Mods { get => _mods; set diff --git a/Torch.Server/ViewModels/Entities/Blocks/BlockViewModel.cs b/Torch.Server/ViewModels/Entities/Blocks/BlockViewModel.cs index e93eaac1..7e6a454f 100644 --- a/Torch.Server/ViewModels/Entities/Blocks/BlockViewModel.cs +++ b/Torch.Server/ViewModels/Entities/Blocks/BlockViewModel.cs @@ -10,6 +10,7 @@ using Sandbox.ModAPI; using Sandbox.ModAPI.Interfaces; using Torch.Collections; +using Torch.Collections.Concurrent; using Torch.Server.ViewModels.Entities; using VRage.Game.ModAPI; @@ -18,7 +19,7 @@ namespace Torch.Server.ViewModels.Blocks public class BlockViewModel : EntityViewModel { public IMyTerminalBlock Block => (IMyTerminalBlock) Entity; - public MtObservableList Properties { get; } = new MtObservableList(); + public ObservableConcurrentList Properties { get; } = new ObservableConcurrentList(); public string FullName => $"{Block?.CubeGrid.CustomName} - {Block?.CustomName}"; diff --git a/Torch.Server/ViewModels/Entities/EntityViewModel.cs b/Torch.Server/ViewModels/Entities/EntityViewModel.cs index ada41ad6..0651b875 100644 --- a/Torch.Server/ViewModels/Entities/EntityViewModel.cs +++ b/Torch.Server/ViewModels/Entities/EntityViewModel.cs @@ -3,7 +3,7 @@ using NLog; using Sandbox.Game.Entities; using Torch.API.Managers; -using Torch.Collections; +using Torch.Collections.Concurrent; using Torch.Server.Managers; using Torch.Utils; using VRage.ModAPI; @@ -33,7 +33,7 @@ private set public long Id => Entity?.EntityId ?? 0; // Throws null then gives entity id - public MtObservableList EntityControls { get; private set; } + public ObservableConcurrentList EntityControls { get; private set; } public virtual string Name { diff --git a/Torch.Server/ViewModels/Entities/GridViewModel.cs b/Torch.Server/ViewModels/Entities/GridViewModel.cs index f6886544..1d914339 100644 --- a/Torch.Server/ViewModels/Entities/GridViewModel.cs +++ b/Torch.Server/ViewModels/Entities/GridViewModel.cs @@ -6,7 +6,7 @@ using Sandbox.Game.Entities.Cube; using Sandbox.Game.World; using SharpDX.Toolkit.Collections; -using Torch.Collections; +using Torch.Collections.Concurrent; using Torch.Server.ViewModels.Blocks; using VRage.Game; @@ -45,10 +45,9 @@ public int Compare(MyCubeBlockDefinition x, MyCubeBlockDefinition y) private MyCubeGrid Grid => (MyCubeGrid) Entity; - public MtObservableSortedDictionary> + public ObservableConcurrentDictionary> Blocks { get; } = - new MtObservableSortedDictionary>( - CubeBlockDefinitionComparer.Default); + new ObservableConcurrentDictionary>(); public GridViewModel() { @@ -58,7 +57,7 @@ public GridViewModel() public GridViewModel(MyCubeGrid grid, EntityTreeViewModel tree) : base(grid, tree) { //DescriptiveName = $"{grid.DisplayName} ({grid.BlocksCount} blocks)"; - Blocks.Add(_fillerDefinition, new MtObservableSortedDictionary()); + Blocks.Add(_fillerDefinition, new ObservableConcurrentDictionary()); } private void Grid_OnBlockRemoved(MySlimBlock obj) @@ -99,7 +98,7 @@ private void AddBlock(MyTerminalBlock block) try { if (!Blocks.TryGetValue(block.BlockDefinition, out var group)) - group = Blocks[block.BlockDefinition] = new MtObservableSortedDictionary(); + group = Blocks[block.BlockDefinition] = new ObservableConcurrentDictionary(); group.Add(block.EntityId, new BlockViewModel(block, Tree)); long ownerId = block.OwnerId; diff --git a/Torch.Server/ViewModels/Entities/VoxelMapViewModel.cs b/Torch.Server/ViewModels/Entities/VoxelMapViewModel.cs index bec7847e..5e0ab501 100644 --- a/Torch.Server/ViewModels/Entities/VoxelMapViewModel.cs +++ b/Torch.Server/ViewModels/Entities/VoxelMapViewModel.cs @@ -5,6 +5,7 @@ using VRage.Game.ModAPI; using System.Threading.Tasks; using Torch.Collections; +using Torch.Collections.Concurrent; namespace Torch.Server.ViewModels.Entities { @@ -16,7 +17,7 @@ public class VoxelMapViewModel : EntityViewModel public override bool CanStop => false; - public MtObservableList AttachedGrids { get; } = new MtObservableList(); + public ObservableConcurrentList AttachedGrids { get; } = new ObservableConcurrentList(); public async Task UpdateAttachedGrids() { diff --git a/Torch.Server/ViewModels/EntityTreeViewModel.cs b/Torch.Server/ViewModels/EntityTreeViewModel.cs index c3d34c64..46d328cf 100644 --- a/Torch.Server/ViewModels/EntityTreeViewModel.cs +++ b/Torch.Server/ViewModels/EntityTreeViewModel.cs @@ -11,7 +11,7 @@ using Torch.API; using Torch.API.Managers; using Torch.API.Session; -using Torch.Collections; +using Torch.Collections.Concurrent; using VRage.Game.ModAPI; using PlayerViewModel = Torch.Server.ViewModels.Entities.PlayerViewModel; @@ -31,21 +31,21 @@ public enum SortEnum private static readonly Logger _log = LogManager.GetCurrentClassLogger(); //TODO: these should be sorted sets for speed - public MtObservableSortedDictionary Grids { get; set; } = new MtObservableSortedDictionary(); - public MtObservableSortedDictionary Characters { get; set; } = new MtObservableSortedDictionary(); - public MtObservableSortedDictionary FloatingObjects { get; set; } = new MtObservableSortedDictionary(); - public MtObservableSortedDictionary VoxelMaps { get; set; } = new MtObservableSortedDictionary(); - public MtObservableSortedDictionary Players { get; set; } = new MtObservableSortedDictionary(); - public MtObservableSortedDictionary Factions { get; set; } = new MtObservableSortedDictionary(); + public ObservableConcurrentDictionary Grids { get; set; } = new ObservableConcurrentDictionary(); + public ObservableConcurrentDictionary Characters { get; set; } = new ObservableConcurrentDictionary(); + public ObservableConcurrentDictionary FloatingObjects { get; set; } = new ObservableConcurrentDictionary(); + public ObservableConcurrentDictionary VoxelMaps { get; set; } = new ObservableConcurrentDictionary(); + public ObservableConcurrentDictionary Players { get; set; } = new ObservableConcurrentDictionary(); + public ObservableConcurrentDictionary Factions { get; set; } = new ObservableConcurrentDictionary(); public Dispatcher ControlDispatcher => _control.Dispatcher; - public SortedView SortedGrids { get; } - public SortedView FilteredSortedGrids { get; } - public SortedView SortedCharacters { get; } - public SortedView SortedFloatingObjects { get; } - public SortedView SortedVoxelMaps { get; } - public SortedView SortedPlayers { get; } - public SortedView SortedFactions { get; } + public ObservableConcurrentSortedList SortedGrids { get; } + public ObservableConcurrentSortedList FilteredSortedGrids { get; } + public ObservableConcurrentSortedList SortedCharacters { get; } + public ObservableConcurrentSortedList SortedFloatingObjects { get; } + public ObservableConcurrentSortedList SortedVoxelMaps { get; } + public ObservableConcurrentSortedList SortedPlayers { get; } + public ObservableConcurrentSortedList SortedFactions { get; } private EntityViewModel _currentEntity; private SortEnum _currentSort; @@ -60,7 +60,11 @@ public EntityViewModel CurrentEntity public SortEnum CurrentSort { get => _currentSort; - set => SetValue(ref _currentSort, value); + set + { + SetValue(ref _currentSort, value); + UpdateSortComparer(); + } } // Westin miller still hates you today WPF @@ -72,16 +76,16 @@ public EntityTreeViewModel(UserControl control, ITorchServer server) { _control = control; var entityComparer = new EntityViewModel.Comparer(_currentSort); - SortedGrids = new SortedView(Grids.Values, entityComparer); - FilteredSortedGrids = new SortedView(Grids.Values, entityComparer); - SortedCharacters = new SortedView(Characters.Values, entityComparer); - SortedFloatingObjects = new SortedView(FloatingObjects.Values, entityComparer); - SortedVoxelMaps = new SortedView(VoxelMaps.Values, entityComparer); - SortedPlayers = new SortedView(Players.Values, Comparer + SortedGrids = new ObservableConcurrentSortedList(Grids.Values, entityComparer); + FilteredSortedGrids = new ObservableConcurrentSortedList(Grids.Values, entityComparer); + SortedCharacters = new ObservableConcurrentSortedList(Characters.Values, entityComparer); + SortedFloatingObjects = new ObservableConcurrentSortedList(FloatingObjects.Values, entityComparer); + SortedVoxelMaps = new ObservableConcurrentSortedList(VoxelMaps.Values, entityComparer); + SortedPlayers = new ObservableConcurrentSortedList(Players.Values, Comparer .Create((x, y) => string.Compare(x?.Name, y?.Name, StringComparison.InvariantCultureIgnoreCase)) ); - SortedFactions = new SortedView(Factions.Values, Comparer + SortedFactions = new ObservableConcurrentSortedList(Factions.Values, Comparer .Create((x, y) => string.Compare(x?.Name, y?.Name, StringComparison.InvariantCultureIgnoreCase)) ); @@ -93,6 +97,16 @@ public EntityTreeViewModel(UserControl control, ITorchServer server) } } + private void UpdateSortComparer() + { + var comparer = new EntityViewModel.Comparer(_currentSort); + SortedGrids.SetComparer(comparer); + FilteredSortedGrids.SetComparer(comparer); + SortedCharacters.SetComparer(comparer); + SortedFloatingObjects.SetComparer(comparer); + SortedVoxelMaps.SetComparer(comparer); + } + private void RegisterLiveNonEntities(ITorchSession session, TorchSessionState newState) { switch (newState) @@ -105,12 +119,12 @@ private void RegisterLiveNonEntities(ITorchSession session, TorchSessionState ne if (player is null) continue; if (Players.ContainsKey(player.IdentityId)) continue; - Players.Add(new KeyValuePair(player.IdentityId, new PlayerViewModel(player, identity))); + Players.Add(player.IdentityId, new PlayerViewModel(player, identity)); } foreach (MyFaction faction in MySession.Static.Factions.GetAllFactions()) { - Factions.Add(new KeyValuePair(faction.FactionId, new FactionViewModel(faction))); + Factions.Add(faction.FactionId, new FactionViewModel(faction)); } Sync.Players.RealPlayerIdentityCreated += NewPlayerCreated; @@ -121,7 +135,9 @@ private void RegisterLiveNonEntities(ITorchSession session, TorchSessionState ne case TorchSessionState.Unloading: Sync.Players.RealPlayerIdentityCreated -= NewPlayerCreated; MySession.Static.Factions.FactionCreated -= NewFactionCreated; + MySession.Static.Factions.FactionStateChanged -= FactionChanged; Players.Clear(); + Factions.Clear(); break; } } @@ -155,7 +171,7 @@ private void NewFactionCreated(long id) { var faction = MySession.Static.Factions.GetPlayerFaction(id); if (faction is null) return; - Factions.Add(new KeyValuePair(faction.FactionId, new FactionViewModel(faction))); + Factions.Add(faction.FactionId, new FactionViewModel(faction)); }); } @@ -163,7 +179,7 @@ private void NewPlayerCreated(long identityId) { var player = MySession.Static.Players.TryGetPlayer(identityId); if (player is null) return; - Players.Add(new KeyValuePair(player.Identity.IdentityId, new PlayerViewModel(player.Identity, new MyPlayer.PlayerId()))); + Players.Add(player.Identity.IdentityId, new PlayerViewModel(player.Identity, player.Id)); } public void Init() diff --git a/Torch.Server/ViewModels/PluginManagerViewModel.cs b/Torch.Server/ViewModels/PluginManagerViewModel.cs index 48ecd76a..96761d7f 100644 --- a/Torch.Server/ViewModels/PluginManagerViewModel.cs +++ b/Torch.Server/ViewModels/PluginManagerViewModel.cs @@ -1,18 +1,14 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Torch.API; using Torch.API.Managers; using Torch.API.Plugins; -using Torch.Collections; +using Torch.Collections.Concurrent; namespace Torch.Server.ViewModels { public class PluginManagerViewModel : ViewModel { - public MtObservableList Plugins { get; } = new MtObservableList(); + public ObservableConcurrentList Plugins { get; } = new ObservableConcurrentList(); private PluginViewModel _selectedPlugin; public PluginViewModel SelectedPlugin diff --git a/Torch.Server/Views/ChatControl.xaml b/Torch.Server/Views/ChatControl.xaml index 115f0063..aa617d4e 100644 --- a/Torch.Server/Views/ChatControl.xaml +++ b/Torch.Server/Views/ChatControl.xaml @@ -10,7 +10,12 @@ - + diff --git a/Torch.Server/Views/Converters/ModToIdConverter.cs b/Torch.Server/Views/Converters/ModToIdConverter.cs index e8781404..2e0aff0b 100644 --- a/Torch.Server/Views/Converters/ModToIdConverter.cs +++ b/Torch.Server/Views/Converters/ModToIdConverter.cs @@ -9,6 +9,7 @@ using Torch.Server.ViewModels; using NLog; using Torch.Collections; +using Torch.Collections.Concurrent; namespace Torch.Server.Views.Converters { @@ -32,7 +33,7 @@ public object Convert(object[] values, Type targetType, object parameter, Cultur { //if (targetType != typeof(int)) // throw new NotSupportedException("ModToIdConverter can only convert mods into int values or vise versa!"); - if (values[0] is ModItemInfo mod && values[1] is MtObservableList modList) + if (values[0] is ModItemInfo mod && values[1] is ObservableConcurrentList modList) { return modList.IndexOf(mod); } diff --git a/Torch.Server/Views/LogEventViewer.xaml b/Torch.Server/Views/LogEventViewer.xaml index 51304923..8385b638 100644 --- a/Torch.Server/Views/LogEventViewer.xaml +++ b/Torch.Server/Views/LogEventViewer.xaml @@ -28,7 +28,18 @@ - + diff --git a/Torch.Server/Views/ModListControl.xaml.cs b/Torch.Server/Views/ModListControl.xaml.cs index 16b93640..7adb9157 100644 --- a/Torch.Server/Views/ModListControl.xaml.cs +++ b/Torch.Server/Views/ModListControl.xaml.cs @@ -1,32 +1,23 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; -using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; -using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; using System.Runtime.CompilerServices; -using System.Windows.Threading; -using VRage.Game; using NLog; -using Sandbox.Engine.Networking; using Torch.API; using Torch.Server.Managers; using Torch.API.Managers; using Torch.Server.ViewModels; using Torch.Server.Annotations; -using Torch.Collections; +using Torch.Collections.Concurrent; using Torch.Utils; using Torch.Views; @@ -82,9 +73,9 @@ private void _instanceManager_InstanceLoaded(ConfigDedicatedViewModel obj) { Log.Info("Instance loaded."); Dispatcher.Invoke(() => { - DataContext = obj?.Mods ?? new MtObservableList(); + DataContext = obj?.Mods ?? new ObservableConcurrentList(); UpdateLayout(); - ((MtObservableList)DataContext).CollectionChanged += OnModlistUpdate; + ((ObservableConcurrentList)DataContext).CollectionChanged += OnModlistUpdate; }); } @@ -127,7 +118,7 @@ private void AddBtn_OnClick(object sender, RoutedEventArgs e) } private void RemoveBtn_OnClick(object sender, RoutedEventArgs e) { - var modList = ((MtObservableList)DataContext); + var modList = ((ObservableConcurrentList)DataContext); if (ModList.SelectedItem is ModItemInfo mod && modList.Contains(mod)) modList.Remove(mod); } @@ -213,7 +204,7 @@ private void UserControl_MouseMove(object sender, MouseEventArgs e) if( targetMod != null && !ReferenceEquals(_draggedMod, targetMod)) { _hasOrderChanged = true; - var modList = (MtObservableList)DataContext; + var modList = (ObservableConcurrentList)DataContext; modList.Move(modList.IndexOf(targetMod), _draggedMod); //modList.RemoveAt(modList.IndexOf(_draggedMod)); //modList.Insert(modList.IndexOf(targetMod), _draggedMod); @@ -260,7 +251,7 @@ private void BulkButton_OnClick(object sender, RoutedEventArgs e) var editor = new CollectionEditor(); //let's see just how poorly we can do this - var modList = ((MtObservableList)DataContext).ToList(); + var modList = ((ObservableConcurrentList)DataContext).ToList(); var idList = modList.Select(m => m.ToString()).ToList(); var tasks = new List(); //blocking diff --git a/Torch.Server/Views/ModsControl.xaml b/Torch.Server/Views/ModsControl.xaml index bcaba5f3..9bb00b1f 100644 --- a/Torch.Server/Views/ModsControl.xaml +++ b/Torch.Server/Views/ModsControl.xaml @@ -6,7 +6,15 @@ mc:Ignorable="d">