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 3f803b8107.

* 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.
This commit is contained in:
Tobias K
2019-02-02 12:26:55 +01:00
committed by Brant Martin
parent 4e2e58bb4c
commit f265f7e773
21 changed files with 1150 additions and 40 deletions

View File

@@ -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)!

View File

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

View File

@@ -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<ModItemInfo>();
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<ModItemInfo>();
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);

View File

@@ -80,6 +80,9 @@
<HintPath>..\GameBinaries\Microsoft.CodeAnalysis.CSharp.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.Win32.Registry, Version=4.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.Win32.Registry.4.4.0\lib\net461\Microsoft.Win32.Registry.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
@@ -87,6 +90,9 @@
<HintPath>..\packages\NLog.4.4.12\lib\net45\NLog.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="protobuf-net, Version=2.1.0.0, Culture=neutral, PublicKeyToken=257b51d87d2e4d67, processorArchitecture=MSIL">
<HintPath>..\packages\protobuf-net.2.1.0\lib\net451\protobuf-net.dll</HintPath>
</Reference>
<Reference Include="Sandbox.Common, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\GameBinaries\Sandbox.Common.dll</HintPath>
@@ -126,6 +132,12 @@
<Reference Include="System.Data" />
<Reference Include="System.Drawing" />
<Reference Include="System.IO.Compression.FileSystem" />
<Reference Include="System.Security.AccessControl, Version=4.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Security.AccessControl.4.4.0\lib\net461\System.Security.AccessControl.dll</HintPath>
</Reference>
<Reference Include="System.Security.Principal.Windows, Version=4.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Security.Principal.Windows.4.4.0\lib\net461\System.Security.Principal.Windows.dll</HintPath>
</Reference>
<Reference Include="System.ServiceProcess" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Windows.Interactivity, Version=4.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
@@ -245,15 +257,21 @@
<Compile Include="ViewModels\Entities\CharacterViewModel.cs" />
<Compile Include="ViewModels\ConfigDedicatedViewModel.cs" />
<Compile Include="ViewModels\Entities\EntityControlViewModel.cs" />
<Compile Include="ViewModels\ModItemInfo.cs" />
<Compile Include="ViewModels\SessionSettingsViewModel.cs" />
<Compile Include="Views\Converters\DefinitionToIdConverter.cs" />
<Compile Include="Views\Converters\BooleanAndConverter.cs" />
<Compile Include="Views\Converters\ListConverter.cs" />
<Compile Include="MultiTextWriter.cs" />
<Compile Include="RichTextBoxWriter.cs" />
<Compile Include="Views\Converters\ListConverterWorkshopId.cs" />
<Compile Include="Views\Converters\ModToIdConverter.cs" />
<Compile Include="Views\Entities\CharacterView.xaml.cs">
<DependentUpon>CharacterView.xaml</DependentUpon>
</Compile>
<Compile Include="Views\ModListControl.xaml.cs">
<DependentUpon>ModListControl.xaml</DependentUpon>
</Compile>
<Compile Include="Views\ThemeControl.xaml.cs">
<DependentUpon>ThemeControl.xaml</DependentUpon>
</Compile>
@@ -414,6 +432,10 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\ModListControl.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\PluginsControl.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>

View File

@@ -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<string> messageHandler = null)
{
if (Mods.Count() == 0)
return;
var ids = Mods.Select(m => m.PublishedFileId);
var workshopService = WebAPI.Instance;
Dictionary<ulong, PublishedItemDetails> 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<string> Administrators { get => _config.Administrators; set => SetValue(x => _config.Administrators = x, value); }
public List<ulong> Banned { get => _config.Banned; set => SetValue(x => _config.Banned = x, value); }
private MtObservableList<ModItemInfo> _mods = new MtObservableList<ModItemInfo>();
public MtObservableList<ModItemInfo> Mods
{
get => _mods;
set
{
SetValue(x => _mods = x, value);
Task.Run(() => UpdateAllModInfosAsync());
}
}
public List<ulong> Reserved { get => _config.Reserved; set => SetValue(x => _config.Reserved = x, value); }
private List<ulong> _mods = new List<ulong>();
public List<ulong> Mods { get => _mods; set => SetValue(x => _mods = x, value); }
public int AsteroidAmount { get => _config.AsteroidAmount; set => SetValue(x => _config.AsteroidAmount = x, value); }

View File

@@ -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
{
/// <summary>
/// Wrapper around VRage.Game.Objectbuilder_Checkpoint.ModItem
/// that holds additional meta information
/// (e.g. workshop description)
/// </summary>
public class ModItemInfo : ViewModel
{
MyObjectBuilder_Checkpoint.ModItem _modItem;
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
/// <summary>
/// Human friendly name of the mod
/// </summary>
public string FriendlyName
{
get { return _modItem.FriendlyName; }
set {
SetValue(ref _modItem.FriendlyName, value);
}
}
/// <summary>
/// Workshop ID of the mod
/// </summary>
public ulong PublishedFileId
{
get { return _modItem.PublishedFileId; }
set
{
SetValue(ref _modItem.PublishedFileId, value);
}
}
/// <summary>
/// Local filename of the mod
/// </summary>
public string Name
{
get { return _modItem.Name; }
set
{
SetValue(ref _modItem.FriendlyName, value);
}
}
/// <summary>
/// Whether or not the mod was added
/// because another mod depends on it
/// </summary>
public bool IsDependency
{
get { return _modItem.IsDependency; }
set
{
SetValue(ref _modItem.IsDependency, value);
}
}
private string _description;
/// <summary>
/// Workshop description of the mod
/// </summary>
public string Description
{
get { return _description; }
set
{
SetValue(ref _description, value);
}
}
/// <summary>
/// Constructor, returns a new ModItemInfo instance
/// </summary>
/// <param name="mod">The wrapped mod</param>
public ModItemInfo(MyObjectBuilder_Checkpoint.ModItem mod)
{
_modItem = mod;
}
/// <summary>
/// Retrieve information about the
/// wrapped mod from the workhop asynchronously
/// via the Steam web API.
/// </summary>
/// <returns></returns>
public async Task<bool> 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;
}
}
}
}

View File

@@ -58,7 +58,7 @@
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ScrollViewer VerticalScrollBarVisibility="Disabled">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />

View File

@@ -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; }
/// <summary>
/// Converts a list of ModItemInfo objects into a list of their workshop IDs (PublishedFileIds).
/// </summary>
/// <param name="valueList">
/// Expected to contain a list of ModItemInfo objects
/// </param>
/// <param name="targetType">This parameter will be ignored</param>
/// <param name="parameter">This parameter will be ignored</param>
/// <param name="culture"> This parameter will be ignored</param>
/// <returns>A string containing the workshop ids of all mods, one per line</returns>
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();
}
/// <summary>
/// Converts a list of workshop ids into a list of ModItemInfo objects
/// </summary>
/// <param name="value">A string containing workshop ids separated by new lines</param>
/// <param name="targetType">This parameter will be ignored</param>
/// <param name="parameter">
/// A list of ModItemInfos which should
/// contain the requestted mods
/// (or they will be dropped)
/// </param>
/// <param name="culture">This parameter will be ignored</param>
/// <returns>A list of ModItemInfo objects</returns>
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<ModItemInfo>;
if (mods == null)
throw new ArgumentException("parameter needs to be of type ICollection<ModItemInfo>!");
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;
}
}
}

View File

@@ -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
{
/// <summary>
/// A converter to get the index of a ModItemInfo object within a collection of ModItemInfo objects
/// </summary>
public class ModToListIdConverter : IMultiValueConverter
{
/// <summary>
/// Converts a ModItemInfo object into its index within a Collection of ModItemInfo objects
/// </summary>
/// <param name="values">
/// Expected to contain a ModItemInfo object at index 0
/// and a Collection of ModItemInfo objects at index 1
/// </param>
/// <param name="targetType">This parameter will be ignored</param>
/// <param name="parameter">This parameter will be ignored</param>
/// <param name="culture"> This parameter will be ignored</param>
/// <returns>the index of the mod within the provided mod list.</returns>
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<ModItemInfo>) values[1];
return theModList.IndexOf(mod);
}
/// <summary>
/// It is not supported to reverse this converter
/// </summary>
/// <param name="values"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns>Raises a NotSupportedException</returns>
public object[] ConvertBack(object values, Type[] targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException("ModToIdConverter can not convert back!");
}
}
}

View File

@@ -0,0 +1,125 @@
<UserControl x:Class="Torch.Server.Views.ModListControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:viewModels="clr-namespace:Torch.Server.ViewModels"
xmlns:s="clr-namespace:System"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
MouseMove="UserControl_MouseMove">
<!--<UserControl.DataContext>
<viewModels:ConfigDedicatedViewModel />
</UserControl.DataContext>-->
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources.xaml" />
</ResourceDictionary.MergedDictionaries>
<Style TargetType="Grid" x:Key="RootGridStyle">
<Style.Triggers>
<DataTrigger Binding="{Binding Mode=OneWay, UpdateSourceTrigger=PropertyChanged, BindingGroupName=RootEnabledBinding}" Value="{x:Null}">
<Setter Property="IsEnabled" Value="False"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
</UserControl.Resources>
<UserControl.DataContext>
<viewModels:ConfigDedicatedViewModel />
</UserControl.DataContext>
<Grid Style="{StaticResource RootGridStyle}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="500px"/>
<ColumnDefinition Width="10px"/>
<ColumnDefinition Width="*" MinWidth="200px"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="80px"/>
</Grid.RowDefinitions>
<DataGrid Name="ModList" Grid.Column="0" Grid.ColumnSpan="1" ItemsSource="{Binding UpdateSourceTrigger=PropertyChanged}"
Sorting="ModList_Sorting"
SelectionMode="Single"
SelectionUnit="FullRow"
AllowDrop="True"
CanUserReorderColumns="False"
CanUserSortColumns="True"
PreviewMouseLeftButtonDown="ModList_MouseLeftButtonDown"
MouseLeftButtonUp="ModList_MouseLeftButtonUp"
SelectedCellsChanged="ModList_Selected"
AutoGenerateColumns="False">
<!--:DesignSource="{d:DesignInstance Type={x:Type MyObjectBuilder_Checkpoint:ModItem, CreateList=True}}">-->
<DataGrid.Columns>
<DataGridTextColumn Header="Load Order"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Binding>
<MultiBinding Converter="{StaticResource ModToListIdConverter}" StringFormat="{}{0}">
<Binding />
<Binding ElementName="ModList" Path="DataContext"></Binding>
</MultiBinding>
</DataGridTextColumn.Binding>
</DataGridTextColumn>
<DataGridTextColumn Header="Workshop Id"
IsReadOnly="True"
Binding="{Binding PublishedFileId, NotifyOnTargetUpdated=True, UpdateSourceTrigger=PropertyChanged}">
</DataGridTextColumn>
<DataGridTextColumn Header="Name"
Width="*"
IsReadOnly="True"
Binding="{Binding FriendlyName, NotifyOnTargetUpdated=True, UpdateSourceTrigger=PropertyChanged}">
</DataGridTextColumn>
</DataGrid.Columns>
<DataGrid.ItemContainerStyle>
<Style TargetType="DataGridRow">
<Style.Triggers>
<DataTrigger Binding="{Binding IsDependency}" Value="True">
<Setter Property="Foreground" Value="#222222"/>
<Setter Property="Background" Value="#FFCCAA"/>
</DataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsDependency}" Value="True"/>
<Condition Binding="{Binding ElementName=ShowDependencyModsCheckBox, Path=IsChecked}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter Property="Height" Value="0px"/>
</MultiDataTrigger>
</Style.Triggers>
</Style>
</DataGrid.ItemContainerStyle>
</DataGrid>
<ScrollViewer Grid.Column="2" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" Background="#1b2838">
<TextBlock Name="ModDescription" TextWrapping="Wrap" Foreground="White" Padding="2px"
Text="{Binding ElementName=ModList, Path=SelectedItem.Description}">
</TextBlock>
</ScrollViewer>
<Grid Grid.Row="2" Margin="0 0 0 6px">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition MinHeight="40px"/>
</Grid.RowDefinitions>
<CheckBox Name="ShowDependencyModsCheckBox" VerticalAlignment="Center"
HorizontalAlignment="Left" Margin="6px 0" Grid.Column="0" Grid.Row="0"/>
<Label Content="Show Dependency Mods" Padding="0" Margin="6px 0" Grid.Column="1" VerticalAlignment="Center"/>
<Label Content="ID/URL:" Padding="0" Margin="6px 0" HorizontalAlignment="Left"
VerticalAlignment="Center" Grid.Column="0" Grid.Row="1"/>
<TextBox Name="AddModIDTextBox" Grid.Column="1" VerticalContentAlignment="Center"
HorizontalAlignment="Stretch" MinWidth="100px" Margin="6px 4px" Grid.Row="1"/>
<Button Content="Add" Grid.Column="2" Margin="6px 0" Width="60px" Height="40px" Click="AddBtn_OnClick" Grid.Row="1"/>
<Button Content="Remove" Grid.Column="4" Margin="6px 0" Width="60px" Height="40px" Click="RemoveBtn_OnClick" Grid.Row="1"
IsEnabled="{Binding ElementName=ModList, Path=SelectedItems.Count}"/>
</Grid>
<Button Content="Save Config" Grid.Row="2" VerticalAlignment="Bottom" HorizontalAlignment="Right" Margin="6px" Grid.Column="3" Width="80px" Height="40px" Click="SaveBtn_OnClick"/>
</Grid>
</UserControl>

View File

@@ -0,0 +1,248 @@
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 Torch.Server.Managers;
using Torch.API.Managers;
using Torch.Server.ViewModels;
using Torch.Server.Annotations;
using Torch.Collections;
namespace Torch.Server.Views
{
/// <summary>
/// Interaction logic for ModListControl.xaml
/// </summary>
public partial class ModListControl : UserControl, INotifyPropertyChanged
{
private static Logger Log = LogManager.GetLogger(nameof(ModListControl));
private InstanceManager _instanceManager;
ModItemInfo _draggedMod;
bool _hasOrderChanged = false;
bool _isSortedByLoadOrder = true;
//private List<BindingExpression> _bindingExpressions = new List<BindingExpression>();
/// <summary>
/// Constructor for ModListControl
/// </summary>
public ModListControl()
{
InitializeComponent();
_instanceManager = TorchBase.Instance.Managers.GetManager<InstanceManager>();
_instanceManager.InstanceLoaded += _instanceManager_InstanceLoaded;
//var mods = _instanceManager.DedicatedConfig?.Mods;
//if( mods != null)
// DataContext = new ObservableCollection<MyObjectBuilder_Checkpoint.ModItem>();
DataContext = _instanceManager.DedicatedConfig?.Mods;
// Gets called once all children are loaded
//Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(ApplyStyles));
}
private void ModListControl_SizeChanged(object sender, SizeChangedEventArgs e)
{
throw new NotImplementedException();
}
private void ResetSorting()
{
CollectionViewSource.GetDefaultView(ModList.ItemsSource).SortDescriptions.Clear();
}
private void _instanceManager_InstanceLoaded(ConfigDedicatedViewModel obj)
{
Log.Info("Instance loaded.");
Dispatcher.Invoke(() => {
DataContext = obj?.Mods ?? new MtObservableList<ModItemInfo>();
UpdateLayout();
((MtObservableList<ModItemInfo>)DataContext).CollectionChanged += OnModlistUpdate;
});
}
private void OnModlistUpdate(object sender, NotifyCollectionChangedEventArgs e)
{
ModList.Items.Refresh();
//if (e.Action == NotifyCollectionChangedAction.Remove)
// _instanceManager.SaveConfig();
}
private void SaveBtn_OnClick(object sender, RoutedEventArgs e)
{
_instanceManager.SaveConfig();
}
private void AddBtn_OnClick(object sender, RoutedEventArgs e)
{
if (TryExtractId(AddModIDTextBox.Text, out ulong id))
{
var mod = new ModItemInfo(new MyObjectBuilder_Checkpoint.ModItem(id));
//mod.PublishedFileId = id;
_instanceManager.DedicatedConfig.Mods.Add(mod);
Task.Run(mod.UpdateModInfoAsync)
.ContinueWith((t) =>
{
Dispatcher.Invoke(() =>
{
_instanceManager.DedicatedConfig.Save();
});
});
AddModIDTextBox.Text = "";
}
else
{
AddModIDTextBox.BorderBrush = Brushes.Red;
Log.Warn("Invalid mod id!");
MessageBox.Show("Invalid mod id!");
}
}
private void RemoveBtn_OnClick(object sender, RoutedEventArgs e)
{
var modList = ((MtObservableList<ModItemInfo>)DataContext);
if (ModList.SelectedItem is ModItemInfo mod && modList.Contains(mod))
modList.Remove(mod);
}
private bool TryExtractId(string input, out ulong result)
{
var match = Regex.Match(input, @"(?<=id=)\d+").Value;
bool success;
if (string.IsNullOrEmpty(match))
success = ulong.TryParse(input, out result);
else
success = ulong.TryParse(match, out result);
return success;
}
private void ModList_Sorting(object sender, DataGridSortingEventArgs e)
{
Log.Info($"Sorting by '{e.Column.Header}'");
if (e.Column == ModList.Columns[0])
{
var dataView = CollectionViewSource.GetDefaultView(ModList.ItemsSource);
dataView.SortDescriptions.Clear();
dataView.Refresh();
_isSortedByLoadOrder = true;
}
else
_isSortedByLoadOrder = false;
}
private void ModList_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
//return;
_draggedMod = (ModItemInfo) TryFindRowAtPoint((UIElement) sender, e.GetPosition(ModList))?.DataContext;
//DraggedMod = (ModItemInfo) ModList.SelectedItem;
}
private static DataGridRow TryFindRowAtPoint(UIElement reference, Point point)
{
var element = reference.InputHitTest(point) as DependencyObject;
if (element == null)
return null;
if (element is DataGridRow row)
return row;
else
return TryFindParent<DataGridRow>(element);
}
private static T TryFindParent<T>(DependencyObject child) where T : DependencyObject
{
DependencyObject parent;
if (child == null)
return null;
if (child is ContentElement contentElement)
{
parent = ContentOperations.GetParent(contentElement);
if (parent == null && child is FrameworkContentElement fce)
parent = fce.Parent;
}
else
{
parent = VisualTreeHelper.GetParent(child);
}
if (parent is T result)
return result;
else
return TryFindParent<T>(parent);
}
private void UserControl_MouseMove(object sender, MouseEventArgs e)
{
if (_draggedMod == null)
return;
if (!_isSortedByLoadOrder)
return;
var targetMod = (ModItemInfo)TryFindRowAtPoint((UIElement)sender, e.GetPosition(ModList))?.DataContext;
if( targetMod != null && !ReferenceEquals(_draggedMod, targetMod))
{
_hasOrderChanged = true;
var modList = (MtObservableList<ModItemInfo>)DataContext;
//modList.Move(modList.IndexOf(_draggedMod), modList.IndexOf(targetMod));
modList.RemoveAt(modList.IndexOf(_draggedMod));
modList.Insert(modList.IndexOf(targetMod), _draggedMod);
ModList.Items.Refresh();
ModList.SelectedItem = _draggedMod;
}
}
private void ModList_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (!_isSortedByLoadOrder)
{
var targetMod = (ModItemInfo)TryFindRowAtPoint((UIElement)sender, e.GetPosition(ModList))?.DataContext;
if (targetMod != null && !ReferenceEquals(_draggedMod, targetMod))
{
var msg = "Drag and drop is only available when sorted by load order!";
Log.Warn(msg);
MessageBox.Show(msg);
}
}
//if (DraggedMod != null && HasOrderChanged)
//Log.Info("Dragging over, saving...");
//_instanceManager.SaveConfig();
_draggedMod = null;
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void ModList_Selected(object sender, SelectedCellsChangedEventArgs e)
{
if (_draggedMod != null)
ModList.SelectedItem = _draggedMod;
else if( e.AddedCells.Count > 0)
ModList.SelectedItem = e.AddedCells[0].Item;
}
}
}

View File

@@ -18,5 +18,7 @@
</Style>
<converters:ListConverter x:Key="ListConverterString" Type="system:String"/>
<converters:ListConverter x:Key="ListConverterUInt64" Type="system:UInt64"/>
<converters:ModToListIdConverter x:Key="ModToListIdConverter"/>
<converters:ListConverterWorkshopId x:Key="ListConverterWorkshopId"/>
<converters:BooleanAndConverter x:Key="BooleanAndConverter"/>
</ResourceDictionary>

View File

@@ -80,6 +80,9 @@
<views:ConfigControl Grid.Row="1" x:Name="ConfigControl" Margin="3" DockPanel.Dock="Bottom" IsEnabled="{Binding CanRun}"/>
</Grid>
</TabItem>
<TabItem Header="Mods">
<views:ModListControl/>
</TabItem>
<TabItem Header="Chat/Players">
<Grid>
<Grid.ColumnDefinitions>

View File

@@ -2,7 +2,12 @@
<packages>
<package id="ControlzEx" version="3.0.2.4" targetFramework="net461" />
<package id="MahApps.Metro" version="1.6.1" targetFramework="net461" />
<package id="Microsoft.Win32.Registry" version="4.4.0" targetFramework="net461" />
<package id="Mono.TextTransform" version="1.0.0" targetFramework="net461" />
<package id="Newtonsoft.Json" version="10.0.3" targetFramework="net461" />
<package id="NLog" version="4.4.12" targetFramework="net461" />
<package id="protobuf-net" version="2.1.0" targetFramework="net461" />
<package id="SteamKit2" version="2.1.0" targetFramework="net461" />
<package id="System.Security.AccessControl" version="4.4.0" targetFramework="net461" />
<package id="System.Security.Principal.Windows" version="4.4.0" targetFramework="net461" />
</packages>

View File

@@ -55,10 +55,8 @@ Global
{FBA5D932-6254-4A1E-BAF4-E229FA94E3C2}.Release|x64.Build.0 = Release|x64
{E36DF745-260B-4956-A2E8-09F08B2E7161}.Debug|Any CPU.ActiveCfg = Debug|x64
{E36DF745-260B-4956-A2E8-09F08B2E7161}.Debug|x64.ActiveCfg = Debug|x64
{E36DF745-260B-4956-A2E8-09F08B2E7161}.Debug|x64.Build.0 = Debug|x64
{E36DF745-260B-4956-A2E8-09F08B2E7161}.Release|Any CPU.ActiveCfg = Release|x64
{E36DF745-260B-4956-A2E8-09F08B2E7161}.Release|x64.ActiveCfg = Release|x64
{E36DF745-260B-4956-A2E8-09F08B2E7161}.Release|x64.Build.0 = Release|x64
{CA50886B-7B22-4CD8-93A0-C06F38D4F77D}.Debug|Any CPU.ActiveCfg = Debug|x64
{CA50886B-7B22-4CD8-93A0-C06F38D4F77D}.Debug|x64.ActiveCfg = Debug|x64
{CA50886B-7B22-4CD8-93A0-C06F38D4F77D}.Debug|x64.Build.0 = Debug|x64
@@ -79,10 +77,8 @@ Global
{9EFD1D91-2FA2-47ED-B537-D8BC3B0E543E}.Release|x64.Build.0 = Release|x64
{632E78C0-0DAC-4B71-B411-2F1B333CC310}.Debug|Any CPU.ActiveCfg = Debug|x64
{632E78C0-0DAC-4B71-B411-2F1B333CC310}.Debug|x64.ActiveCfg = Debug|x64
{632E78C0-0DAC-4B71-B411-2F1B333CC310}.Debug|x64.Build.0 = Debug|x64
{632E78C0-0DAC-4B71-B411-2F1B333CC310}.Release|Any CPU.ActiveCfg = Release|x64
{632E78C0-0DAC-4B71-B411-2F1B333CC310}.Release|x64.ActiveCfg = Release|x64
{632E78C0-0DAC-4B71-B411-2F1B333CC310}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
@@ -123,10 +124,10 @@ namespace Torch.Collections
private readonly Timer _flushEventQueue;
private const int _eventRaiseDelay = 50;
private readonly Queue<NotifyCollectionChangedEventArgs> _collectionEventQueue =
new Queue<NotifyCollectionChangedEventArgs>();
private readonly ConcurrentQueue<NotifyCollectionChangedEventArgs> _collectionEventQueue =
new ConcurrentQueue<NotifyCollectionChangedEventArgs>();
private readonly Queue<string> _propertyEventQueue = new Queue<string>();
private readonly ConcurrentQueue<string> _propertyEventQueue = new ConcurrentQueue<string>();
private void FlushEventQueue(object data)
{
@@ -137,7 +138,8 @@ namespace Torch.Collections
// :/, but works better
bool reset = _collectionEventQueue.Count > 0;
if (reset)
_collectionEventQueue.Clear();
while (_collectionEventQueue.Count > 0)
_collectionEventQueue.TryDequeue(out _);
else
while (_collectionEventQueue.TryDequeue(out NotifyCollectionChangedEventArgs e))
_collectionChangedEvent.Raise(this, e);

View File

@@ -47,6 +47,9 @@
<Reference Include="MahApps.Metro, Version=1.6.1.4, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\MahApps.Metro.1.6.1\lib\net45\MahApps.Metro.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Win32.Registry, Version=4.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.Win32.Registry.4.4.0\lib\net461\Microsoft.Win32.Registry.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json, Version=9.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\GameBinaries\Newtonsoft.Json.dll</HintPath>
@@ -84,6 +87,9 @@
<HintPath>..\GameBinaries\SpaceEngineers.ObjectBuilders.XmlSerializers.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="SteamKit2, Version=2.1.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\SteamKit2.2.1.0\lib\netstandard2.0\SteamKit2.dll</HintPath>
</Reference>
<Reference Include="Steamworks.NET">
<HintPath>..\GameBinaries\Steamworks.NET.dll</HintPath>
</Reference>
@@ -92,6 +98,12 @@
<Reference Include="System.Core" />
<Reference Include="System.IO.Compression" />
<Reference Include="System.IO.Compression.FileSystem" />
<Reference Include="System.Security.AccessControl, Version=4.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Security.AccessControl.4.4.0\lib\net461\System.Security.AccessControl.dll</HintPath>
</Reference>
<Reference Include="System.Security.Principal.Windows, Version=4.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Security.Principal.Windows.4.4.0\lib\net461\System.Security.Principal.Windows.dll</HintPath>
</Reference>
<Reference Include="System.Windows.Interactivity, Version=4.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\packages\ControlzEx.3.0.2.4\lib\net45\System.Windows.Interactivity.dll</HintPath>
<Private>True</Private>
@@ -233,6 +245,7 @@
<Compile Include="Managers\UpdateManager.cs" />
<Compile Include="Persistent.cs" />
<Compile Include="Plugins\PluginManifest.cs" />
<Compile Include="Utils\SteamWorkshopTools\KeyValueExtensions.cs" />
<Compile Include="Utils\MiscExtensions.cs" />
<Compile Include="Utils\Reflected\ReflectedEventReplaceAttribute.cs" />
<Compile Include="Utils\Reflected\ReflectedEventReplacer.cs" />
@@ -247,6 +260,7 @@
<Compile Include="Utils\Reflected\ReflectedStaticMethodAttribute.cs" />
<Compile Include="Utils\Reflection.cs" />
<Compile Include="Managers\ScriptingManager.cs" />
<Compile Include="Utils\SteamWorkshopTools\WebAPI.cs" />
<Compile Include="Utils\StringUtils.cs" />
<Compile Include="Utils\SynchronizationExtensions.cs" />
<Compile Include="Utils\TorchAssemblyResolver.cs" />
@@ -256,6 +270,7 @@
<Compile Include="TorchPluginBase.cs" />
<Compile Include="Session\TorchSession.cs" />
<Compile Include="Utils\TorchLauncher.cs" />
<Compile Include="Utils\SteamWorkshopTools\PublishedItemDetails.cs" />
<Compile Include="ViewModels\ModViewModel.cs" />
<Compile Include="Extensions\MyPlayerCollectionExtensions.cs" />
<Compile Include="Extensions\StringExtensions.cs" />

View File

@@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Reflection;
using System.Linq;
using NLog;
using SteamKit2;
namespace Torch.Utils.SteamWorkshopTools
{
public static class KeyValueExtensions
{
private static Logger Log = LogManager.GetLogger("SteamWorkshopService");
public static T GetValueOrDefault<T>(this KeyValue kv, string key)
{
kv.TryGetValueOrDefault<T>(key, out T result);
return result;
}
public static bool TryGetValueOrDefault<T>(this KeyValue kv, string key, out T typedValue)
{
var match = kv.Children?.Find((KeyValue item) => item.Name == key);
object result = default(T);
if (match == null)
{
typedValue = (T) result;
return false;
}
var value = match.Value ?? "";
try
{
var converter = TypeDescriptor.GetConverter(typeof(T));
result = converter.ConvertFromString(value);
typedValue = (T)result;
return true;
}
catch (NotSupportedException)
{
throw new Exception($"Unexpected Type '{typeof(T)}'!");
}
}
}
}

View File

@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Torch.Utils.SteamWorkshopTools
{
public class PublishedItemDetails
{
public ulong PublishedFileId;
public uint Views;
public uint Subscriptions;
public DateTime TimeUpdated;
public DateTime TimeCreated;
public string Description;
public string Title;
public string FileUrl;
public long FileSize;
public string FileName;
public ulong ConsumerAppId;
public ulong CreatorAppId;
public ulong Creator;
public string[] Tags;
}
}

View File

@@ -0,0 +1,291 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Serialization;
using System.IO;
using System.Net;
using NLog;
using SteamKit2;
using System.Net.Http;
namespace Torch.Utils.SteamWorkshopTools
{
public class WebAPI
{
private static Logger Log = LogManager.GetLogger("SteamWorkshopService");
public const uint AppID = 244850U;
public string Username { get; private set; }
private string password;
public bool IsReady { get; private set; }
public bool IsRunning { get; private set; }
private TaskCompletionSource<bool> logonTaskCompletionSource;
private SteamClient steamClient;
private CallbackManager cbManager;
private SteamUser steamUser;
private static WebAPI _instance;
public static WebAPI Instance
{
get
{
return _instance ?? (_instance = new WebAPI());
}
}
private WebAPI()
{
steamClient = new SteamClient();
cbManager = new CallbackManager(steamClient);
IsRunning = true;
}
public async Task<bool> Logon(string user = "anonymous", string pw = "")
{
if (string.IsNullOrEmpty(user))
throw new ArgumentNullException("User can't be null!");
if (!user.Equals("anonymous") && !pw.Equals(""))
throw new ArgumentNullException("Password can't be null if user is not anonymous!");
Username = user;
password = pw;
logonTaskCompletionSource = new TaskCompletionSource<bool>();
steamUser = steamClient.GetHandler<SteamUser>();
cbManager.Subscribe<SteamClient.ConnectedCallback>(OnConnected);
cbManager.Subscribe<SteamClient.DisconnectedCallback>(OnDisconnected);
cbManager.Subscribe<SteamUser.LoggedOnCallback>(OnLoggedOn);
cbManager.Subscribe<SteamUser.LoggedOffCallback>(OnLoggedOff);
Log.Info("Connecting to Steam...");
steamClient.Connect();
await logonTaskCompletionSource.Task;
return logonTaskCompletionSource.Task.Result;
}
public void CancelLogon()
{
logonTaskCompletionSource?.SetCanceled();
}
public async Task<Dictionary<ulong, PublishedItemDetails>> GetPublishedFileDetails(IEnumerable<ulong> workshopIds)
{
//if (!IsReady)
// throw new Exception("SteamWorkshopService not initialized!");
using (dynamic remoteStorage = SteamKit2.WebAPI.GetInterface("ISteamRemoteStorage"))
{
KeyValue allFilesDetails = null ;
remoteStorage.Timeout = TimeSpan.FromSeconds(30);
allFilesDetails = await Task.Run(delegate {
try
{
return remoteStorage.GetPublishedFileDetails1(
itemcount: workshopIds.Count(),
publishedfileids: workshopIds,
method: HttpMethod.Post);
// var ifaceArgs = new Dictionary<string, string>();
// ifaceArgs["itemcount"] = workshopIds.Count().ToString();
// no idea if that formatting is correct - in fact I get a 404 response
// ifaceArgs["publishedfileids"] = string.Join(",", workshopIds);
// return remoteStorage.Call(HttpMethod.Post, "GetPublishedFileDetails", args: ifaceArgs);
}
catch (HttpRequestException e)
{
Log.Error($"Fetching File Details failed: {e.Message}");
return null;
}
});
if (allFilesDetails == null)
return null;
//fileDetails = remoteStorage.Call(HttpMethod.Post, "GetPublishedFileDetails", 1, new Dictionary<string, string>() { { "itemcount", workshopIds.Count().ToString() }, { "publishedfileids", workshopIds.ToString() } });
var detailsList = allFilesDetails?.Children.Find((KeyValue kv) => kv.Name == "publishedfiledetails")?.Children;
var resultCount = allFilesDetails?.GetValueOrDefault<int>("resultcount");
if( detailsList == null || resultCount == null)
{
Log.Error("Received invalid data: ");
#if DEBUG
if(allFilesDetails != null)
PrintKeyValue(allFilesDetails);
return null;
#endif
}
if ( detailsList.Count != workshopIds.Count() || resultCount != workshopIds.Count())
{
Log.Error($"Received unexpected number of fileDetails. Expected: {workshopIds.Count()}, Received: {resultCount}");
return null;
}
var result = new Dictionary<ulong, PublishedItemDetails>();
for( int i = 0; i < resultCount; i++ )
{
var fileDetails = detailsList[i];
var tagContainer = fileDetails.Children.Find(item => item.Name == "tags");
List<string> tags = new List<string>();
if (tagContainer != null)
foreach (var tagKv in tagContainer.Children)
{
var tag = tagKv.Children.Find(item => item.Name == "tag")?.Value;
if( tag != null)
tags.Add(tag);
}
var publishedFileId = fileDetails.GetValueOrDefault<ulong>("publishedfileid");
result[publishedFileId] = new PublishedItemDetails()
{
PublishedFileId = publishedFileId,
Views = fileDetails.GetValueOrDefault<uint>("views"),
Subscriptions = fileDetails.GetValueOrDefault<uint>("subscriptions"),
TimeUpdated = DateTimeOffset.FromUnixTimeSeconds(fileDetails.GetValueOrDefault<long>("time_updated")).DateTime,
TimeCreated = DateTimeOffset.FromUnixTimeSeconds(fileDetails.GetValueOrDefault<long>("time_created")).DateTime,
Description = fileDetails.GetValueOrDefault<string>("description"),
Title = fileDetails.GetValueOrDefault<string>("title"),
FileUrl = fileDetails.GetValueOrDefault<string>("file_url"),
FileSize = fileDetails.GetValueOrDefault<long>("file_size"),
FileName = fileDetails.GetValueOrDefault<string>("filename"),
ConsumerAppId = fileDetails.GetValueOrDefault<ulong>("consumer_app_id"),
CreatorAppId = fileDetails.GetValueOrDefault<ulong>("creator_app_id"),
Creator = fileDetails.GetValueOrDefault<ulong>("creator"),
Tags = tags.ToArray()
};
}
return result;
}
}
[Obsolete("Space Engineers has transitioned to Steam's UGC api, therefore this method might not always work!")]
public async Task DownloadPublishedFile(PublishedItemDetails fileDetails, string dir, string name = null)
{
var fullPath = Path.Combine(dir, name);
if (name == null)
name = fileDetails.FileName;
var expectedSize = (fileDetails.FileSize == 0) ? -1 : fileDetails.FileSize;
using (var client = new WebClient())
{
try
{
var downloadTask = client.DownloadFileTaskAsync(fileDetails.FileUrl, Path.Combine(dir, name));
DateTime start = DateTime.Now;
for (int i = 0; i < 30; i++)
{
await Task.Delay(1000);
if (downloadTask.IsCompleted)
break;
}
if ( !downloadTask.IsCompleted )
{
client.CancelAsync();
throw new Exception("Timeout while attempting to downloading published workshop item!");
}
//var text = await client.DownloadStringTaskAsync(url);
//File.WriteAllText(fullPath, text);
}
catch (Exception e)
{
Log.Error("Failed to download workshop item! /n" +
$"{e.Message} - url: {fileDetails.FileUrl}, path: {Path.Combine(dir, name)}");
throw e;
}
}
}
class Printable
{
public KeyValue Data;
public int Offset;
public void Print()
{
Log.Info($"{new string(' ', Offset)}{Data.Name}: {Data.Value}");
}
}
private static void PrintKeyValue(KeyValue data)
{
var dataSet = new Stack<Printable>();
dataSet.Push(new Printable()
{
Data = data,
Offset = 0
});
while (dataSet.Count != 0)
{
var printable = dataSet.Pop();
foreach (var child in printable.Data.Children)
dataSet.Push(new Printable()
{
Data = child,
Offset = printable.Offset + 2
});
printable.Print();
}
}
#region CALLBACKS
private void OnConnected( SteamClient.ConnectedCallback callback)
{
Log.Info("Connected to Steam! Logging in '{0}'...", Username);
if( Username == "anonymous" )
steamUser.LogOnAnonymous();
else
steamUser.LogOn(new SteamUser.LogOnDetails
{
Username = Username,
Password = password
});
}
private void OnDisconnected( SteamClient.DisconnectedCallback callback )
{
Log.Info("Disconnected from Steam");
IsReady = false;
IsRunning = false;
}
private void OnLoggedOn( SteamUser.LoggedOnCallback callback )
{
if( callback.Result != EResult.OK )
{
string msg;
if( callback.Result == EResult.AccountLogonDenied )
{
msg = "Unable to logon to Steam: This account is Steamguard protected.";
Log.Warn(msg);
logonTaskCompletionSource.SetException(new Exception(msg));
IsRunning = false;
return;
}
msg = $"Unable to logon to Steam: {callback.Result} / {callback.ExtendedResult}";
Log.Warn(msg);
logonTaskCompletionSource.SetException(new Exception(msg));
IsRunning = false;
return;
}
IsReady = true;
Log.Info("Successfully logged on!");
logonTaskCompletionSource.SetResult(true);
}
private void OnLoggedOff( SteamUser.LoggedOffCallback callback )
{
IsReady = false;
Log.Info($"Logged off of Steam: {callback.Result}");
}
#endregion
}
}

View File

@@ -2,7 +2,12 @@
<packages>
<package id="ControlzEx" version="3.0.2.4" targetFramework="net461" />
<package id="MahApps.Metro" version="1.6.1" targetFramework="net461" />
<package id="Microsoft.Win32.Registry" version="4.4.0" targetFramework="net461" />
<package id="Mono.TextTransform" version="1.0.0" targetFramework="net461" />
<package id="NLog" version="4.4.12" targetFramework="net461" />
<package id="Octokit" version="0.24.0" targetFramework="net461" />
<package id="protobuf-net" version="2.1.0" targetFramework="net461" />
<package id="SteamKit2" version="2.1.0" targetFramework="net461" />
<package id="System.Security.AccessControl" version="4.4.0" targetFramework="net461" />
<package id="System.Security.Principal.Windows" version="4.4.0" targetFramework="net461" />
</packages>