This commit is contained in:
@@ -2,8 +2,6 @@
|
|||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CringeBootstrap", "CringeBootstrap\CringeBootstrap.csproj", "{219C897E-452D-49B5-80C4-F3008718C16A}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CringeBootstrap", "CringeBootstrap\CringeBootstrap.csproj", "{219C897E-452D-49B5-80C4-F3008718C16A}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginLoader", "PluginLoader\PluginLoader.csproj", "{A7C22A74-56EA-4DC2-89AA-A1134BFB8497}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CringeLauncher", "CringeLauncher\CringeLauncher.csproj", "{2A1B48E9-ED82-4EEB-A18A-E4148DFE3A19}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CringeLauncher", "CringeLauncher\CringeLauncher.csproj", "{2A1B48E9-ED82-4EEB-A18A-E4148DFE3A19}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CringeBootstrap.Abstractions", "CringeBootstrap.Abstractions\CringeBootstrap.Abstractions.csproj", "{12AA2BBC-E795-4065-AF4A-9A44AFF69D92}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CringeBootstrap.Abstractions", "CringeBootstrap.Abstractions\CringeBootstrap.Abstractions.csproj", "{12AA2BBC-E795-4065-AF4A-9A44AFF69D92}"
|
||||||
@@ -26,10 +24,6 @@ Global
|
|||||||
{219C897E-452D-49B5-80C4-F3008718C16A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{219C897E-452D-49B5-80C4-F3008718C16A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{219C897E-452D-49B5-80C4-F3008718C16A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{219C897E-452D-49B5-80C4-F3008718C16A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{219C897E-452D-49B5-80C4-F3008718C16A}.Release|Any CPU.Build.0 = Release|Any CPU
|
{219C897E-452D-49B5-80C4-F3008718C16A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{A7C22A74-56EA-4DC2-89AA-A1134BFB8497}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{A7C22A74-56EA-4DC2-89AA-A1134BFB8497}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{A7C22A74-56EA-4DC2-89AA-A1134BFB8497}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{A7C22A74-56EA-4DC2-89AA-A1134BFB8497}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{2A1B48E9-ED82-4EEB-A18A-E4148DFE3A19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{2A1B48E9-ED82-4EEB-A18A-E4148DFE3A19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{2A1B48E9-ED82-4EEB-A18A-E4148DFE3A19}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{2A1B48E9-ED82-4EEB-A18A-E4148DFE3A19}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{2A1B48E9-ED82-4EEB-A18A-E4148DFE3A19}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{2A1B48E9-ED82-4EEB-A18A-E4148DFE3A19}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
@@ -1,103 +0,0 @@
|
|||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp;
|
|
||||||
using Microsoft.CodeAnalysis.Emit;
|
|
||||||
using Microsoft.CodeAnalysis.Text;
|
|
||||||
|
|
||||||
namespace PluginLoader.Compiler;
|
|
||||||
|
|
||||||
public class RoslynCompiler
|
|
||||||
{
|
|
||||||
private readonly List<Source> source = new();
|
|
||||||
private readonly bool debugBuild;
|
|
||||||
|
|
||||||
public RoslynCompiler(bool debugBuild = false)
|
|
||||||
{
|
|
||||||
this.debugBuild = debugBuild;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Load(Stream s, string name)
|
|
||||||
{
|
|
||||||
var mem = new MemoryStream();
|
|
||||||
using (mem)
|
|
||||||
{
|
|
||||||
s.CopyTo(mem);
|
|
||||||
source.Add(new(mem, name, debugBuild));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] Compile(string assemblyName, out byte[] symbols)
|
|
||||||
{
|
|
||||||
symbols = null;
|
|
||||||
|
|
||||||
var compilation = CSharpCompilation.Create(
|
|
||||||
assemblyName,
|
|
||||||
source.Select(x => x.Tree),
|
|
||||||
RoslynReferences.EnumerateAllReferences(),
|
|
||||||
new(
|
|
||||||
OutputKind.DynamicallyLinkedLibrary,
|
|
||||||
optimizationLevel: debugBuild ? OptimizationLevel.Debug : OptimizationLevel.Release, allowUnsafe: true));
|
|
||||||
|
|
||||||
using (var pdb = new MemoryStream())
|
|
||||||
using (var ms = new MemoryStream())
|
|
||||||
{
|
|
||||||
// write IL code into memory
|
|
||||||
EmitResult result;
|
|
||||||
if (debugBuild)
|
|
||||||
result = compilation.Emit(ms, pdb,
|
|
||||||
embeddedTexts: source.Select(x => x.Text),
|
|
||||||
options: new(debugInformationFormat: DebugInformationFormat.PortablePdb,
|
|
||||||
pdbFilePath: Path.ChangeExtension(assemblyName, "pdb")));
|
|
||||||
else
|
|
||||||
result = compilation.Emit(ms);
|
|
||||||
|
|
||||||
if (!result.Success)
|
|
||||||
{
|
|
||||||
// handle exceptions
|
|
||||||
var failures = result.Diagnostics.Where(diagnostic =>
|
|
||||||
diagnostic.IsWarningAsError ||
|
|
||||||
diagnostic.Severity == DiagnosticSeverity.Error);
|
|
||||||
|
|
||||||
foreach (var diagnostic in failures)
|
|
||||||
{
|
|
||||||
var location = diagnostic.Location;
|
|
||||||
var source = this.source.FirstOrDefault(x => x.Tree == location.SourceTree);
|
|
||||||
LogFile.Log.Debug(
|
|
||||||
$"{diagnostic.Id}: {diagnostic.GetMessage()} in file:\n{source?.Name ?? "null"} ({location.GetLineSpan().StartLinePosition})");
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new("Compilation failed!");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (debugBuild)
|
|
||||||
{
|
|
||||||
pdb.Seek(0, SeekOrigin.Begin);
|
|
||||||
symbols = pdb.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
ms.Seek(0, SeekOrigin.Begin);
|
|
||||||
return ms.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class Source
|
|
||||||
{
|
|
||||||
public Source(Stream s, string name, bool includeText)
|
|
||||||
{
|
|
||||||
Name = name;
|
|
||||||
var source = SourceText.From(s, canBeEmbedded: includeText);
|
|
||||||
if (includeText)
|
|
||||||
{
|
|
||||||
Text = EmbeddedText.FromSource(name, source);
|
|
||||||
Tree = CSharpSyntaxTree.ParseText(source, new(LanguageVersion.Latest), name);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Tree = CSharpSyntaxTree.ParseText(source, new(LanguageVersion.Latest));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Name { get; }
|
|
||||||
public SyntaxTree Tree { get; }
|
|
||||||
public EmbeddedText Text { get; }
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,150 +0,0 @@
|
|||||||
using System.Reflection;
|
|
||||||
using System.Runtime.Loader;
|
|
||||||
using System.Text;
|
|
||||||
using HarmonyLib;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
|
|
||||||
namespace PluginLoader.Compiler;
|
|
||||||
|
|
||||||
public static class RoslynReferences
|
|
||||||
{
|
|
||||||
private static readonly Dictionary<string, MetadataReference> allReferences = new();
|
|
||||||
private static readonly HashSet<string> referenceBlacklist = new(new[] { "System.ValueTuple" });
|
|
||||||
|
|
||||||
public static void GenerateAssemblyList()
|
|
||||||
{
|
|
||||||
if (allReferences.Count > 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var harmonyInfo = typeof(Harmony).Assembly.GetName();
|
|
||||||
|
|
||||||
var loadedAssemblies = new Stack<Assembly>(AppDomain.CurrentDomain.GetAssemblies().Where(IsValidReference));
|
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
|
|
||||||
sb.AppendLine();
|
|
||||||
var line = "===================================";
|
|
||||||
sb.AppendLine(line);
|
|
||||||
sb.AppendLine("Assembly References");
|
|
||||||
sb.AppendLine(line);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
foreach (var a in loadedAssemblies)
|
|
||||||
{
|
|
||||||
// Prevent other Harmony versions from being loaded
|
|
||||||
var name = a.GetName();
|
|
||||||
if (name.Name == harmonyInfo.Name && name.Version != harmonyInfo.Version)
|
|
||||||
{
|
|
||||||
LogFile.Log.Debug(
|
|
||||||
$"WARNING: Multiple Harmony assemblies are loaded. Plugin Loader is using {harmonyInfo} but found {name}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
AddAssemblyReference(a);
|
|
||||||
sb.AppendLine(a.FullName);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach(var a in GetOtherReferences())
|
|
||||||
{
|
|
||||||
AddAssemblyReference(a);
|
|
||||||
sb.AppendLine(a.FullName);
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.AppendLine(line);
|
|
||||||
while (loadedAssemblies.Count > 0)
|
|
||||||
{
|
|
||||||
var a = loadedAssemblies.Pop();
|
|
||||||
|
|
||||||
foreach (var name in a.GetReferencedAssemblies())
|
|
||||||
{
|
|
||||||
// Prevent other Harmony versions from being loaded
|
|
||||||
if (name.Name == harmonyInfo.Name && name.Version != harmonyInfo.Version)
|
|
||||||
{
|
|
||||||
LogFile.Log.Debug(
|
|
||||||
$"WARNING: Multiple Harmony assemblies are loaded. Plugin Loader is using {harmonyInfo} but found {name}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ContainsReference(name) && TryLoadAssembly(name, out var aRef) && IsValidReference(aRef))
|
|
||||||
{
|
|
||||||
AddAssemblyReference(aRef);
|
|
||||||
sb.AppendLine(name.FullName);
|
|
||||||
loadedAssemblies.Push(aRef);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.AppendLine(line);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
sb.Append("Error: ").Append(e).AppendLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
LogFile.Log.Debug(sb.ToString(), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// This method is used to load references that otherwise would not exist or be optimized out
|
|
||||||
/// </summary>
|
|
||||||
private static IEnumerable<Assembly> GetOtherReferences()
|
|
||||||
{
|
|
||||||
yield return typeof(Microsoft.CSharp.RuntimeBinder.Binder).Assembly;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool ContainsReference(AssemblyName name)
|
|
||||||
{
|
|
||||||
return allReferences.ContainsKey(name.Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryLoadAssembly(AssemblyName name, out Assembly aRef)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
aRef = AssemblyLoadContext.GetLoadContext(typeof(RoslynReferences).Assembly)!.LoadFromAssemblyName(name);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (IOException)
|
|
||||||
{
|
|
||||||
aRef = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AddAssemblyReference(Assembly a)
|
|
||||||
{
|
|
||||||
var name = a.GetName().Name;
|
|
||||||
if (!allReferences.ContainsKey(name))
|
|
||||||
allReferences.Add(name, MetadataReference.CreateFromFile(a.Location));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IEnumerable<MetadataReference> EnumerateAllReferences()
|
|
||||||
{
|
|
||||||
return allReferences.Values;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsValidReference(Assembly a)
|
|
||||||
{
|
|
||||||
return !a.IsDynamic && !string.IsNullOrWhiteSpace(a.Location) && !referenceBlacklist.Contains(a.GetName().Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void LoadReference(string name)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var aName = new AssemblyName(name);
|
|
||||||
if (!allReferences.ContainsKey(aName.Name))
|
|
||||||
{
|
|
||||||
var a = Assembly.Load(aName);
|
|
||||||
LogFile.Log.Debug("Reference added at runtime: " + a.FullName);
|
|
||||||
MetadataReference aRef = MetadataReference.CreateFromFile(a.Location);
|
|
||||||
allReferences[a.GetName().Name] = aRef;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (IOException)
|
|
||||||
{
|
|
||||||
LogFile.Log.Debug("WARNING: Unable to find the assembly '" + name + "'!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,168 +0,0 @@
|
|||||||
using System.IO.Compression;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Text;
|
|
||||||
using System.Xml.Serialization;
|
|
||||||
using PluginLoader.Compiler;
|
|
||||||
using PluginLoader.Network;
|
|
||||||
using ProtoBuf;
|
|
||||||
using Sandbox.Graphics.GUI;
|
|
||||||
|
|
||||||
namespace PluginLoader.Data;
|
|
||||||
|
|
||||||
[ProtoContract]
|
|
||||||
public class GitHubPlugin : PluginData
|
|
||||||
{
|
|
||||||
private const string pluginFile = "plugin.dll";
|
|
||||||
private const string commitHashFile = "commit.sha1";
|
|
||||||
private string cacheDir, assemblyName;
|
|
||||||
|
|
||||||
public GitHubPlugin()
|
|
||||||
{
|
|
||||||
Status = PluginStatus.None;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string Source => "GitHub";
|
|
||||||
|
|
||||||
[ProtoMember(1)] public string Commit { get; set; }
|
|
||||||
|
|
||||||
[ProtoMember(2)]
|
|
||||||
[XmlArray]
|
|
||||||
[XmlArrayItem("Directory")]
|
|
||||||
public string[] SourceDirectories { get; set; }
|
|
||||||
|
|
||||||
public void Init(string mainDirectory)
|
|
||||||
{
|
|
||||||
var nameArgs = Id.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
if (nameArgs.Length != 2)
|
|
||||||
throw new("Invalid GitHub name: " + Id);
|
|
||||||
|
|
||||||
if (SourceDirectories != null)
|
|
||||||
for (var i = SourceDirectories.Length - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
var path = SourceDirectories[i].Replace('\\', '/').TrimStart('/');
|
|
||||||
|
|
||||||
if (path.Length == 0)
|
|
||||||
{
|
|
||||||
SourceDirectories.RemoveAtFast(i);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (path[path.Length - 1] != '/')
|
|
||||||
path += '/';
|
|
||||||
|
|
||||||
SourceDirectories[i] = path;
|
|
||||||
}
|
|
||||||
|
|
||||||
assemblyName = MakeSafeString(nameArgs[1]);
|
|
||||||
cacheDir = Path.Combine(mainDirectory, "GitHub", nameArgs[0], nameArgs[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string MakeSafeString(string s)
|
|
||||||
{
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
foreach (var ch in s)
|
|
||||||
if (char.IsLetterOrDigit(ch))
|
|
||||||
sb.Append(ch);
|
|
||||||
else
|
|
||||||
sb.Append('_');
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Assembly? GetAssembly()
|
|
||||||
{
|
|
||||||
if (!Directory.Exists(cacheDir))
|
|
||||||
Directory.CreateDirectory(cacheDir);
|
|
||||||
|
|
||||||
Assembly? a;
|
|
||||||
|
|
||||||
var dllFile = Path.Combine(cacheDir, pluginFile);
|
|
||||||
var commitFile = Path.Combine(cacheDir, commitHashFile);
|
|
||||||
if (!File.Exists(dllFile) || !File.Exists(commitFile) || File.ReadAllText(commitFile) != Commit)
|
|
||||||
{
|
|
||||||
var lbl = Main.Instance.Splash;
|
|
||||||
lbl.SetText($"Downloading '{FriendlyName}'");
|
|
||||||
var data = CompileFromSource(x => lbl.SetBarValue(x));
|
|
||||||
File.WriteAllBytes(dllFile, data);
|
|
||||||
File.WriteAllText(commitFile, Commit);
|
|
||||||
Status = PluginStatus.Updated;
|
|
||||||
lbl.SetText($"Compiled '{FriendlyName}'");
|
|
||||||
a = Assembly.Load(data);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
a = Assembly.LoadFile(dllFile);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
LogFile.Log.Debug($"Error loading {dllFile}, deleting file");
|
|
||||||
File.Delete(dllFile);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Version = a.GetName().Version;
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public byte[] CompileFromSource(Action<float> callback = null)
|
|
||||||
{
|
|
||||||
var compiler = new RoslynCompiler();
|
|
||||||
using (var s = GitHub.DownloadRepo(Id, Commit, out var fileName))
|
|
||||||
using (var zip = new ZipArchive(s))
|
|
||||||
{
|
|
||||||
callback?.Invoke(0);
|
|
||||||
for (var i = 0; i < zip.Entries.Count; i++)
|
|
||||||
{
|
|
||||||
var entry = zip.Entries[i];
|
|
||||||
CompileFromSource(compiler, entry);
|
|
||||||
callback?.Invoke(i / (float)zip.Entries.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
callback?.Invoke(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return compiler.Compile(assemblyName + '_' + Path.GetRandomFileName(), out _);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CompileFromSource(RoslynCompiler compiler, ZipArchiveEntry entry)
|
|
||||||
{
|
|
||||||
if (AllowedZipPath(entry.FullName))
|
|
||||||
using (var entryStream = entry.Open())
|
|
||||||
{
|
|
||||||
compiler.Load(entryStream, entry.FullName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool AllowedZipPath(string path)
|
|
||||||
{
|
|
||||||
if (!path.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (SourceDirectories == null || SourceDirectories.Length == 0)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
path = RemoveRoot(path); // Make the base of the path the root of the repository
|
|
||||||
|
|
||||||
foreach (var dir in SourceDirectories)
|
|
||||||
if (path.StartsWith(dir, StringComparison.Ordinal))
|
|
||||||
return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string RemoveRoot(string path)
|
|
||||||
{
|
|
||||||
path = path.Replace('\\', '/').TrimStart('/');
|
|
||||||
var index = path.IndexOf('/');
|
|
||||||
if (index >= 0 && index + 1 < path.Length)
|
|
||||||
return path.Substring(index + 1);
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Show()
|
|
||||||
{
|
|
||||||
MyGuiSandbox.OpenUrl("https://github.com/" + Id, UrlOpenMode.SteamOrExternalWithConfirm);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,7 +0,0 @@
|
|||||||
namespace PluginLoader.Data;
|
|
||||||
|
|
||||||
public interface ISteamItem
|
|
||||||
{
|
|
||||||
string Id { get; }
|
|
||||||
ulong WorkshopId { get; }
|
|
||||||
}
|
|
@@ -1,252 +0,0 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Text;
|
|
||||||
using System.Xml.Serialization;
|
|
||||||
using PluginLoader.Compiler;
|
|
||||||
using PluginLoader.GUI;
|
|
||||||
using Sandbox.Graphics.GUI;
|
|
||||||
using VRage;
|
|
||||||
|
|
||||||
namespace PluginLoader.Data;
|
|
||||||
|
|
||||||
public class LocalFolderPlugin : PluginData
|
|
||||||
{
|
|
||||||
private const string XmlDataType = "Xml files (*.xml)|*.xml|All files (*.*)|*.*";
|
|
||||||
private const int GitTimeout = 10000;
|
|
||||||
private string[] sourceDirectories;
|
|
||||||
|
|
||||||
public LocalFolderPlugin(Config settings)
|
|
||||||
{
|
|
||||||
Id = settings.Folder;
|
|
||||||
FriendlyName = Path.GetFileName(Id);
|
|
||||||
Status = PluginStatus.None;
|
|
||||||
FolderSettings = settings;
|
|
||||||
DeserializeFile(settings.DataFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
private LocalFolderPlugin(string folder)
|
|
||||||
{
|
|
||||||
Id = folder;
|
|
||||||
Status = PluginStatus.None;
|
|
||||||
FolderSettings = new()
|
|
||||||
{
|
|
||||||
Folder = folder
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string Source => MyTexts.GetString(MyCommonTexts.Local);
|
|
||||||
|
|
||||||
public Config FolderSettings { get; }
|
|
||||||
|
|
||||||
public override Assembly? GetAssembly()
|
|
||||||
{
|
|
||||||
if (Directory.Exists(Id))
|
|
||||||
{
|
|
||||||
var compiler = new RoslynCompiler(FolderSettings.DebugBuild);
|
|
||||||
var hasFile = false;
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
sb.Append("Compiling files from ").Append(Id).Append(":").AppendLine();
|
|
||||||
foreach (var file in GetProjectFiles(Id))
|
|
||||||
using (var fileStream = File.OpenRead(file))
|
|
||||||
{
|
|
||||||
hasFile = true;
|
|
||||||
var name = file.Substring(Id.Length + 1, file.Length - (Id.Length + 1));
|
|
||||||
sb.Append(name).Append(", ");
|
|
||||||
compiler.Load(fileStream, file);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasFile)
|
|
||||||
{
|
|
||||||
sb.Length -= 2;
|
|
||||||
LogFile.Log.Debug(sb.ToString());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new IOException("No files were found in the directory specified.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var data = compiler.Compile(FriendlyName + '_' + Path.GetRandomFileName(), out var symbols);
|
|
||||||
var a = Assembly.Load(data, symbols);
|
|
||||||
Version = a.GetName().Version;
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new DirectoryNotFoundException("Unable to find directory '" + Id + "'");
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<string> GetProjectFiles(string folder)
|
|
||||||
{
|
|
||||||
string gitError = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var p = new Process();
|
|
||||||
|
|
||||||
// Redirect the output stream of the child process.
|
|
||||||
p.StartInfo.UseShellExecute = false;
|
|
||||||
p.StartInfo.RedirectStandardOutput = true;
|
|
||||||
p.StartInfo.RedirectStandardError = true;
|
|
||||||
p.StartInfo.FileName = "git";
|
|
||||||
p.StartInfo.Arguments = "ls-files --cached --others --exclude-standard";
|
|
||||||
p.StartInfo.WorkingDirectory = folder;
|
|
||||||
p.Start();
|
|
||||||
|
|
||||||
// Do not wait for the child process to exit before
|
|
||||||
// reading to the end of its redirected stream.
|
|
||||||
// Read the output stream first and then wait.
|
|
||||||
var gitOutput = p.StandardOutput.ReadToEnd();
|
|
||||||
gitError = p.StandardError.ReadToEnd();
|
|
||||||
if (!p.WaitForExit(GitTimeout))
|
|
||||||
{
|
|
||||||
p.Kill();
|
|
||||||
throw new TimeoutException("Git operation timed out.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (p.ExitCode == 0)
|
|
||||||
{
|
|
||||||
var files = gitOutput.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
return files.Where(x => x.EndsWith(".cs") && IsValidProjectFile(x))
|
|
||||||
.Select(x => Path.Combine(folder, x.Trim().Replace('/', Path.DirectorySeparatorChar)))
|
|
||||||
.Where(x => File.Exists(x));
|
|
||||||
}
|
|
||||||
|
|
||||||
LogFile.Log.Error("An error occurred while checking git for project files. Git output: {GitOutput}", gitError);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
LogFile.Log.Error(e, "An error occurred while checking git for project files. Git output: {GitOutput}", gitError);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var sep = Path.DirectorySeparatorChar;
|
|
||||||
return Directory.EnumerateFiles(folder, "*.cs", SearchOption.AllDirectories)
|
|
||||||
.Where(x => !x.Contains(sep + "bin" + sep) && !x.Contains(sep + "obj" + sep) &&
|
|
||||||
IsValidProjectFile(x));
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsValidProjectFile(string file)
|
|
||||||
{
|
|
||||||
if (sourceDirectories == null || sourceDirectories.Length == 0)
|
|
||||||
return true;
|
|
||||||
file = file.Replace('\\', '/');
|
|
||||||
foreach (var dir in sourceDirectories)
|
|
||||||
if (file.StartsWith(dir))
|
|
||||||
return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return Id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Show()
|
|
||||||
{
|
|
||||||
var folder = Path.GetFullPath(Id);
|
|
||||||
if (Directory.Exists(folder))
|
|
||||||
Process.Start("explorer.exe", $"\"{folder}\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool OpenContextMenu(MyGuiControlContextMenu menu)
|
|
||||||
{
|
|
||||||
menu.Clear();
|
|
||||||
menu.AddItem(new("Remove"));
|
|
||||||
menu.AddItem(new("Load data file"));
|
|
||||||
if (FolderSettings.DebugBuild)
|
|
||||||
menu.AddItem(new("Switch to release build"));
|
|
||||||
else
|
|
||||||
menu.AddItem(new("Switch to debug build"));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void ContextMenuClicked(MyGuiScreenPluginConfig screen, MyGuiControlContextMenu.EventArgs args)
|
|
||||||
{
|
|
||||||
switch (args.ItemIndex)
|
|
||||||
{
|
|
||||||
case 0:
|
|
||||||
Main.Instance.Config.PluginFolders.Remove(Id);
|
|
||||||
screen.RemovePlugin(this);
|
|
||||||
screen.RequireRestart();
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
LoaderTools.OpenFileDialog("Open an xml data file", Path.GetDirectoryName(FolderSettings.DataFile),
|
|
||||||
XmlDataType, file => DeserializeFile(file, screen));
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
FolderSettings.DebugBuild = !FolderSettings.DebugBuild;
|
|
||||||
screen.RequireRestart();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deserializes a file and refreshes the plugin screen
|
|
||||||
private void DeserializeFile(string file, MyGuiScreenPluginConfig screen = null)
|
|
||||||
{
|
|
||||||
if (!File.Exists(file))
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var xml = new XmlSerializer(typeof(PluginData));
|
|
||||||
|
|
||||||
using (var reader = File.OpenText(file))
|
|
||||||
{
|
|
||||||
var resultObj = xml.Deserialize(reader);
|
|
||||||
if (resultObj.GetType() != typeof(GitHubPlugin)) throw new("Xml file is not of type GitHubPlugin!");
|
|
||||||
|
|
||||||
var github = (GitHubPlugin)resultObj;
|
|
||||||
github.Init(LoaderTools.PluginsDir);
|
|
||||||
FriendlyName = github.FriendlyName;
|
|
||||||
Tooltip = github.Tooltip;
|
|
||||||
Author = github.Author;
|
|
||||||
Description = github.Description;
|
|
||||||
sourceDirectories = github.SourceDirectories;
|
|
||||||
FolderSettings.DataFile = file;
|
|
||||||
if (screen != null && screen.Visible && screen.IsOpened)
|
|
||||||
screen.RefreshSidePanel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
LogFile.Log.Error(e, "Error while reading the xml file");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void CreateNew(Action<LocalFolderPlugin> onComplete)
|
|
||||||
{
|
|
||||||
LoaderTools.OpenFolderDialog("Open the root of your project", LoaderTools.PluginsDir, folder =>
|
|
||||||
{
|
|
||||||
if (Main.Instance.List.Contains(folder))
|
|
||||||
{
|
|
||||||
MyGuiSandbox.CreateMessageBox(MyMessageBoxStyleEnum.Error,
|
|
||||||
messageText: new("That folder already exists in the list!"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var plugin = new LocalFolderPlugin(folder);
|
|
||||||
LoaderTools.OpenFileDialog("Open the xml data file", folder, XmlDataType, file =>
|
|
||||||
{
|
|
||||||
plugin.DeserializeFile(file);
|
|
||||||
onComplete(plugin);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public class Config
|
|
||||||
{
|
|
||||||
public Config()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public Config(string folder, string dataFile)
|
|
||||||
{
|
|
||||||
Folder = folder;
|
|
||||||
DataFile = dataFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Folder { get; set; }
|
|
||||||
public string DataFile { get; set; }
|
|
||||||
public bool DebugBuild { get; set; } = true;
|
|
||||||
public bool Valid => Directory.Exists(Folder) && File.Exists(DataFile);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,105 +0,0 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using System.Reflection;
|
|
||||||
using Sandbox.Graphics.GUI;
|
|
||||||
using VRage;
|
|
||||||
|
|
||||||
namespace PluginLoader.Data;
|
|
||||||
|
|
||||||
public class LocalPlugin : PluginData
|
|
||||||
{
|
|
||||||
private LocalPlugin()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalPlugin(string dll)
|
|
||||||
{
|
|
||||||
Id = dll;
|
|
||||||
Status = PluginStatus.None;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string Source => MyTexts.GetString(MyCommonTexts.Local);
|
|
||||||
|
|
||||||
public override string Id
|
|
||||||
{
|
|
||||||
get => base.Id;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
base.Id = value;
|
|
||||||
if (File.Exists(value))
|
|
||||||
FriendlyName = Path.GetFileName(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Assembly? GetAssembly()
|
|
||||||
{
|
|
||||||
if (!File.Exists(Id)) return null;
|
|
||||||
//prevent random unloading if being used by another process
|
|
||||||
int maxRetries = 10;
|
|
||||||
while (maxRetries > 0)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
AppDomain.CurrentDomain.AssemblyResolve += LoadFromSameFolder;
|
|
||||||
var a = Assembly.LoadFile(Id);
|
|
||||||
Version = a?.GetName()?.Version ?? Version;
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
catch (IOException)
|
|
||||||
{
|
|
||||||
LogFile.Log.Debug($"Waiting to load {Id} because it's being used by another process");
|
|
||||||
Thread.Sleep(250);
|
|
||||||
maxRetries--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return Id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Show()
|
|
||||||
{
|
|
||||||
var file = Path.GetFullPath(Id);
|
|
||||||
if (File.Exists(file))
|
|
||||||
Process.Start("explorer.exe", $"/select, \"{file}\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
private Assembly? LoadFromSameFolder(object sender, ResolveEventArgs args)
|
|
||||||
{
|
|
||||||
if (args.RequestingAssembly?.IsDynamic ?? false)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (args.Name.Contains("0Harmony") || args.Name.Contains("SEPluginManager"))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var location = args.RequestingAssembly?.Location;
|
|
||||||
if (string.IsNullOrWhiteSpace(location) || !Path.GetFullPath(location)
|
|
||||||
.StartsWith(Path.GetDirectoryName(Id)!,
|
|
||||||
StringComparison.OrdinalIgnoreCase))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var folderPath = Path.GetDirectoryName(Id);
|
|
||||||
if (string.IsNullOrEmpty(folderPath)) return null;
|
|
||||||
var assemblyPath = Path.Combine(folderPath, new AssemblyName(args.Name).Name + ".dll");
|
|
||||||
if (!File.Exists(assemblyPath))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var assembly = Assembly.LoadFile(assemblyPath);
|
|
||||||
LogFile.Log.Debug("Resolving " + assembly.GetName().Name + " for " + args.RequestingAssembly?.FullName, false);
|
|
||||||
|
|
||||||
var main = Main.Instance;
|
|
||||||
if (!main.Config.IsEnabled(assemblyPath))
|
|
||||||
main.List.Remove(assemblyPath);
|
|
||||||
|
|
||||||
return assembly;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void GetDescriptionText(MyGuiControlMultilineText textbox)
|
|
||||||
{
|
|
||||||
textbox.Visible = false;
|
|
||||||
textbox.Clear();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,100 +0,0 @@
|
|||||||
using System.Reflection;
|
|
||||||
using System.Xml.Serialization;
|
|
||||||
using ProtoBuf;
|
|
||||||
using Sandbox.Graphics.GUI;
|
|
||||||
using VRage.Game;
|
|
||||||
using VRage.GameServices;
|
|
||||||
|
|
||||||
namespace PluginLoader.Data;
|
|
||||||
|
|
||||||
[ProtoContract]
|
|
||||||
public class ModPlugin : PluginData, ISteamItem
|
|
||||||
{
|
|
||||||
private bool isLegacy;
|
|
||||||
|
|
||||||
private string modLocation;
|
|
||||||
|
|
||||||
public override string Source => "Mod";
|
|
||||||
|
|
||||||
[ProtoMember(1)]
|
|
||||||
[XmlArray]
|
|
||||||
[XmlArrayItem("Id")]
|
|
||||||
public ulong[] DependencyIds { get; set; } = new ulong[0];
|
|
||||||
|
|
||||||
[XmlIgnore] public ModPlugin[] Dependencies { get; set; } = new ModPlugin[0];
|
|
||||||
|
|
||||||
public string ModLocation
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (modLocation != null)
|
|
||||||
return modLocation;
|
|
||||||
modLocation = Path.Combine(Path.GetFullPath(@"..\..\..\workshop\content\244850\"), WorkshopId.ToString());
|
|
||||||
if (Directory.Exists(modLocation) && !Directory.Exists(Path.Combine(modLocation, "Data")))
|
|
||||||
{
|
|
||||||
var legacyFile = Directory.EnumerateFiles(modLocation, "*_legacy.bin").FirstOrDefault();
|
|
||||||
if (legacyFile != null)
|
|
||||||
{
|
|
||||||
isLegacy = true;
|
|
||||||
modLocation = legacyFile;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return modLocation;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Exists => Directory.Exists(ModLocation) || (isLegacy && File.Exists(modLocation));
|
|
||||||
|
|
||||||
[XmlIgnore] public ulong WorkshopId { get; private set; }
|
|
||||||
|
|
||||||
public override string Id
|
|
||||||
{
|
|
||||||
get => base.Id;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
base.Id = value;
|
|
||||||
WorkshopId = ulong.Parse(Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Assembly? GetAssembly()
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool TryLoadAssembly(out Assembly? a)
|
|
||||||
{
|
|
||||||
a = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Show()
|
|
||||||
{
|
|
||||||
MyGuiSandbox.OpenUrl("https://steamcommunity.com/workshop/filedetails/?id=" + Id,
|
|
||||||
UrlOpenMode.SteamOrExternalWithConfirm);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MyObjectBuilder_Checkpoint.ModItem GetModItem()
|
|
||||||
{
|
|
||||||
var modItem = new MyObjectBuilder_Checkpoint.ModItem(WorkshopId, "Steam");
|
|
||||||
modItem.SetModData(new WorkshopItem(ModLocation));
|
|
||||||
return modItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MyModContext GetModContext()
|
|
||||||
{
|
|
||||||
var modContext = new MyModContext();
|
|
||||||
modContext.Init(GetModItem());
|
|
||||||
modContext.Init(WorkshopId.ToString(), null, ModLocation);
|
|
||||||
return modContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class WorkshopItem : MyWorkshopItem
|
|
||||||
{
|
|
||||||
public WorkshopItem(string folder)
|
|
||||||
{
|
|
||||||
Folder = folder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,220 +0,0 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Xml.Serialization;
|
|
||||||
using PluginLoader.GUI;
|
|
||||||
using ProtoBuf;
|
|
||||||
using Sandbox.Graphics.GUI;
|
|
||||||
using VRage;
|
|
||||||
using VRage.Utils;
|
|
||||||
|
|
||||||
namespace PluginLoader.Data;
|
|
||||||
|
|
||||||
[XmlInclude(typeof(WorkshopPlugin))]
|
|
||||||
[XmlInclude(typeof(SEPMPlugin))]
|
|
||||||
[XmlInclude(typeof(GitHubPlugin))]
|
|
||||||
[XmlInclude(typeof(ModPlugin))]
|
|
||||||
[ProtoContract]
|
|
||||||
[ProtoInclude(100, typeof(SteamPlugin))]
|
|
||||||
[ProtoInclude(103, typeof(GitHubPlugin))]
|
|
||||||
[ProtoInclude(104, typeof(ModPlugin))]
|
|
||||||
public abstract class PluginData : IEquatable<PluginData>
|
|
||||||
{
|
|
||||||
public abstract string Source { get; }
|
|
||||||
|
|
||||||
[XmlIgnore] public Version Version { get; protected set; }
|
|
||||||
|
|
||||||
[XmlIgnore] public virtual PluginStatus Status { get; set; } = PluginStatus.None;
|
|
||||||
|
|
||||||
public virtual string StatusString
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
switch (Status)
|
|
||||||
{
|
|
||||||
case PluginStatus.PendingUpdate:
|
|
||||||
return "Pending Update";
|
|
||||||
case PluginStatus.Updated:
|
|
||||||
return "Updated";
|
|
||||||
case PluginStatus.Error:
|
|
||||||
return "Error!";
|
|
||||||
case PluginStatus.Blocked:
|
|
||||||
return "Not whitelisted!";
|
|
||||||
default:
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[XmlIgnore] public bool IsLocal => Source == MyTexts.GetString(MyCommonTexts.Local);
|
|
||||||
|
|
||||||
[ProtoMember(1)] public virtual string Id { get; set; }
|
|
||||||
|
|
||||||
[ProtoMember(2)] public string FriendlyName { get; set; } = "Unknown";
|
|
||||||
|
|
||||||
[ProtoMember(3)] public bool Hidden { get; set; } = false;
|
|
||||||
|
|
||||||
[ProtoMember(4)] public string GroupId { get; set; }
|
|
||||||
|
|
||||||
[ProtoMember(5)] public string Tooltip { get; set; }
|
|
||||||
|
|
||||||
[ProtoMember(6)] public string Author { get; set; }
|
|
||||||
|
|
||||||
[ProtoMember(7)] public string Description { get; set; }
|
|
||||||
|
|
||||||
[XmlIgnore] public List<PluginData> Group { get; } = new();
|
|
||||||
|
|
||||||
[XmlIgnore] public bool Enabled => Main.Instance.Config.IsEnabled(Id);
|
|
||||||
|
|
||||||
public bool Equals(PluginData other)
|
|
||||||
{
|
|
||||||
return other != null &&
|
|
||||||
Id == other.Id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract Assembly? GetAssembly();
|
|
||||||
|
|
||||||
public virtual bool TryLoadAssembly(out Assembly? a)
|
|
||||||
{
|
|
||||||
if (Status == PluginStatus.Error)
|
|
||||||
{
|
|
||||||
a = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Get the file path
|
|
||||||
a = GetAssembly();
|
|
||||||
if (Status == PluginStatus.Blocked)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (a == null)
|
|
||||||
{
|
|
||||||
LogFile.Log.Debug("Failed to load " + ToString());
|
|
||||||
Error();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
var name = ToString();
|
|
||||||
LogFile.Log.Debug($"Failed to load {name} because of an error: " + e);
|
|
||||||
if (e is MissingMemberException)
|
|
||||||
LogFile.Log.Debug($"Is {name} up to date?");
|
|
||||||
|
|
||||||
if (e is NotSupportedException && e.Message.Contains("loadFromRemoteSources"))
|
|
||||||
Error($"The plugin {name} was blocked by windows. Please unblock the file in the dll file properties.");
|
|
||||||
else
|
|
||||||
Error();
|
|
||||||
a = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
|
||||||
{
|
|
||||||
return Equals(obj as PluginData);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return 2108858624 + EqualityComparer<string>.Default.GetHashCode(Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator ==(PluginData left, PluginData right)
|
|
||||||
{
|
|
||||||
return EqualityComparer<PluginData>.Default.Equals(left, right);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator !=(PluginData left, PluginData right)
|
|
||||||
{
|
|
||||||
return !(left == right);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return Id + '|' + FriendlyName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Error(string msg = null)
|
|
||||||
{
|
|
||||||
Status = PluginStatus.Error;
|
|
||||||
if (msg == null)
|
|
||||||
msg =
|
|
||||||
$"The plugin '{this}' caused an error. It is recommended that you disable this plugin and restart. The game may be unstable beyond this point. See loader.log or the game log for details.";
|
|
||||||
var file = MyLog.Default.GetFilePath();
|
|
||||||
if (File.Exists(file) && file.EndsWith(".log"))
|
|
||||||
{
|
|
||||||
MyLog.Default.Flush();
|
|
||||||
msg += "\n\nWould you like to open the game log?";
|
|
||||||
var result = LoaderTools.ShowMessageBox(msg, "Plugin Loader", MessageBoxButtons.YesNo,
|
|
||||||
MessageBoxIcon.Error);
|
|
||||||
if (result == DialogResult.Yes)
|
|
||||||
Process.Start(file);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
LoaderTools.ShowMessageBox(msg, "Plugin Loader", MessageBoxButtons.OK,
|
|
||||||
MessageBoxIcon.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void ErrorSecurity(string hash)
|
|
||||||
{
|
|
||||||
Status = PluginStatus.Blocked;
|
|
||||||
LoaderTools.ShowMessageBox($"Unable to load the plugin {this} because it is not whitelisted!",
|
|
||||||
"Plugin Loader", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
||||||
LogFile.Log.Debug("Error: " + this + " with an sha256 of " + hash + " is not on the whitelist!");
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract void Show();
|
|
||||||
|
|
||||||
public virtual void GetDescriptionText(MyGuiControlMultilineText textbox)
|
|
||||||
{
|
|
||||||
textbox.Visible = true;
|
|
||||||
textbox.Clear();
|
|
||||||
if (string.IsNullOrEmpty(Description))
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(Tooltip))
|
|
||||||
textbox.AppendText("No description");
|
|
||||||
else
|
|
||||||
textbox.AppendText(CapLength(Tooltip, 1000));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var text = CapLength(Description, 1000);
|
|
||||||
var textStart = 0;
|
|
||||||
foreach (Match m in Regex.Matches(text, @"https?:\/\/(www\.)?[\w-.]{2,256}\.[a-z]{2,4}\b[\w-.@:%\+~#?&//=]*"))
|
|
||||||
{
|
|
||||||
var textLen = m.Index - textStart;
|
|
||||||
if (textLen > 0)
|
|
||||||
textbox.AppendText(text.Substring(textStart, textLen));
|
|
||||||
|
|
||||||
textbox.AppendLink(m.Value, m.Value);
|
|
||||||
textStart = m.Index + m.Length;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (textStart < text.Length)
|
|
||||||
textbox.AppendText(text.Substring(textStart));
|
|
||||||
}
|
|
||||||
|
|
||||||
private string CapLength(string s, int len)
|
|
||||||
{
|
|
||||||
if (s.Length > len)
|
|
||||||
return s.Substring(0, len);
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual bool OpenContextMenu(MyGuiControlContextMenu menu)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual void ContextMenuClicked(MyGuiScreenPluginConfig screen, MyGuiControlContextMenu.EventArgs args)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,10 +0,0 @@
|
|||||||
namespace PluginLoader.Data;
|
|
||||||
|
|
||||||
public enum PluginStatus
|
|
||||||
{
|
|
||||||
None,
|
|
||||||
PendingUpdate,
|
|
||||||
Updated,
|
|
||||||
Error,
|
|
||||||
Blocked
|
|
||||||
}
|
|
@@ -1,45 +0,0 @@
|
|||||||
using System.IO.Compression;
|
|
||||||
using ProtoBuf;
|
|
||||||
|
|
||||||
namespace PluginLoader.Data;
|
|
||||||
|
|
||||||
[ProtoContract]
|
|
||||||
public class SEPMPlugin : SteamPlugin
|
|
||||||
{
|
|
||||||
private const string NameFile = "name.txt";
|
|
||||||
|
|
||||||
private string dataFolder;
|
|
||||||
|
|
||||||
protected SEPMPlugin()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string Source => "SEPM";
|
|
||||||
protected override string HashFile => "sepm-plugin.txt";
|
|
||||||
|
|
||||||
protected override void CheckForUpdates()
|
|
||||||
{
|
|
||||||
dataFolder = Path.Combine(root, "sepm-plugin");
|
|
||||||
|
|
||||||
if (Directory.Exists(dataFolder))
|
|
||||||
base.CheckForUpdates();
|
|
||||||
else
|
|
||||||
Status = PluginStatus.PendingUpdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void ApplyUpdate()
|
|
||||||
{
|
|
||||||
if (Directory.Exists(dataFolder))
|
|
||||||
Directory.Delete(dataFolder, true);
|
|
||||||
|
|
||||||
ZipFile.ExtractToDirectory(sourceFile, dataFolder);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override string GetAssemblyFile()
|
|
||||||
{
|
|
||||||
if (!Directory.Exists(dataFolder))
|
|
||||||
return null;
|
|
||||||
return Directory.EnumerateFiles(dataFolder, "*.dll")
|
|
||||||
.Where(s => !s.Equals("0Harmony.dll", StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,105 +0,0 @@
|
|||||||
using System.Reflection;
|
|
||||||
using System.Xml.Serialization;
|
|
||||||
using ProtoBuf;
|
|
||||||
using Sandbox.Graphics.GUI;
|
|
||||||
|
|
||||||
namespace PluginLoader.Data;
|
|
||||||
|
|
||||||
[ProtoContract]
|
|
||||||
[ProtoInclude(101, typeof(SEPMPlugin))]
|
|
||||||
[ProtoInclude(102, typeof(WorkshopPlugin))]
|
|
||||||
public abstract class SteamPlugin : PluginData, ISteamItem
|
|
||||||
{
|
|
||||||
protected string root, sourceFile, hashFile;
|
|
||||||
|
|
||||||
[XmlArray] [ProtoMember(1)] public string[] AllowedHashes { get; set; }
|
|
||||||
|
|
||||||
protected abstract string HashFile { get; }
|
|
||||||
|
|
||||||
[XmlIgnore] public ulong WorkshopId { get; private set; }
|
|
||||||
|
|
||||||
public override string Id
|
|
||||||
{
|
|
||||||
get => base.Id;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
base.Id = value;
|
|
||||||
WorkshopId = ulong.Parse(Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Init(string sourceFile)
|
|
||||||
{
|
|
||||||
Status = PluginStatus.None;
|
|
||||||
this.sourceFile = sourceFile;
|
|
||||||
root = Path.GetDirectoryName(sourceFile);
|
|
||||||
hashFile = Path.Combine(root, HashFile);
|
|
||||||
|
|
||||||
CheckForUpdates();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void CheckForUpdates()
|
|
||||||
{
|
|
||||||
if (File.Exists(hashFile))
|
|
||||||
{
|
|
||||||
var oldHash = File.ReadAllText(hashFile);
|
|
||||||
var newHash = LoaderTools.GetHash1(sourceFile);
|
|
||||||
if (oldHash != newHash)
|
|
||||||
Status = PluginStatus.PendingUpdate;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Status = PluginStatus.PendingUpdate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Assembly? GetAssembly()
|
|
||||||
{
|
|
||||||
if (Status == PluginStatus.PendingUpdate)
|
|
||||||
{
|
|
||||||
LogFile.Log.Debug("Updating " + this);
|
|
||||||
ApplyUpdate();
|
|
||||||
if (Status == PluginStatus.PendingUpdate)
|
|
||||||
{
|
|
||||||
File.WriteAllText(hashFile, LoaderTools.GetHash1(sourceFile));
|
|
||||||
Status = PluginStatus.Updated;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var dll = GetAssemblyFile();
|
|
||||||
if (dll == null || !File.Exists(dll))
|
|
||||||
return null;
|
|
||||||
if (!VerifyAllowed(dll))
|
|
||||||
return null;
|
|
||||||
var a = Assembly.LoadFile(dll);
|
|
||||||
Version = a.GetName().Version;
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract void ApplyUpdate();
|
|
||||||
protected abstract string GetAssemblyFile();
|
|
||||||
|
|
||||||
public override void Show()
|
|
||||||
{
|
|
||||||
MyGuiSandbox.OpenUrl("https://steamcommunity.com/workshop/filedetails/?id=" + Id,
|
|
||||||
UrlOpenMode.SteamOrExternalWithConfirm);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool VerifyAllowed(string dll)
|
|
||||||
{
|
|
||||||
if (AllowedHashes == null || AllowedHashes.Length == 0)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
var hash = LoaderTools.GetHash256(dll);
|
|
||||||
foreach (var s in AllowedHashes)
|
|
||||||
if (s == hash)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
ErrorSecurity(hash);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,43 +0,0 @@
|
|||||||
using ProtoBuf;
|
|
||||||
using VRage;
|
|
||||||
|
|
||||||
namespace PluginLoader.Data;
|
|
||||||
|
|
||||||
[ProtoContract]
|
|
||||||
public class WorkshopPlugin : SteamPlugin
|
|
||||||
{
|
|
||||||
private string assembly;
|
|
||||||
|
|
||||||
protected WorkshopPlugin()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string Source => MyTexts.GetString(MyCommonTexts.Workshop);
|
|
||||||
protected override string HashFile => "hash.txt";
|
|
||||||
|
|
||||||
protected override void CheckForUpdates()
|
|
||||||
{
|
|
||||||
assembly = Path.Combine(root, Path.GetFileNameWithoutExtension(sourceFile) + ".dll");
|
|
||||||
|
|
||||||
var found = false;
|
|
||||||
foreach (var dll in Directory.EnumerateFiles(root, "*.dll"))
|
|
||||||
if (dll == assembly)
|
|
||||||
found = true;
|
|
||||||
else
|
|
||||||
File.Delete(dll);
|
|
||||||
if (!found)
|
|
||||||
Status = PluginStatus.PendingUpdate;
|
|
||||||
else
|
|
||||||
base.CheckForUpdates();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void ApplyUpdate()
|
|
||||||
{
|
|
||||||
File.Copy(sourceFile, assembly, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override string GetAssemblyFile()
|
|
||||||
{
|
|
||||||
return assembly;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,65 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
using Sandbox;
|
|
||||||
using Sandbox.Graphics.GUI;
|
|
||||||
using VRage.Utils;
|
|
||||||
using VRageMath;
|
|
||||||
|
|
||||||
namespace PluginLoader.GUI;
|
|
||||||
|
|
||||||
public static class ConfirmationDialog
|
|
||||||
{
|
|
||||||
public static MyGuiScreenMessageBox CreateMessageBox(
|
|
||||||
MyMessageBoxStyleEnum styleEnum = MyMessageBoxStyleEnum.Error,
|
|
||||||
MyMessageBoxButtonsType buttonType = MyMessageBoxButtonsType.OK,
|
|
||||||
StringBuilder messageText = null,
|
|
||||||
StringBuilder messageCaption = null,
|
|
||||||
MyStringId? okButtonText = null,
|
|
||||||
MyStringId? cancelButtonText = null,
|
|
||||||
MyStringId? yesButtonText = null,
|
|
||||||
MyStringId? noButtonText = null,
|
|
||||||
Action<MyGuiScreenMessageBox.ResultEnum> callback = null,
|
|
||||||
int timeoutInMiliseconds = 0,
|
|
||||||
MyGuiScreenMessageBox.ResultEnum focusedResult = MyGuiScreenMessageBox.ResultEnum.YES,
|
|
||||||
bool canHideOthers = true,
|
|
||||||
Vector2? size = null,
|
|
||||||
bool useOpacity = true,
|
|
||||||
Vector2? position = null,
|
|
||||||
bool focusable = true,
|
|
||||||
bool canBeHidden = false,
|
|
||||||
Action onClosing = null)
|
|
||||||
{
|
|
||||||
var num1 = (int)styleEnum;
|
|
||||||
var num2 = (int)buttonType;
|
|
||||||
var messageText1 = messageText;
|
|
||||||
var messageCaption1 = messageCaption;
|
|
||||||
var nullable = okButtonText;
|
|
||||||
var okButtonText1 = nullable ?? MyCommonTexts.Ok;
|
|
||||||
nullable = cancelButtonText;
|
|
||||||
var cancelButtonText1 = nullable ?? MyCommonTexts.Cancel;
|
|
||||||
nullable = yesButtonText;
|
|
||||||
var yesButtonText1 = nullable ?? MyCommonTexts.Yes;
|
|
||||||
nullable = noButtonText;
|
|
||||||
var noButtonText1 = nullable ?? MyCommonTexts.No;
|
|
||||||
var callback1 = callback;
|
|
||||||
var timeoutInMiliseconds1 = timeoutInMiliseconds;
|
|
||||||
var num3 = (int)focusedResult;
|
|
||||||
var num4 = canHideOthers ? 1 : 0;
|
|
||||||
var size1 = size;
|
|
||||||
var num5 = useOpacity ? MySandboxGame.Config.UIBkOpacity : 1.0;
|
|
||||||
var num6 = useOpacity ? MySandboxGame.Config.UIOpacity : 1.0;
|
|
||||||
var position1 = position;
|
|
||||||
var num7 = focusable ? 1 : 0;
|
|
||||||
var num8 = canBeHidden ? 1 : 0;
|
|
||||||
var onClosing1 = onClosing;
|
|
||||||
var dlg = new MyGuiScreenMessageBox((MyMessageBoxStyleEnum)num1, (MyMessageBoxButtonsType)num2, messageText1,
|
|
||||||
messageCaption1, okButtonText1, cancelButtonText1, yesButtonText1,
|
|
||||||
noButtonText1, callback1, timeoutInMiliseconds1,
|
|
||||||
(MyGuiScreenMessageBox.ResultEnum)num3, num4 != 0, size1, (float)num5,
|
|
||||||
(float)num6, position1, num7 != 0, num8 != 0, onClosing1);
|
|
||||||
|
|
||||||
if (dlg.Controls.GetControlByName("MyGuiControlMultilineText") is MyGuiControlMultilineText text)
|
|
||||||
text.TextAlign = MyGuiDrawAlignEnum.HORISONTAL_LEFT_AND_VERTICAL_CENTER;
|
|
||||||
|
|
||||||
return dlg;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,81 +0,0 @@
|
|||||||
using Sandbox.Graphics;
|
|
||||||
using Sandbox.Graphics.GUI;
|
|
||||||
using VRage.Utils;
|
|
||||||
using VRageMath;
|
|
||||||
|
|
||||||
namespace PluginLoader.GUI.GuiControls;
|
|
||||||
|
|
||||||
// From Sandbox.Game.Screens.Helpers.MyGuiControlRating
|
|
||||||
internal class RatingControl : MyGuiControlBase
|
|
||||||
{
|
|
||||||
private readonly float m_space = 8f;
|
|
||||||
|
|
||||||
public string EmptyTexture = "Textures\\GUI\\Icons\\Rating\\NoStar.png";
|
|
||||||
|
|
||||||
public string FilledTexture = "Textures\\GUI\\Icons\\Rating\\FullStar.png";
|
|
||||||
|
|
||||||
public string HalfFilledTexture = "Textures\\GUI\\Icons\\Rating\\HalfStar.png";
|
|
||||||
|
|
||||||
private int m_maxValue;
|
|
||||||
private readonly Vector2 m_textureSize = new(32f);
|
|
||||||
|
|
||||||
public RatingControl(int value = 0, int maxValue = 10)
|
|
||||||
{
|
|
||||||
Value = value;
|
|
||||||
m_maxValue = maxValue;
|
|
||||||
BackgroundTexture = null;
|
|
||||||
ColorMask = Vector4.One;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int MaxValue
|
|
||||||
{
|
|
||||||
get => m_maxValue;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
m_maxValue = value;
|
|
||||||
RecalculateSize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int Value { get; set; }
|
|
||||||
|
|
||||||
private void RecalculateSize()
|
|
||||||
{
|
|
||||||
var vector = MyGuiManager.GetHudNormalizedSizeFromPixelSize(m_textureSize) * new Vector2(0.75f, 1f);
|
|
||||||
var hudNormalizedSizeFromPixelSize = MyGuiManager.GetHudNormalizedSizeFromPixelSize(new(m_space * 0.75f, 0f));
|
|
||||||
Size = new((vector.X + hudNormalizedSizeFromPixelSize.X) * m_maxValue, vector.Y);
|
|
||||||
}
|
|
||||||
|
|
||||||
public float GetWidth()
|
|
||||||
{
|
|
||||||
var num = MyGuiManager.GetHudNormalizedSizeFromPixelSize(m_textureSize).X * 0.75f;
|
|
||||||
var num2 = MyGuiManager.GetHudNormalizedSizeFromPixelSize(new(m_space * 0.75f, 0f)).X;
|
|
||||||
return (num + num2) * MaxValue / 2f;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Draw(float transitionAlpha, float backgroundTransitionAlpha)
|
|
||||||
{
|
|
||||||
base.Draw(transitionAlpha, backgroundTransitionAlpha);
|
|
||||||
if (MaxValue <= 0) return;
|
|
||||||
var normalizedSize = MyGuiManager.GetHudNormalizedSizeFromPixelSize(m_textureSize) * new Vector2(0.75f, 1f);
|
|
||||||
var hudNormalizedSizeFromPixelSize = MyGuiManager.GetHudNormalizedSizeFromPixelSize(new(m_space * 0.75f, 0f));
|
|
||||||
var vector = GetPositionAbsoluteTopLeft() + new Vector2(0f, (Size.Y - normalizedSize.Y) / 2f);
|
|
||||||
var vector2 = new Vector2((normalizedSize.X + hudNormalizedSizeFromPixelSize.X) * 0.5f, normalizedSize.Y);
|
|
||||||
for (var i = 0; i < MaxValue; i += 2)
|
|
||||||
{
|
|
||||||
var normalizedCoord = vector + new Vector2(vector2.X * i, 0f);
|
|
||||||
if (i == Value - 1)
|
|
||||||
MyGuiManager.DrawSpriteBatch(HalfFilledTexture, normalizedCoord, normalizedSize,
|
|
||||||
ApplyColorMaskModifiers(ColorMask, Enabled, transitionAlpha),
|
|
||||||
MyGuiDrawAlignEnum.HORISONTAL_LEFT_AND_VERTICAL_TOP, false, false);
|
|
||||||
else if (i < Value)
|
|
||||||
MyGuiManager.DrawSpriteBatch(FilledTexture, normalizedCoord, normalizedSize,
|
|
||||||
ApplyColorMaskModifiers(ColorMask, Enabled, transitionAlpha),
|
|
||||||
MyGuiDrawAlignEnum.HORISONTAL_LEFT_AND_VERTICAL_TOP, false, false);
|
|
||||||
else
|
|
||||||
MyGuiManager.DrawSpriteBatch(EmptyTexture, normalizedCoord, normalizedSize,
|
|
||||||
ApplyColorMaskModifiers(ColorMask, Enabled, transitionAlpha),
|
|
||||||
MyGuiDrawAlignEnum.HORISONTAL_LEFT_AND_VERTICAL_TOP, false, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,13 +0,0 @@
|
|||||||
namespace PluginLoader.GUI;
|
|
||||||
|
|
||||||
public class ItemView
|
|
||||||
{
|
|
||||||
public readonly string[] Labels;
|
|
||||||
public readonly object[] Values;
|
|
||||||
|
|
||||||
public ItemView(string[] labels, object[] values)
|
|
||||||
{
|
|
||||||
Labels = labels;
|
|
||||||
Values = values;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,838 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
using PluginLoader.Data;
|
|
||||||
using PluginLoader.Patch;
|
|
||||||
using PluginLoader.Stats;
|
|
||||||
using PluginLoader.Stats.Model;
|
|
||||||
using Sandbox;
|
|
||||||
using Sandbox.Game.Gui;
|
|
||||||
using Sandbox.Game.Multiplayer;
|
|
||||||
using Sandbox.Game.Screens.Helpers;
|
|
||||||
using Sandbox.Game.World;
|
|
||||||
using Sandbox.Graphics.GUI;
|
|
||||||
using VRage;
|
|
||||||
using VRage.Audio;
|
|
||||||
using VRage.Game;
|
|
||||||
using VRage.Input;
|
|
||||||
using VRage.Utils;
|
|
||||||
using VRageMath;
|
|
||||||
using static Sandbox.Graphics.GUI.MyGuiScreenMessageBox;
|
|
||||||
|
|
||||||
namespace PluginLoader.GUI;
|
|
||||||
|
|
||||||
public class MyGuiScreenPluginConfig : MyGuiScreenBase
|
|
||||||
{
|
|
||||||
private const float BarWidth = 0.85f;
|
|
||||||
private const float Spacing = 0.0175f;
|
|
||||||
|
|
||||||
private static bool allItemsVisible = true;
|
|
||||||
private static bool _hideLocalPlugins = true;
|
|
||||||
|
|
||||||
public readonly Dictionary<string, bool> AfterRebootEnableFlags = new();
|
|
||||||
|
|
||||||
private readonly Dictionary<string, MyGuiControlCheckbox> pluginCheckboxes = new();
|
|
||||||
private readonly PluginDetailsPanel pluginDetails;
|
|
||||||
private MyGuiControlButton buttonMore;
|
|
||||||
private MyGuiControlContextMenu contextMenu;
|
|
||||||
private bool forceRestart;
|
|
||||||
private MyGuiControlContextMenu pluginContextMenu;
|
|
||||||
private MyGuiControlLabel pluginCountLabel;
|
|
||||||
public PluginStats PluginStats;
|
|
||||||
|
|
||||||
private MyGuiControlTable pluginTable;
|
|
||||||
private string[] tableFilter;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The plugins screen, the constructor itself sets up the menu properties.
|
|
||||||
/// </summary>
|
|
||||||
private MyGuiScreenPluginConfig() : base(new Vector2(0.5f, 0.5f), MyGuiConstants.SCREEN_BACKGROUND_COLOR,
|
|
||||||
new Vector2(1f, 0.97f), false, null, MySandboxGame.Config.UIBkOpacity,
|
|
||||||
MySandboxGame.Config.UIOpacity)
|
|
||||||
{
|
|
||||||
EnabledBackgroundFade = true;
|
|
||||||
m_closeOnEsc = true;
|
|
||||||
m_drawEvenWithoutFocus = true;
|
|
||||||
CanHideOthers = true;
|
|
||||||
CanBeHidden = true;
|
|
||||||
CloseButtonEnabled = true;
|
|
||||||
|
|
||||||
foreach (var plugin in Main.Instance.List)
|
|
||||||
AfterRebootEnableFlags[plugin.Id] = plugin.Enabled;
|
|
||||||
|
|
||||||
pluginDetails = new(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static PluginConfig Config => Main.Instance.Config;
|
|
||||||
|
|
||||||
private PluginData SelectedPlugin
|
|
||||||
{
|
|
||||||
get => pluginDetails.Plugin;
|
|
||||||
set => pluginDetails.Plugin = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool RequiresRestart => forceRestart ||
|
|
||||||
Main.Instance.List.Any(
|
|
||||||
plugin => plugin.Enabled != AfterRebootEnableFlags[plugin.Id]);
|
|
||||||
|
|
||||||
public static void OpenMenu()
|
|
||||||
{
|
|
||||||
if (Main.Instance.List.HasError)
|
|
||||||
MyGuiSandbox.AddScreen(MyGuiSandbox.CreateMessageBox(buttonType: MyMessageBoxButtonsType.OK,
|
|
||||||
messageText: new(
|
|
||||||
"An error occurred while downloading the plugin list.\nPlease send your game log to the developers of Plugin Loader."),
|
|
||||||
messageCaption: MyTexts.Get(
|
|
||||||
MyCommonTexts.MessageBoxCaptionError),
|
|
||||||
callback: x =>
|
|
||||||
MyGuiSandbox.AddScreen(
|
|
||||||
new MyGuiScreenPluginConfig())));
|
|
||||||
else
|
|
||||||
MyGuiSandbox.AddScreen(new MyGuiScreenPluginConfig());
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string GetFriendlyName()
|
|
||||||
{
|
|
||||||
return "MyGuiScreenPluginConfig";
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void LoadContent()
|
|
||||||
{
|
|
||||||
base.LoadContent();
|
|
||||||
RecreateControls(true);
|
|
||||||
PlayerConsent.OnConsentChanged += OnConsentChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void HandleUnhandledInput(bool receivedFocusInThisUpdate)
|
|
||||||
{
|
|
||||||
var input = MyInput.Static;
|
|
||||||
if (input.IsNewKeyPressed(MyKeys.F5) && input.IsAnyAltKeyPressed() && input.IsAnyCtrlKeyPressed())
|
|
||||||
Patch_IngameRestart.ShowRestartMenu();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void UnloadContent()
|
|
||||||
{
|
|
||||||
PlayerConsent.OnConsentChanged -= OnConsentChanged;
|
|
||||||
pluginDetails.OnPluginToggled -= EnablePlugin;
|
|
||||||
base.UnloadContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnConsentChanged()
|
|
||||||
{
|
|
||||||
DownloadStats();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DownloadStats()
|
|
||||||
{
|
|
||||||
LogFile.Log.Debug("Downloading user statistics");
|
|
||||||
Task.Run(() =>
|
|
||||||
{
|
|
||||||
StatsClient.DownloadStats();
|
|
||||||
pluginDetails?.LoadPluginData();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes the controls of the menu on the left side of the menu.
|
|
||||||
/// </summary>
|
|
||||||
public override void RecreateControls(bool constructor)
|
|
||||||
{
|
|
||||||
base.RecreateControls(constructor);
|
|
||||||
|
|
||||||
var title = AddCaption("Plugins List");
|
|
||||||
|
|
||||||
// Sets the origin relative to the center of the caption on the X axis and to the bottom the caption on the y axis.
|
|
||||||
var origin = title.Position += new Vector2(0f, title.Size.Y / 2);
|
|
||||||
|
|
||||||
origin.Y += Spacing;
|
|
||||||
|
|
||||||
// Adds a bar right below the caption.
|
|
||||||
var titleBar = new MyGuiControlSeparatorList();
|
|
||||||
titleBar.AddHorizontal(new(origin.X - BarWidth / 2, origin.Y), BarWidth);
|
|
||||||
Controls.Add(titleBar);
|
|
||||||
|
|
||||||
origin.Y += Spacing;
|
|
||||||
|
|
||||||
// Change the position of this to move the entire middle section of the menu, the menu bars, menu title, and bottom buttons won't move
|
|
||||||
// Adds a search bar right below the bar on the left side of the menu.
|
|
||||||
var searchBox = new MyGuiControlSearchBox(new Vector2(origin.X - BarWidth / 2, origin.Y),
|
|
||||||
originAlign: MyGuiDrawAlignEnum.HORISONTAL_LEFT_AND_VERTICAL_TOP);
|
|
||||||
|
|
||||||
// Changing the search box X size will change the plugin list length.
|
|
||||||
searchBox.Size = new(0.4f, searchBox.Size.Y);
|
|
||||||
searchBox.OnTextChanged += SearchBox_TextChanged;
|
|
||||||
Controls.Add(searchBox);
|
|
||||||
|
|
||||||
#region Visibility Button
|
|
||||||
|
|
||||||
// Adds a button to show only enabled plugins. Located right of the search bar.
|
|
||||||
var buttonVisibility = new MyGuiControlButton(
|
|
||||||
new Vector2(origin.X - BarWidth / 2 + searchBox.Size.X, origin.Y) + new Vector2(0.003f, 0.002f),
|
|
||||||
MyGuiControlButtonStyleEnum.Rectangular, new Vector2(searchBox.Size.Y * 2.52929769833f),
|
|
||||||
onButtonClick: OnVisibilityClick, originAlign: MyGuiDrawAlignEnum.HORISONTAL_LEFT_AND_VERTICAL_TOP,
|
|
||||||
toolTip: "Show only enabled plugins.", buttonScale: 0.5f);
|
|
||||||
|
|
||||||
if (allItemsVisible || Config.Count == 0)
|
|
||||||
{
|
|
||||||
allItemsVisible = true;
|
|
||||||
buttonVisibility.Icon = IconHide;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
buttonVisibility.Icon = IconShow;
|
|
||||||
}
|
|
||||||
|
|
||||||
Controls.Add(buttonVisibility);
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
origin.Y += searchBox.Size.Y + Spacing;
|
|
||||||
|
|
||||||
#region Plugin List
|
|
||||||
|
|
||||||
// Adds the plugin list on the right of the menu below the search bar.
|
|
||||||
pluginTable = new()
|
|
||||||
{
|
|
||||||
Position = new(origin.X - BarWidth / 2, origin.Y),
|
|
||||||
Size = new(searchBox.Size.X + buttonVisibility.Size.X + 0.001f,
|
|
||||||
0.6f), // The y value can be bigger than the visible rows count as the visibleRowsCount controls the height.
|
|
||||||
OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_LEFT_AND_VERTICAL_TOP,
|
|
||||||
ColumnsCount = 3,
|
|
||||||
VisibleRowsCount = 20
|
|
||||||
};
|
|
||||||
|
|
||||||
pluginTable.SetCustomColumnWidths(new[]
|
|
||||||
{
|
|
||||||
0.22f,
|
|
||||||
0.6f,
|
|
||||||
0.22f
|
|
||||||
});
|
|
||||||
|
|
||||||
pluginTable.SetColumnName(0, new("Source"));
|
|
||||||
pluginTable.SetColumnComparison(0, CellTextOrDataComparison);
|
|
||||||
pluginTable.SetColumnName(1, new("Name"));
|
|
||||||
pluginTable.SetColumnComparison(1, CellTextComparison);
|
|
||||||
pluginTable.SetColumnName(2, new("Enable"));
|
|
||||||
pluginTable.SetColumnComparison(2, CellTextComparison);
|
|
||||||
|
|
||||||
// Default sorting
|
|
||||||
pluginTable.SortByColumn(2, MyGuiControlTable.SortStateEnum.Ascending);
|
|
||||||
|
|
||||||
// Selecting list items load their details in OnItemSelected
|
|
||||||
pluginTable.ItemSelected += OnItemSelected;
|
|
||||||
Controls.Add(pluginTable);
|
|
||||||
|
|
||||||
// Double clicking list items toggles the enable flag
|
|
||||||
pluginTable.ItemDoubleClicked += OnItemDoubleClicked;
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
origin.Y += Spacing + pluginTable.Size.Y;
|
|
||||||
|
|
||||||
// Adds the bar at the bottom between just above the buttons.
|
|
||||||
var bottomBar = new MyGuiControlSeparatorList();
|
|
||||||
bottomBar.AddHorizontal(new(origin.X - BarWidth / 2, origin.Y), BarWidth);
|
|
||||||
Controls.Add(bottomBar);
|
|
||||||
|
|
||||||
origin.Y += Spacing;
|
|
||||||
|
|
||||||
// Adds buttons at bottom of menu
|
|
||||||
var buttonRestart = new MyGuiControlButton(origin, MyGuiControlButtonStyleEnum.Default, null, null,
|
|
||||||
MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_TOP,
|
|
||||||
"Restart the game and apply changes.", new("Apply"), 0.8f,
|
|
||||||
MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_CENTER,
|
|
||||||
MyGuiControlHighlightType.WHEN_ACTIVE, OnRestartButtonClick);
|
|
||||||
var buttonClose = new MyGuiControlButton(origin, MyGuiControlButtonStyleEnum.Default, null, null,
|
|
||||||
MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_TOP,
|
|
||||||
"Closes the dialog without saving changes to plugin selection",
|
|
||||||
new("Cancel"), 0.8f,
|
|
||||||
MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_CENTER,
|
|
||||||
MyGuiControlHighlightType.WHEN_ACTIVE, OnCancelButtonClick);
|
|
||||||
buttonMore = new(origin, MyGuiControlButtonStyleEnum.Tiny, null, null,
|
|
||||||
MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_TOP, "Advanced", new("..."), 0.8f,
|
|
||||||
MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_CENTER,
|
|
||||||
MyGuiControlHighlightType.WHEN_ACTIVE, OnMoreButtonClick);
|
|
||||||
|
|
||||||
// FIXME: Use MyLayoutHorizontal instead
|
|
||||||
AlignRow(origin, 0.05f, buttonRestart, buttonClose);
|
|
||||||
Controls.Add(buttonRestart);
|
|
||||||
Controls.Add(buttonClose);
|
|
||||||
buttonMore.Position = buttonClose.Position + new Vector2(buttonClose.Size.X / 2 + 0.05f, 0);
|
|
||||||
Controls.Add(buttonMore);
|
|
||||||
|
|
||||||
// Adds a place to show the total amount of plugins and to show the total amount of visible plugins.
|
|
||||||
pluginCountLabel = new(new Vector2(origin.X - BarWidth / 2, buttonRestart.Position.Y),
|
|
||||||
originAlign: MyGuiDrawAlignEnum.HORISONTAL_LEFT_AND_VERTICAL_TOP);
|
|
||||||
Controls.Add(pluginCountLabel);
|
|
||||||
|
|
||||||
// Right side panel showing the details of the selected plugin
|
|
||||||
var rightSideOrigin = buttonVisibility.Position +
|
|
||||||
new Vector2(Spacing * 1.778f + buttonVisibility.Size.X / 2,
|
|
||||||
-(buttonVisibility.Size.Y / 2));
|
|
||||||
pluginDetails.CreateControls(rightSideOrigin);
|
|
||||||
Controls.Add(pluginDetails);
|
|
||||||
pluginDetails.OnPluginToggled += EnablePlugin;
|
|
||||||
|
|
||||||
// Context menu for the more (...) button
|
|
||||||
contextMenu = new();
|
|
||||||
contextMenu.Deactivate();
|
|
||||||
contextMenu.CreateNewContextMenu();
|
|
||||||
contextMenu.AddItem(new("Add development folder"), "Open and compile a folder for development",
|
|
||||||
userData: nameof(OnLoadFolder));
|
|
||||||
contextMenu.AddItem(new("Save profile"), "Saved the current plugin selection", userData: nameof(OnSaveProfile));
|
|
||||||
contextMenu.AddItem(new("Load profile"), "Loads a saved plugin selection", userData: nameof(OnLoadProfile));
|
|
||||||
contextMenu.AddItem(new("------------"));
|
|
||||||
contextMenu.AddItem(
|
|
||||||
new(PlayerConsent.ConsentGiven ? "Revoke consent" : "Give consent"),
|
|
||||||
PlayerConsent.ConsentGiven
|
|
||||||
? "Revoke consent to data handling, clear my votes"
|
|
||||||
: "Give consent to data handling, allow me to vote",
|
|
||||||
userData: nameof(OnConsent));
|
|
||||||
contextMenu.Enabled = true;
|
|
||||||
contextMenu.ItemClicked += OnContextMenuItemClicked;
|
|
||||||
contextMenu.OnDeactivated += OnContextMenuDeactivated;
|
|
||||||
// contextMenu.SetMaxSize(new Vector2(0.2f, 0.7f));
|
|
||||||
Controls.Add(contextMenu);
|
|
||||||
|
|
||||||
// Context menu for the plugin list
|
|
||||||
pluginContextMenu = new();
|
|
||||||
pluginContextMenu.Deactivate();
|
|
||||||
pluginContextMenu.CreateNewContextMenu();
|
|
||||||
pluginContextMenu.ItemClicked += OnPluginContextMenuItemClicked;
|
|
||||||
pluginContextMenu.OnDeactivated += OnContextMenuDeactivated;
|
|
||||||
Controls.Add(pluginContextMenu);
|
|
||||||
|
|
||||||
// Refreshes the table to show plugins on plugin list
|
|
||||||
RefreshTable();
|
|
||||||
|
|
||||||
DownloadStats();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RequireRestart()
|
|
||||||
{
|
|
||||||
forceRestart = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnLoadFolder()
|
|
||||||
{
|
|
||||||
LocalFolderPlugin.CreateNew(plugin =>
|
|
||||||
{
|
|
||||||
Config.PluginFolders[plugin.Id] = plugin.FolderSettings;
|
|
||||||
CreatePlugin(plugin);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CreatePlugin(PluginData data)
|
|
||||||
{
|
|
||||||
Main.Instance.List.Add(data);
|
|
||||||
AfterRebootEnableFlags[data.Id] = true;
|
|
||||||
Config.SetEnabled(data.Id, true);
|
|
||||||
forceRestart = true;
|
|
||||||
RefreshTable(tableFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RemovePlugin(PluginData data)
|
|
||||||
{
|
|
||||||
Main.Instance.List.Remove(data.Id);
|
|
||||||
AfterRebootEnableFlags.Remove(data.Id);
|
|
||||||
Config.SetEnabled(data.Id, false);
|
|
||||||
forceRestart = true;
|
|
||||||
RefreshTable(tableFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RefreshSidePanel()
|
|
||||||
{
|
|
||||||
pluginDetails?.LoadPluginData();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Event that triggers when the visibility button is clicked. This method shows all plugins or only enabled plugins.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="btn">The button to assign this event to.</param>
|
|
||||||
private void OnVisibilityClick(MyGuiControlButton btn)
|
|
||||||
{
|
|
||||||
if (allItemsVisible && _hideLocalPlugins)
|
|
||||||
{
|
|
||||||
allItemsVisible = false;
|
|
||||||
btn.Icon = IconShow;
|
|
||||||
}
|
|
||||||
else if (_hideLocalPlugins)
|
|
||||||
{
|
|
||||||
allItemsVisible = true;
|
|
||||||
_hideLocalPlugins = false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_hideLocalPlugins = true;
|
|
||||||
btn.Icon = IconHide;
|
|
||||||
}
|
|
||||||
|
|
||||||
RefreshTable(tableFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int CellTextOrDataComparison(MyGuiControlTable.Cell x, MyGuiControlTable.Cell y)
|
|
||||||
{
|
|
||||||
var result = TextComparison(x.Text, y.Text);
|
|
||||||
if (result != 0) return result;
|
|
||||||
|
|
||||||
return TextComparison((StringBuilder)x.UserData, (StringBuilder)y.UserData);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int CellTextComparison(MyGuiControlTable.Cell x, MyGuiControlTable.Cell y)
|
|
||||||
{
|
|
||||||
return TextComparison(x.Text, y.Text);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int TextComparison(StringBuilder x, StringBuilder y)
|
|
||||||
{
|
|
||||||
if (x == null)
|
|
||||||
{
|
|
||||||
if (y == null)
|
|
||||||
return 0;
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (y == null)
|
|
||||||
return -1;
|
|
||||||
|
|
||||||
return x.CompareTo(y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Clears the table and adds the list of plugins and their information.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="filter">Text filter</param>
|
|
||||||
private void RefreshTable(string[] filter = null)
|
|
||||||
{
|
|
||||||
pluginTable.Clear();
|
|
||||||
pluginTable.Controls.Clear();
|
|
||||||
pluginCheckboxes.Clear();
|
|
||||||
var list = Main.Instance.List;
|
|
||||||
var noFilter = filter == null || filter.Length == 0;
|
|
||||||
foreach (var plugin in list)
|
|
||||||
{
|
|
||||||
var enabled = AfterRebootEnableFlags[plugin.Id];
|
|
||||||
|
|
||||||
if (plugin.IsLocal && _hideLocalPlugins) continue;
|
|
||||||
|
|
||||||
if (noFilter && (plugin.Hidden || !allItemsVisible) && !enabled)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (!noFilter && !FilterName(plugin.FriendlyName, filter))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var row = new MyGuiControlTable.Row(plugin);
|
|
||||||
pluginTable.Add(row);
|
|
||||||
|
|
||||||
var name = new StringBuilder(plugin.FriendlyName);
|
|
||||||
row.AddCell(new(plugin.Source, name));
|
|
||||||
|
|
||||||
var tip = plugin.FriendlyName;
|
|
||||||
if (!string.IsNullOrWhiteSpace(plugin.Tooltip))
|
|
||||||
tip += "\n" + plugin.Tooltip;
|
|
||||||
row.AddCell(new(plugin.FriendlyName, toolTip: tip));
|
|
||||||
|
|
||||||
var text = new StringBuilder(FormatCheckboxSortKey(plugin, enabled));
|
|
||||||
var enabledCell = new MyGuiControlTable.Cell(text, name);
|
|
||||||
var enabledCheckbox = new MyGuiControlCheckbox(isChecked: enabled)
|
|
||||||
{
|
|
||||||
UserData = plugin,
|
|
||||||
Visible = true
|
|
||||||
};
|
|
||||||
enabledCheckbox.IsCheckedChanged += OnPluginCheckboxChanged;
|
|
||||||
enabledCell.Control = enabledCheckbox;
|
|
||||||
pluginTable.Controls.Add(enabledCheckbox);
|
|
||||||
pluginCheckboxes[plugin.Id] = enabledCheckbox;
|
|
||||||
row.AddCell(enabledCell);
|
|
||||||
}
|
|
||||||
|
|
||||||
pluginCountLabel.Text = pluginTable.RowsCount + "/" + list.Count + " visible";
|
|
||||||
pluginTable.Sort(false);
|
|
||||||
pluginTable.SelectedRowIndex = null;
|
|
||||||
tableFilter = filter;
|
|
||||||
pluginTable.SelectedRowIndex = 0;
|
|
||||||
|
|
||||||
var args = new MyGuiControlTable.EventArgs { RowIndex = 0 };
|
|
||||||
OnItemSelected(pluginTable, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string FormatCheckboxSortKey(PluginData plugin, bool enabled)
|
|
||||||
{
|
|
||||||
// Uses a prefix of + and - to list plugins to enable to the top
|
|
||||||
return enabled ? $"+{plugin.FriendlyName}|{plugin.Source}" : $"-{plugin.FriendlyName}|{plugin.Source}";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Event that triggers when the text in the searchbox is changed.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="txt">The text that was entered into the searchbox.</param>
|
|
||||||
private void SearchBox_TextChanged(string txt)
|
|
||||||
{
|
|
||||||
var args = txt.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
RefreshTable(args);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool FilterName(string name, IEnumerable<string> filter)
|
|
||||||
{
|
|
||||||
return filter.All(s => name.Contains(s, StringComparison.OrdinalIgnoreCase));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets text on right side of screen.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="table">Table to get the plugin data.</param>
|
|
||||||
/// <param name="args">Event arguments.</param>
|
|
||||||
private void OnItemSelected(MyGuiControlTable table, MyGuiControlTable.EventArgs args)
|
|
||||||
{
|
|
||||||
if (!TryGetPluginByRowIndex(args.RowIndex, out var plugin))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (args.MouseButton == MyMouseButtonsEnum.Right && plugin.OpenContextMenu(pluginContextMenu))
|
|
||||||
{
|
|
||||||
pluginContextMenu.ItemList_UseSimpleItemListMouseOverCheck = true;
|
|
||||||
pluginContextMenu.Activate();
|
|
||||||
}
|
|
||||||
|
|
||||||
contextMenu.Deactivate();
|
|
||||||
SelectedPlugin = plugin;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnItemDoubleClicked(MyGuiControlTable table, MyGuiControlTable.EventArgs args)
|
|
||||||
{
|
|
||||||
if (!TryGetPluginByRowIndex(args.RowIndex, out var data))
|
|
||||||
return;
|
|
||||||
|
|
||||||
EnablePlugin(data, !AfterRebootEnableFlags[data.Id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryGetPluginByRowIndex(int rowIndex, out PluginData plugin)
|
|
||||||
{
|
|
||||||
if (rowIndex < 0 || rowIndex >= pluginTable.RowsCount)
|
|
||||||
{
|
|
||||||
plugin = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var row = pluginTable.GetRow(rowIndex);
|
|
||||||
plugin = row.UserData as PluginData;
|
|
||||||
return plugin != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AlignRow(Vector2 origin, float spacing, params MyGuiControlBase[] elements)
|
|
||||||
{
|
|
||||||
if (elements.Length == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
float totalWidth = 0;
|
|
||||||
for (var i = 0; i < elements.Length; i++)
|
|
||||||
{
|
|
||||||
var btn = elements[i];
|
|
||||||
totalWidth += btn.Size.X;
|
|
||||||
if (i < elements.Length - 1)
|
|
||||||
totalWidth += spacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
var originX = origin.X - totalWidth / 2;
|
|
||||||
foreach (var btn in elements)
|
|
||||||
{
|
|
||||||
var halfWidth = btn.Size.X / 2;
|
|
||||||
originX += halfWidth;
|
|
||||||
btn.Position = new(originX, origin.Y);
|
|
||||||
originX += spacing + halfWidth;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPluginCheckboxChanged(MyGuiControlCheckbox checkbox)
|
|
||||||
{
|
|
||||||
var plugin = (PluginData)checkbox.UserData;
|
|
||||||
EnablePlugin(plugin, checkbox.IsChecked);
|
|
||||||
|
|
||||||
if (ReferenceEquals(plugin, SelectedPlugin))
|
|
||||||
pluginDetails.LoadPluginData();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void EnablePlugin(PluginData plugin, bool enable)
|
|
||||||
{
|
|
||||||
if (enable == AfterRebootEnableFlags[plugin.Id])
|
|
||||||
return;
|
|
||||||
|
|
||||||
AfterRebootEnableFlags[plugin.Id] = enable;
|
|
||||||
|
|
||||||
SetPluginCheckbox(plugin, enable);
|
|
||||||
|
|
||||||
if (enable)
|
|
||||||
{
|
|
||||||
DisableOtherPluginsInSameGroup(plugin);
|
|
||||||
EnableDependencies(plugin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetPluginCheckbox(PluginData plugin, bool enable)
|
|
||||||
{
|
|
||||||
if (!pluginCheckboxes.TryGetValue(plugin.Id, out var checkbox))
|
|
||||||
return; // The checkbox might not exist if the target plugin is a dependency not currently in the table
|
|
||||||
checkbox.IsChecked = enable;
|
|
||||||
|
|
||||||
var row = pluginTable.Find(x => ReferenceEquals(x.UserData as PluginData, plugin));
|
|
||||||
row?.GetCell(2).Text.Clear().Append(FormatCheckboxSortKey(plugin, enable));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DisableOtherPluginsInSameGroup(PluginData plugin)
|
|
||||||
{
|
|
||||||
foreach (var other in plugin.Group)
|
|
||||||
if (!ReferenceEquals(other, plugin))
|
|
||||||
EnablePlugin(other, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EnableDependencies(PluginData plugin)
|
|
||||||
{
|
|
||||||
if (plugin is not ModPlugin mod || mod.Dependencies == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
foreach (PluginData other in mod.Dependencies)
|
|
||||||
if (!ReferenceEquals(other, plugin))
|
|
||||||
EnablePlugin(other, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnCancelButtonClick(MyGuiControlButton btn)
|
|
||||||
{
|
|
||||||
CloseScreen();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnMoreButtonClick(MyGuiControlButton _)
|
|
||||||
{
|
|
||||||
contextMenu.ItemList_UseSimpleItemListMouseOverCheck = true;
|
|
||||||
contextMenu.Enabled = false;
|
|
||||||
contextMenu.Activate(false);
|
|
||||||
contextMenu.OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_LEFT_AND_VERTICAL_TOP;
|
|
||||||
contextMenu.Position = buttonMore.Position + buttonMore.Size * new Vector2(-1.3f, -1.9f);
|
|
||||||
FocusContextMenuList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void FocusContextMenuList()
|
|
||||||
{
|
|
||||||
var guiControlsOwner = (IMyGuiControlsOwner)contextMenu;
|
|
||||||
while (guiControlsOwner.Owner != null)
|
|
||||||
{
|
|
||||||
guiControlsOwner = guiControlsOwner.Owner;
|
|
||||||
if (guiControlsOwner is not MyGuiScreenBase myGuiScreenBase)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
myGuiScreenBase.FocusedControl = contextMenu.GetInnerList();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnContextMenuDeactivated()
|
|
||||||
{
|
|
||||||
contextMenu.Enabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnContextMenuItemClicked(MyGuiControlContextMenu _, MyGuiControlContextMenu.EventArgs args)
|
|
||||||
{
|
|
||||||
contextMenu.Deactivate();
|
|
||||||
|
|
||||||
switch ((string)args.UserData)
|
|
||||||
{
|
|
||||||
case nameof(OnLoadFolder):
|
|
||||||
OnLoadFolder();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case nameof(OnSaveProfile):
|
|
||||||
OnSaveProfile();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case nameof(OnLoadProfile):
|
|
||||||
OnLoadProfile();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case nameof(OnConsent):
|
|
||||||
OnConsent();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPluginContextMenuItemClicked(MyGuiControlContextMenu menu, MyGuiControlContextMenu.EventArgs args)
|
|
||||||
{
|
|
||||||
SelectedPlugin?.ContextMenuClicked(this, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnSaveProfile()
|
|
||||||
{
|
|
||||||
var timestamp = DateTime.Now.ToString("O").Substring(0, 19).Replace('T', ' ');
|
|
||||||
MyGuiSandbox.AddScreen(new NameDialog(OnProfileNameProvided, "Save profile", timestamp));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnProfileNameProvided(string name)
|
|
||||||
{
|
|
||||||
var afterRebootEnablePluginIds = AfterRebootEnableFlags
|
|
||||||
.Where(p => p.Value)
|
|
||||||
.Select(p => p.Key);
|
|
||||||
|
|
||||||
var profile = new Profile(name, afterRebootEnablePluginIds.ToArray());
|
|
||||||
Config.ProfileMap[profile.Key] = profile;
|
|
||||||
Config.Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnLoadProfile()
|
|
||||||
{
|
|
||||||
MyGuiSandbox.AddScreen(new ProfilesDialog("Load profile", OnProfileLoaded));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnProfileLoaded(Profile profile)
|
|
||||||
{
|
|
||||||
var pluginsEnabledInProfile = profile.Plugins.ToHashSet();
|
|
||||||
|
|
||||||
foreach (var plugin in Main.Instance.List)
|
|
||||||
EnablePlugin(plugin, pluginsEnabledInProfile.Contains(plugin.Id));
|
|
||||||
|
|
||||||
pluginTable.SortByColumn(2, MyGuiControlTable.SortStateEnum.Ascending);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnConsent()
|
|
||||||
{
|
|
||||||
PlayerConsent.ShowDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnRestartButtonClick(MyGuiControlButton btn)
|
|
||||||
{
|
|
||||||
if (!RequiresRestart)
|
|
||||||
{
|
|
||||||
CloseScreen();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MyGuiSandbox.AddScreen(MyGuiSandbox.CreateMessageBox(MyMessageBoxStyleEnum.Info,
|
|
||||||
MyMessageBoxButtonsType.YES_NO_CANCEL,
|
|
||||||
new(
|
|
||||||
"A restart is required to apply changes. Would you like to restart the game now?"),
|
|
||||||
new("Apply Changes?"), callback: AskRestartResult));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Save()
|
|
||||||
{
|
|
||||||
if (!RequiresRestart)
|
|
||||||
return;
|
|
||||||
|
|
||||||
foreach (var plugin in Main.Instance.List)
|
|
||||||
Config.SetEnabled(plugin.Id, AfterRebootEnableFlags[plugin.Id]);
|
|
||||||
|
|
||||||
Config.Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Icons
|
|
||||||
|
|
||||||
// Source: MyTerminalControlPanel
|
|
||||||
private static readonly MyGuiHighlightTexture IconHide = new()
|
|
||||||
{
|
|
||||||
Normal = "Textures\\GUI\\Controls\\button_hide.dds",
|
|
||||||
Highlight = "Textures\\GUI\\Controls\\button_hide.dds",
|
|
||||||
Focus = "Textures\\GUI\\Controls\\button_hide_focus.dds",
|
|
||||||
SizePx = new(40f, 40f)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Source: MyTerminalControlPanel
|
|
||||||
private static readonly MyGuiHighlightTexture IconShow = new()
|
|
||||||
{
|
|
||||||
Normal = "Textures\\GUI\\Controls\\button_unhide.dds",
|
|
||||||
Highlight = "Textures\\GUI\\Controls\\button_unhide.dds",
|
|
||||||
Focus = "Textures\\GUI\\Controls\\button_unhide_focus.dds",
|
|
||||||
SizePx = new(40f, 40f)
|
|
||||||
};
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Restart
|
|
||||||
|
|
||||||
private void AskRestartResult(ResultEnum result)
|
|
||||||
{
|
|
||||||
if (result == ResultEnum.YES)
|
|
||||||
{
|
|
||||||
Save();
|
|
||||||
if (MyGuiScreenGamePlay.Static != null)
|
|
||||||
{
|
|
||||||
ShowSaveMenu(delegate { LoaderTools.UnloadAndRestart(); });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
LoaderTools.UnloadAndRestart();
|
|
||||||
}
|
|
||||||
else if (result == ResultEnum.NO)
|
|
||||||
{
|
|
||||||
Save();
|
|
||||||
CloseScreen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// From WesternGamer/InGameWorldLoading
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="afterMenu">Action after code is executed.</param>
|
|
||||||
private static void ShowSaveMenu(Action afterMenu)
|
|
||||||
{
|
|
||||||
// Sync.IsServer is backwards
|
|
||||||
if (!Sync.IsServer)
|
|
||||||
{
|
|
||||||
afterMenu();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var message = "";
|
|
||||||
var isCampaign = false;
|
|
||||||
var buttonsType = MyMessageBoxButtonsType.YES_NO_CANCEL;
|
|
||||||
|
|
||||||
// Sync.IsServer is backwards
|
|
||||||
if (Sync.IsServer && !MySession.Static.Settings.EnableSaving)
|
|
||||||
{
|
|
||||||
message +=
|
|
||||||
"Are you sure that you want to restart the game? All progress from the last checkpoint will be lost.";
|
|
||||||
isCampaign = true;
|
|
||||||
buttonsType = MyMessageBoxButtonsType.YES_NO;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
message += "Save changes before restarting game?";
|
|
||||||
}
|
|
||||||
|
|
||||||
var saveMenu = MyGuiSandbox.CreateMessageBox(buttonType: buttonsType, messageText: new(message),
|
|
||||||
messageCaption: MyTexts.Get(
|
|
||||||
MyCommonTexts.MessageBoxCaptionPleaseConfirm),
|
|
||||||
callback: ShowSaveMenuCallback,
|
|
||||||
cancelButtonText: MyStringId.GetOrCompute("Don't Restart"));
|
|
||||||
saveMenu.InstantClose = false;
|
|
||||||
MyGuiSandbox.AddScreen(saveMenu);
|
|
||||||
|
|
||||||
void ShowSaveMenuCallback(ResultEnum callbackReturn)
|
|
||||||
{
|
|
||||||
if (isCampaign)
|
|
||||||
{
|
|
||||||
if (callbackReturn == ResultEnum.YES)
|
|
||||||
afterMenu();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (callbackReturn)
|
|
||||||
{
|
|
||||||
case ResultEnum.YES:
|
|
||||||
MyAsyncSaving.Start(delegate
|
|
||||||
{
|
|
||||||
MySandboxGame.Static.OnScreenshotTaken +=
|
|
||||||
UnloadAndExitAfterScreenshotWasTaken;
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ResultEnum.NO:
|
|
||||||
MyAudio.Static.Mute = true;
|
|
||||||
MyAudio.Static.StopMusic();
|
|
||||||
afterMenu();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void UnloadAndExitAfterScreenshotWasTaken(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
MySandboxGame.Static.OnScreenshotTaken -= UnloadAndExitAfterScreenshotWasTaken;
|
|
||||||
afterMenu();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
@@ -1,123 +0,0 @@
|
|||||||
using Sandbox;
|
|
||||||
using Sandbox.Game.Gui;
|
|
||||||
using Sandbox.Game.Localization;
|
|
||||||
using Sandbox.Graphics.GUI;
|
|
||||||
using VRage;
|
|
||||||
using VRage.Utils;
|
|
||||||
using VRageMath;
|
|
||||||
using Color = VRageMath.Color;
|
|
||||||
|
|
||||||
// ReSharper disable VirtualMemberCallInConstructor
|
|
||||||
#pragma warning disable 618
|
|
||||||
|
|
||||||
namespace PluginLoader.GUI;
|
|
||||||
|
|
||||||
internal class NameDialog : MyGuiScreenDebugBase
|
|
||||||
{
|
|
||||||
private readonly string caption;
|
|
||||||
private readonly string defaultName;
|
|
||||||
private readonly int maxLength;
|
|
||||||
|
|
||||||
private readonly Action<string> onOk;
|
|
||||||
private MyGuiControlButton cancelButton;
|
|
||||||
private MyGuiControlTextbox nameBox;
|
|
||||||
private MyGuiControlButton okButton;
|
|
||||||
|
|
||||||
public NameDialog(
|
|
||||||
Action<string> onOk,
|
|
||||||
string caption = "Name",
|
|
||||||
string defaultName = "",
|
|
||||||
int maxLength = 40)
|
|
||||||
: base(new(0.5f, 0.5f), new Vector2(0.5f, 0.28f),
|
|
||||||
MyGuiConstants.SCREEN_BACKGROUND_COLOR * MySandboxGame.Config.UIBkOpacity, true)
|
|
||||||
{
|
|
||||||
this.onOk = onOk;
|
|
||||||
this.caption = caption;
|
|
||||||
this.defaultName = defaultName;
|
|
||||||
this.maxLength = maxLength;
|
|
||||||
|
|
||||||
RecreateControls(true);
|
|
||||||
|
|
||||||
CanBeHidden = true;
|
|
||||||
CanHideOthers = true;
|
|
||||||
CloseButtonEnabled = true;
|
|
||||||
|
|
||||||
m_onEnterCallback = ReturnOk;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Vector2 DialogSize => m_size ?? Vector2.One;
|
|
||||||
|
|
||||||
public override void RecreateControls(bool constructor)
|
|
||||||
{
|
|
||||||
base.RecreateControls(constructor);
|
|
||||||
|
|
||||||
AddCaption(caption, Color.White.ToVector4(), new Vector2(0.0f, 0.003f));
|
|
||||||
|
|
||||||
var controlSeparatorList1 = new MyGuiControlSeparatorList();
|
|
||||||
controlSeparatorList1.AddHorizontal(new(-0.39f * DialogSize.X, -0.5f * DialogSize.Y + 0.075f),
|
|
||||||
DialogSize.X * 0.78f);
|
|
||||||
Controls.Add(controlSeparatorList1);
|
|
||||||
|
|
||||||
var controlSeparatorList2 = new MyGuiControlSeparatorList();
|
|
||||||
controlSeparatorList2.AddHorizontal(new(-0.39f * DialogSize.X, +0.5f * DialogSize.Y - 0.123f),
|
|
||||||
DialogSize.X * 0.78f);
|
|
||||||
Controls.Add(controlSeparatorList2);
|
|
||||||
|
|
||||||
nameBox = new(new Vector2(0.0f, -0.027f), maxLength: maxLength)
|
|
||||||
{
|
|
||||||
Text = defaultName,
|
|
||||||
Size = new(0.385f, 1f)
|
|
||||||
};
|
|
||||||
nameBox.SelectAll();
|
|
||||||
Controls.Add(nameBox);
|
|
||||||
|
|
||||||
okButton = new(originAlign: MyGuiDrawAlignEnum.HORISONTAL_RIGHT_AND_VERTICAL_CENTER,
|
|
||||||
text: MyTexts.Get(MyCommonTexts.Ok), onButtonClick: OnOk);
|
|
||||||
cancelButton = new(originAlign: MyGuiDrawAlignEnum.HORISONTAL_LEFT_AND_VERTICAL_CENTER,
|
|
||||||
text: MyTexts.Get(MyCommonTexts.Cancel), onButtonClick: OnCancel);
|
|
||||||
|
|
||||||
var okPosition = new Vector2(0.001f, 0.5f * DialogSize.Y - 0.071f);
|
|
||||||
var halfDistance = new Vector2(0.018f, 0.0f);
|
|
||||||
|
|
||||||
okButton.Position = okPosition - halfDistance;
|
|
||||||
cancelButton.Position = okPosition + halfDistance;
|
|
||||||
|
|
||||||
okButton.SetToolTip(MyTexts.GetString(MySpaceTexts.ToolTipNewsletter_Ok));
|
|
||||||
cancelButton.SetToolTip(MyTexts.GetString(MySpaceTexts.ToolTipOptionsSpace_Cancel));
|
|
||||||
|
|
||||||
Controls.Add(okButton);
|
|
||||||
Controls.Add(cancelButton);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CallResultCallback(string text)
|
|
||||||
{
|
|
||||||
if (text == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
onOk(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ReturnOk()
|
|
||||||
{
|
|
||||||
if (nameBox.GetTextLength() <= 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
CallResultCallback(nameBox.Text);
|
|
||||||
CloseScreen();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnOk(MyGuiControlButton button)
|
|
||||||
{
|
|
||||||
ReturnOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnCancel(MyGuiControlButton button)
|
|
||||||
{
|
|
||||||
CloseScreen();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string GetFriendlyName()
|
|
||||||
{
|
|
||||||
return "NameDialog";
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,90 +0,0 @@
|
|||||||
using PluginLoader.Stats;
|
|
||||||
using Sandbox.Graphics.GUI;
|
|
||||||
using VRageMath;
|
|
||||||
|
|
||||||
namespace PluginLoader.GUI;
|
|
||||||
|
|
||||||
public static class PlayerConsent
|
|
||||||
{
|
|
||||||
public static bool ConsentRequested => !string.IsNullOrEmpty(Main.Instance.Config.DataHandlingConsentDate);
|
|
||||||
|
|
||||||
public static bool ConsentGiven => Main.Instance.Config.DataHandlingConsent;
|
|
||||||
public static event Action OnConsentChanged;
|
|
||||||
|
|
||||||
public static void ShowDialog(Action continuation = null)
|
|
||||||
{
|
|
||||||
MyGuiSandbox.AddScreen(
|
|
||||||
ConfirmationDialog.CreateMessageBox(buttonType: MyMessageBoxButtonsType.YES_NO_CANCEL,
|
|
||||||
messageText: new(
|
|
||||||
" Would you like to rate plugins and inform developers?\r\n" +
|
|
||||||
"\r\n" +
|
|
||||||
"\r\n" +
|
|
||||||
"YES: Plugin Loader will send the list of enabled plugins to our server\r\n" +
|
|
||||||
" each time the game starts. Your Steam ID is sent only in hashed form,\r\n" +
|
|
||||||
" which makes it hard to identify you. Plugin usage statistics is kept\r\n" +
|
|
||||||
" for up to 90 days. Votes on plugins are preserved indefinitely.\r\n" +
|
|
||||||
" Server log files and database backups may be kept up to 90 days.\r\n" +
|
|
||||||
" Location of data storage: European Union\r\n" +
|
|
||||||
"\r\n" +
|
|
||||||
"\r\n" +
|
|
||||||
"NO: None of your data will be sent to nor stored on our statistics server.\r\n" +
|
|
||||||
" Plugin Loader will still connect to download the statistics shown.\r\n"),
|
|
||||||
size: new Vector2(0.6f, 0.6f),
|
|
||||||
messageCaption: new("Consent"),
|
|
||||||
callback: result => GetConfirmation(result, continuation)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void GetConfirmation(MyGuiScreenMessageBox.ResultEnum result, Action continuation)
|
|
||||||
{
|
|
||||||
if (result == MyGuiScreenMessageBox.ResultEnum.CANCEL)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var consent = result == MyGuiScreenMessageBox.ResultEnum.YES;
|
|
||||||
|
|
||||||
var consentWithdrawn = ConsentRequested && ConsentGiven && !consent;
|
|
||||||
if (consentWithdrawn)
|
|
||||||
{
|
|
||||||
MyGuiSandbox.AddScreen(MyGuiSandbox.CreateMessageBox(MyMessageBoxStyleEnum.Info,
|
|
||||||
MyMessageBoxButtonsType.YES_NO_CANCEL,
|
|
||||||
new(
|
|
||||||
"Are you sure to withdraw your consent to data handling?\r\n\r\nDoing so would irrecoverably remove all your votes\r\nand usage data from our statistics server."),
|
|
||||||
new("Confirm consent withdrawal"),
|
|
||||||
callback: res =>
|
|
||||||
StoreConsent(res, false, continuation)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
StoreConsent(MyGuiScreenMessageBox.ResultEnum.YES, consent, continuation);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void StoreConsent(MyGuiScreenMessageBox.ResultEnum confirmationResult, bool consent,
|
|
||||||
Action continuation)
|
|
||||||
{
|
|
||||||
if (confirmationResult != MyGuiScreenMessageBox.ResultEnum.YES)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (ConsentRequested && consent == ConsentGiven)
|
|
||||||
{
|
|
||||||
continuation?.Invoke();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!StatsClient.Consent(consent))
|
|
||||||
{
|
|
||||||
LogFile.Log.Debug("Failed to register player consent on statistics server");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var config = Main.Instance.Config;
|
|
||||||
config.DataHandlingConsentDate = Tools.Tools.FormatDateIso8601(DateTime.Today);
|
|
||||||
config.DataHandlingConsent = consent;
|
|
||||||
config.Save();
|
|
||||||
|
|
||||||
if (consent)
|
|
||||||
StatsClient.Track(Main.Instance.TrackablePluginIds);
|
|
||||||
|
|
||||||
OnConsentChanged?.Invoke();
|
|
||||||
|
|
||||||
continuation?.Invoke();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,439 +0,0 @@
|
|||||||
using PluginLoader.Data;
|
|
||||||
using PluginLoader.GUI.GuiControls;
|
|
||||||
using PluginLoader.Stats;
|
|
||||||
using PluginLoader.Stats.Model;
|
|
||||||
using Sandbox.Graphics.GUI;
|
|
||||||
using VRage.Game;
|
|
||||||
using VRage.Utils;
|
|
||||||
using VRageMath;
|
|
||||||
|
|
||||||
namespace PluginLoader.GUI;
|
|
||||||
|
|
||||||
public class PluginDetailsPanel : MyGuiControlParent
|
|
||||||
{
|
|
||||||
private readonly PluginStat dummyStat = new();
|
|
||||||
|
|
||||||
private readonly MyGuiScreenPluginConfig pluginsDialog;
|
|
||||||
private MyGuiControlLabel authorLabel;
|
|
||||||
private MyGuiControlLabel authorText;
|
|
||||||
private MyGuiControlButton configButton;
|
|
||||||
private MyGuiControlCompositePanel descriptionPanel;
|
|
||||||
private MyGuiControlMultilineText descriptionText;
|
|
||||||
private MyGuiControlButton downvoteButton;
|
|
||||||
private MyGuiControlLabel downvoteCountText;
|
|
||||||
private MyGuiControlImage downvoteIcon;
|
|
||||||
private MyGuiControlCheckbox enableCheckbox;
|
|
||||||
private MyGuiControlLabel enableLabel;
|
|
||||||
private MyGuiControlButton infoButton;
|
|
||||||
private PluginInstance instance;
|
|
||||||
|
|
||||||
// Layout management
|
|
||||||
private MyLayoutTable layoutTable;
|
|
||||||
|
|
||||||
// Plugin currently loaded into the panel or null if none are loaded
|
|
||||||
private PluginData plugin;
|
|
||||||
|
|
||||||
// Panel controls
|
|
||||||
private MyGuiControlLabel pluginNameLabel;
|
|
||||||
private MyGuiControlLabel pluginNameText;
|
|
||||||
private RatingControl ratingControl;
|
|
||||||
private MyGuiControlLabel ratingLabel;
|
|
||||||
private MyGuiControlLabel statusLabel;
|
|
||||||
private MyGuiControlLabel statusText;
|
|
||||||
private MyGuiControlButton upvoteButton;
|
|
||||||
private MyGuiControlLabel upvoteCountText;
|
|
||||||
private MyGuiControlImage upvoteIcon;
|
|
||||||
private MyGuiControlLabel usageLabel;
|
|
||||||
private MyGuiControlLabel usageText;
|
|
||||||
private MyGuiControlLabel versionLabel;
|
|
||||||
private MyGuiControlLabel versionText;
|
|
||||||
|
|
||||||
public PluginDetailsPanel(MyGuiScreenPluginConfig dialog)
|
|
||||||
{
|
|
||||||
pluginsDialog = dialog;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PluginData Plugin
|
|
||||||
{
|
|
||||||
get => plugin;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (ReferenceEquals(value, Plugin))
|
|
||||||
return;
|
|
||||||
|
|
||||||
plugin = value;
|
|
||||||
|
|
||||||
if (plugin == null)
|
|
||||||
{
|
|
||||||
DisableControls();
|
|
||||||
ClearPluginData();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Main.Instance.TryGetPluginInstance(plugin.Id, out var instance))
|
|
||||||
this.instance = instance;
|
|
||||||
else
|
|
||||||
this.instance = null;
|
|
||||||
|
|
||||||
EnableControls();
|
|
||||||
LoadPluginData();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private PluginStat PluginStat => pluginsDialog.PluginStats?.Stats.GetValueOrDefault(plugin.Id) ?? dummyStat;
|
|
||||||
public event Action<PluginData, bool> OnPluginToggled;
|
|
||||||
|
|
||||||
private void DisableControls()
|
|
||||||
{
|
|
||||||
foreach (var control in Controls)
|
|
||||||
control.Enabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EnableControls()
|
|
||||||
{
|
|
||||||
foreach (var control in Controls)
|
|
||||||
control.Enabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ClearPluginData()
|
|
||||||
{
|
|
||||||
pluginNameText.Text = "";
|
|
||||||
authorText.Text = "";
|
|
||||||
versionText.Text = "";
|
|
||||||
statusText.Text = "";
|
|
||||||
usageText.Text = "";
|
|
||||||
ratingControl.Value = 0;
|
|
||||||
upvoteButton.Checked = false;
|
|
||||||
downvoteButton.Checked = false;
|
|
||||||
descriptionText.Text.Clear();
|
|
||||||
enableCheckbox.IsChecked = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void LoadPluginData()
|
|
||||||
{
|
|
||||||
if (plugin == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var stat = PluginStat;
|
|
||||||
var vote = stat.Vote;
|
|
||||||
var nonLocal = !plugin.IsLocal;
|
|
||||||
var canVote = (plugin.Enabled || stat.Tried) && nonLocal;
|
|
||||||
var showVotes = canVote || nonLocal;
|
|
||||||
|
|
||||||
pluginNameText.Text = plugin.FriendlyName ?? "N/A";
|
|
||||||
|
|
||||||
authorText.Text = plugin.Author ?? (plugin.IsLocal ? "Local" : "N/A");
|
|
||||||
|
|
||||||
versionText.Text = plugin.Version?.ToString() ?? "N/A";
|
|
||||||
|
|
||||||
statusLabel.Visible = nonLocal;
|
|
||||||
statusText.Visible = nonLocal;
|
|
||||||
statusText.Text = plugin.Status == PluginStatus.None
|
|
||||||
? plugin.Enabled ? "Up to date" : "N/A"
|
|
||||||
: plugin.StatusString;
|
|
||||||
|
|
||||||
usageLabel.Visible = nonLocal;
|
|
||||||
usageText.Visible = nonLocal;
|
|
||||||
usageText.Text = stat.Players.ToString();
|
|
||||||
|
|
||||||
ratingLabel.Visible = showVotes;
|
|
||||||
|
|
||||||
upvoteIcon.Visible = showVotes;
|
|
||||||
upvoteButton.Visible = canVote;
|
|
||||||
upvoteButton.Checked = vote > 0;
|
|
||||||
upvoteCountText.Visible = showVotes;
|
|
||||||
upvoteCountText.Text = $"{stat.Upvotes}";
|
|
||||||
|
|
||||||
downvoteIcon.Visible = showVotes;
|
|
||||||
downvoteButton.Visible = canVote;
|
|
||||||
downvoteButton.Checked = vote < 0;
|
|
||||||
downvoteCountText.Visible = showVotes;
|
|
||||||
downvoteCountText.Text = $"{stat.Downvotes}";
|
|
||||||
|
|
||||||
ratingControl.Value = stat.Rating;
|
|
||||||
|
|
||||||
plugin.GetDescriptionText(descriptionText);
|
|
||||||
descriptionPanel.Visible = descriptionText.Visible;
|
|
||||||
|
|
||||||
enableCheckbox.IsChecked = pluginsDialog.AfterRebootEnableFlags[plugin.Id];
|
|
||||||
|
|
||||||
configButton.Enabled = instance != null && instance.HasConfigDialog;
|
|
||||||
configButton.Visible = instance != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual void CreateControls(Vector2 rightSideOrigin)
|
|
||||||
{
|
|
||||||
// Plugin name
|
|
||||||
pluginNameLabel = new()
|
|
||||||
{
|
|
||||||
Text = "Name",
|
|
||||||
OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_TOP
|
|
||||||
};
|
|
||||||
pluginNameText = new()
|
|
||||||
{
|
|
||||||
OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_TOP
|
|
||||||
};
|
|
||||||
|
|
||||||
// Author
|
|
||||||
authorLabel = new()
|
|
||||||
{
|
|
||||||
Text = "Author",
|
|
||||||
OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_TOP
|
|
||||||
};
|
|
||||||
authorText = new()
|
|
||||||
{
|
|
||||||
OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_TOP
|
|
||||||
};
|
|
||||||
|
|
||||||
// Version
|
|
||||||
versionLabel = new()
|
|
||||||
{
|
|
||||||
Text = "Version",
|
|
||||||
OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_TOP
|
|
||||||
};
|
|
||||||
versionText = new()
|
|
||||||
{
|
|
||||||
OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_TOP
|
|
||||||
};
|
|
||||||
|
|
||||||
// Status
|
|
||||||
statusLabel = new()
|
|
||||||
{
|
|
||||||
Text = "Status",
|
|
||||||
OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_TOP
|
|
||||||
};
|
|
||||||
statusText = new()
|
|
||||||
{
|
|
||||||
OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_TOP
|
|
||||||
};
|
|
||||||
|
|
||||||
// Usage
|
|
||||||
usageLabel = new()
|
|
||||||
{
|
|
||||||
Text = "Usage",
|
|
||||||
OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_TOP
|
|
||||||
};
|
|
||||||
usageText = new()
|
|
||||||
{
|
|
||||||
OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_TOP
|
|
||||||
};
|
|
||||||
|
|
||||||
// Rating
|
|
||||||
ratingLabel = new()
|
|
||||||
{
|
|
||||||
Text = "Rating",
|
|
||||||
OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_TOP
|
|
||||||
};
|
|
||||||
|
|
||||||
upvoteButton = new(null, MyGuiControlButtonStyleEnum.Rectangular, onButtonClick: OnRateUpClicked,
|
|
||||||
size: new Vector2(0.03f))
|
|
||||||
{
|
|
||||||
CanHaveFocus = false
|
|
||||||
};
|
|
||||||
upvoteIcon = CreateRateIcon(upvoteButton, "Textures\\GUI\\Icons\\Blueprints\\like_test.png");
|
|
||||||
upvoteIcon.CanHaveFocus = false;
|
|
||||||
upvoteCountText = new()
|
|
||||||
{
|
|
||||||
OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_TOP
|
|
||||||
};
|
|
||||||
|
|
||||||
downvoteButton = new(null, MyGuiControlButtonStyleEnum.Rectangular, onButtonClick: OnRateDownClicked,
|
|
||||||
size: new Vector2(0.03f))
|
|
||||||
{
|
|
||||||
CanHaveFocus = false
|
|
||||||
};
|
|
||||||
downvoteIcon = CreateRateIcon(downvoteButton, "Textures\\GUI\\Icons\\Blueprints\\dislike_test.png");
|
|
||||||
downvoteIcon.CanHaveFocus = false;
|
|
||||||
downvoteCountText = new()
|
|
||||||
{
|
|
||||||
OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_TOP
|
|
||||||
};
|
|
||||||
|
|
||||||
ratingControl = new()
|
|
||||||
{
|
|
||||||
OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_TOP,
|
|
||||||
Visible = false // FIXME: Make the rating (stars) visible later! Its positioning should already be good.
|
|
||||||
};
|
|
||||||
|
|
||||||
// Plugin description
|
|
||||||
descriptionText = new()
|
|
||||||
{
|
|
||||||
Name = "DescriptionText",
|
|
||||||
OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_TOP,
|
|
||||||
TextAlign = MyGuiDrawAlignEnum.HORISONTAL_LEFT_AND_VERTICAL_TOP,
|
|
||||||
TextBoxAlign = MyGuiDrawAlignEnum.HORISONTAL_LEFT_AND_VERTICAL_TOP
|
|
||||||
};
|
|
||||||
descriptionText.OnLinkClicked += (x, url) => MyGuiSandbox.OpenUrl(url, UrlOpenMode.SteamOrExternalWithConfirm);
|
|
||||||
descriptionPanel = new()
|
|
||||||
{
|
|
||||||
BackgroundTexture = MyGuiConstants.TEXTURE_RECTANGLE_DARK_BORDER
|
|
||||||
};
|
|
||||||
|
|
||||||
// Enable checkbox
|
|
||||||
enableLabel = new()
|
|
||||||
{
|
|
||||||
Text = "Enabled",
|
|
||||||
OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_TOP
|
|
||||||
};
|
|
||||||
enableCheckbox = new(toolTip: "Enables loading the plugin when SE is started.")
|
|
||||||
{
|
|
||||||
OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_TOP,
|
|
||||||
Enabled = false
|
|
||||||
};
|
|
||||||
enableCheckbox.IsCheckedChanged += TogglePlugin;
|
|
||||||
|
|
||||||
// Info button
|
|
||||||
infoButton = new(onButtonClick: _ => Plugin?.Show())
|
|
||||||
{
|
|
||||||
OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_TOP,
|
|
||||||
Text = "Plugin Info"
|
|
||||||
};
|
|
||||||
|
|
||||||
// Plugin config button
|
|
||||||
configButton = new(onButtonClick: _ => instance?.OpenConfig())
|
|
||||||
{
|
|
||||||
OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_TOP,
|
|
||||||
Text = "Plugin Config"
|
|
||||||
};
|
|
||||||
|
|
||||||
LayoutControls(rightSideOrigin);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LayoutControls(Vector2 rightSideOrigin)
|
|
||||||
{
|
|
||||||
layoutTable = new(this, rightSideOrigin, new(1f, 1f));
|
|
||||||
layoutTable.SetColumnWidths(168f, 468f);
|
|
||||||
layoutTable.SetRowHeights(60f, 60f, 60f, 60f, 60f, 60f, 420f, 60f, 60f);
|
|
||||||
|
|
||||||
var row = 0;
|
|
||||||
|
|
||||||
layoutTable.Add(pluginNameLabel, MyAlignH.Left, MyAlignV.Center, row, 0);
|
|
||||||
layoutTable.Add(pluginNameText, MyAlignH.Left, MyAlignV.Center, row, 1);
|
|
||||||
row++;
|
|
||||||
|
|
||||||
layoutTable.Add(authorLabel, MyAlignH.Left, MyAlignV.Center, row, 0);
|
|
||||||
layoutTable.Add(authorText, MyAlignH.Left, MyAlignV.Center, row, 1);
|
|
||||||
row++;
|
|
||||||
|
|
||||||
layoutTable.Add(versionLabel, MyAlignH.Left, MyAlignV.Center, row, 0);
|
|
||||||
layoutTable.Add(versionText, MyAlignH.Left, MyAlignV.Center, row, 1);
|
|
||||||
row++;
|
|
||||||
|
|
||||||
layoutTable.Add(statusLabel, MyAlignH.Left, MyAlignV.Center, row, 0);
|
|
||||||
layoutTable.Add(statusText, MyAlignH.Left, MyAlignV.Center, row, 1);
|
|
||||||
row++;
|
|
||||||
|
|
||||||
layoutTable.Add(usageLabel, MyAlignH.Left, MyAlignV.Center, row, 0);
|
|
||||||
layoutTable.Add(usageText, MyAlignH.Left, MyAlignV.Center, row, 1);
|
|
||||||
row++;
|
|
||||||
|
|
||||||
layoutTable.Add(ratingLabel, MyAlignH.Left, MyAlignV.Center, row, 0);
|
|
||||||
layoutTable.Add(upvoteCountText, MyAlignH.Left, MyAlignV.Center, row, 1);
|
|
||||||
layoutTable.Add(upvoteButton, MyAlignH.Left, MyAlignV.Center, row, 1);
|
|
||||||
layoutTable.Add(upvoteIcon, MyAlignH.Left, MyAlignV.Center, row, 1);
|
|
||||||
layoutTable.Add(downvoteCountText, MyAlignH.Left, MyAlignV.Center, row, 1);
|
|
||||||
layoutTable.Add(downvoteButton, MyAlignH.Left, MyAlignV.Center, row, 1);
|
|
||||||
layoutTable.Add(downvoteIcon, MyAlignH.Left, MyAlignV.Center, row, 1);
|
|
||||||
layoutTable.Add(ratingControl, MyAlignH.Left, MyAlignV.Center, row, 1);
|
|
||||||
|
|
||||||
const float counterWidth = 0.05f;
|
|
||||||
const float spacing = 0.005f;
|
|
||||||
var buttonWidth = upvoteButton.Size.X;
|
|
||||||
var voteWidth = buttonWidth + spacing + counterWidth + 3 * spacing;
|
|
||||||
var buttonToIconOffset = new Vector2(0.004f, -0.001f);
|
|
||||||
upvoteIcon.Position = upvoteButton.Position + buttonToIconOffset;
|
|
||||||
upvoteCountText.Position = upvoteButton.Position + new Vector2(buttonWidth + spacing, 0f);
|
|
||||||
downvoteButton.Position = upvoteButton.Position + new Vector2(voteWidth, 0f);
|
|
||||||
downvoteIcon.Position = downvoteButton.Position + buttonToIconOffset;
|
|
||||||
downvoteCountText.Position = downvoteButton.Position + new Vector2(buttonWidth + spacing, 0f);
|
|
||||||
ratingControl.Position = downvoteButton.Position + new Vector2(voteWidth, 0f);
|
|
||||||
row++;
|
|
||||||
|
|
||||||
layoutTable.AddWithSize(descriptionPanel, MyAlignH.Center, MyAlignV.Top, row, 0, 1, 2);
|
|
||||||
layoutTable.AddWithSize(descriptionText, MyAlignH.Center, MyAlignV.Center, row, 0, 1, 2);
|
|
||||||
row++;
|
|
||||||
|
|
||||||
layoutTable.Add(enableLabel, MyAlignH.Left, MyAlignV.Center, row, 0);
|
|
||||||
layoutTable.Add(enableCheckbox, MyAlignH.Left, MyAlignV.Center, row, 1);
|
|
||||||
row++;
|
|
||||||
|
|
||||||
const float infoConfigSpacing = 0.015f;
|
|
||||||
layoutTable.AddWithSize(infoButton, MyAlignH.Right, MyAlignV.Center, row, 0, 1, 2);
|
|
||||||
layoutTable.AddWithSize(configButton, MyAlignH.Right, MyAlignV.Center, row, 0, 1, 2);
|
|
||||||
configButton.Position += new Vector2(0f, infoConfigSpacing);
|
|
||||||
infoButton.Position = configButton.Position + new Vector2(-configButton.Size.X - infoConfigSpacing, 0);
|
|
||||||
// row++;
|
|
||||||
|
|
||||||
var border = 0.002f * Vector2.One;
|
|
||||||
descriptionPanel.Position -= border;
|
|
||||||
descriptionPanel.Size += 2 * border;
|
|
||||||
|
|
||||||
DisableControls();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TogglePlugin(MyGuiControlCheckbox obj)
|
|
||||||
{
|
|
||||||
if (plugin == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
OnPluginToggled?.Invoke(plugin, enableCheckbox.IsChecked);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnRateUpClicked(MyGuiControlButton button)
|
|
||||||
{
|
|
||||||
Vote(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnRateDownClicked(MyGuiControlButton button)
|
|
||||||
{
|
|
||||||
Vote(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Vote(int vote)
|
|
||||||
{
|
|
||||||
if (PlayerConsent.ConsentGiven)
|
|
||||||
StoreVote(vote);
|
|
||||||
else
|
|
||||||
PlayerConsent.ShowDialog(() => StoreVote(vote));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void StoreVote(int vote)
|
|
||||||
{
|
|
||||||
if (!PlayerConsent.ConsentGiven || pluginsDialog.PluginStats == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var originalStat = PluginStat;
|
|
||||||
if (originalStat.Vote == vote)
|
|
||||||
vote = 0;
|
|
||||||
|
|
||||||
var updatedStat = StatsClient.Vote(plugin.Id, vote);
|
|
||||||
if (updatedStat == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
pluginsDialog.PluginStats.Stats[plugin.Id] = updatedStat;
|
|
||||||
LoadPluginData();
|
|
||||||
}
|
|
||||||
|
|
||||||
// From Sandbox.Game.Screens.MyGuiScreenNewWorkshopGame
|
|
||||||
|
|
||||||
#region Vote buttons
|
|
||||||
|
|
||||||
private MyGuiControlImage CreateRateIcon(MyGuiControlButton button, string texture)
|
|
||||||
{
|
|
||||||
var myGuiControlImage = new MyGuiControlImage(null, null, null, null, new[] { texture });
|
|
||||||
AdjustButtonForIcon(button, myGuiControlImage);
|
|
||||||
myGuiControlImage.Size = button.Size * 0.6f;
|
|
||||||
return myGuiControlImage;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AdjustButtonForIcon(MyGuiControlButton button, MyGuiControlImage icon)
|
|
||||||
{
|
|
||||||
button.Size = new(button.Size.X, button.Size.X * 4f / 3f);
|
|
||||||
button.HighlightChanged += delegate(MyGuiControlBase control)
|
|
||||||
{
|
|
||||||
icon.ColorMask = control.HasHighlight ? MyGuiConstants.HIGHLIGHT_TEXT_COLOR : Vector4.One;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
@@ -1,96 +0,0 @@
|
|||||||
using PluginLoader.Data;
|
|
||||||
|
|
||||||
namespace PluginLoader.GUI;
|
|
||||||
|
|
||||||
public class ProfilesDialog : TableDialogBase
|
|
||||||
{
|
|
||||||
private readonly Action<Profile> onProfileLoaded;
|
|
||||||
|
|
||||||
public ProfilesDialog(string caption, Action<Profile> onProfileLoaded) : base(caption)
|
|
||||||
{
|
|
||||||
this.onProfileLoaded = onProfileLoaded;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static PluginConfig Config => Main.Instance.Config;
|
|
||||||
private static Dictionary<string, Profile> ProfileMap => Config.ProfileMap;
|
|
||||||
private static PluginList PluginList => Main.Instance.List;
|
|
||||||
|
|
||||||
protected override string ItemName => "profile";
|
|
||||||
protected override string[] ColumnHeaders => new[] { "Name", "Enabled plugins and mods" };
|
|
||||||
protected override float[] ColumnWidths => new[] { 0.55f, 0.43f };
|
|
||||||
|
|
||||||
protected override object[] ExampleValues => new object[] { null, 0 };
|
|
||||||
|
|
||||||
protected override IEnumerable<string> IterItemKeys()
|
|
||||||
{
|
|
||||||
return ProfileMap.Keys.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override ItemView GetItemView(string key)
|
|
||||||
{
|
|
||||||
if (!ProfileMap.TryGetValue(key, out var profile))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var locals = 0;
|
|
||||||
var plugins = 0;
|
|
||||||
var mods = 0;
|
|
||||||
foreach (var id in profile.Plugins)
|
|
||||||
{
|
|
||||||
if (!PluginList.TryGetPlugin(id, out var plugin))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
switch (plugin)
|
|
||||||
{
|
|
||||||
case ModPlugin:
|
|
||||||
mods++;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case LocalPlugin:
|
|
||||||
locals++;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
plugins++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var infoItems = new List<string>();
|
|
||||||
if (locals > 0)
|
|
||||||
infoItems.Add(locals > 1 ? $"{locals} local plugins" : "1 local plugin");
|
|
||||||
if (plugins > 0)
|
|
||||||
infoItems.Add(plugins > 1 ? $"{plugins} plugins" : "1 plugin");
|
|
||||||
if (mods > 0)
|
|
||||||
infoItems.Add(mods > 1 ? $"{mods} mods" : "1 mod");
|
|
||||||
|
|
||||||
var info = string.Join(", ", infoItems);
|
|
||||||
var labels = new[] { profile.Name, info };
|
|
||||||
|
|
||||||
var total = locals + plugins + mods;
|
|
||||||
var values = new object[] { null, total };
|
|
||||||
|
|
||||||
return new(labels, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnLoad(string key)
|
|
||||||
{
|
|
||||||
if (!ProfileMap.TryGetValue(key, out var profile))
|
|
||||||
return;
|
|
||||||
|
|
||||||
onProfileLoaded(profile);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnRenamed(string key, string name)
|
|
||||||
{
|
|
||||||
if (!ProfileMap.TryGetValue(key, out var profile))
|
|
||||||
return;
|
|
||||||
|
|
||||||
profile.Name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnDelete(string key)
|
|
||||||
{
|
|
||||||
ProfileMap.Remove(key);
|
|
||||||
Config.Save();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,124 +0,0 @@
|
|||||||
using System.Reflection;
|
|
||||||
using VRage;
|
|
||||||
|
|
||||||
namespace PluginLoader.GUI;
|
|
||||||
|
|
||||||
public sealed class SplashScreen : Form
|
|
||||||
{
|
|
||||||
private const float barWidth = 0.98f; // 98% of width
|
|
||||||
private const float barHeight = 0.06f; // 6% of height
|
|
||||||
private readonly RectangleF bar;
|
|
||||||
private readonly PictureBox gifBox;
|
|
||||||
|
|
||||||
private readonly bool invalid;
|
|
||||||
private readonly Label lbl;
|
|
||||||
|
|
||||||
private float barValue = float.NaN;
|
|
||||||
|
|
||||||
public SplashScreen()
|
|
||||||
{
|
|
||||||
CheckForIllegalCrossThreadCalls = false;
|
|
||||||
if (!TryLoadImage(out var gif))
|
|
||||||
{
|
|
||||||
invalid = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Name = "SplashScreenPluginLoader";
|
|
||||||
TopMost = true;
|
|
||||||
FormBorderStyle = FormBorderStyle.None;
|
|
||||||
Size = new((int)(gif.Width * 1.65), (int)(gif.Height * 1.25));
|
|
||||||
BackColor = Color.Black;
|
|
||||||
UseWaitCursor = true;
|
|
||||||
ShowInTaskbar = false;
|
|
||||||
|
|
||||||
var barSize = new SizeF(Size.Width * barWidth, Size.Height * barHeight);
|
|
||||||
var padding = (1 - barWidth) * Size.Width * 0.5f;
|
|
||||||
var barStart = new PointF(padding, Size.Height - barSize.Height - padding);
|
|
||||||
bar = new(barStart, barSize);
|
|
||||||
|
|
||||||
var lblFont = new Font(FontFamily.GenericSansSerif, 12, FontStyle.Bold);
|
|
||||||
lbl = new()
|
|
||||||
{
|
|
||||||
Name = "PluginLoaderInfo",
|
|
||||||
Font = lblFont,
|
|
||||||
BackColor = Color.Black,
|
|
||||||
ForeColor = Color.White,
|
|
||||||
MaximumSize = Size,
|
|
||||||
Size = new(Size.Width, lblFont.Height),
|
|
||||||
TextAlign = ContentAlignment.MiddleCenter,
|
|
||||||
Location = new(0, (int)(barStart.Y - lblFont.Height - 1))
|
|
||||||
};
|
|
||||||
Controls.Add(lbl);
|
|
||||||
|
|
||||||
gifBox = new()
|
|
||||||
{
|
|
||||||
Name = "PluginLoaderAnimation",
|
|
||||||
Image = gif,
|
|
||||||
Size = Size,
|
|
||||||
AutoSize = false,
|
|
||||||
SizeMode = PictureBoxSizeMode.CenterImage
|
|
||||||
};
|
|
||||||
Controls.Add(gifBox);
|
|
||||||
|
|
||||||
gifBox.Paint += OnPictureBoxDraw;
|
|
||||||
|
|
||||||
CenterToScreen();
|
|
||||||
}
|
|
||||||
|
|
||||||
public object GameInfo { get; private set; }
|
|
||||||
|
|
||||||
private bool TryLoadImage(out Image img)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var myAssembly = Assembly.GetExecutingAssembly();
|
|
||||||
var myStream = myAssembly.GetManifestResourceStream("PluginLoader.splash.gif")!;
|
|
||||||
img = new Bitmap(myStream);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
img = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetText(string msg)
|
|
||||||
{
|
|
||||||
if (invalid)
|
|
||||||
return;
|
|
||||||
|
|
||||||
lbl.Text = msg;
|
|
||||||
barValue = float.NaN;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetBarValue(float percent = float.NaN)
|
|
||||||
{
|
|
||||||
if (invalid)
|
|
||||||
return;
|
|
||||||
|
|
||||||
barValue = percent;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPictureBoxDraw(object sender, PaintEventArgs e)
|
|
||||||
{
|
|
||||||
if (!float.IsNaN(barValue))
|
|
||||||
{
|
|
||||||
var graphics = e.Graphics;
|
|
||||||
graphics.FillRectangle(Brushes.DarkSlateGray, bar);
|
|
||||||
graphics.FillRectangle(Brushes.White, new RectangleF(bar.Location, new(bar.Width * barValue, bar.Height)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Delete()
|
|
||||||
{
|
|
||||||
if (invalid)
|
|
||||||
return;
|
|
||||||
|
|
||||||
gifBox.Paint -= OnPictureBoxDraw;
|
|
||||||
Close();
|
|
||||||
Dispose();
|
|
||||||
MyVRage.Platform.Windows.Window.ShowAndFocus();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,320 +0,0 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using Sandbox;
|
|
||||||
using Sandbox.Game.Gui;
|
|
||||||
using Sandbox.Game.Localization;
|
|
||||||
using Sandbox.Graphics.GUI;
|
|
||||||
using VRage;
|
|
||||||
using VRage.Game;
|
|
||||||
using VRage.Utils;
|
|
||||||
using VRageMath;
|
|
||||||
using Color = VRageMath.Color;
|
|
||||||
|
|
||||||
namespace PluginLoader.GUI;
|
|
||||||
|
|
||||||
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
|
|
||||||
public abstract class TableDialogBase : MyGuiScreenDebugBase
|
|
||||||
{
|
|
||||||
protected readonly string Caption;
|
|
||||||
protected readonly string DefaultKey;
|
|
||||||
protected readonly Dictionary<string, string> NamesByKey = new();
|
|
||||||
protected MyGuiControlButton CancelButton;
|
|
||||||
|
|
||||||
protected int ColumnCount;
|
|
||||||
protected MyGuiControlButton DeleteButton;
|
|
||||||
|
|
||||||
protected MyGuiControlButton LoadButton;
|
|
||||||
protected MyGuiControlButton RenameButton;
|
|
||||||
|
|
||||||
protected MyGuiControlTable Table;
|
|
||||||
|
|
||||||
protected TableDialogBase(
|
|
||||||
string caption,
|
|
||||||
string defaultKey = null)
|
|
||||||
: base(new(0.5f, 0.5f), new Vector2(1f, 0.8f),
|
|
||||||
MyGuiConstants.SCREEN_BACKGROUND_COLOR * MySandboxGame.Config.UIBkOpacity, true)
|
|
||||||
{
|
|
||||||
Caption = caption;
|
|
||||||
DefaultKey = defaultKey;
|
|
||||||
|
|
||||||
// ReSharper disable once VirtualMemberCallInConstructor
|
|
||||||
RecreateControls(true);
|
|
||||||
|
|
||||||
CanBeHidden = true;
|
|
||||||
CanHideOthers = true;
|
|
||||||
CloseButtonEnabled = true;
|
|
||||||
|
|
||||||
m_onEnterCallback = LoadAndClose;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract string ItemName { get; }
|
|
||||||
protected abstract string[] ColumnHeaders { get; }
|
|
||||||
protected abstract float[] ColumnWidths { get; }
|
|
||||||
protected abstract object[] ExampleValues { get; }
|
|
||||||
|
|
||||||
private Vector2 DialogSize => m_size ?? Vector2.One;
|
|
||||||
|
|
||||||
private string SelectedKey => Table.SelectedRow?.UserData as string;
|
|
||||||
|
|
||||||
public override string GetFriendlyName()
|
|
||||||
{
|
|
||||||
return "ListDialog";
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual string NormalizeName(string name)
|
|
||||||
{
|
|
||||||
return name.Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual int CompareNames(string a, string b)
|
|
||||||
{
|
|
||||||
return string.Compare(a, b, StringComparison.CurrentCultureIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract IEnumerable<string> IterItemKeys();
|
|
||||||
protected abstract ItemView GetItemView(string key);
|
|
||||||
|
|
||||||
protected abstract void OnLoad(string key);
|
|
||||||
protected abstract void OnRenamed(string key, string name);
|
|
||||||
protected abstract void OnDelete(string key);
|
|
||||||
|
|
||||||
public override void RecreateControls(bool constructor)
|
|
||||||
{
|
|
||||||
base.RecreateControls(constructor);
|
|
||||||
|
|
||||||
AddCaption(Caption, Color.White.ToVector4(), new Vector2(0.0f, 0.003f));
|
|
||||||
|
|
||||||
CreateTable();
|
|
||||||
CreateButtons();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CreateTable()
|
|
||||||
{
|
|
||||||
var columnHeaders = ColumnHeaders;
|
|
||||||
var columnWidths = ColumnWidths;
|
|
||||||
|
|
||||||
if (columnHeaders == null || columnWidths == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
ColumnCount = columnHeaders.Length;
|
|
||||||
|
|
||||||
Table = new()
|
|
||||||
{
|
|
||||||
Position = new(0.001f, -0.5f * DialogSize.Y + 0.1f),
|
|
||||||
Size = new(0.85f * DialogSize.X, DialogSize.Y - 0.25f),
|
|
||||||
OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_TOP,
|
|
||||||
ColumnsCount = ColumnCount,
|
|
||||||
VisibleRowsCount = 15
|
|
||||||
};
|
|
||||||
|
|
||||||
Table.SetCustomColumnWidths(columnWidths);
|
|
||||||
|
|
||||||
var exampleValues = ExampleValues;
|
|
||||||
for (var colIdx = 0; colIdx < ColumnCount; colIdx++)
|
|
||||||
{
|
|
||||||
Table.SetColumnName(colIdx, new(columnHeaders[colIdx]));
|
|
||||||
|
|
||||||
switch (exampleValues[colIdx])
|
|
||||||
{
|
|
||||||
case int _:
|
|
||||||
Table.SetColumnComparison(colIdx, CellIntComparison);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
Table.SetColumnComparison(colIdx, CellTextComparison);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AddItems();
|
|
||||||
|
|
||||||
Table.SortByColumn(0);
|
|
||||||
|
|
||||||
Table.ItemDoubleClicked += OnItemDoubleClicked;
|
|
||||||
|
|
||||||
Controls.Add(Table);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int CellTextComparison(MyGuiControlTable.Cell x, MyGuiControlTable.Cell y)
|
|
||||||
{
|
|
||||||
var a = NormalizeName(x.Text.ToString());
|
|
||||||
var b = NormalizeName(y.Text.ToString());
|
|
||||||
return CompareNames(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int CellIntComparison(MyGuiControlTable.Cell x, MyGuiControlTable.Cell y)
|
|
||||||
{
|
|
||||||
return (x.UserData as int? ?? 0) - (y.UserData as int? ?? 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CreateButtons()
|
|
||||||
{
|
|
||||||
LoadButton = new(
|
|
||||||
visualStyle: MyGuiControlButtonStyleEnum.Default,
|
|
||||||
originAlign: MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_CENTER,
|
|
||||||
text: new("Load"), onButtonClick: OnLoadButtonClick);
|
|
||||||
|
|
||||||
RenameButton = new(
|
|
||||||
visualStyle: MyGuiControlButtonStyleEnum.Small,
|
|
||||||
originAlign: MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_CENTER,
|
|
||||||
text: new("Rename"), onButtonClick: OnRenameButtonClick);
|
|
||||||
|
|
||||||
DeleteButton = new(
|
|
||||||
visualStyle: MyGuiControlButtonStyleEnum.Small,
|
|
||||||
originAlign: MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_CENTER,
|
|
||||||
text: new("Delete"), onButtonClick: OnDeleteButtonClick);
|
|
||||||
|
|
||||||
CancelButton = new(
|
|
||||||
visualStyle: MyGuiControlButtonStyleEnum.Default,
|
|
||||||
originAlign: MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_CENTER,
|
|
||||||
text: MyTexts.Get(MyCommonTexts.Cancel), onButtonClick: OnCancelButtonClick);
|
|
||||||
|
|
||||||
var xs = 0.85f * DialogSize.X;
|
|
||||||
var y = 0.5f * (DialogSize.Y - 0.15f);
|
|
||||||
LoadButton.Position = new(-0.39f * xs, y);
|
|
||||||
RenameButton.Position = new(-0.08f * xs, y);
|
|
||||||
DeleteButton.Position = new(0.08f * xs, y);
|
|
||||||
CancelButton.Position = new(0.39f * xs, y);
|
|
||||||
|
|
||||||
LoadButton.SetToolTip($"Loads the selected {ItemName}");
|
|
||||||
RenameButton.SetToolTip($"Renames the selected {ItemName}");
|
|
||||||
DeleteButton.SetToolTip($"Deletes the selected {ItemName}");
|
|
||||||
CancelButton.SetToolTip(MyTexts.GetString(MySpaceTexts.ToolTipOptionsSpace_Cancel));
|
|
||||||
|
|
||||||
Controls.Add(LoadButton);
|
|
||||||
Controls.Add(RenameButton);
|
|
||||||
Controls.Add(DeleteButton);
|
|
||||||
Controls.Add(CancelButton);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddItems()
|
|
||||||
{
|
|
||||||
NamesByKey.Clear();
|
|
||||||
|
|
||||||
foreach (var key in IterItemKeys())
|
|
||||||
AddRow(key);
|
|
||||||
|
|
||||||
if (TryFindRow(DefaultKey, out var rowIdx))
|
|
||||||
Table.SelectedRowIndex = rowIdx;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddRow(string key)
|
|
||||||
{
|
|
||||||
var view = GetItemView(key);
|
|
||||||
if (view == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var row = new MyGuiControlTable.Row(key);
|
|
||||||
for (var i = 0; i < ColumnCount; i++)
|
|
||||||
row.AddCell(new(view.Labels[i], view.Values[i]));
|
|
||||||
|
|
||||||
Table.Add(row);
|
|
||||||
NamesByKey[key] = view.Labels[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnItemDoubleClicked(MyGuiControlTable table, MyGuiControlTable.EventArgs args)
|
|
||||||
{
|
|
||||||
LoadAndClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnLoadButtonClick(MyGuiControlButton _)
|
|
||||||
{
|
|
||||||
LoadAndClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LoadAndClose()
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(SelectedKey))
|
|
||||||
return;
|
|
||||||
|
|
||||||
OnLoad(SelectedKey);
|
|
||||||
CloseScreen();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnCancelButtonClick(MyGuiControlButton _)
|
|
||||||
{
|
|
||||||
CloseScreen();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnRenameButtonClick(MyGuiControlButton _)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(SelectedKey))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!NamesByKey.TryGetValue(SelectedKey, out var oldName))
|
|
||||||
return;
|
|
||||||
|
|
||||||
MyGuiSandbox.AddScreen(new NameDialog(newName => OnNewNameSpecified(SelectedKey, newName),
|
|
||||||
$"Rename saved {ItemName}", oldName));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnNewNameSpecified(string key, string newName)
|
|
||||||
{
|
|
||||||
newName = NormalizeName(newName);
|
|
||||||
|
|
||||||
if (!TryFindRow(key, out var rowIdx))
|
|
||||||
return;
|
|
||||||
|
|
||||||
OnRenamed(key, newName);
|
|
||||||
|
|
||||||
var view = GetItemView(key);
|
|
||||||
|
|
||||||
NamesByKey[key] = view.Labels[0];
|
|
||||||
|
|
||||||
var row = Table.GetRow(rowIdx);
|
|
||||||
for (var colIdx = 0; colIdx < ColumnCount; colIdx++)
|
|
||||||
{
|
|
||||||
var cell = row.GetCell(colIdx);
|
|
||||||
var sb = cell.Text;
|
|
||||||
sb.Clear();
|
|
||||||
sb.Append(view.Labels[colIdx]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Table.Sort();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDeleteButtonClick(MyGuiControlButton _)
|
|
||||||
{
|
|
||||||
var key = SelectedKey;
|
|
||||||
if (string.IsNullOrEmpty(key))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var name = NamesByKey.GetValueOrDefault(key) ?? "?";
|
|
||||||
|
|
||||||
MyGuiSandbox.AddScreen(
|
|
||||||
MyGuiSandbox.CreateMessageBox(buttonType: MyMessageBoxButtonsType.YES_NO,
|
|
||||||
messageText: new(
|
|
||||||
$"Are you sure to delete this saved {ItemName}?\r\n\r\n{name}"),
|
|
||||||
messageCaption: new("Confirmation"),
|
|
||||||
callback: result => OnDeleteForSure(result, key)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDeleteForSure(MyGuiScreenMessageBox.ResultEnum result, string key)
|
|
||||||
{
|
|
||||||
if (result != MyGuiScreenMessageBox.ResultEnum.YES)
|
|
||||||
return;
|
|
||||||
|
|
||||||
NamesByKey.Remove(key);
|
|
||||||
|
|
||||||
if (TryFindRow(key, out var rowIdx))
|
|
||||||
Table.Remove(Table.GetRow(rowIdx));
|
|
||||||
|
|
||||||
OnDelete(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryFindRow(string key, out int index)
|
|
||||||
{
|
|
||||||
if (key == null)
|
|
||||||
{
|
|
||||||
index = -1;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var count = Table.RowsCount;
|
|
||||||
for (index = 0; index < count; index++)
|
|
||||||
if (Table.GetRow(index).UserData as string == key)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
index = -1;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,211 +0,0 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text;
|
|
||||||
using Windows.UI.Popups;
|
|
||||||
using NLog;
|
|
||||||
using PluginLoader.SEPM;
|
|
||||||
using Sandbox;
|
|
||||||
using Sandbox.Game.World;
|
|
||||||
using Sandbox.Graphics.GUI;
|
|
||||||
using VRage.FileSystem;
|
|
||||||
using VRage.Input;
|
|
||||||
using VRage.Plugins;
|
|
||||||
|
|
||||||
namespace PluginLoader;
|
|
||||||
|
|
||||||
public static class LoaderTools
|
|
||||||
{
|
|
||||||
public static string PluginsDir => Path.GetFullPath(Path.Combine(MyFileSystem.ExePath, "Plugins"));
|
|
||||||
|
|
||||||
public static DialogResult ShowMessageBox(string message, string title, MessageBoxButtons buttons, MessageBoxIcon icon)
|
|
||||||
{
|
|
||||||
var dialog = new MessageDialog(message, title);
|
|
||||||
|
|
||||||
switch (buttons)
|
|
||||||
{
|
|
||||||
case MessageBoxButtons.OK:
|
|
||||||
dialog.Commands.Add(new UICommand("Ok"));
|
|
||||||
break;
|
|
||||||
case MessageBoxButtons.OKCancel:
|
|
||||||
dialog.Commands.Add(new UICommand("Ok"));
|
|
||||||
dialog.Commands.Add(new UICommand("Cancel"));
|
|
||||||
break;
|
|
||||||
case MessageBoxButtons.AbortRetryIgnore:
|
|
||||||
break;
|
|
||||||
case MessageBoxButtons.YesNoCancel:
|
|
||||||
dialog.Commands.Add(new UICommand("Yes"));
|
|
||||||
dialog.Commands.Add(new UICommand("No"));
|
|
||||||
dialog.Commands.Add(new UICommand("Cancel"));
|
|
||||||
break;
|
|
||||||
case MessageBoxButtons.YesNo:
|
|
||||||
dialog.Commands.Add(new UICommand("Yes"));
|
|
||||||
dialog.Commands.Add(new UICommand("No"));
|
|
||||||
break;
|
|
||||||
case MessageBoxButtons.RetryCancel:
|
|
||||||
dialog.Commands.Add(new UICommand("Retry"));
|
|
||||||
dialog.Commands.Add(new UICommand("Cancel"));
|
|
||||||
break;
|
|
||||||
case MessageBoxButtons.CancelTryContinue:
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(buttons), buttons, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
var hwnd = Process.GetCurrentProcess().MainWindowHandle;
|
|
||||||
|
|
||||||
if (hwnd != IntPtr.Zero)
|
|
||||||
WinRT.Interop.InitializeWithWindow.Initialize(dialog, hwnd);
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Console.WriteLine(message);
|
|
||||||
return DialogResult.Cancel;
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = dialog.ShowAsync().AsTask().Result;
|
|
||||||
|
|
||||||
return buttons switch
|
|
||||||
{
|
|
||||||
MessageBoxButtons.OK => DialogResult.OK,
|
|
||||||
MessageBoxButtons.OKCancel => result.Label == "Ok" ? DialogResult.OK : DialogResult.Cancel,
|
|
||||||
MessageBoxButtons.AbortRetryIgnore => DialogResult.Ignore,
|
|
||||||
MessageBoxButtons.YesNoCancel => result.Label switch
|
|
||||||
{
|
|
||||||
"Yes" => DialogResult.Yes,
|
|
||||||
"No" => DialogResult.No,
|
|
||||||
_ => DialogResult.Cancel
|
|
||||||
},
|
|
||||||
MessageBoxButtons.YesNo => result.Label switch
|
|
||||||
{
|
|
||||||
"Yes" => DialogResult.Yes,
|
|
||||||
_ => DialogResult.No
|
|
||||||
},
|
|
||||||
MessageBoxButtons.RetryCancel => result.Label switch
|
|
||||||
{
|
|
||||||
"Retry" => DialogResult.Retry,
|
|
||||||
_ => DialogResult.Cancel
|
|
||||||
},
|
|
||||||
MessageBoxButtons.CancelTryContinue => DialogResult.Cancel,
|
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(buttons), buttons, null)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static void UnloadAndRestart()
|
|
||||||
{
|
|
||||||
MySessionLoader.Unload();
|
|
||||||
MySandboxGame.Config.ControllerDefaultOnStart = MyInput.Static.IsJoystickLastUsed;
|
|
||||||
MySandboxGame.Config.Save();
|
|
||||||
MyScreenManager.CloseAllScreensNowExcept(null);
|
|
||||||
MyPlugins.Unload();
|
|
||||||
LogManager.Flush();
|
|
||||||
Restart();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Restart()
|
|
||||||
{
|
|
||||||
Process.Start("explorer.exe", "steam://rungameid/244850");
|
|
||||||
Process.GetCurrentProcess().Kill();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void ExecuteMain(SEPMPlugin plugin)
|
|
||||||
{
|
|
||||||
var name = plugin.GetType().ToString();
|
|
||||||
plugin.Main(new(name), new());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string GetHash1(string file)
|
|
||||||
{
|
|
||||||
using (var sha = new SHA1Managed())
|
|
||||||
{
|
|
||||||
return GetHash(file, sha);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string GetHash256(string file)
|
|
||||||
{
|
|
||||||
using (var sha = new SHA256CryptoServiceProvider())
|
|
||||||
{
|
|
||||||
return GetHash(file, sha);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string GetHash(string file, HashAlgorithm hash)
|
|
||||||
{
|
|
||||||
using (var fileStream = new FileStream(file, FileMode.Open))
|
|
||||||
{
|
|
||||||
using (var bufferedStream = new BufferedStream(fileStream))
|
|
||||||
{
|
|
||||||
var data = hash.ComputeHash(bufferedStream);
|
|
||||||
var sb = new StringBuilder(2 * data.Length);
|
|
||||||
foreach (var b in data)
|
|
||||||
sb.AppendFormat("{0:x2}", b);
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static void OpenFileDialog(string title, string directory, string filter, Action<string> onOk)
|
|
||||||
{
|
|
||||||
var t = new Thread(() => OpenFileDialogThread(title, directory, filter, onOk));
|
|
||||||
t.SetApartmentState(ApartmentState.STA);
|
|
||||||
t.Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void OpenFileDialogThread(string title, string directory, string filter, Action<string> onOk)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Get the file path via prompt
|
|
||||||
using (var openFileDialog = new OpenFileDialog())
|
|
||||||
{
|
|
||||||
if (Directory.Exists(directory))
|
|
||||||
openFileDialog.InitialDirectory = directory;
|
|
||||||
openFileDialog.Title = title;
|
|
||||||
openFileDialog.Filter = filter;
|
|
||||||
openFileDialog.RestoreDirectory = true;
|
|
||||||
|
|
||||||
if (openFileDialog.ShowDialog() == DialogResult.OK)
|
|
||||||
// Move back to the main thread so that we can interact with keen code again
|
|
||||||
MySandboxGame.Static.Invoke(
|
|
||||||
() => onOk(openFileDialog.FileName),
|
|
||||||
"PluginLoader");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
LogFile.Log.Error(e, "Error while opening file dialog");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void OpenFolderDialog(string title, string directory, Action<string> onOk)
|
|
||||||
{
|
|
||||||
var t = new Thread(() => OpenFolderDialogThread(title, directory, onOk));
|
|
||||||
t.SetApartmentState(ApartmentState.STA);
|
|
||||||
t.Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void OpenFolderDialogThread(string title, string directory, Action<string> onOk)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Get the file path via prompt
|
|
||||||
using (var openFileDialog = new FolderBrowserDialog())
|
|
||||||
{
|
|
||||||
if (Directory.Exists(directory))
|
|
||||||
openFileDialog.SelectedPath = directory;
|
|
||||||
openFileDialog.Description = title;
|
|
||||||
|
|
||||||
if (openFileDialog.ShowDialog() == DialogResult.OK)
|
|
||||||
// Move back to the main thread so that we can interact with keen code again
|
|
||||||
MySandboxGame.Static.Invoke(
|
|
||||||
() => onOk(openFileDialog.SelectedPath),
|
|
||||||
"PluginLoader");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
LogFile.Log.Error(e, "Error while opening file dialog");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,37 +0,0 @@
|
|||||||
using System.Runtime.CompilerServices;
|
|
||||||
using NLog;
|
|
||||||
using NLog.Layouts;
|
|
||||||
using NLog.Targets;
|
|
||||||
using NLog.Targets.Wrappers;
|
|
||||||
|
|
||||||
namespace PluginLoader;
|
|
||||||
|
|
||||||
public static class LogFile
|
|
||||||
{
|
|
||||||
private const string FileName = "loader.log";
|
|
||||||
private const string LoggerName = "PluginLoader";
|
|
||||||
|
|
||||||
public static readonly Logger Log = LogManager.GetLogger(LoggerName);
|
|
||||||
|
|
||||||
public static void Init(string mainPath)
|
|
||||||
{
|
|
||||||
RuntimeHelpers.RunClassConstructor(
|
|
||||||
Type.GetType("GameAnalyticsSDK.Net.Logging.GALogger, GameAnalytics.Mono", true)!.TypeHandle);
|
|
||||||
|
|
||||||
var target = new AsyncTargetWrapper(new FileTarget
|
|
||||||
{
|
|
||||||
Name = "pluginLog",
|
|
||||||
Layout = Layout.FromString("${longdate:universaltime=true} ${level} ${message:withexception=true}"),
|
|
||||||
FileName = Layout.FromString(Path.Combine(mainPath, FileName)),
|
|
||||||
FileNameKind = FilePathKind.Absolute,
|
|
||||||
EnableFileDelete = true,
|
|
||||||
DeleteOldFileOnStartup = true,
|
|
||||||
});
|
|
||||||
LogManager.Configuration.AddTarget(target);
|
|
||||||
LogManager.Configuration.LoggingRules.Insert(0, new(LoggerName, LogLevel.Trace, target)
|
|
||||||
{
|
|
||||||
FinalMinLevel = LogLevel.Info
|
|
||||||
});
|
|
||||||
LogManager.ReconfigExistingLoggers();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,254 +0,0 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using System.Net;
|
|
||||||
using System.Reflection;
|
|
||||||
using HarmonyLib;
|
|
||||||
using PluginLoader.Compiler;
|
|
||||||
using PluginLoader.Data;
|
|
||||||
using PluginLoader.GUI;
|
|
||||||
using PluginLoader.Stats;
|
|
||||||
using Sandbox.Game.World;
|
|
||||||
using VRage.Plugins;
|
|
||||||
using SEPMPlugin = PluginLoader.SEPM.SEPMPlugin;
|
|
||||||
|
|
||||||
namespace PluginLoader;
|
|
||||||
|
|
||||||
public class Main : IHandleInputPlugin
|
|
||||||
{
|
|
||||||
public static Main Instance;
|
|
||||||
|
|
||||||
private readonly List<PluginInstance> plugins = new();
|
|
||||||
|
|
||||||
private bool init;
|
|
||||||
|
|
||||||
public Main()
|
|
||||||
{
|
|
||||||
var sw = Stopwatch.StartNew();
|
|
||||||
|
|
||||||
RunSplash();
|
|
||||||
|
|
||||||
Instance = this;
|
|
||||||
|
|
||||||
var temp = Cursor.Current;
|
|
||||||
Cursor.Current = Cursors.AppStarting;
|
|
||||||
|
|
||||||
var pluginsDir = LoaderTools.PluginsDir;
|
|
||||||
|
|
||||||
LogFile.Init(Directory.CreateDirectory(pluginsDir).FullName);
|
|
||||||
LogFile.Log.Debug("Starting - v{Version}", Assembly.GetExecutingAssembly().GetName().Version.ToString(3));
|
|
||||||
|
|
||||||
// Fix tls 1.2 not supported on Windows 7 - github.com is tls 1.2 only
|
|
||||||
try
|
|
||||||
{
|
|
||||||
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
|
|
||||||
}
|
|
||||||
catch (NotSupportedException e)
|
|
||||||
{
|
|
||||||
LogFile.Log.Warn(e, "An error occurred while setting up networking, web requests will probably fail");
|
|
||||||
}
|
|
||||||
|
|
||||||
Splash?.SetText("Finding references...");
|
|
||||||
RoslynReferences.GenerateAssemblyList();
|
|
||||||
|
|
||||||
AppDomain.CurrentDomain.AssemblyResolve += ResolveDependencies;
|
|
||||||
|
|
||||||
Config = PluginConfig.Load(pluginsDir);
|
|
||||||
List = new(pluginsDir, Config);
|
|
||||||
|
|
||||||
Config.Init(List);
|
|
||||||
|
|
||||||
StatsClient.OverrideBaseUrl(Config.StatsServerBaseUrl);
|
|
||||||
|
|
||||||
Splash?.SetText("Patching...");
|
|
||||||
LogFile.Log.Debug("Patching");
|
|
||||||
|
|
||||||
new Harmony("avaness.PluginLoader").PatchAll(Assembly.GetExecutingAssembly());
|
|
||||||
|
|
||||||
Splash?.SetText("Instantiating plugins...");
|
|
||||||
LogFile.Log.Debug("Instantiating plugins");
|
|
||||||
foreach (var id in Config)
|
|
||||||
{
|
|
||||||
var data = List[id];
|
|
||||||
if (data is GitHubPlugin github)
|
|
||||||
github.Init(pluginsDir);
|
|
||||||
if (PluginInstance.TryGet(data, out var p))
|
|
||||||
{
|
|
||||||
plugins.Add(p);
|
|
||||||
if (data.IsLocal)
|
|
||||||
HasLocal = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sw.Stop();
|
|
||||||
|
|
||||||
// FIXME: It can potentially run in the background speeding up the game's startup
|
|
||||||
//ReportEnabledPlugins();
|
|
||||||
|
|
||||||
LogFile.Log.Debug("Finished startup. Took {Time}ms", sw.ElapsedMilliseconds);
|
|
||||||
|
|
||||||
Cursor.Current = temp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PluginList List { get; }
|
|
||||||
public PluginConfig Config { get; }
|
|
||||||
public SplashScreen? Splash { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// True if a local plugin was loaded
|
|
||||||
/// </summary>
|
|
||||||
public bool HasLocal { get; }
|
|
||||||
|
|
||||||
// Skip local plugins, keep only enabled ones
|
|
||||||
public string[] TrackablePluginIds => Config.EnabledPlugins.Where(id => !List[id].IsLocal).ToArray();
|
|
||||||
|
|
||||||
public void Init(object gameInstance)
|
|
||||||
{
|
|
||||||
LogFile.Log.Debug("Initializing {PluginsCount} plugins", plugins.Count);
|
|
||||||
for (var i = plugins.Count - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
var p = plugins[i];
|
|
||||||
if (!p.Init(gameInstance))
|
|
||||||
plugins.RemoveAtFast(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
init = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Update()
|
|
||||||
{
|
|
||||||
if (init)
|
|
||||||
for (var i = plugins.Count - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
var p = plugins[i];
|
|
||||||
if (!p.Update())
|
|
||||||
plugins.RemoveAtFast(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void HandleInput()
|
|
||||||
{
|
|
||||||
if (init)
|
|
||||||
for (var i = plugins.Count - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
var p = plugins[i];
|
|
||||||
if (!p.HandleInput())
|
|
||||||
plugins.RemoveAtFast(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
foreach (var p in plugins)
|
|
||||||
p.Dispose();
|
|
||||||
plugins.Clear();
|
|
||||||
|
|
||||||
AppDomain.CurrentDomain.AssemblyResolve -= ResolveDependencies;
|
|
||||||
Instance = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryGetPluginInstance(string id, out PluginInstance instance)
|
|
||||||
{
|
|
||||||
instance = null;
|
|
||||||
if (!init)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
foreach (var p in plugins)
|
|
||||||
if (p.Id == id)
|
|
||||||
{
|
|
||||||
instance = p;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RunSplash()
|
|
||||||
{
|
|
||||||
var resetEvent = new ManualResetEventSlim();
|
|
||||||
var thread = new Thread(() =>
|
|
||||||
{
|
|
||||||
Application.EnableVisualStyles();
|
|
||||||
Application.SetHighDpiMode(HighDpiMode.PerMonitorV2);
|
|
||||||
Splash = new();
|
|
||||||
resetEvent.Set();
|
|
||||||
Task.Run(() =>
|
|
||||||
{
|
|
||||||
Sandbox.MySandboxGame.m_windowCreatedEvent.WaitOne();
|
|
||||||
Splash.Invoke(() => Splash.Delete());
|
|
||||||
});
|
|
||||||
Application.Run(Splash);
|
|
||||||
});
|
|
||||||
|
|
||||||
thread.SetApartmentState(ApartmentState.STA);
|
|
||||||
thread.Start();
|
|
||||||
resetEvent.Wait();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ReportEnabledPlugins()
|
|
||||||
{
|
|
||||||
if (!PlayerConsent.ConsentGiven)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Splash?.SetText("Reporting plugin usage...");
|
|
||||||
LogFile.Log.Debug("Reporting plugin usage");
|
|
||||||
|
|
||||||
// Config has already been validated at this point so all enabled plugins will have list items
|
|
||||||
// FIXME: Move into a background thread
|
|
||||||
if (StatsClient.Track(TrackablePluginIds))
|
|
||||||
LogFile.Log.Debug("List of enabled plugins has been sent to the statistics server");
|
|
||||||
else
|
|
||||||
LogFile.Log.Debug("Failed to send the list of enabled plugins to the statistics server");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RegisterComponents()
|
|
||||||
{
|
|
||||||
LogFile.Log.Debug("Registering {PluginsCount} components", plugins.Count);
|
|
||||||
foreach (var plugin in plugins)
|
|
||||||
plugin.RegisterSession(MySession.Static);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DisablePlugins()
|
|
||||||
{
|
|
||||||
Config.Disable();
|
|
||||||
plugins.Clear();
|
|
||||||
LogFile.Log.Debug("Disabled all plugins");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void InstantiatePlugins()
|
|
||||||
{
|
|
||||||
LogFile.Log.Debug($"Loading {plugins.Count} plugins");
|
|
||||||
for (var i = plugins.Count - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
var p = plugins[i];
|
|
||||||
if (!p.Instantiate())
|
|
||||||
plugins.RemoveAtFast(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private Assembly? ResolveDependencies(object? sender, ResolveEventArgs args)
|
|
||||||
{
|
|
||||||
var assembly = args.RequestingAssembly?.GetName().ToString();
|
|
||||||
var requestedName = new AssemblyName(args.Name);
|
|
||||||
switch (requestedName.Name)
|
|
||||||
{
|
|
||||||
case "0Harmony":
|
|
||||||
{
|
|
||||||
if (assembly != null)
|
|
||||||
LogFile.Log.Debug("Resolving 0Harmony for {AssemblyName}", assembly);
|
|
||||||
else
|
|
||||||
LogFile.Log.Debug("Resolving 0Harmony");
|
|
||||||
return typeof(Harmony).Assembly;
|
|
||||||
}
|
|
||||||
case "SEPluginManager":
|
|
||||||
{
|
|
||||||
if (assembly != null)
|
|
||||||
LogFile.Log.Debug("Resolving SEPluginManager for {AssemblyName}", assembly);
|
|
||||||
else
|
|
||||||
LogFile.Log.Debug("Resolving SEPluginManager");
|
|
||||||
return typeof(SEPMPlugin).Assembly;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,35 +0,0 @@
|
|||||||
namespace PluginLoader.Network;
|
|
||||||
|
|
||||||
public static class GitHub
|
|
||||||
{
|
|
||||||
public const string listRepoName = "sepluginloader/PluginHub";
|
|
||||||
public const string listRepoCommit = "main";
|
|
||||||
public const string listRepoHash = "plugins.sha1";
|
|
||||||
|
|
||||||
private const string repoZipUrl = "https://github.com/{0}/archive/{1}.zip";
|
|
||||||
private const string rawUrl = "https://raw.githubusercontent.com/{0}/{1}/";
|
|
||||||
|
|
||||||
private static readonly HttpClient Client = new();
|
|
||||||
|
|
||||||
public static Stream DownloadRepo(string name, string commit, out string? fileName)
|
|
||||||
{
|
|
||||||
var uri = new Uri(string.Format(repoZipUrl, name, commit), UriKind.Absolute);
|
|
||||||
LogFile.Log.Debug("Downloading {Uri}", uri);
|
|
||||||
using var response = Client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead).Result;
|
|
||||||
|
|
||||||
fileName = response.Content.Headers.ContentDisposition?.FileName;
|
|
||||||
|
|
||||||
using var stream = response.Content.ReadAsStream();
|
|
||||||
var mem = new MemoryStream();
|
|
||||||
stream.CopyTo(mem);
|
|
||||||
mem.Position = 0;
|
|
||||||
return mem;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Stream DownloadFile(string name, string commit, string path)
|
|
||||||
{
|
|
||||||
var uri = new Uri(string.Format(rawUrl, name, commit) + path.TrimStart('/'), UriKind.Absolute);
|
|
||||||
LogFile.Log.Debug("Downloading {Uri}", uri);
|
|
||||||
return Client.GetStreamAsync(uri).Result;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,18 +0,0 @@
|
|||||||
using System.Reflection;
|
|
||||||
using HarmonyLib;
|
|
||||||
using Sandbox.Game.World;
|
|
||||||
using VRage.Game;
|
|
||||||
using VRage.Plugins;
|
|
||||||
|
|
||||||
namespace PluginLoader.Patch;
|
|
||||||
|
|
||||||
[HarmonyPatch(typeof(MySession), "RegisterComponentsFromAssembly")]
|
|
||||||
[HarmonyPatch(new[] { typeof(Assembly), typeof(bool), typeof(MyModContext) })]
|
|
||||||
public static class Patch_ComponentRegistered
|
|
||||||
{
|
|
||||||
public static void Prefix(Assembly assembly)
|
|
||||||
{
|
|
||||||
if (assembly == MyPlugins.GameAssembly)
|
|
||||||
Main.Instance?.RegisterComponents();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,60 +0,0 @@
|
|||||||
using HarmonyLib;
|
|
||||||
using PluginLoader.GUI;
|
|
||||||
using Sandbox.Graphics.GUI;
|
|
||||||
using SpaceEngineers.Game.GUI;
|
|
||||||
using VRage.Game;
|
|
||||||
using VRage.Utils;
|
|
||||||
using VRageMath;
|
|
||||||
|
|
||||||
// ReSharper disable InconsistentNaming
|
|
||||||
|
|
||||||
namespace PluginLoader.Patch;
|
|
||||||
|
|
||||||
[HarmonyPatch(typeof(MyGuiScreenMainMenu), "CreateMainMenu")]
|
|
||||||
public static class Patch_CreateMainMenu
|
|
||||||
{
|
|
||||||
public static void Postfix(MyGuiScreenMainMenu __instance, Vector2 leftButtonPositionOrigin,
|
|
||||||
ref Vector2 lastButtonPosition)
|
|
||||||
{
|
|
||||||
MyGuiControlButton lastBtn = null;
|
|
||||||
foreach (var control in __instance.Controls)
|
|
||||||
if (control is MyGuiControlButton btn && btn.Position == lastButtonPosition)
|
|
||||||
{
|
|
||||||
lastBtn = btn;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector2 position;
|
|
||||||
if (lastBtn == null)
|
|
||||||
{
|
|
||||||
position = lastButtonPosition + MyGuiConstants.MENU_BUTTONS_POSITION_DELTA;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
position = lastBtn.Position;
|
|
||||||
lastBtn.Position = lastButtonPosition + MyGuiConstants.MENU_BUTTONS_POSITION_DELTA;
|
|
||||||
}
|
|
||||||
|
|
||||||
var openBtn = new MyGuiControlButton(position, MyGuiControlButtonStyleEnum.StripeLeft,
|
|
||||||
originAlign: MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_BOTTOM,
|
|
||||||
text: new("Plugins"),
|
|
||||||
onButtonClick: _ => MyGuiScreenPluginConfig.OpenMenu())
|
|
||||||
{
|
|
||||||
BorderEnabled = false,
|
|
||||||
BorderSize = 0,
|
|
||||||
BorderHighlightEnabled = false,
|
|
||||||
BorderColor = Vector4.Zero
|
|
||||||
};
|
|
||||||
__instance.Controls.Add(openBtn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HarmonyPatch(typeof(MyGuiScreenMainMenu), "CreateInGameMenu")]
|
|
||||||
public static class Patch_CreateInGameMenu
|
|
||||||
{
|
|
||||||
public static void Postfix(MyGuiScreenMainMenu __instance, Vector2 leftButtonPositionOrigin,
|
|
||||||
ref Vector2 lastButtonPosition)
|
|
||||||
{
|
|
||||||
Patch_CreateMainMenu.Postfix(__instance, leftButtonPositionOrigin, ref lastButtonPosition);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,26 +0,0 @@
|
|||||||
using HarmonyLib;
|
|
||||||
using Sandbox;
|
|
||||||
using VRage.Input;
|
|
||||||
|
|
||||||
namespace PluginLoader.Patch;
|
|
||||||
|
|
||||||
[HarmonyPatch(typeof(MySandboxGame), "LoadData")]
|
|
||||||
public static class Patch_DisableConfig
|
|
||||||
{
|
|
||||||
public static void Postfix()
|
|
||||||
{
|
|
||||||
// This is the earliest point in which I can use MyInput.Static
|
|
||||||
if (Main.Instance == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var main = Main.Instance;
|
|
||||||
var config = main.Config;
|
|
||||||
if (config != null && config.Count > 0 && MyInput.Static is MyVRageInput &&
|
|
||||||
MyInput.Static.IsKeyPress(MyKeys.Escape)
|
|
||||||
&& LoaderTools.ShowMessageBox("Escape pressed. Start the game with all plugins disabled?",
|
|
||||||
"Plugin Loader", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
|
|
||||||
main.DisablePlugins();
|
|
||||||
else
|
|
||||||
main.InstantiatePlugins();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,38 +0,0 @@
|
|||||||
using HarmonyLib;
|
|
||||||
using Sandbox.Game.Gui;
|
|
||||||
using Sandbox.Graphics.GUI;
|
|
||||||
using VRage;
|
|
||||||
using VRage.Input;
|
|
||||||
|
|
||||||
namespace PluginLoader.Patch;
|
|
||||||
|
|
||||||
[HarmonyPatch(typeof(MyGuiScreenGamePlay), "ShowLoadMessageBox")]
|
|
||||||
public static class Patch_IngameRestart
|
|
||||||
{
|
|
||||||
public static bool Prefix()
|
|
||||||
{
|
|
||||||
if (Main.Instance.HasLocal && MyInput.Static.IsAnyAltKeyPressed() && MyInput.Static.IsAnyCtrlKeyPressed())
|
|
||||||
{
|
|
||||||
ShowRestartMenu();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void ShowRestartMenu()
|
|
||||||
{
|
|
||||||
var box = MyGuiSandbox.CreateMessageBox(MyMessageBoxStyleEnum.Error, MyMessageBoxButtonsType.YES_NO,
|
|
||||||
new("Plugin Loader: Are you sure you want to restart the game?"),
|
|
||||||
MyTexts.Get(MyCommonTexts.MessageBoxCaptionPleaseConfirm),
|
|
||||||
callback: OnMessageClosed);
|
|
||||||
box.SkipTransition = true;
|
|
||||||
box.CloseBeforeCallback = true;
|
|
||||||
MyGuiSandbox.AddScreen(box);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void OnMessageClosed(MyGuiScreenMessageBox.ResultEnum result)
|
|
||||||
{
|
|
||||||
if (result == MyGuiScreenMessageBox.ResultEnum.YES) LoaderTools.UnloadAndRestart();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,37 +0,0 @@
|
|||||||
using HarmonyLib;
|
|
||||||
using PluginLoader.Data;
|
|
||||||
using Sandbox.Definitions;
|
|
||||||
using VRage.Game;
|
|
||||||
|
|
||||||
namespace PluginLoader.Patch;
|
|
||||||
|
|
||||||
[HarmonyPatch(typeof(MyDefinitionManager), "LoadData")]
|
|
||||||
public static class Patch_MyDefinitionManager
|
|
||||||
{
|
|
||||||
public static void Prefix(ref List<MyObjectBuilder_Checkpoint.ModItem> mods)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var currentMods = new HashSet<ulong>(mods.Select(x => x.PublishedFileId));
|
|
||||||
var newMods = new List<MyObjectBuilder_Checkpoint.ModItem>(mods);
|
|
||||||
|
|
||||||
var list = Main.Instance.List;
|
|
||||||
foreach (var id in Main.Instance.Config.EnabledPlugins)
|
|
||||||
{
|
|
||||||
var data = list[id];
|
|
||||||
if (data is ModPlugin mod && !currentMods.Contains(mod.WorkshopId) && mod.Exists)
|
|
||||||
{
|
|
||||||
LogFile.Log.Debug("Loading client mod definitions for " + mod.WorkshopId);
|
|
||||||
newMods.Add(mod.GetModItem());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mods = newMods;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
LogFile.Log.Debug("An error occured while loading client mods: " + e);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,48 +0,0 @@
|
|||||||
using System.Reflection;
|
|
||||||
using HarmonyLib;
|
|
||||||
using PluginLoader.Data;
|
|
||||||
using Sandbox.Game.World;
|
|
||||||
using VRage.Game;
|
|
||||||
|
|
||||||
namespace PluginLoader.Patch;
|
|
||||||
|
|
||||||
[HarmonyPatch(typeof(MyScriptManager), "LoadData")]
|
|
||||||
public static class Patch_MyScripManager
|
|
||||||
{
|
|
||||||
private static readonly Action<MyScriptManager, string, MyModContext> loadScripts;
|
|
||||||
|
|
||||||
static Patch_MyScripManager()
|
|
||||||
{
|
|
||||||
loadScripts = (Action<MyScriptManager, string, MyModContext>)Delegate.CreateDelegate(
|
|
||||||
typeof(Action<MyScriptManager, string, MyModContext>),
|
|
||||||
typeof(MyScriptManager).GetMethod("LoadScripts", BindingFlags.Instance | BindingFlags.NonPublic));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Postfix(MyScriptManager __instance)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
HashSet<ulong> currentMods;
|
|
||||||
if (MySession.Static.Mods != null)
|
|
||||||
currentMods = new(MySession.Static.Mods.Select(x => x.PublishedFileId));
|
|
||||||
else
|
|
||||||
currentMods = new();
|
|
||||||
|
|
||||||
var list = Main.Instance.List;
|
|
||||||
foreach (var id in Main.Instance.Config.EnabledPlugins)
|
|
||||||
{
|
|
||||||
var data = list[id];
|
|
||||||
if (data is ModPlugin mod && !currentMods.Contains(mod.WorkshopId) && mod.Exists)
|
|
||||||
{
|
|
||||||
LogFile.Log.Debug("Loading client mod scripts for " + mod.WorkshopId);
|
|
||||||
loadScripts(__instance, mod.ModLocation, mod.GetModContext());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
LogFile.Log.Debug("An error occured while loading client mods: " + e);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,180 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
using System.Xml.Serialization;
|
|
||||||
using PluginLoader.Data;
|
|
||||||
|
|
||||||
namespace PluginLoader;
|
|
||||||
|
|
||||||
public class PluginConfig
|
|
||||||
{
|
|
||||||
private const string fileName = "config.xml";
|
|
||||||
|
|
||||||
[XmlIgnore] public readonly Dictionary<string, Profile> ProfileMap = new();
|
|
||||||
|
|
||||||
private string filePath;
|
|
||||||
|
|
||||||
private int networkTimeout = 5000;
|
|
||||||
|
|
||||||
[XmlArray]
|
|
||||||
[XmlArrayItem("Id")]
|
|
||||||
public string[] Plugins
|
|
||||||
{
|
|
||||||
get => EnabledPlugins.ToArray();
|
|
||||||
set => EnabledPlugins = new(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
[XmlIgnore] public HashSet<string> EnabledPlugins { get; private set; } = new();
|
|
||||||
|
|
||||||
[XmlArray]
|
|
||||||
[XmlArrayItem("Plugin")]
|
|
||||||
public LocalFolderPlugin.Config[] LocalFolderPlugins
|
|
||||||
{
|
|
||||||
get => PluginFolders.Values.ToArray();
|
|
||||||
set { PluginFolders = value.ToDictionary(x => x.Folder); }
|
|
||||||
}
|
|
||||||
|
|
||||||
[XmlIgnore] public Dictionary<string, LocalFolderPlugin.Config> PluginFolders { get; private set; } = new();
|
|
||||||
|
|
||||||
[XmlArray]
|
|
||||||
[XmlArrayItem("Profile")]
|
|
||||||
public Profile[] Profiles
|
|
||||||
{
|
|
||||||
get => ProfileMap.Values.ToArray();
|
|
||||||
set
|
|
||||||
{
|
|
||||||
ProfileMap.Clear();
|
|
||||||
foreach (var profile in value)
|
|
||||||
ProfileMap[profile.Key] = profile;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ListHash { get; set; }
|
|
||||||
|
|
||||||
// Base URL for the statistics server, change to http://localhost:5000 in config.xml for local development
|
|
||||||
// ReSharper disable once UnassignedGetOnlyAutoProperty
|
|
||||||
public string StatsServerBaseUrl { get; }
|
|
||||||
|
|
||||||
// User consent to use the StatsServer
|
|
||||||
public bool DataHandlingConsent { get; set; }
|
|
||||||
public string DataHandlingConsentDate { get; set; }
|
|
||||||
|
|
||||||
public int Count => EnabledPlugins.Count;
|
|
||||||
|
|
||||||
public void Init(PluginList plugins)
|
|
||||||
{
|
|
||||||
// Remove plugins from config that no longer exist
|
|
||||||
var toRemove = new List<string>();
|
|
||||||
|
|
||||||
var sb = new StringBuilder("Enabled plugins: ");
|
|
||||||
var localPlugins = new StringBuilder("Local plugins: ");
|
|
||||||
foreach (var id in EnabledPlugins)
|
|
||||||
{
|
|
||||||
if (!plugins.TryGetPlugin(id, out var plugin))
|
|
||||||
{
|
|
||||||
LogFile.Log.Debug($"{id} was in the config but is no longer available", false);
|
|
||||||
toRemove.Add(id);
|
|
||||||
}
|
|
||||||
else if (!plugin.IsLocal)
|
|
||||||
{
|
|
||||||
sb.Append(id).Append(", ");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
localPlugins.Append(id).Append(", ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (EnabledPlugins.Count > 0)
|
|
||||||
sb.Length -= 2;
|
|
||||||
else
|
|
||||||
sb.Append("None");
|
|
||||||
LogFile.Log.Debug(sb.ToString());
|
|
||||||
|
|
||||||
if (localPlugins.Length > 15)
|
|
||||||
localPlugins.Length -= 2;
|
|
||||||
else
|
|
||||||
localPlugins.Append("None");
|
|
||||||
LogFile.Log.Debug(localPlugins.ToString(), false);
|
|
||||||
|
|
||||||
foreach (var id in toRemove)
|
|
||||||
EnabledPlugins.Remove(id);
|
|
||||||
|
|
||||||
if (toRemove.Count > 0)
|
|
||||||
Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Disable()
|
|
||||||
{
|
|
||||||
EnabledPlugins.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void Save()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
LogFile.Log.Debug("Saving config");
|
|
||||||
var serializer = new XmlSerializer(typeof(PluginConfig));
|
|
||||||
if (File.Exists(filePath))
|
|
||||||
File.Delete(filePath);
|
|
||||||
var fs = File.OpenWrite(filePath);
|
|
||||||
serializer.Serialize(fs, this);
|
|
||||||
fs.Flush();
|
|
||||||
fs.Close();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
LogFile.Log.Debug("An error occurred while saving plugin config: " + e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static PluginConfig Load(string mainDirectory)
|
|
||||||
{
|
|
||||||
var path = Path.Combine(mainDirectory, fileName);
|
|
||||||
if (File.Exists(path))
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var serializer = new XmlSerializer(typeof(PluginConfig));
|
|
||||||
var fs = File.OpenRead(path);
|
|
||||||
var config = (PluginConfig)serializer.Deserialize(fs);
|
|
||||||
fs.Close();
|
|
||||||
config.filePath = path;
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
LogFile.Log.Debug("An error occurred while loading plugin config: " + e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new()
|
|
||||||
{
|
|
||||||
filePath = path
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerator<string> GetEnumerator()
|
|
||||||
{
|
|
||||||
return EnabledPlugins.GetEnumerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsEnabled(string id)
|
|
||||||
{
|
|
||||||
return EnabledPlugins.Contains(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetEnabled(string id, bool enabled)
|
|
||||||
{
|
|
||||||
if (EnabledPlugins.Contains(id) == enabled)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (enabled)
|
|
||||||
{
|
|
||||||
EnabledPlugins.Add(id);
|
|
||||||
Main.Instance.List.SubscribeToItem(id);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
EnabledPlugins.Remove(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,176 +0,0 @@
|
|||||||
using System.Reflection;
|
|
||||||
using HarmonyLib;
|
|
||||||
using PluginLoader.Data;
|
|
||||||
using Sandbox.Game.World;
|
|
||||||
using VRage.Game.Components;
|
|
||||||
using VRage.Plugins;
|
|
||||||
using SEPMPlugin = PluginLoader.SEPM.SEPMPlugin;
|
|
||||||
|
|
||||||
namespace PluginLoader;
|
|
||||||
|
|
||||||
public class PluginInstance
|
|
||||||
{
|
|
||||||
private readonly PluginData data;
|
|
||||||
private readonly Assembly? mainAssembly;
|
|
||||||
private readonly Type mainType;
|
|
||||||
private IHandleInputPlugin inputPlugin;
|
|
||||||
private MethodInfo openConfigDialog;
|
|
||||||
private IPlugin plugin;
|
|
||||||
|
|
||||||
private PluginInstance(PluginData data, Assembly? mainAssembly, Type mainType)
|
|
||||||
{
|
|
||||||
this.data = data;
|
|
||||||
this.mainAssembly = mainAssembly;
|
|
||||||
this.mainType = mainType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Id => data.Id;
|
|
||||||
public bool HasConfigDialog => openConfigDialog != null;
|
|
||||||
|
|
||||||
public bool Instantiate()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
plugin = (IPlugin)Activator.CreateInstance(mainType);
|
|
||||||
inputPlugin = plugin as IHandleInputPlugin;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
ThrowError($"Failed to instantiate {data} because of an error: {e}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
openConfigDialog = AccessTools.DeclaredMethod(mainType, "OpenConfigDialog", Array.Empty<Type>());
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
LogFile.Log.Error(e, $"Unable to find OpenConfigDialog() in {data} due to an error");
|
|
||||||
openConfigDialog = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OpenConfig()
|
|
||||||
{
|
|
||||||
if (plugin == null || openConfigDialog == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
openConfigDialog.Invoke(plugin, Array.Empty<object>());
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
ThrowError($"Failed to open plugin config for {data} because of an error: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Init(object gameInstance)
|
|
||||||
{
|
|
||||||
if (plugin == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (plugin is SEPMPlugin sepm)
|
|
||||||
LoaderTools.ExecuteMain(sepm);
|
|
||||||
plugin.Init(gameInstance);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
ThrowError($"Failed to initialize {data} because of an error: {e}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RegisterSession(MySession session)
|
|
||||||
{
|
|
||||||
if (plugin != null)
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var descType = typeof(MySessionComponentDescriptor);
|
|
||||||
var count = 0;
|
|
||||||
foreach (var t in mainAssembly.GetTypes().Where(t => Attribute.IsDefined(t, descType)))
|
|
||||||
{
|
|
||||||
var comp = (MySessionComponentBase)Activator.CreateInstance(t);
|
|
||||||
session.RegisterComponent(comp, comp.UpdateOrder, comp.Priority);
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count > 0)
|
|
||||||
LogFile.Log.Debug($"Registered {count} session components from: {mainAssembly.FullName}", !data.IsLocal);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
ThrowError($"Failed to register {data} because of an error: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Update()
|
|
||||||
{
|
|
||||||
if (plugin == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
plugin.Update();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool HandleInput()
|
|
||||||
{
|
|
||||||
if (plugin == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
inputPlugin?.HandleInput();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (plugin != null)
|
|
||||||
try
|
|
||||||
{
|
|
||||||
plugin.Dispose();
|
|
||||||
plugin = null;
|
|
||||||
inputPlugin = null;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
data.Status = PluginStatus.Error;
|
|
||||||
LogFile.Log.Error(e, $"Failed to dispose {data} because of an error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ThrowError(string error)
|
|
||||||
{
|
|
||||||
LogFile.Log.Debug(error);
|
|
||||||
data.Error();
|
|
||||||
Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool TryGet(PluginData data, out PluginInstance instance)
|
|
||||||
{
|
|
||||||
instance = null;
|
|
||||||
if (data.Status == PluginStatus.Error || !data.TryLoadAssembly(out var a))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var pluginType = a.GetTypes().FirstOrDefault(t => t.IsAssignableTo(typeof(IPlugin)));
|
|
||||||
if (pluginType == null)
|
|
||||||
{
|
|
||||||
LogFile.Log.Warn($"Failed to load {data} because it does not contain an IPlugin");
|
|
||||||
data.Error();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
instance = new(data, a, pluginType);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return data.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,361 +0,0 @@
|
|||||||
using System.Collections;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.IO.Compression;
|
|
||||||
using System.Xml.Serialization;
|
|
||||||
using PluginLoader.Data;
|
|
||||||
using PluginLoader.Network;
|
|
||||||
using ProtoBuf;
|
|
||||||
|
|
||||||
namespace PluginLoader;
|
|
||||||
|
|
||||||
public class PluginList : IEnumerable<PluginData>
|
|
||||||
{
|
|
||||||
private Dictionary<string, PluginData> plugins = new();
|
|
||||||
|
|
||||||
public PluginList(string mainDirectory, PluginConfig config)
|
|
||||||
{
|
|
||||||
var lbl = Main.Instance.Splash;
|
|
||||||
|
|
||||||
lbl.SetText("Downloading plugin list...");
|
|
||||||
DownloadList(mainDirectory, config);
|
|
||||||
|
|
||||||
if (plugins.Count == 0)
|
|
||||||
{
|
|
||||||
LogFile.Log.Warn("WARNING: No plugins in the plugin list. Plugin list will contain local plugins only.");
|
|
||||||
HasError = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
FindWorkshopPlugins(config);
|
|
||||||
FindLocalPlugins(config, mainDirectory);
|
|
||||||
LogFile.Log.Debug($"Found {plugins.Count} plugins");
|
|
||||||
FindPluginGroups();
|
|
||||||
FindModDependencies();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int Count => plugins.Count;
|
|
||||||
|
|
||||||
public bool HasError { get; }
|
|
||||||
|
|
||||||
public PluginData this[string key]
|
|
||||||
{
|
|
||||||
get => plugins[key];
|
|
||||||
set => plugins[key] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public IEnumerator<PluginData> GetEnumerator()
|
|
||||||
{
|
|
||||||
return plugins.Values.GetEnumerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator()
|
|
||||||
{
|
|
||||||
return plugins.Values.GetEnumerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Contains(string id)
|
|
||||||
{
|
|
||||||
return plugins.ContainsKey(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryGetPlugin(string id, out PluginData pluginData)
|
|
||||||
{
|
|
||||||
return plugins.TryGetValue(id, out pluginData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ensures the user is subscribed to the steam plugin.
|
|
||||||
/// </summary>
|
|
||||||
public void SubscribeToItem(string id)
|
|
||||||
{
|
|
||||||
if (plugins.TryGetValue(id, out var data) && data is ISteamItem steam)
|
|
||||||
SteamAPI.SubscribeToItem(steam.WorkshopId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Remove(string id)
|
|
||||||
{
|
|
||||||
return plugins.Remove(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Add(PluginData data)
|
|
||||||
{
|
|
||||||
plugins[data.Id] = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void FindPluginGroups()
|
|
||||||
{
|
|
||||||
var groups = 0;
|
|
||||||
foreach (var group in plugins.Values.Where(x => !string.IsNullOrWhiteSpace(x.GroupId)).GroupBy(x => x.GroupId))
|
|
||||||
{
|
|
||||||
groups++;
|
|
||||||
foreach (var data in group)
|
|
||||||
data.Group.AddRange(group.Where(x => x != data));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (groups > 0)
|
|
||||||
LogFile.Log.Debug($"Found {groups} plugin groups");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void FindModDependencies()
|
|
||||||
{
|
|
||||||
foreach (var data in plugins.Values)
|
|
||||||
if (data is ModPlugin mod)
|
|
||||||
FindModDependencies(mod);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void FindModDependencies(ModPlugin mod)
|
|
||||||
{
|
|
||||||
if (mod.DependencyIds == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var dependencies = new Dictionary<ulong, ModPlugin>();
|
|
||||||
dependencies.Add(mod.WorkshopId, mod);
|
|
||||||
var toProcess = new Stack<ModPlugin>();
|
|
||||||
toProcess.Push(mod);
|
|
||||||
|
|
||||||
while (toProcess.Count > 0)
|
|
||||||
{
|
|
||||||
var temp = toProcess.Pop();
|
|
||||||
|
|
||||||
if (temp.DependencyIds == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
foreach (var id in temp.DependencyIds)
|
|
||||||
if (!dependencies.ContainsKey(id) && plugins.TryGetValue(id.ToString(), out var data) &&
|
|
||||||
data is ModPlugin dependency)
|
|
||||||
{
|
|
||||||
toProcess.Push(dependency);
|
|
||||||
dependencies[id] = dependency;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies.Remove(mod.WorkshopId);
|
|
||||||
mod.Dependencies = dependencies.Values.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DownloadList(string mainDirectory, PluginConfig config)
|
|
||||||
{
|
|
||||||
var whitelist = Path.Combine(mainDirectory, "whitelist.bin");
|
|
||||||
|
|
||||||
PluginData[] list;
|
|
||||||
var currentHash = config.ListHash;
|
|
||||||
string newHash;
|
|
||||||
if (!TryDownloadWhitelistHash(out newHash))
|
|
||||||
{
|
|
||||||
// No connection to plugin hub, read from cache
|
|
||||||
if (!TryReadWhitelistFile(whitelist, out list))
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if (currentHash == null || currentHash != newHash)
|
|
||||||
{
|
|
||||||
// Plugin list changed, try downloading new version first
|
|
||||||
if (!TryDownloadWhitelistFile(whitelist, newHash, config, out list)
|
|
||||||
&& !TryReadWhitelistFile(whitelist, out list))
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Plugin list did not change, try reading the current version first
|
|
||||||
if (!TryReadWhitelistFile(whitelist, out list)
|
|
||||||
&& !TryDownloadWhitelistFile(whitelist, newHash, config, out list))
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (list != null)
|
|
||||||
plugins = list.ToDictionary(x => x.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryReadWhitelistFile(string file, [NotNullWhen(true)] out PluginData[]? list)
|
|
||||||
{
|
|
||||||
list = null;
|
|
||||||
|
|
||||||
if (File.Exists(file) && new FileInfo(file).Length > 0)
|
|
||||||
{
|
|
||||||
LogFile.Log.Debug("Reading whitelist from cache");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (Stream binFile = File.OpenRead(file))
|
|
||||||
{
|
|
||||||
list = Serializer.Deserialize<PluginData[]>(binFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
LogFile.Log.Debug("Whitelist retrieved from disk");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
LogFile.Log.Warn(e, "Error while reading whitelist");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
LogFile.Log.Debug("No whitelist cache exists");
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryDownloadWhitelistFile(string file, string hash, PluginConfig config, out PluginData[] list)
|
|
||||||
{
|
|
||||||
var newPlugins = new Dictionary<string, PluginData>();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (var zipFileStream = GitHub.DownloadRepo(GitHub.listRepoName, GitHub.listRepoCommit, out _))
|
|
||||||
using (var zipFile = new ZipArchive(zipFileStream))
|
|
||||||
{
|
|
||||||
var xml = new XmlSerializer(typeof(PluginData));
|
|
||||||
foreach (var entry in zipFile.Entries)
|
|
||||||
{
|
|
||||||
if (!entry.FullName.EndsWith("xml", StringComparison.OrdinalIgnoreCase))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
using var entryStream = entry.Open();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var data = (PluginData?)xml.Deserialize(entryStream) ?? throw new InvalidOperationException($"Deserialized data is null for {entry.FullName}");
|
|
||||||
newPlugins[data.Id] = data;
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException e)
|
|
||||||
{
|
|
||||||
LogFile.Log.Error(e, "An error occurred while reading the plugin xml");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
list = newPlugins.Values.ToArray();
|
|
||||||
return TrySaveWhitelist(file, list, hash, config);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
LogFile.Log.Error(e, "Error while downloading whitelist");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TrySaveWhitelist(string file, PluginData[] list, string hash, PluginConfig config)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
LogFile.Log.Debug("Saving whitelist to disk");
|
|
||||||
using (var binFile = File.Create(file))
|
|
||||||
{
|
|
||||||
Serializer.Serialize(binFile, list);
|
|
||||||
}
|
|
||||||
|
|
||||||
config.ListHash = hash;
|
|
||||||
config.Save();
|
|
||||||
|
|
||||||
LogFile.Log.Debug("Whitelist updated");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
LogFile.Log.Error(e, "Error while saving whitelist");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
File.Delete(file);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryDownloadWhitelistHash(out string hash)
|
|
||||||
{
|
|
||||||
hash = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (var hashStream =
|
|
||||||
GitHub.DownloadFile(GitHub.listRepoName, GitHub.listRepoCommit, GitHub.listRepoHash))
|
|
||||||
using (var hashStreamReader = new StreamReader(hashStream))
|
|
||||||
{
|
|
||||||
hash = hashStreamReader.ReadToEnd().Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
LogFile.Log.Debug("Error while downloading whitelist hash: " + e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void FindLocalPlugins(PluginConfig config, string mainDirectory)
|
|
||||||
{
|
|
||||||
foreach (var dll in Directory.EnumerateFiles(mainDirectory, "*.dll", SearchOption.AllDirectories))
|
|
||||||
if (!dll.Contains(Path.DirectorySeparatorChar + "GitHub" + Path.DirectorySeparatorChar,
|
|
||||||
StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
var local = new LocalPlugin(dll);
|
|
||||||
var name = local.FriendlyName;
|
|
||||||
if (!name.StartsWith("0Harmony") && !name.StartsWith("Microsoft"))
|
|
||||||
plugins[local.Id] = local;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var folderConfig in config.PluginFolders.Values)
|
|
||||||
if (folderConfig.Valid)
|
|
||||||
{
|
|
||||||
var local = new LocalFolderPlugin(folderConfig);
|
|
||||||
plugins[local.Id] = local;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void FindWorkshopPlugins(PluginConfig config)
|
|
||||||
{
|
|
||||||
var steamPlugins = new List<ISteamItem>(plugins.Values.Select(x => x as ISteamItem).Where(x => x != null));
|
|
||||||
|
|
||||||
Main.Instance.Splash?.SetText("Updating workshop items...");
|
|
||||||
|
|
||||||
SteamAPI.Update(steamPlugins.Where(x => config.IsEnabled(x.Id)).Select(x => x.WorkshopId));
|
|
||||||
|
|
||||||
var workshop = Path.GetFullPath(@"..\..\..\workshop\content\244850\");
|
|
||||||
foreach (var steam in steamPlugins)
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var path = Path.Combine(workshop, steam.Id);
|
|
||||||
if (Directory.Exists(path))
|
|
||||||
{
|
|
||||||
if (steam is SteamPlugin plugin && TryGetPlugin(path, out string dllFile))
|
|
||||||
plugin.Init(dllFile);
|
|
||||||
}
|
|
||||||
else if (config.IsEnabled(steam.Id))
|
|
||||||
{
|
|
||||||
((PluginData)steam).Status = PluginStatus.Error;
|
|
||||||
LogFile.Log.Debug($"The plugin '{steam}' is missing and cannot be loaded.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
LogFile.Log.Debug($"An error occurred while searching for the workshop plugin {steam}: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryGetPlugin(string modRoot, out string pluginFile)
|
|
||||||
{
|
|
||||||
foreach (var file in Directory.EnumerateFiles(modRoot, "*.plugin"))
|
|
||||||
{
|
|
||||||
var name = Path.GetFileName(file);
|
|
||||||
if (!name.StartsWith("0Harmony", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
pluginFile = file;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var sepm = Path.Combine(modRoot, "Data", "sepm-plugin.zip");
|
|
||||||
if (File.Exists(sepm))
|
|
||||||
{
|
|
||||||
pluginFile = sepm;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pluginFile = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,39 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.Net.SDK">
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<ImplicitUsings>true</ImplicitUsings>
|
|
||||||
<UseWindowsForms>true</UseWindowsForms>
|
|
||||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
|
||||||
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Publicize Include="Sandbox.Game:Sandbox.MySandboxGame.m_windowCreatedEvent" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Krafs.Publicizer" Version="2.2.1">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="NLog" Version="5.3.4" />
|
|
||||||
<PackageReference Include="Lib.Harmony.Thin" Version="2.3.3" />
|
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
|
|
||||||
<PackageReference Include="SpaceEngineersDedicated.ReferenceAssemblies" Version="1.*" ExcludeAssets="runtime" />
|
|
||||||
<PackageReference Include="Steamworks.NET" Version="20.1.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<EmbeddedResource Include="splash.gif" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Remove="steam_api64.dll" />
|
|
||||||
<Content Include="steam_api64.dll">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
|
||||||
</Content>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
@@ -1,24 +0,0 @@
|
|||||||
namespace PluginLoader;
|
|
||||||
|
|
||||||
public class Profile
|
|
||||||
{
|
|
||||||
public Profile()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public Profile(string name, string[] plugins)
|
|
||||||
{
|
|
||||||
Key = Guid.NewGuid().ToString();
|
|
||||||
Name = name;
|
|
||||||
Plugins = plugins;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unique key of the profile
|
|
||||||
public string Key { get; set; }
|
|
||||||
|
|
||||||
// Name of the profile
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
// Plugin IDs
|
|
||||||
public string[] Plugins { get; set; }
|
|
||||||
}
|
|
@@ -1,8 +0,0 @@
|
|||||||
namespace PluginLoader.SEPM;
|
|
||||||
|
|
||||||
public class Logger
|
|
||||||
{
|
|
||||||
public void Log(string text)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,9 +0,0 @@
|
|||||||
using HarmonyLib;
|
|
||||||
using VRage.Plugins;
|
|
||||||
|
|
||||||
namespace PluginLoader.SEPM;
|
|
||||||
|
|
||||||
public interface SEPMPlugin : IPlugin
|
|
||||||
{
|
|
||||||
void Main(Harmony harmony, Logger log);
|
|
||||||
}
|
|
@@ -1,14 +0,0 @@
|
|||||||
// ReSharper disable UnusedAutoPropertyAccessor.Global
|
|
||||||
|
|
||||||
namespace PluginLoader.Stats.Model;
|
|
||||||
|
|
||||||
// Request data received from the Plugin Loader to store user consent or withdrawal,
|
|
||||||
// this request is NOT sent if the user does not give consent in the first place
|
|
||||||
public class ConsentRequest
|
|
||||||
{
|
|
||||||
// Hash of the player's Steam ID
|
|
||||||
public string PlayerHash { get; set; }
|
|
||||||
|
|
||||||
// True if the consent has just given, false if has just withdrawn
|
|
||||||
public bool Consent { get; set; }
|
|
||||||
}
|
|
@@ -1,24 +0,0 @@
|
|||||||
namespace PluginLoader.Stats.Model;
|
|
||||||
|
|
||||||
// Statistics for a single plugin
|
|
||||||
public class PluginStat
|
|
||||||
{
|
|
||||||
// Number of players who successfully started SE with this plugin enabled anytime during the past 30 days
|
|
||||||
public int Players { get; set; }
|
|
||||||
|
|
||||||
// Total number of upvotes and downvotes since the beginning (votes do not expire)
|
|
||||||
public int Upvotes { get; set; }
|
|
||||||
public int Downvotes { get; set; }
|
|
||||||
|
|
||||||
// Whether the requesting player tried the plugin
|
|
||||||
public bool Tried { get; set; }
|
|
||||||
|
|
||||||
// Current vote of the requesting player
|
|
||||||
// +1: Upvoted
|
|
||||||
// 0: No vote (or cleared it)
|
|
||||||
// -1: Downvoted
|
|
||||||
public int Vote { get; set; }
|
|
||||||
|
|
||||||
// Number of half stars [1-10] based on the upvote ratio, zero if there are not enough votes on the plugin yet
|
|
||||||
public int Rating { get; set; }
|
|
||||||
}
|
|
@@ -1,11 +0,0 @@
|
|||||||
namespace PluginLoader.Stats.Model;
|
|
||||||
|
|
||||||
// Statistics for all plugins
|
|
||||||
public class PluginStats
|
|
||||||
{
|
|
||||||
// Key: pluginId
|
|
||||||
public Dictionary<string, PluginStat> Stats { get; set; } = new();
|
|
||||||
|
|
||||||
// Token the player is required to present for voting (making it harder to spoof votes)
|
|
||||||
public string VotingToken { get; set; }
|
|
||||||
}
|
|
@@ -1,17 +0,0 @@
|
|||||||
namespace PluginLoader.Stats.Model;
|
|
||||||
|
|
||||||
// Request data sent to the StatsServer each time the game is started
|
|
||||||
public class TrackRequest
|
|
||||||
{
|
|
||||||
// Hash of the player's Steam ID
|
|
||||||
// Hexdump of the first 80 bits of SHA1($"{steamId}")
|
|
||||||
// The client determines the ID of the player, never the server.
|
|
||||||
// Using a hash is required for data protection and privacy.
|
|
||||||
// Using a hash makes it impractical to track back usage or votes to
|
|
||||||
// individual players, while still allowing for near-perfect deduplication.
|
|
||||||
// It also prevents stealing all the Steam IDs from the server's database.
|
|
||||||
public string PlayerHash { get; set; }
|
|
||||||
|
|
||||||
// Ids of enabled plugins when the game started
|
|
||||||
public string[] EnabledPluginIds { get; set; }
|
|
||||||
}
|
|
@@ -1,20 +0,0 @@
|
|||||||
namespace PluginLoader.Stats.Model;
|
|
||||||
|
|
||||||
// Request data sent to the StatsServer each time the user changes his/her vote on a plugin
|
|
||||||
public class VoteRequest
|
|
||||||
{
|
|
||||||
// Id of the plugin
|
|
||||||
public string PluginId { get; set; }
|
|
||||||
|
|
||||||
// Obfuscated player identifier, see Track.PlayerHash
|
|
||||||
public string PlayerHash { get; set; }
|
|
||||||
|
|
||||||
// Voting token returned with the plugin stats
|
|
||||||
public string VotingToken { get; set; }
|
|
||||||
|
|
||||||
// Vote to store
|
|
||||||
// +1: Upvote
|
|
||||||
// 0: Clear vote
|
|
||||||
// -1: Downvote
|
|
||||||
public int Vote { get; set; }
|
|
||||||
}
|
|
@@ -1,101 +0,0 @@
|
|||||||
using PluginLoader.GUI;
|
|
||||||
using PluginLoader.Stats.Model;
|
|
||||||
using PluginLoader.Tools;
|
|
||||||
|
|
||||||
namespace PluginLoader.Stats;
|
|
||||||
|
|
||||||
public static class StatsClient
|
|
||||||
{
|
|
||||||
// API address
|
|
||||||
private static string baseUri = "https://pluginstats.ferenczi.eu";
|
|
||||||
private static string playerHash;
|
|
||||||
|
|
||||||
// Latest voting token received
|
|
||||||
private static string votingToken;
|
|
||||||
|
|
||||||
// API endpoints
|
|
||||||
private static string ConsentUri => $"{baseUri}/Consent";
|
|
||||||
private static string StatsUri => $"{baseUri}/Stats";
|
|
||||||
private static string TrackUri => $"{baseUri}/Track";
|
|
||||||
private static string VoteUri => $"{baseUri}/Vote";
|
|
||||||
|
|
||||||
// Hashed Steam ID of the player
|
|
||||||
private static string PlayerHash =>
|
|
||||||
playerHash ??= Tools.Tools.Sha1HexDigest($"{Tools.Tools.GetSteamId()}").Substring(0, 20);
|
|
||||||
|
|
||||||
public static void OverrideBaseUrl(string uri)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(uri))
|
|
||||||
return;
|
|
||||||
|
|
||||||
baseUri = uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool Consent(bool consent)
|
|
||||||
{
|
|
||||||
if (consent)
|
|
||||||
LogFile.Log.Debug("Registering player consent on the statistics server");
|
|
||||||
else
|
|
||||||
LogFile.Log.Debug("Withdrawing player consent, removing user data from the statistics server");
|
|
||||||
|
|
||||||
var consentRequest = new ConsentRequest
|
|
||||||
{
|
|
||||||
PlayerHash = PlayerHash,
|
|
||||||
Consent = consent
|
|
||||||
};
|
|
||||||
|
|
||||||
return SimpleHttpClient.Post(ConsentUri, consentRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This function may be called from another thread.
|
|
||||||
public static PluginStats DownloadStats()
|
|
||||||
{
|
|
||||||
if (!PlayerConsent.ConsentGiven)
|
|
||||||
{
|
|
||||||
LogFile.Log.Info("Downloading plugin statistics anonymously...");
|
|
||||||
votingToken = null;
|
|
||||||
return SimpleHttpClient.Get<PluginStats>(StatsUri);
|
|
||||||
}
|
|
||||||
|
|
||||||
LogFile.Log.Info("Downloading plugin statistics, ratings and votes for " + PlayerHash);
|
|
||||||
|
|
||||||
var parameters = new Dictionary<string, string> { ["playerHash"] = PlayerHash };
|
|
||||||
var pluginStats = SimpleHttpClient.Get<PluginStats>(StatsUri, parameters);
|
|
||||||
|
|
||||||
votingToken = pluginStats?.VotingToken;
|
|
||||||
|
|
||||||
return pluginStats;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool Track(string[] pluginIds)
|
|
||||||
{
|
|
||||||
var trackRequest = new TrackRequest
|
|
||||||
{
|
|
||||||
PlayerHash = PlayerHash,
|
|
||||||
EnabledPluginIds = pluginIds
|
|
||||||
};
|
|
||||||
|
|
||||||
return SimpleHttpClient.Post(TrackUri, trackRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static PluginStat Vote(string pluginId, int vote)
|
|
||||||
{
|
|
||||||
if (votingToken == null)
|
|
||||||
{
|
|
||||||
LogFile.Log.Debug("Voting token is not available, cannot vote");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
LogFile.Log.Debug($"Voting {vote} on plugin {pluginId}");
|
|
||||||
var voteRequest = new VoteRequest
|
|
||||||
{
|
|
||||||
PlayerHash = PlayerHash,
|
|
||||||
PluginId = pluginId,
|
|
||||||
VotingToken = votingToken,
|
|
||||||
Vote = vote
|
|
||||||
};
|
|
||||||
|
|
||||||
var stat = SimpleHttpClient.Post<PluginStat, VoteRequest>(VoteUri, voteRequest);
|
|
||||||
return stat;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,64 +0,0 @@
|
|||||||
using NLog;
|
|
||||||
using Steamworks;
|
|
||||||
|
|
||||||
namespace PluginLoader;
|
|
||||||
|
|
||||||
public static class SteamAPI
|
|
||||||
{
|
|
||||||
public static bool IsSubscribed(ulong id)
|
|
||||||
{
|
|
||||||
var state = (EItemState)SteamUGC.GetItemState(new(id));
|
|
||||||
return (state & EItemState.k_EItemStateSubscribed) == EItemState.k_EItemStateSubscribed;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void SubscribeToItem(ulong id)
|
|
||||||
{
|
|
||||||
SteamUGC.SubscribeItem(new(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Update(IEnumerable<ulong> ids)
|
|
||||||
{
|
|
||||||
var enumerable = ids as ulong[] ?? ids.ToArray();
|
|
||||||
|
|
||||||
LogFile.Log.Info("Updating {Count} workshop items", enumerable.Length);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
UpdateInternal(enumerable);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
LogFile.Log.Error(e, "An error occurred while updating workshop items");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void UpdateInternal(IEnumerable<ulong> ids)
|
|
||||||
{
|
|
||||||
var count = 0;
|
|
||||||
using var callback = Callback<DownloadItemResult_t>.Create(t =>
|
|
||||||
{
|
|
||||||
if (t.m_eResult == EResult.k_EResultOK)
|
|
||||||
Interlocked.Increment(ref count);
|
|
||||||
|
|
||||||
LogFile.Log.Log(t.m_eResult == EResult.k_EResultOK ? LogLevel.Info : LogLevel.Error,
|
|
||||||
"Download finished for {Id} with {State}", t.m_nPublishedFileId.m_PublishedFileId, t.m_eResult);
|
|
||||||
});
|
|
||||||
|
|
||||||
//items could have other flags besides installed
|
|
||||||
var toDownload = ids.Where(b =>
|
|
||||||
((EItemState)SteamUGC.GetItemState(new(b)) & EItemState.k_EItemStateInstalled) == 0).ToArray();
|
|
||||||
foreach (var id in toDownload)
|
|
||||||
{
|
|
||||||
LogFile.Log.Info("Updating workshop item {Id}", id);
|
|
||||||
|
|
||||||
SteamUGC.DownloadItem(new(id), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (count < toDownload.Length)
|
|
||||||
{
|
|
||||||
Steamworks.SteamAPI.RunCallbacks();
|
|
||||||
Thread.Sleep(10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,25 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
|
|
||||||
namespace PluginLoader.Tools;
|
|
||||||
|
|
||||||
public class PostHttpContent : HttpContent
|
|
||||||
{
|
|
||||||
private readonly byte[] content;
|
|
||||||
|
|
||||||
public PostHttpContent(string content)
|
|
||||||
{
|
|
||||||
this.content = content == null ? null : Tools.Utf8.GetBytes(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context)
|
|
||||||
{
|
|
||||||
if (content != null && content.Length > 0)
|
|
||||||
await stream.WriteAsync(content, 0, content.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool TryComputeLength(out long length)
|
|
||||||
{
|
|
||||||
length = content.Length;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,190 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
using System.Text;
|
|
||||||
using LitJson;
|
|
||||||
|
|
||||||
namespace PluginLoader.Tools;
|
|
||||||
|
|
||||||
public static class SimpleHttpClient
|
|
||||||
{
|
|
||||||
// REST API request timeout in milliseconds
|
|
||||||
private const int TimeoutMs = 3000;
|
|
||||||
|
|
||||||
public static TV Get<TV>(string url)
|
|
||||||
where TV : class, new()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var response = (HttpWebResponse)CreateRequest(HttpMethod.Get, url).GetResponse();
|
|
||||||
|
|
||||||
using var responseStream = response.GetResponseStream();
|
|
||||||
if (responseStream == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
using var streamReader = new StreamReader(responseStream, Encoding.UTF8);
|
|
||||||
return JsonMapper.ToObject<TV>(streamReader.ReadToEnd());
|
|
||||||
}
|
|
||||||
catch (WebException e)
|
|
||||||
{
|
|
||||||
LogFile.Log.Info($"REST API request failed: GET {url} [{e.Message}]");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TV Get<TV>(string url, Dictionary<string, string> parameters)
|
|
||||||
where TV : class, new()
|
|
||||||
{
|
|
||||||
var uriBuilder = new StringBuilder(url);
|
|
||||||
AppendQueryParameters(uriBuilder, parameters);
|
|
||||||
var uri = uriBuilder.ToString();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var response = (HttpWebResponse)CreateRequest(HttpMethod.Get, uri).GetResponse();
|
|
||||||
|
|
||||||
using var responseStream = response.GetResponseStream();
|
|
||||||
if (responseStream == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
using var streamReader = new StreamReader(responseStream, Encoding.UTF8);
|
|
||||||
return JsonMapper.ToObject<TV>(streamReader.ReadToEnd());
|
|
||||||
}
|
|
||||||
catch (WebException e)
|
|
||||||
{
|
|
||||||
LogFile.Log.Info($"REST API request failed: GET {uri} [{e.Message}]");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TV Post<TV>(string url)
|
|
||||||
where TV : class, new()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var request = CreateRequest(HttpMethod.Post, url);
|
|
||||||
request.ContentLength = 0L;
|
|
||||||
return PostRequest<TV>(request);
|
|
||||||
}
|
|
||||||
catch (WebException e)
|
|
||||||
{
|
|
||||||
LogFile.Log.Info($"REST API request failed: POST {url} [{e.Message}]");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TV Post<TV>(string url, Dictionary<string, string> parameters)
|
|
||||||
where TV : class, new()
|
|
||||||
{
|
|
||||||
var uriBuilder = new StringBuilder(url);
|
|
||||||
AppendQueryParameters(uriBuilder, parameters);
|
|
||||||
var uri = uriBuilder.ToString();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var request = CreateRequest(HttpMethod.Post, uri);
|
|
||||||
request.ContentType = "application/x-www-form-urlencoded";
|
|
||||||
request.ContentLength = 0;
|
|
||||||
return PostRequest<TV>(request);
|
|
||||||
}
|
|
||||||
catch (WebException e)
|
|
||||||
{
|
|
||||||
LogFile.Log.Info($"REST API request failed: POST {uri} [{e.Message}]");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TV Post<TV, TR>(string url, TR body)
|
|
||||||
where TR : class, new()
|
|
||||||
where TV : class, new()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var request = CreateRequest(HttpMethod.Post, url);
|
|
||||||
var requestJson = JsonMapper.ToJson(body);
|
|
||||||
var requestBytes = Encoding.UTF8.GetBytes(requestJson);
|
|
||||||
request.ContentType = "application/json";
|
|
||||||
request.ContentLength = requestBytes.Length;
|
|
||||||
return PostRequest<TV>(request, requestBytes);
|
|
||||||
}
|
|
||||||
catch (WebException e)
|
|
||||||
{
|
|
||||||
LogFile.Log.Info($"REST API request failed: POST {url} [{e.Message}]");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool Post<TR>(string url, TR body)
|
|
||||||
where TR : class, new()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var request = CreateRequest(HttpMethod.Post, url);
|
|
||||||
var requestJson = JsonMapper.ToJson(body);
|
|
||||||
var requestBytes = Encoding.UTF8.GetBytes(requestJson);
|
|
||||||
request.ContentType = "application/json";
|
|
||||||
request.ContentLength = requestBytes.Length;
|
|
||||||
return PostRequest(request, requestBytes);
|
|
||||||
}
|
|
||||||
catch (WebException e)
|
|
||||||
{
|
|
||||||
LogFile.Log.Info($"REST API request failed: POST {url} [{e.Message}]");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static TV PostRequest<TV>(HttpWebRequest request, byte[] body = null) where TV : class, new()
|
|
||||||
{
|
|
||||||
if (body != null)
|
|
||||||
{
|
|
||||||
using var requestStream = request.GetRequestStream();
|
|
||||||
requestStream.Write(body, 0, body.Length);
|
|
||||||
requestStream.Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
using var response = (HttpWebResponse)request.GetResponse();
|
|
||||||
using var responseStream = response.GetResponseStream();
|
|
||||||
if (responseStream == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
using var streamReader = new StreamReader(responseStream, Encoding.UTF8);
|
|
||||||
var data = JsonMapper.ToObject<TV>(streamReader.ReadToEnd());
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool PostRequest(HttpWebRequest request, byte[] body = null)
|
|
||||||
{
|
|
||||||
if (body != null)
|
|
||||||
{
|
|
||||||
using var requestStream = request.GetRequestStream();
|
|
||||||
requestStream.Write(body, 0, body.Length);
|
|
||||||
requestStream.Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
using var response = (HttpWebResponse)request.GetResponse();
|
|
||||||
|
|
||||||
return response.StatusCode == HttpStatusCode.OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static HttpWebRequest CreateRequest(HttpMethod method, string url)
|
|
||||||
{
|
|
||||||
var http = WebRequest.CreateHttp(url);
|
|
||||||
http.Method = method.ToString().ToUpper();
|
|
||||||
http.Timeout = TimeoutMs;
|
|
||||||
return http;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AppendQueryParameters(StringBuilder stringBuilder, Dictionary<string, string> parameters)
|
|
||||||
{
|
|
||||||
if (parameters == null || parameters.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var first = true;
|
|
||||||
foreach (var p in parameters)
|
|
||||||
{
|
|
||||||
stringBuilder.Append(first ? '?' : '&');
|
|
||||||
first = false;
|
|
||||||
stringBuilder.Append(Uri.EscapeDataString(p.Key));
|
|
||||||
stringBuilder.Append('=');
|
|
||||||
stringBuilder.Append(Uri.EscapeDataString(p.Value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,52 +0,0 @@
|
|||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text;
|
|
||||||
using Steamworks;
|
|
||||||
|
|
||||||
namespace PluginLoader.Tools;
|
|
||||||
|
|
||||||
public static class Tools
|
|
||||||
{
|
|
||||||
public static readonly UTF8Encoding Utf8 = new();
|
|
||||||
|
|
||||||
public static string Sha1HexDigest(string text)
|
|
||||||
{
|
|
||||||
using var sha1 = new SHA1Managed();
|
|
||||||
var buffer = Utf8.GetBytes(text);
|
|
||||||
var digest = sha1.ComputeHash(buffer);
|
|
||||||
return BytesToHex(digest);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string BytesToHex(IReadOnlyCollection<byte> ba)
|
|
||||||
{
|
|
||||||
var hex = new StringBuilder(2 * ba.Count);
|
|
||||||
|
|
||||||
foreach (var t in ba)
|
|
||||||
hex.Append(t.ToString("x2"));
|
|
||||||
|
|
||||||
return hex.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string FormatDateIso8601(DateTime dt)
|
|
||||||
{
|
|
||||||
return dt.ToString("s").Substring(0, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ulong GetSteamId()
|
|
||||||
{
|
|
||||||
return SteamUser.GetSteamID().m_SteamID;
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: Replace this with the proper library call, I could not find one
|
|
||||||
public static string FormatUriQueryString(Dictionary<string, string> parameters)
|
|
||||||
{
|
|
||||||
var query = new StringBuilder();
|
|
||||||
foreach (var p in parameters)
|
|
||||||
{
|
|
||||||
if (query.Length > 0)
|
|
||||||
query.Append('&');
|
|
||||||
query.Append($"{Uri.EscapeDataString(p.Key)}={Uri.EscapeDataString(p.Value)}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return query.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,147 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 1,
|
|
||||||
"dependencies": {
|
|
||||||
"net8.0-windows10.0.19041": {
|
|
||||||
"Krafs.Publicizer": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[2.2.1, )",
|
|
||||||
"resolved": "2.2.1",
|
|
||||||
"contentHash": "QGI4nMGQbKsuFUUboixVHu4mv3lHB5RejIa7toIlzTmwLkuCYYEpUBJjmy3OpXYyj5dVSZAXVbr4oeMSloE67Q=="
|
|
||||||
},
|
|
||||||
"Lib.Harmony.Thin": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[2.3.3, )",
|
|
||||||
"resolved": "2.3.3",
|
|
||||||
"contentHash": "jsaFv7XnWJnyfyvFbkgIkZtV6tWMteNUcDK3idq+3LwPqpTFNxsOv2eKmj4qqP8QR8UynG1Y9AUaC/+dVruMHg==",
|
|
||||||
"dependencies": {
|
|
||||||
"MonoMod.Core": "1.1.0",
|
|
||||||
"System.Text.Json": "8.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.CodeAnalysis.CSharp": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[4.11.0, )",
|
|
||||||
"resolved": "4.11.0",
|
|
||||||
"contentHash": "6XYi2EusI8JT4y2l/F3VVVS+ISoIX9nqHsZRaG6W5aFeJ5BEuBosHfT/ABb73FN0RZ1Z3cj2j7cL28SToJPXOw==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.CodeAnalysis.Analyzers": "3.3.4",
|
|
||||||
"Microsoft.CodeAnalysis.Common": "[4.11.0]",
|
|
||||||
"System.Collections.Immutable": "8.0.0",
|
|
||||||
"System.Reflection.Metadata": "8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"NLog": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[5.3.4, )",
|
|
||||||
"resolved": "5.3.4",
|
|
||||||
"contentHash": "gLy7+O1hEYJXIlcTr1/VWjGXrZTQFZzYNO18IWasD64pNwz0BreV+nHLxWKXWZzERRzoKnsk2XYtwLkTVk7J1A=="
|
|
||||||
},
|
|
||||||
"SpaceEngineersDedicated.ReferenceAssemblies": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[1.*, )",
|
|
||||||
"resolved": "1.204.18",
|
|
||||||
"contentHash": "GT7/9CBMx4jjor41zLOOl87YYM/JdJD8xp9ccXyuhP2oUaz25H3ZmCQuGeAuZNENKru1a/7hZrId4PwlMDGoew==",
|
|
||||||
"dependencies": {
|
|
||||||
"SharpDX": "4.2.0-keen-cringe",
|
|
||||||
"protobuf-net": "1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Steamworks.NET": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[20.1.0, )",
|
|
||||||
"resolved": "20.1.0",
|
|
||||||
"contentHash": "+GntwnyJ5tCNvUIaQxv2+ehDvZJzGUqlSB5xRBk1hTj1qqBJ6s4vK/OfGD/jae7aTmXiGSm8wpJORosNtQevJQ=="
|
|
||||||
},
|
|
||||||
"Microsoft.CodeAnalysis.Analyzers": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "3.3.4",
|
|
||||||
"contentHash": "AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g=="
|
|
||||||
},
|
|
||||||
"Microsoft.CodeAnalysis.Common": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.11.0",
|
|
||||||
"contentHash": "djf8ujmqYImFgB04UGtcsEhHrzVqzHowS+EEl/Yunc5LdrYrZhGBWUTXoCF0NzYXJxtfuD+UVQarWpvrNc94Qg==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.CodeAnalysis.Analyzers": "3.3.4",
|
|
||||||
"System.Collections.Immutable": "8.0.0",
|
|
||||||
"System.Reflection.Metadata": "8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Mono.Cecil": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "0.11.5",
|
|
||||||
"contentHash": "fxfX+0JGTZ8YQeu1MYjbBiK2CYTSzDyEeIixt+yqKKTn7FW8rv7JMY70qevup4ZJfD7Kk/VG/jDzQQTpfch87g=="
|
|
||||||
},
|
|
||||||
"MonoMod.Backports": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "1.1.0",
|
|
||||||
"contentHash": "GUAjCrCZEddqHKHFA7Lh61PgTzoKY7gfBShFe0hQe0p8iynHhBK3TWGyRi+QIw/PGfaRPwx6c33CPGFURBVM6g==",
|
|
||||||
"dependencies": {
|
|
||||||
"MonoMod.ILHelpers": "1.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"MonoMod.Core": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "1.1.0",
|
|
||||||
"contentHash": "Ks8RntZGVcktr2QF/AovTEbuOkrgXz6omjrvT5LRveOIQJuy+IFuEQPBVWu+cSKVIoZD5XkpRFvlVrItgPIrXw==",
|
|
||||||
"dependencies": {
|
|
||||||
"Mono.Cecil": "0.11.5",
|
|
||||||
"MonoMod.Backports": "1.1.0",
|
|
||||||
"MonoMod.ILHelpers": "1.0.1",
|
|
||||||
"MonoMod.Utils": "25.0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"MonoMod.ILHelpers": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "1.0.1",
|
|
||||||
"contentHash": "6djj/Hz+/eTomo1H/sJEJNxBz2ZdhXjvH0MOmyU2xRtbjaIfBQuyVV0zNUbJhMY/8qoWrz7WXfskfFhdaY0afA=="
|
|
||||||
},
|
|
||||||
"MonoMod.Utils": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "25.0.4",
|
|
||||||
"contentHash": "cB94MaZtFD9u4clYEFTwM4jGXnJnzXsxYF3yBpMZKHhXOas66tMF2frbdYte023i0MH4C5iRJbDjxHmA4x5VgA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Mono.Cecil": "0.11.5",
|
|
||||||
"MonoMod.Backports": "1.1.0",
|
|
||||||
"MonoMod.ILHelpers": "1.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"protobuf-net": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "1.0.0",
|
|
||||||
"contentHash": "kTGOK0E87473sOImOjgZOnz3kTC2aMLffoRWQLYNuBLJnwNNmjanF9IkevZ9Q7yYLeABQfcF3BpeepuMntMVNw=="
|
|
||||||
},
|
|
||||||
"SharpDX": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.2.0-keen-cringe",
|
|
||||||
"contentHash": "LaJN3h1Gi1FWVdef2I5WtOH9gwzKCBniH0CragarbkN2QheYY6Lqm+91PcOfp1w/4wdVb+k8Kjv3sO393Tphtw=="
|
|
||||||
},
|
|
||||||
"System.Collections.Immutable": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.0",
|
|
||||||
"contentHash": "AurL6Y5BA1WotzlEvVaIDpqzpIPvYnnldxru8oXJU2yFxFUy3+pNXjXd1ymO+RA0rq0+590Q8gaz2l3Sr7fmqg=="
|
|
||||||
},
|
|
||||||
"System.Reflection.Metadata": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.0",
|
|
||||||
"contentHash": "ptvgrFh7PvWI8bcVqG5rsA/weWM09EnthFHR5SCnS6IN+P4mj6rE1lBDC4U8HL9/57htKAqy4KQ3bBj84cfYyQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"System.Collections.Immutable": "8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.Text.Encodings.Web": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.0",
|
|
||||||
"contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ=="
|
|
||||||
},
|
|
||||||
"System.Text.Json": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.0.1",
|
|
||||||
"contentHash": "7AWk2za1hSEJBppe/Lg+uDcam2TrDqwIKa9XcPssSwyjC2xa39EKEGul3CO5RWNF+hMuZG4zlBDrvhBdDTg4lg==",
|
|
||||||
"dependencies": {
|
|
||||||
"System.Text.Encodings.Web": "8.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 515 KiB |
Binary file not shown.
Reference in New Issue
Block a user