From f265f7e773d93f1c20e8ac7a098b114859338265 Mon Sep 17 00:00:00 2001 From: Tobias K <3knoeppl@informatik.uni-hamburg.de> Date: Sat, 2 Feb 2019 12:26:55 +0100 Subject: [PATCH] Replace mod text box by separate tab with workshop support (#263) * Implement ModList tab which fetches and displays mod information from the workshop. * ModListEditor: Implement drag and drop ordering, adding, removing and saving. * Add SteamWorkshopService to VCS * Add missing file to SteamworkshopService project. * ModlistControl: Implement checkbox for hiding/showing dependency mods disable until config is loaded. design improvements. * Add documentation for the new classes. * Comply to naming conventions. * Update Torch.Server.csproj * Fix Mod.IsDependency not being serialized when saving * Remove superfluous update of mod meta data. Remove commented section in ConfigControl.xaml. * Optimized SteamworkshopService according to commit review. * Move SteamWorkshopService to Torch.Utils.SteamworkshopTools * Remove debug output. * Don't break stack trace with custom exception in SteamWorkshopTools. * User ViewModel base class for ModItemInfo instead of implementing INotifyProperty directly. * Wrap ModListControl in ScrollViewer. * Rename SteamWorkshopTools utility to WebAPI. * Revert steamkit call to use dynamic typing for clarity :/ * Mark webAPI based method for downloading workshop content as obsolete. * Update Torch project definition. * Disable building Torch client * Update readme * Change init order to ensure paths are initialized for plugins * Reorder exception logging to reduce duplication * Use thread safe queues in MtObservableCollectionBase * Revert "Change init order to ensure paths are initialized for plugins" This reverts commit 3f803b8107bcc16516662f024af8ecd3ac6032e5. * Fix layout of ModListControl * Combine Invokes to reduce allocations * Replace string comparisons by string.Equals / string.IsNullOrEmpty * Replace string comparisons by string.Equals / string.IsNullOrEmpty * Use MtObservableList for Modlist to avoid race conditions. --- README.md | 8 - Torch.Server/Initializer.cs | 30 +- Torch.Server/Managers/InstanceManager.cs | 31 +- Torch.Server/Torch.Server.csproj | 22 ++ .../ViewModels/ConfigDedicatedViewModel.cs | 54 +++- Torch.Server/ViewModels/ModItemInfo.cs | 131 ++++++++ Torch.Server/Views/ConfigControl.xaml | 2 +- .../Converters/ListConverterWorkshopId.cs | 77 +++++ .../Views/Converters/ModToIdConverter.cs | 53 ++++ Torch.Server/Views/ModListControl.xaml | 125 ++++++++ Torch.Server/Views/ModListControl.xaml.cs | 248 +++++++++++++++ Torch.Server/Views/Resources.xaml | 2 + Torch.Server/Views/TorchUI.xaml | 3 + Torch.Server/packages.config | 5 + Torch.sln | 4 - .../Collections/MtObservableCollectionBase.cs | 10 +- Torch/Torch.csproj | 15 + .../SteamWorkshopTools/KeyValueExtensions.cs | 48 +++ .../PublishedItemDetails.cs | 26 ++ Torch/Utils/SteamWorkshopTools/WebAPI.cs | 291 ++++++++++++++++++ Torch/packages.config | 5 + 21 files changed, 1150 insertions(+), 40 deletions(-) create mode 100644 Torch.Server/ViewModels/ModItemInfo.cs create mode 100644 Torch.Server/Views/Converters/ListConverterWorkshopId.cs create mode 100644 Torch.Server/Views/Converters/ModToIdConverter.cs create mode 100644 Torch.Server/Views/ModListControl.xaml create mode 100644 Torch.Server/Views/ModListControl.xaml.cs create mode 100644 Torch/Utils/SteamWorkshopTools/KeyValueExtensions.cs create mode 100644 Torch/Utils/SteamWorkshopTools/PublishedItemDetails.cs create mode 100644 Torch/Utils/SteamWorkshopTools/WebAPI.cs diff --git a/README.md b/README.md index f80cbe2..f553aaa 100644 --- a/README.md +++ b/README.md @@ -18,18 +18,10 @@ Torch is the successor to SE Server Extender and gives server admins the tools t * Unzip the Torch release into its own directory and run the executable. It will automatically download the SE DS and generate the other necessary files. - If you already have a DS installed you can unzip the Torch files into the folder that contains the DedicatedServer64 folder. -## Torch.Client -* An optional client-side version of Torch. More documentation to come. - # Building To build Torch you must first have a complete SE Dedicated installation somewhere. Before you open the solution, run the Setup batch file and enter the path of that installation's DedicatedServer64 folder. The script will make a symlink to that folder so the Torch solution can find the DLL references it needs. In both cases you will need to set the InstancePath in TorchConfig.xml to an existing dedicated server instance as Torch can't fully generate it on its own yet. -# Official Plugins -Install plugins by unzipping them into the 'Plugins' folder which should be in the same location as the Torch files. If it doesn't exist you can simply create it. -* [Essentials](https://github.com/TorchAPI/Essentials): Adds a slew of chat commands and other tools to help manage your server. -* [Concealment](https://github.com/TorchAPI/Concealment): Adds game logic and physics optimizations that significantly improve sim speed. - If you have a more enjoyable server experience because of Torch, please consider supporting us on Patreon. [![Patreon](http://i.imgur.com/VzzIMgn.png)](https://www.patreon.com/bePatron?u=847269)! diff --git a/Torch.Server/Initializer.cs b/Torch.Server/Initializer.cs index 3e83980..3a7e11e 100644 --- a/Torch.Server/Initializer.cs +++ b/Torch.Server/Initializer.cs @@ -190,21 +190,29 @@ quit"; } private void LogException(Exception ex) - { + { + if (ex is AggregateException ag) + { + foreach (var e in ag.InnerExceptions) + LogException(e); + + return; + } + + Log.Fatal(ex); + + if (ex is ReflectionTypeLoadException extl) + { + foreach (var exl in extl.LoaderExceptions) + LogException(exl); + + return; + } + if (ex.InnerException != null) { LogException(ex.InnerException); } - - Log.Fatal(ex); - - if (ex is ReflectionTypeLoadException exti) - foreach (Exception exl in exti.LoaderExceptions) - LogException(exl); - - if (ex is AggregateException ag) - foreach (Exception e in ag.InnerExceptions) - LogException(e); } private void HandleException(object sender, UnhandledExceptionEventArgs e) diff --git a/Torch.Server/Managers/InstanceManager.cs b/Torch.Server/Managers/InstanceManager.cs index 245f8d1..b1d313c 100644 --- a/Torch.Server/Managers/InstanceManager.cs +++ b/Torch.Server/Managers/InstanceManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.ComponentModel; using System.IO; using System.Linq; @@ -14,6 +15,7 @@ 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; @@ -94,7 +96,8 @@ namespace Torch.Server.Managers //remove the Torch mod to avoid running multiple copies of it DedicatedConfig.SelectedWorld.Checkpoint.Mods.RemoveAll(m => m.PublishedFileId == TorchModCore.MOD_ID); foreach (var m in DedicatedConfig.SelectedWorld.Checkpoint.Mods) - DedicatedConfig.Mods.Add(m.PublishedFileId); + DedicatedConfig.Mods.Add(new ModItemInfo(m)); + Task.Run(() => DedicatedConfig.UpdateAllModInfosAsync()); } } @@ -108,7 +111,8 @@ namespace Torch.Server.Managers //remove the Torch mod to avoid running multiple copies of it DedicatedConfig.SelectedWorld.Checkpoint.Mods.RemoveAll(m => m.PublishedFileId == TorchModCore.MOD_ID); foreach (var m in DedicatedConfig.SelectedWorld.Checkpoint.Mods) - DedicatedConfig.Mods.Add(m.PublishedFileId); + DedicatedConfig.Mods.Add(new ModItemInfo(m)); + Task.Run(() => DedicatedConfig.UpdateAllModInfosAsync()); } } @@ -119,11 +123,10 @@ namespace Torch.Server.Managers private void ImportWorldConfig(WorldViewModel world, bool modsOnly = true) { - var sb = new StringBuilder(); + var mods = new MtObservableList(); foreach (var mod in world.Checkpoint.Mods) - sb.AppendLine(mod.PublishedFileId.ToString()); - - DedicatedConfig.Mods = world.Checkpoint.Mods.Select(x => x.PublishedFileId).ToList(); + mods.Add(new ModItemInfo(mod)); + DedicatedConfig.Mods = mods; Log.Debug("Loaded mod list from world"); @@ -151,7 +154,10 @@ namespace Torch.Server.Managers return; } - DedicatedConfig.Mods = checkpoint.Mods.Select(x => x.PublishedFileId).ToList(); + var mods = new MtObservableList(); + foreach (var mod in checkpoint.Mods) + mods.Add(new ModItemInfo(mod)); + DedicatedConfig.Mods = mods; Log.Debug("Loaded mod list from world"); @@ -193,9 +199,14 @@ namespace Torch.Server.Managers checkpoint.SessionName = DedicatedConfig.WorldName; checkpoint.Settings = DedicatedConfig.SessionSettings; checkpoint.Mods.Clear(); - - foreach (var modId in DedicatedConfig.Mods) - checkpoint.Mods.Add(new MyObjectBuilder_Checkpoint.ModItem(modId)); + + foreach (var mod in DedicatedConfig.Mods) + { + var savedMod = new MyObjectBuilder_Checkpoint.ModItem(mod.Name, mod.PublishedFileId, mod.FriendlyName); + savedMod.IsDependency = mod.IsDependency; + checkpoint.Mods.Add(savedMod); + } + Task.Run(() => DedicatedConfig.UpdateAllModInfosAsync()); MyObjectBuilderSerializer.SerializeXML(sandboxPath, false, checkpoint); diff --git a/Torch.Server/Torch.Server.csproj b/Torch.Server/Torch.Server.csproj index ed9e330..798b576 100644 --- a/Torch.Server/Torch.Server.csproj +++ b/Torch.Server/Torch.Server.csproj @@ -80,6 +80,9 @@ ..\GameBinaries\Microsoft.CodeAnalysis.CSharp.dll False + + ..\packages\Microsoft.Win32.Registry.4.4.0\lib\net461\Microsoft.Win32.Registry.dll + ..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll @@ -87,6 +90,9 @@ ..\packages\NLog.4.4.12\lib\net45\NLog.dll True + + ..\packages\protobuf-net.2.1.0\lib\net451\protobuf-net.dll + False ..\GameBinaries\Sandbox.Common.dll @@ -126,6 +132,12 @@ + + ..\packages\System.Security.AccessControl.4.4.0\lib\net461\System.Security.AccessControl.dll + + + ..\packages\System.Security.Principal.Windows.4.4.0\lib\net461\System.Security.Principal.Windows.dll + @@ -245,15 +257,21 @@ + + + CharacterView.xaml + + ModListControl.xaml + ThemeControl.xaml @@ -414,6 +432,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + Designer MSBuild:Compile diff --git a/Torch.Server/ViewModels/ConfigDedicatedViewModel.cs b/Torch.Server/ViewModels/ConfigDedicatedViewModel.cs index e1b4456..abe4c33 100644 --- a/Torch.Server/ViewModels/ConfigDedicatedViewModel.cs +++ b/Torch.Server/ViewModels/ConfigDedicatedViewModel.cs @@ -10,6 +10,8 @@ using Torch.Collections; using Torch.Server.Managers; using VRage.Game; using VRage.Game.ModAPI; +using Torch.Utils.SteamWorkshopTools; +using Torch.Collections; namespace Torch.Server.ViewModels { @@ -29,6 +31,7 @@ namespace Torch.Server.ViewModels _config = configDedicated; _config.IgnoreLastSession = true; SessionSettings = new SessionSettingsViewModel(_config.SessionSettings); + Task.Run(() => UpdateAllModInfosAsync()); } public void Save(string path = null) @@ -73,14 +76,61 @@ namespace Torch.Server.ViewModels } } + public async Task UpdateAllModInfosAsync(Action messageHandler = null) + { + if (Mods.Count() == 0) + return; + + var ids = Mods.Select(m => m.PublishedFileId); + var workshopService = WebAPI.Instance; + Dictionary modInfos = null; + + try + { + modInfos = (await workshopService.GetPublishedFileDetails(ids.ToArray())); + } + catch (Exception e) + { + Log.Error(e.Message); + return; + } + + Log.Info($"Mods Info successfully retrieved!"); + + foreach (var mod in Mods) + { + if (!modInfos.ContainsKey(mod.PublishedFileId) || modInfos[mod.PublishedFileId] == null) + { + Log.Error($"Failed to retrieve info for mod with workshop id '{mod.PublishedFileId}'!"); + } + //else if (!modInfo.Tags.Contains("")) + else + { + mod.FriendlyName = modInfos[mod.PublishedFileId].Title; + mod.Description = modInfos[mod.PublishedFileId].Description; + //mod.Name = modInfos[mod.PublishedFileId].FileName; + } + } + + } + public List Administrators { get => _config.Administrators; set => SetValue(x => _config.Administrators = x, value); } public List Banned { get => _config.Banned; set => SetValue(x => _config.Banned = x, value); } + private MtObservableList _mods = new MtObservableList(); + public MtObservableList Mods + { + get => _mods; + set + { + SetValue(x => _mods = x, value); + Task.Run(() => UpdateAllModInfosAsync()); + } + } + public List Reserved { get => _config.Reserved; set => SetValue(x => _config.Reserved = x, value); } - private List _mods = new List(); - public List Mods { get => _mods; set => SetValue(x => _mods = x, value); } public int AsteroidAmount { get => _config.AsteroidAmount; set => SetValue(x => _config.AsteroidAmount = x, value); } diff --git a/Torch.Server/ViewModels/ModItemInfo.cs b/Torch.Server/ViewModels/ModItemInfo.cs new file mode 100644 index 0000000..9212fa1 --- /dev/null +++ b/Torch.Server/ViewModels/ModItemInfo.cs @@ -0,0 +1,131 @@ +using System; +using System.ComponentModel; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Threading; +using System.Runtime.CompilerServices; +using NLog; +using VRage.Game; +using Torch.Server.Annotations; +using Torch.Utils.SteamWorkshopTools; + +namespace Torch.Server.ViewModels +{ + /// + /// Wrapper around VRage.Game.Objectbuilder_Checkpoint.ModItem + /// that holds additional meta information + /// (e.g. workshop description) + /// + public class ModItemInfo : ViewModel + { + MyObjectBuilder_Checkpoint.ModItem _modItem; + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + + /// + /// Human friendly name of the mod + /// + public string FriendlyName + { + get { return _modItem.FriendlyName; } + set { + SetValue(ref _modItem.FriendlyName, value); + } + } + + /// + /// Workshop ID of the mod + /// + public ulong PublishedFileId + { + get { return _modItem.PublishedFileId; } + set + { + SetValue(ref _modItem.PublishedFileId, value); + } + } + + /// + /// Local filename of the mod + /// + public string Name + { + get { return _modItem.Name; } + set + { + SetValue(ref _modItem.FriendlyName, value); + } + } + + /// + /// Whether or not the mod was added + /// because another mod depends on it + /// + public bool IsDependency + { + get { return _modItem.IsDependency; } + set + { + SetValue(ref _modItem.IsDependency, value); + } + } + + private string _description; + /// + /// Workshop description of the mod + /// + public string Description + { + get { return _description; } + set + { + SetValue(ref _description, value); + } + } + + /// + /// Constructor, returns a new ModItemInfo instance + /// + /// The wrapped mod + public ModItemInfo(MyObjectBuilder_Checkpoint.ModItem mod) + { + _modItem = mod; + } + + /// + /// Retrieve information about the + /// wrapped mod from the workhop asynchronously + /// via the Steam web API. + /// + /// + public async Task UpdateModInfoAsync() + { + var msg = ""; + var workshopService = WebAPI.Instance; + PublishedItemDetails modInfo = null; + try + { + modInfo = (await workshopService.GetPublishedFileDetails(new ulong[] { PublishedFileId }))?[PublishedFileId]; + } + catch( Exception e ) + { + Log.Error(e.Message); + } + if (modInfo == null) + { + Log.Error($"Failed to retrieve mod with workshop id '{PublishedFileId}'!"); + return false; + } + //else if (!modInfo.Tags.Contains("")) + else + { + Log.Info($"Mod Info successfully retrieved!"); + FriendlyName = modInfo.Title; + Description = modInfo.Description; + //Name = modInfo.FileName; + return true; + } + } + } +} diff --git a/Torch.Server/Views/ConfigControl.xaml b/Torch.Server/Views/ConfigControl.xaml index 9d0cb99..898b4ca 100644 --- a/Torch.Server/Views/ConfigControl.xaml +++ b/Torch.Server/Views/ConfigControl.xaml @@ -58,7 +58,7 @@ - + diff --git a/Torch.Server/Views/Converters/ListConverterWorkshopId.cs b/Torch.Server/Views/Converters/ListConverterWorkshopId.cs new file mode 100644 index 0000000..f58afac --- /dev/null +++ b/Torch.Server/Views/Converters/ListConverterWorkshopId.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Data; +using Torch.Server.ViewModels; +using VRage.Game; + +namespace Torch.Server.Views.Converters +{ + class ListConverterWorkshopId : IValueConverter + { + public Type Type { get; set; } + + /// + /// Converts a list of ModItemInfo objects into a list of their workshop IDs (PublishedFileIds). + /// + /// + /// Expected to contain a list of ModItemInfo objects + /// + /// This parameter will be ignored + /// This parameter will be ignored + /// This parameter will be ignored + /// A string containing the workshop ids of all mods, one per line + public object Convert(object valueList, Type targetType, object parameter, CultureInfo culture) + { + if (!(valueList is IList list)) + throw new InvalidOperationException("Value is not the proper type."); + + var sb = new StringBuilder(); + foreach (var item in list) + { + sb.AppendLine(((ModItemInfo) item).PublishedFileId.ToString()); + } + + return sb.ToString(); + } + + /// + /// Converts a list of workshop ids into a list of ModItemInfo objects + /// + /// A string containing workshop ids separated by new lines + /// This parameter will be ignored + /// + /// A list of ModItemInfos which should + /// contain the requestted mods + /// (or they will be dropped) + /// + /// This parameter will be ignored + /// A list of ModItemInfo objects + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + var list = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(Type)); + var mods = parameter as ICollection; + if (mods == null) + throw new ArgumentException("parameter needs to be of type ICollection!"); + var input = ((string)value).Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); + foreach (var item in input) + { + if( ulong.TryParse(item, out ulong id)) + { + var mod = mods.FirstOrDefault((m) => m.PublishedFileId == id); + if (mod != null) + list.Add(mod); + else + list.Add(new MyObjectBuilder_Checkpoint.ModItem(id)); + } + } + + return list; + } + } +} diff --git a/Torch.Server/Views/Converters/ModToIdConverter.cs b/Torch.Server/Views/Converters/ModToIdConverter.cs new file mode 100644 index 0000000..512bbf2 --- /dev/null +++ b/Torch.Server/Views/Converters/ModToIdConverter.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Windows.Data; +using System.Threading.Tasks; +using Torch.Server.ViewModels; +using NLog; +using Torch.Collections; + +namespace Torch.Server.Views.Converters +{ + /// + /// A converter to get the index of a ModItemInfo object within a collection of ModItemInfo objects + /// + public class ModToListIdConverter : IMultiValueConverter + { + /// + /// Converts a ModItemInfo object into its index within a Collection of ModItemInfo objects + /// + /// + /// Expected to contain a ModItemInfo object at index 0 + /// and a Collection of ModItemInfo objects at index 1 + /// + /// This parameter will be ignored + /// This parameter will be ignored + /// This parameter will be ignored + /// the index of the mod within the provided mod list. + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + //if (targetType != typeof(int)) + // throw new NotSupportedException("ModToIdConverter can only convert mods into int values or vise versa!"); + var mod = (ModItemInfo) values[0]; + var theModList = (MtObservableList) values[1]; + return theModList.IndexOf(mod); + } + + /// + /// It is not supported to reverse this converter + /// + /// + /// + /// + /// + /// Raises a NotSupportedException + public object[] ConvertBack(object values, Type[] targetType, object parameter, CultureInfo culture) + { + throw new NotSupportedException("ModToIdConverter can not convert back!"); + } + } +} diff --git a/Torch.Server/Views/ModListControl.xaml b/Torch.Server/Views/ModListControl.xaml new file mode 100644 index 0000000..89a092f --- /dev/null +++ b/Torch.Server/Views/ModListControl.xaml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +