Merge branch 'Patron' of https://github.com/TorchAPI/Torch into Patron

This commit is contained in:
Brant Martin
2019-06-25 16:48:24 -04:00
6 changed files with 389 additions and 225 deletions

View File

@@ -10,6 +10,7 @@ using System.Windows.Controls;
using System.Windows.Data; using System.Windows.Data;
using System.Windows.Documents; using System.Windows.Documents;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using System.Windows.Navigation; using System.Windows.Navigation;
using System.Windows.Shapes; using System.Windows.Shapes;
@@ -31,6 +32,8 @@ namespace Torch.Server
private TorchServer _server; private TorchServer _server;
private TorchConfig _config; private TorchConfig _config;
private bool _autoscrollLog = true;
public TorchUI(TorchServer server) public TorchUI(TorchServer server)
{ {
WindowStartupLocation = WindowStartupLocation.CenterScreen; WindowStartupLocation = WindowStartupLocation.CenterScreen;
@@ -57,6 +60,14 @@ namespace Torch.Server
Themes.uiSource = this; Themes.uiSource = this;
Themes.SetConfig(_config); Themes.SetConfig(_config);
Title = $"{_config.InstanceName} - Torch {server.TorchVersion}, SE {server.GameVersion}"; Title = $"{_config.InstanceName} - Torch {server.TorchVersion}, SE {server.GameVersion}";
Loaded += TorchUI_Loaded;
}
private void TorchUI_Loaded(object sender, RoutedEventArgs e)
{
var scrollViewer = FindDescendant<ScrollViewer>(ConsoleText);
scrollViewer.ScrollChanged += ConsoleText_OnScrollChanged;
} }
private void AttachConsole() private void AttachConsole()
@@ -69,7 +80,52 @@ namespace Torch.Server
doc = (wrapped?.WrappedTarget as FlowDocumentTarget)?.Document; doc = (wrapped?.WrappedTarget as FlowDocumentTarget)?.Document;
} }
ConsoleText.Document = doc ?? new FlowDocument(new Paragraph(new Run("No target!"))); ConsoleText.Document = doc ?? new FlowDocument(new Paragraph(new Run("No target!")));
ConsoleText.TextChanged += (sender, args) => ConsoleText.ScrollToEnd(); ConsoleText.TextChanged += ConsoleText_OnTextChanged;
}
public static T FindDescendant<T>(DependencyObject obj) where T : DependencyObject
{
if (obj == null) return default(T);
int numberChildren = VisualTreeHelper.GetChildrenCount(obj);
if (numberChildren == 0) return default(T);
for (int i = 0; i < numberChildren; i++)
{
DependencyObject child = VisualTreeHelper.GetChild(obj, i);
if (child is T)
{
return (T)child;
}
}
for (int i = 0; i < numberChildren; i++)
{
DependencyObject child = VisualTreeHelper.GetChild(obj, i);
var potentialMatch = FindDescendant<T>(child);
if (potentialMatch != default(T))
{
return potentialMatch;
}
}
return default(T);
}
private void ConsoleText_OnTextChanged(object sender, TextChangedEventArgs args)
{
var textBox = (RichTextBox) sender;
if (_autoscrollLog)
ConsoleText.ScrollToEnd();
}
private void ConsoleText_OnScrollChanged(object sender, ScrollChangedEventArgs e)
{
var scrollViewer = (ScrollViewer) sender;
if (e.ExtentHeightChange == 0)
{
// User change.
_autoscrollLog = scrollViewer.VerticalOffset == scrollViewer.ScrollableHeight;
}
} }
public void LoadConfig(TorchConfig config) public void LoadConfig(TorchConfig config)

View File

@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
namespace Torch
{
public static class LinqExtensions
{
public static IEnumerable<T> TSort<T>( this IEnumerable<T> source, Func<T, IEnumerable<T>> dependencies, bool throwOnCycle = false )
{
var sorted = new List<T>();
var visited = new HashSet<T>();
foreach( var item in source )
Visit( item, visited, sorted, dependencies, throwOnCycle );
return sorted;
}
private static void Visit<T>( T item, HashSet<T> visited, List<T> sorted, Func<T, IEnumerable<T>> dependencies, bool throwOnCycle )
{
if( !visited.Contains( item ) )
{
visited.Add( item );
var resolvedDependencies = dependencies(item);
if (resolvedDependencies != null)
{
foreach (var dep in resolvedDependencies)
Visit(dep, visited, sorted, dependencies, throwOnCycle);
}
sorted.Add( item );
}
else
{
if( throwOnCycle && !sorted.Contains( item ) )
throw new Exception( "Cyclic dependency found" );
}
}
}
}

View File

@@ -0,0 +1,17 @@
using System;
namespace Torch
{
public class PluginDependency
{
/// <summary>
/// A unique identifier for the plugin that identifies the dependency.
/// </summary>
public Guid Plugin { get; set; }
/// <summary>
/// The plugin minimum version. This must include a string in the format of #[.#[.#]] for update checking purposes.
/// </summary>
public string MinVersion { get; set; }
}
}

View File

@@ -25,10 +25,23 @@ namespace Torch.Managers
/// <inheritdoc /> /// <inheritdoc />
public class PluginManager : Manager, IPluginManager public class PluginManager : Manager, IPluginManager
{ {
private class PluginItem
{
public string Filename { get; set; }
public string Path { get; set; }
public PluginManifest Manifest { get; set; }
public bool IsZip { get; set; }
public List<PluginItem> ResolvedDependencies { get; set; }
}
private static Logger _log = LogManager.GetCurrentClassLogger(); private static Logger _log = LogManager.GetCurrentClassLogger();
private const string MANIFEST_NAME = "manifest.xml"; private const string MANIFEST_NAME = "manifest.xml";
public readonly string PluginDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins"); public readonly string PluginDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins");
private readonly MtObservableSortedDictionary<Guid, ITorchPlugin> _plugins = new MtObservableSortedDictionary<Guid, ITorchPlugin>(); private readonly MtObservableSortedDictionary<Guid, ITorchPlugin> _plugins = new MtObservableSortedDictionary<Guid, ITorchPlugin>();
private CommandManager _mgr;
#pragma warning disable 649 #pragma warning disable 649
[Dependency] [Dependency]
private ITorchSessionManager _sessionManager; private ITorchSessionManager _sessionManager;
@@ -70,8 +83,6 @@ namespace Torch.Managers
_sessionManager.SessionStateChanged += SessionManagerOnSessionStateChanged; _sessionManager.SessionStateChanged += SessionManagerOnSessionStateChanged;
} }
private CommandManager _mgr;
private void SessionManagerOnSessionStateChanged(ITorchSession session, TorchSessionState newState) private void SessionManagerOnSessionStateChanged(ITorchSession session, TorchSessionState newState)
{ {
_mgr = session.Managers.GetManager<CommandManager>(); _mgr = session.Managers.GetManager<CommandManager>();
@@ -108,18 +119,102 @@ namespace Torch.Managers
public void LoadPlugins() public void LoadPlugins()
{ {
bool firstLoad = Torch.Config.Plugins.Count == 0;
List<Guid> foundPlugins = new List<Guid>();
if (Torch.Config.ShouldUpdatePlugins)
DownloadPluginUpdates();
_log.Info("Loading plugins..."); _log.Info("Loading plugins...");
var pluginItems = Directory.EnumerateFiles(PluginDir, "*.zip").Union(Directory.EnumerateDirectories(PluginDir));
var pluginItems = GetLocalPlugins(PluginDir);
var pluginsToLoad = new List<PluginItem>();
foreach (var item in pluginItems)
{
var pluginItem = item;
if (!TryValidatePluginDependencies(pluginItems, ref pluginItem, out var missingPlugins))
{
// We have some missing dependencies.
// Future fix would be to download them, but instead for now let's
// just warn the user it's missing
foreach(var missingPlugin in missingPlugins)
_log.Warn($"{item.Manifest.Name} is missing dependency {missingPlugin}. Skipping plugin.");
continue;
}
pluginsToLoad.Add(pluginItem);
}
// Download any plugin updates.
bool updatesGotten = DownloadPluginUpdates(pluginsToLoad);
if (updatesGotten)
{
// Resort the plugins just in case updates changed load hints.
pluginItems = GetLocalPlugins(PluginDir);
pluginsToLoad.Clear();
foreach (var item in pluginItems)
{
var pluginItem = item;
if (!TryValidatePluginDependencies(pluginItems, ref pluginItem, out var missingPlugins))
{
foreach (var missingPlugin in missingPlugins)
_log.Warn($"{item.Manifest.Name} is missing dependency {missingPlugin}. Skipping plugin.");
continue;
}
pluginsToLoad.Add(pluginItem);
}
}
// Sort based on dependencies.
try
{
pluginsToLoad = pluginsToLoad.TSort(item => item.ResolvedDependencies)
.ToList();
}
catch (Exception e)
{
// This will happen on cylic dependencies.
_log.Error(e);
}
// Actually load the plugins now.
foreach (var item in pluginsToLoad)
{
LoadPlugin(item);
}
foreach (var plugin in _plugins.Values)
{
plugin.Init(Torch);
}
_log.Info($"Loaded {_plugins.Count} plugins.");
PluginsLoaded?.Invoke(_plugins.Values.AsReadOnly());
}
private List<PluginItem> GetLocalPlugins(string pluginDir)
{
var firstLoad = Torch.Config.Plugins.Count == 0;
var pluginItems = Directory.EnumerateFiles(pluginDir, "*.zip")
.Union(Directory.EnumerateDirectories(PluginDir));
var results = new List<PluginItem>();
foreach (var item in pluginItems) foreach (var item in pluginItems)
{ {
var path = Path.Combine(PluginDir, item); var path = Path.Combine(PluginDir, item);
var isZip = item.EndsWith(".zip", StringComparison.CurrentCultureIgnoreCase); var isZip = item.EndsWith(".zip", StringComparison.CurrentCultureIgnoreCase);
var manifest = isZip ? GetManifestFromZip(path) : GetManifestFromDirectory(path); var manifest = isZip ? GetManifestFromZip(path) : GetManifestFromDirectory(path);
if (manifest == null)
{
_log.Warn($"Item '{item}' is missing a manifest, skipping.");
continue;
}
var duplicatePlugin = results.FirstOrDefault(r => r.Manifest.Guid == manifest.Guid);
if (duplicatePlugin != null)
{
_log.Warn(
$"The GUID provided by {manifest.Name} ({item}) is already in use by {duplicatePlugin.Manifest.Name}.");
continue;
}
if (!Torch.Config.LocalPlugins) if (!Torch.Config.LocalPlugins)
{ {
if (isZip && !Torch.Config.Plugins.Contains(manifest.Guid)) if (isZip && !Torch.Config.Plugins.Contains(manifest.Guid))
@@ -132,126 +227,42 @@ namespace Torch.Managers
_log.Info($"First-time load: Plugin {manifest.Name} added to torch.cfg."); _log.Info($"First-time load: Plugin {manifest.Name} added to torch.cfg.");
Torch.Config.Plugins.Add(manifest.Guid); Torch.Config.Plugins.Add(manifest.Guid);
} }
if(isZip)
foundPlugins.Add(manifest.Guid);
} }
LoadPlugin(item); results.Add(new PluginItem
{
Filename = item,
IsZip = isZip,
Manifest = manifest,
Path = path
});
} }
if (!Torch.Config.LocalPlugins && firstLoad) if (!Torch.Config.LocalPlugins && firstLoad)
Torch.Config.Save(); Torch.Config.Save();
if (!Torch.Config.LocalPlugins) return results;
{
List<string> toLoad = new List<string>();
//This is actually the easiest way to batch process async tasks and block until completion (????)
Task.WhenAll(Torch.Config.Plugins.Select(async g =>
{
try
{
if (foundPlugins.Contains(g))
{
return;
}
var item = await PluginQuery.Instance.QueryOne(g);
string s = Path.Combine(PluginDir, item.Name + ".zip");
await PluginQuery.Instance.DownloadPlugin(item, s);
lock (toLoad)
toLoad.Add(s);
}
catch (Exception ex)
{
_log.Error(ex);
}
}));
foreach (var l in toLoad)
{
LoadPlugin(l);
}
} }
//just reuse the list from earlier private bool DownloadPluginUpdates(List<PluginItem> plugins)
foundPlugins.Clear();
foreach (var plugin in _plugins.Values)
{
try
{
plugin.Init(Torch);
}
catch (Exception e)
{
_log.Error(e, $"Plugin {plugin.Name} threw an exception during init! Unloading plugin!");
foundPlugins.Add(plugin.Id);
}
}
foreach (var id in foundPlugins)
{
var p = _plugins[id];
_plugins.Remove(id);
_mgr.UnregisterPluginCommands(p);
p.Dispose();
}
_log.Info($"Loaded {_plugins.Count} plugins.");
PluginsLoaded?.Invoke(_plugins.Values.AsReadOnly());
}
private void LoadPlugin(string item)
{
var path = Path.Combine(PluginDir, item);
var isZip = item.EndsWith(".zip", StringComparison.CurrentCultureIgnoreCase);
var manifest = isZip ? GetManifestFromZip(path) : GetManifestFromDirectory(path);
if (manifest == null)
{
_log.Warn($"Item '{item}' is missing a manifest, skipping.");
return;
}
if (_plugins.ContainsKey(manifest.Guid))
{
_log.Error($"The GUID provided by {manifest.Name} ({item}) is already in use by {_plugins[manifest.Guid].Name}");
return;
}
if (isZip)
LoadPluginFromZip(path);
else
LoadPluginFromFolder(path);
}
private void DownloadPluginUpdates()
{ {
_log.Info("Checking for plugin updates..."); _log.Info("Checking for plugin updates...");
var count = 0; var count = 0;
var pluginItems = Directory.EnumerateFiles(PluginDir, "*.zip"); Task.WhenAll(plugins.Select(async item =>
Task.WhenAll(pluginItems.Select(async item =>
{ {
PluginManifest manifest = null;
try try
{ {
var path = Path.Combine(PluginDir, item); if (!item.IsZip)
var isZip = item.EndsWith(".zip", StringComparison.CurrentCultureIgnoreCase);
if (!isZip)
{ {
_log.Warn($"Unzipped plugins cannot be auto-updated. Skipping plugin {item}"); _log.Warn($"Unzipped plugins cannot be auto-updated. Skipping plugin {item}");
return; return;
} }
manifest = GetManifestFromZip(path); item.Manifest.Version.TryExtractVersion(out Version currentVersion);
if (manifest == null) var latest = await PluginQuery.Instance.QueryOne(item.Manifest.Guid);
{
_log.Warn($"Item '{item}' is missing a manifest, skipping update check.");
return;
}
manifest.Version.TryExtractVersion(out Version currentVersion);
var latest = await PluginQuery.Instance.QueryOne(manifest.Guid);
if (latest?.LatestVersion == null) if (latest?.LatestVersion == null)
{ {
_log.Warn($"Plugin {manifest.Name} does not have any releases on torchapi.net. Cannot update."); _log.Warn($"Plugin {item.Manifest.Name} does not have any releases on torchapi.net. Cannot update.");
return; return;
} }
@@ -259,41 +270,73 @@ namespace Torch.Managers
if (currentVersion == null || newVersion == null) if (currentVersion == null || newVersion == null)
{ {
_log.Error($"Error parsing version from manifest or website for plugin '{manifest.Name}.'"); _log.Error($"Error parsing version from manifest or website for plugin '{item.Manifest.Name}.'");
return; return;
} }
if (newVersion <= currentVersion) if (newVersion <= currentVersion)
{ {
_log.Debug($"{manifest.Name} {manifest.Version} is up to date."); _log.Debug($"{item.Manifest.Name} {item.Manifest.Version} is up to date.");
return; return;
} }
_log.Info($"Updating plugin '{manifest.Name}' from {currentVersion} to {newVersion}."); _log.Info($"Updating plugin '{item.Manifest.Name}' from {currentVersion} to {newVersion}.");
await PluginQuery.Instance.DownloadPlugin(latest, path); await PluginQuery.Instance.DownloadPlugin(latest, item.Path);
Interlocked.Increment(ref count); Interlocked.Increment(ref count);
} }
catch (Exception e) catch (Exception e)
{ {
_log.Warn($"An error occurred updating the plugin {manifest?.Name ?? item}."); _log.Warn($"An error occurred updating the plugin {item.Manifest.Name}.");
_log.Warn(e); _log.Warn(e);
} }
})); }));
_log.Info($"Updated {count} plugins."); _log.Info($"Updated {count} plugins.");
return count > 0;
} }
private void LoadPluginFromFolder(string directory) private void LoadPlugin(PluginItem item)
{ {
var assemblies = new List<Assembly>(); var assemblies = new List<Assembly>();
var files = Directory.EnumerateFiles(directory, "*.*", SearchOption.AllDirectories).ToList();
var manifest = GetManifestFromDirectory(directory); if (item.IsZip)
if (manifest == null)
{ {
_log.Warn($"Directory {directory} is missing a manifest, skipping load."); using (var zipFile = ZipFile.OpenRead(item.Path))
return; {
foreach (var entry in zipFile.Entries)
{
if (!entry.Name.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase))
continue;
using (var stream = entry.Open())
{
var data = stream.ReadToEnd((int) entry.Length);
byte[] symbol = null;
var symbolEntryName =
entry.FullName.Substring(0, entry.FullName.Length - "dll".Length) + "pdb";
var symbolEntry = zipFile.GetEntry(symbolEntryName);
if (symbolEntry != null)
try
{
using (var symbolStream = symbolEntry.Open())
symbol = symbolStream.ReadToEnd((int) symbolEntry.Length);
} }
catch (Exception e)
{
_log.Warn(e, $"Failed to read debugging symbols from {item.Filename}:{symbolEntryName}");
}
assemblies.Add(symbol != null ? Assembly.Load(data, symbol) : Assembly.Load(data));
}
}
}
}
else
{
var files = Directory
.EnumerateFiles(item.Path, "*.*", SearchOption.AllDirectories)
.ToList();
foreach (var file in files) foreach (var file in files)
{ {
@@ -316,56 +359,16 @@ namespace Torch.Managers
{ {
_log.Warn(e, $"Failed to read debugging symbols from {symbolPath}"); _log.Warn(e, $"Failed to read debugging symbols from {symbolPath}");
} }
assemblies.Add(symbol != null ? Assembly.Load(data, symbol) : Assembly.Load(data)); assemblies.Add(symbol != null ? Assembly.Load(data, symbol) : Assembly.Load(data));
} }
} }
RegisterAllAssemblies(assemblies);
InstantiatePlugin(manifest, assemblies);
}
private void LoadPluginFromZip(string path)
{
PluginManifest manifest;
var assemblies = new List<Assembly>();
using (var zipFile = ZipFile.OpenRead(path))
{
manifest = GetManifestFromZip(path);
if (manifest == null)
{
_log.Warn($"Zip file {path} is missing a manifest, skipping.");
return;
}
foreach (var entry in zipFile.Entries)
{
if (!entry.Name.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase))
continue;
using (var stream = entry.Open())
{
var data = stream.ReadToEnd((int)entry.Length);
byte[] symbol = null;
var symbolEntryName = entry.FullName.Substring(0, entry.FullName.Length - "dll".Length) + "pdb";
var symbolEntry = zipFile.GetEntry(symbolEntryName);
if (symbolEntry != null)
try
{
using (var symbolStream = symbolEntry.Open())
symbol = symbolStream.ReadToEnd((int)symbolEntry.Length);
}
catch (Exception e)
{
_log.Warn(e, $"Failed to read debugging symbols from {path}:{symbolEntryName}");
}
assemblies.Add(symbol != null ? Assembly.Load(data, symbol) : Assembly.Load(data));
}
}
} }
RegisterAllAssemblies(assemblies); RegisterAllAssemblies(assemblies);
InstantiatePlugin(manifest, assemblies); InstantiatePlugin(item.Manifest, assemblies);
} }
private void RegisterAllAssemblies(IReadOnlyCollection<Assembly> assemblies) private void RegisterAllAssemblies(IReadOnlyCollection<Assembly> assemblies)
@@ -401,32 +404,6 @@ namespace Torch.Managers
return a.Name == b.Name && a.Version.Major == b.Version.Major && a.Version.Minor == b.Version.Minor; return a.Name == b.Name && a.Version.Major == b.Version.Major && a.Version.Minor == b.Version.Minor;
} }
private PluginManifest GetManifestFromZip(string path)
{
using (var zipFile = ZipFile.OpenRead(path))
{
foreach (var entry in zipFile.Entries)
{
if (!entry.Name.Equals(MANIFEST_NAME, StringComparison.CurrentCultureIgnoreCase))
continue;
using (var stream = new StreamReader(entry.Open()))
{
return PluginManifest.Load(stream);
}
}
}
return null;
}
private PluginManifest GetManifestFromDirectory(string directory)
{
var path = Path.Combine(directory, MANIFEST_NAME);
return !File.Exists(path) ? null : PluginManifest.Load(path);
}
private void InstantiatePlugin(PluginManifest manifest, IEnumerable<Assembly> assemblies) private void InstantiatePlugin(PluginManifest manifest, IEnumerable<Assembly> assemblies)
{ {
Type pluginType = null; Type pluginType = null;
@@ -496,6 +473,74 @@ namespace Torch.Managers
_plugins.Add(manifest.Guid, plugin); _plugins.Add(manifest.Guid, plugin);
} }
private PluginManifest GetManifestFromZip(string path)
{
using (var zipFile = ZipFile.OpenRead(path))
{
foreach (var entry in zipFile.Entries)
{
if (!entry.Name.Equals(MANIFEST_NAME, StringComparison.CurrentCultureIgnoreCase))
continue;
using (var stream = new StreamReader(entry.Open()))
{
return PluginManifest.Load(stream);
}
}
}
return null;
}
private bool TryValidatePluginDependencies(List<PluginItem> items, ref PluginItem item, out List<Guid> missingDependencies)
{
var dependencies = new List<PluginItem>();
missingDependencies = new List<Guid>();
foreach (var pluginDependency in item.Manifest.Dependencies)
{
var dependency = items
.FirstOrDefault(pi => pi?.Manifest.Guid == pluginDependency.Plugin);
if (dependency == null)
{
missingDependencies.Add(pluginDependency.Plugin);
continue;
}
if (!string.IsNullOrEmpty(pluginDependency.MinVersion)
&& dependency.Manifest.Version.TryExtractVersion(out var dependencyVersion)
&& pluginDependency.MinVersion.TryExtractVersion(out var minVersion))
{
// really only care about version if it is defined.
if (dependencyVersion < minVersion)
{
// If dependency version is too low, we can try to update. Otherwise
// it's a missing dependency.
// For now let's just warn the user. bitMuse is lazy.
_log.Warn($"{dependency.Manifest.Name} is below the requested version for {item.Manifest.Name}."
+ Environment.NewLine
+ $" Desired version: {pluginDependency.MinVersion}, Available version: {dependency.Manifest.Version}");
missingDependencies.Add(pluginDependency.Plugin);
continue;
}
}
dependencies.Add(dependency);
}
item.ResolvedDependencies = dependencies;
if (missingDependencies.Count > 0)
return false;
return true;
}
private PluginManifest GetManifestFromDirectory(string directory)
{
var path = Path.Combine(directory, MANIFEST_NAME);
return !File.Exists(path) ? null : PluginManifest.Load(path);
}
/// <inheritdoc cref="IEnumerable.GetEnumerator"/> /// <inheritdoc cref="IEnumerable.GetEnumerator"/>
public IEnumerator<ITorchPlugin> GetEnumerator() public IEnumerator<ITorchPlugin> GetEnumerator()
{ {

View File

@@ -37,7 +37,7 @@ namespace Torch
/// <summary> /// <summary>
/// A list of dependent plugin repositories. This may be updated to include GUIDs in the future. /// A list of dependent plugin repositories. This may be updated to include GUIDs in the future.
/// </summary> /// </summary>
public List<string> Dependencies { get; } = new List<string>(); public List<PluginDependency> Dependencies { get; } = new List<PluginDependency>();
public void Save(string path) public void Save(string path)
{ {

View File

@@ -168,9 +168,12 @@
<HintPath>..\GameBinaries\VRage.OpenVRWrapper.dll</HintPath> <HintPath>..\GameBinaries\VRage.OpenVRWrapper.dll</HintPath>
<Private>False</Private> <Private>False</Private>
</Reference> </Reference>
<Reference Include="VRage.Platform.Windows, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"> <Reference Include="VRage.Platform.Windows, Culture=neutral, PublicKeyToken=null">
<HintPath>..\bin\x64\Release\DedicatedServer64\VRage.Platform.Windows.dll</HintPath> <HintPath>..\bin\x64\Release\DedicatedServer64\VRage.Platform.Windows.dll</HintPath>
</Reference> </Reference>
<Reference Include="VRage.Platform.Windows, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>..\GameBinaries\VRage.Platform.Windows.dll</HintPath>
</Reference>
<Reference Include="VRage.Render"> <Reference Include="VRage.Render">
<HintPath>..\GameBinaries\VRage.Render.dll</HintPath> <HintPath>..\GameBinaries\VRage.Render.dll</HintPath>
<Private>False</Private> <Private>False</Private>
@@ -205,6 +208,7 @@
<Compile Include="Collections\TransformEnumerator.cs" /> <Compile Include="Collections\TransformEnumerator.cs" />
<Compile Include="Commands\ConsoleCommandContext.cs" /> <Compile Include="Commands\ConsoleCommandContext.cs" />
<Compile Include="Event\EventShimAttribute.cs" /> <Compile Include="Event\EventShimAttribute.cs" />
<Compile Include="Extensions\LinqExtensions.cs" />
<Compile Include="Managers\ChatManager\ChatManagerClient.cs" /> <Compile Include="Managers\ChatManager\ChatManagerClient.cs" />
<Compile Include="Managers\ChatManager\ChatManagerServer.cs" /> <Compile Include="Managers\ChatManager\ChatManagerServer.cs" />
<Compile Include="Extensions\DispatcherExtensions.cs" /> <Compile Include="Extensions\DispatcherExtensions.cs" />
@@ -244,6 +248,7 @@
<Compile Include="Patches\PhysicsMemoryPatch.cs" /> <Compile Include="Patches\PhysicsMemoryPatch.cs" />
<Compile Include="Patches\SessionDownloadPatch.cs" /> <Compile Include="Patches\SessionDownloadPatch.cs" />
<Compile Include="Patches\TorchAsyncSaving.cs" /> <Compile Include="Patches\TorchAsyncSaving.cs" />
<Compile Include="Plugins\PluginDependency.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Collections\KeyTree.cs" /> <Compile Include="Collections\KeyTree.cs" />
<Compile Include="Collections\RollingAverage.cs" /> <Compile Include="Collections\RollingAverage.cs" />