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/Initializer.cs b/Torch.Server/Initializer.cs index f7524005..09b9f051 100644 --- a/Torch.Server/Initializer.cs +++ b/Torch.Server/Initializer.cs @@ -8,18 +8,11 @@ using System.Reflection; using System.Text; using System.Threading; -using System.Threading.Tasks; using System.Windows; -using System.Windows.Threading; +using Microsoft.Win32; 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 +28,222 @@ 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"); + + 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; public TorchServer Server => _server; @@ -64,7 +266,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 @@ -79,11 +281,17 @@ 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 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 +309,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 +465,74 @@ 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 = 3; + 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})..."); + log.Info("This may take awhile depending on your internet connection, please be patient."); + 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/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 9a36675f..80c00e61 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.Collections.Concurrent; 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 { @@ -52,7 +40,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 +94,7 @@ public void SelectWorld(string worldPath, bool modsOnly = true) } DedicatedConfig.SelectedWorld = worldInfo; + DedicatedConfig.RefreshModel(); if (DedicatedConfig.SelectedWorld?.Checkpoint != null) { DedicatedConfig.Mods.Clear(); @@ -114,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(); } } @@ -121,6 +111,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(); @@ -129,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(); } } @@ -139,16 +131,44 @@ 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; - 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) @@ -170,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; @@ -179,6 +199,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) { 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 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">