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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +