embed plugin loader directly into the launcher
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CringeLauncher", "CringeLauncher\CringeLauncher.csproj", "{219C897E-452D-49B5-80C4-F3008718C16A}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CringeLauncher", "CringeLauncher\CringeLauncher.csproj", "{219C897E-452D-49B5-80C4-F3008718C16A}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginLoader", "PluginLoader\PluginLoader.csproj", "{A7C22A74-56EA-4DC2-89AA-A1134BFB8497}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -12,5 +14,9 @@ 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
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net6.0-windows</TargetFramework>
|
<TargetFramework>net6.0-windows10.0.19041.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<UseWindowsForms>true</UseWindowsForms>
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
@@ -16,6 +16,11 @@
|
|||||||
<PackageReference Include="System.Diagnostics.PerformanceCounter" Version="6.0.1" />
|
<PackageReference Include="System.Diagnostics.PerformanceCounter" Version="6.0.1" />
|
||||||
<PackageReference Include="System.Management" Version="6.0.0" />
|
<PackageReference Include="System.Management" Version="6.0.0" />
|
||||||
<PackageReference Include="System.Private.ServiceModel" Version="4.10.0" />
|
<PackageReference Include="System.Private.ServiceModel" Version="4.10.0" />
|
||||||
|
<PackageReference Include="Torch.SixLabors.ImageSharp" Version="1.0.0-beta6" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\PluginLoader\PluginLoader.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="Unfuckit" AfterTargets="Build">
|
<Target Name="Unfuckit" AfterTargets="Build">
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
using HarmonyLib;
|
using System.Reflection;
|
||||||
|
using HarmonyLib;
|
||||||
using Sandbox;
|
using Sandbox;
|
||||||
using Sandbox.Engine.Multiplayer;
|
using Sandbox.Engine.Multiplayer;
|
||||||
using Sandbox.Engine.Networking;
|
using Sandbox.Engine.Networking;
|
||||||
@@ -16,6 +17,7 @@ using VRage.Game;
|
|||||||
using VRage.GameServices;
|
using VRage.GameServices;
|
||||||
using VRage.Mod.Io;
|
using VRage.Mod.Io;
|
||||||
using VRage.Platform.Windows;
|
using VRage.Platform.Windows;
|
||||||
|
using VRage.Plugins;
|
||||||
using VRage.Steam;
|
using VRage.Steam;
|
||||||
using VRage.UserInterface;
|
using VRage.UserInterface;
|
||||||
using VRageRender;
|
using VRageRender;
|
||||||
@@ -51,6 +53,10 @@ public class Launcher : IDisposable
|
|||||||
MyVRage.Platform.System.OnThreadpoolInitialized();
|
MyVRage.Platform.System.OnThreadpoolInitialized();
|
||||||
InitRender();
|
InitRender();
|
||||||
MyFileSystem.InitUserSpecific(MyGameService.UserId.ToString());
|
MyFileSystem.InitUserSpecific(MyGameService.UserId.ToString());
|
||||||
|
AccessTools.MethodDelegate<Action<List<Assembly>>>(AccessTools.Method(typeof(MyPlugins), "LoadPlugins"))(new()
|
||||||
|
{
|
||||||
|
typeof(PluginLoader.Main).Assembly
|
||||||
|
});
|
||||||
_game = new(args);
|
_game = new(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
103
PluginLoader/Compiler/RoslynCompiler.cs
Normal file
103
PluginLoader/Compiler/RoslynCompiler.cs
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
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));
|
||||||
|
|
||||||
|
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.WriteLine(
|
||||||
|
$"{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; }
|
||||||
|
}
|
||||||
|
}
|
135
PluginLoader/Compiler/RoslynReferences.cs
Normal file
135
PluginLoader/Compiler/RoslynReferences.cs
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
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.WriteLine(
|
||||||
|
$"WARNING: Multiple Harmony assemblies are loaded. Plugin Loader is using {harmonyInfo} but found {name}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.WriteLine(
|
||||||
|
$"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.WriteLine(sb.ToString(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ContainsReference(AssemblyName name)
|
||||||
|
{
|
||||||
|
return allReferences.ContainsKey(name.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryLoadAssembly(AssemblyName name, out Assembly aRef)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
aRef = Assembly.Load(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.WriteLine("Reference added at runtime: " + a.FullName);
|
||||||
|
MetadataReference aRef = MetadataReference.CreateFromFile(a.Location);
|
||||||
|
allReferences[a.GetName().Name] = aRef;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
LogFile.WriteLine("WARNING: Unable to find the assembly '" + name + "'!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
159
PluginLoader/Data/GitHubPlugin.cs
Normal file
159
PluginLoader/Data/GitHubPlugin.cs
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
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
|
||||||
|
{
|
||||||
|
a = Assembly.LoadFile(dllFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
7
PluginLoader/Data/ISteamItem.cs
Normal file
7
PluginLoader/Data/ISteamItem.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace PluginLoader.Data;
|
||||||
|
|
||||||
|
public interface ISteamItem
|
||||||
|
{
|
||||||
|
string Id { get; }
|
||||||
|
ulong WorkshopId { get; }
|
||||||
|
}
|
268
PluginLoader/Data/LocalFolderPlugin.cs
Normal file
268
PluginLoader/Data/LocalFolderPlugin.cs
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
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.WriteLine(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));
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb = new StringBuilder("An error occurred while checking git for project files.").AppendLine();
|
||||||
|
if (!string.IsNullOrWhiteSpace(gitError))
|
||||||
|
{
|
||||||
|
sb.AppendLine("Git output: ");
|
||||||
|
sb.Append(gitError).AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
LogFile.WriteLine(sb.ToString());
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder("An error occurred while checking git for project files.").AppendLine();
|
||||||
|
if (!string.IsNullOrWhiteSpace(gitError))
|
||||||
|
{
|
||||||
|
sb.AppendLine(" Git output: ");
|
||||||
|
sb.Append(gitError).AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.AppendLine("Exception: ");
|
||||||
|
sb.Append(e).AppendLine();
|
||||||
|
LogFile.WriteLine(sb.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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.WriteLine("Error while reading the xml file: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
92
PluginLoader/Data/LocalPlugin.cs
Normal file
92
PluginLoader/Data/LocalPlugin.cs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
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))
|
||||||
|
{
|
||||||
|
AppDomain.CurrentDomain.AssemblyResolve += LoadFromSameFolder;
|
||||||
|
var a = Assembly.LoadFile(Id);
|
||||||
|
Version = a.GetName().Version;
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
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(location);
|
||||||
|
var assemblyPath = Path.Combine(folderPath, new AssemblyName(args.Name).Name + ".dll");
|
||||||
|
if (!File.Exists(assemblyPath))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var assembly = Assembly.LoadFile(assemblyPath);
|
||||||
|
LogFile.WriteLine("Resolving " + assembly.GetName().Name + " for " + args.RequestingAssembly.FullName);
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
100
PluginLoader/Data/ModPlugin.cs
Normal file
100
PluginLoader/Data/ModPlugin.cs
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
223
PluginLoader/Data/PluginData.cs
Normal file
223
PluginLoader/Data/PluginData.cs
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
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.WriteLine("Failed to load " + ToString());
|
||||||
|
Error();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Precompile the entire assembly in order to force any missing method exceptions
|
||||||
|
LogFile.WriteLine("Precompiling " + a);
|
||||||
|
LoaderTools.Precompile(a);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
var name = ToString();
|
||||||
|
LogFile.WriteLine($"Failed to load {name} because of an error: " + e);
|
||||||
|
if (e is MissingMemberException)
|
||||||
|
LogFile.WriteLine($"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.WriteLine("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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
10
PluginLoader/Data/PluginStatus.cs
Normal file
10
PluginLoader/Data/PluginStatus.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace PluginLoader.Data;
|
||||||
|
|
||||||
|
public enum PluginStatus
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
PendingUpdate,
|
||||||
|
Updated,
|
||||||
|
Error,
|
||||||
|
Blocked
|
||||||
|
}
|
45
PluginLoader/Data/SEPMPlugin.cs
Normal file
45
PluginLoader/Data/SEPMPlugin.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
105
PluginLoader/Data/SteamPlugin.cs
Normal file
105
PluginLoader/Data/SteamPlugin.cs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
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.WriteLine("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;
|
||||||
|
}
|
||||||
|
}
|
43
PluginLoader/Data/WorkshopPlugin.cs
Normal file
43
PluginLoader/Data/WorkshopPlugin.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
65
PluginLoader/GUI/ConfirmationDialog.cs
Normal file
65
PluginLoader/GUI/ConfirmationDialog.cs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
81
PluginLoader/GUI/GuiControls/RatingControl.cs
Normal file
81
PluginLoader/GUI/GuiControls/RatingControl.cs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
PluginLoader/GUI/ItemView.cs
Normal file
13
PluginLoader/GUI/ItemView.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace PluginLoader.GUI;
|
||||||
|
|
||||||
|
public class ItemView
|
||||||
|
{
|
||||||
|
public readonly string[] Labels;
|
||||||
|
public readonly object[] Values;
|
||||||
|
|
||||||
|
public ItemView(string[] labels, object[] values)
|
||||||
|
{
|
||||||
|
Labels = labels;
|
||||||
|
Values = values;
|
||||||
|
}
|
||||||
|
}
|
832
PluginLoader/GUI/MyGuiScreenPluginConfig.cs
Normal file
832
PluginLoader/GUI/MyGuiScreenPluginConfig.cs
Normal file
@@ -0,0 +1,832 @@
|
|||||||
|
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;
|
||||||
|
using Parallel = ParallelTasks.Parallel;
|
||||||
|
|
||||||
|
namespace PluginLoader.GUI;
|
||||||
|
|
||||||
|
public class MyGuiScreenPluginConfig : MyGuiScreenBase
|
||||||
|
{
|
||||||
|
private const float BarWidth = 0.85f;
|
||||||
|
private const float Spacing = 0.0175f;
|
||||||
|
|
||||||
|
private static bool allItemsVisible = 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.WriteLine("Downloading user statistics", false);
|
||||||
|
Parallel.Start(() => { PluginStats = StatsClient.DownloadStats(); }, OnDownloadedStats);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDownloadedStats()
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
allItemsVisible = false;
|
||||||
|
btn.Icon = IconShow;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
allItemsVisible = 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 (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
|
||||||
|
}
|
123
PluginLoader/GUI/NameDialog.cs
Normal file
123
PluginLoader/GUI/NameDialog.cs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
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";
|
||||||
|
}
|
||||||
|
}
|
90
PluginLoader/GUI/PlayerConsent.cs
Normal file
90
PluginLoader/GUI/PlayerConsent.cs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
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.WriteLine("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();
|
||||||
|
}
|
||||||
|
}
|
439
PluginLoader/GUI/PluginDetails.cs
Normal file
439
PluginLoader/GUI/PluginDetails.cs
Normal file
@@ -0,0 +1,439 @@
|
|||||||
|
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
|
||||||
|
}
|
96
PluginLoader/GUI/ProfilesDialog.cs
Normal file
96
PluginLoader/GUI/ProfilesDialog.cs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
138
PluginLoader/GUI/SplashScreen.cs
Normal file
138
PluginLoader/GUI/SplashScreen.cs
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using Sandbox.Game;
|
||||||
|
using VRage;
|
||||||
|
|
||||||
|
namespace PluginLoader.GUI;
|
||||||
|
|
||||||
|
public 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()
|
||||||
|
{
|
||||||
|
Image gif;
|
||||||
|
if (Application.OpenForms.Count == 0 || !TryLoadImage(out gif))
|
||||||
|
{
|
||||||
|
invalid = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultSplash = Application.OpenForms[0];
|
||||||
|
Size = defaultSplash.Size;
|
||||||
|
ClientSize = defaultSplash.ClientSize;
|
||||||
|
MyVRage.Platform.Windows.HideSplashScreen();
|
||||||
|
|
||||||
|
Name = "SplashScreenPluginLoader";
|
||||||
|
TopMost = true;
|
||||||
|
FormBorderStyle = FormBorderStyle.None;
|
||||||
|
|
||||||
|
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.StretchImage
|
||||||
|
};
|
||||||
|
Controls.Add(gifBox);
|
||||||
|
|
||||||
|
gifBox.Paint += OnPictureBoxDraw;
|
||||||
|
|
||||||
|
CenterToScreen();
|
||||||
|
Show();
|
||||||
|
ForceUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
gifBox.Invalidate();
|
||||||
|
ForceUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetBarValue(float percent = float.NaN)
|
||||||
|
{
|
||||||
|
if (invalid)
|
||||||
|
return;
|
||||||
|
|
||||||
|
barValue = percent;
|
||||||
|
gifBox.Invalidate();
|
||||||
|
ForceUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ForceUpdate()
|
||||||
|
{
|
||||||
|
Application.DoEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
ForceUpdate();
|
||||||
|
MyVRage.Platform.Windows.ShowSplashScreen(MyPerGameSettings.BasicGameInfo.SplashScreenImage, new(0.7f, 0.7f));
|
||||||
|
}
|
||||||
|
}
|
320
PluginLoader/GUI/TableDialogBase.cs
Normal file
320
PluginLoader/GUI/TableDialogBase.cs
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
255
PluginLoader/LoaderTools.cs
Normal file
255
PluginLoader/LoaderTools.cs
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Windows.UI.Popups;
|
||||||
|
using HarmonyLib;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
WinRT.Interop.InitializeWithWindow.Initialize(dialog, Process.GetCurrentProcess().MainWindowHandle);
|
||||||
|
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
LogFile.Dispose();
|
||||||
|
MySessionLoader.Unload();
|
||||||
|
MySandboxGame.Config.ControllerDefaultOnStart = MyInput.Static.IsJoystickLastUsed;
|
||||||
|
MySandboxGame.Config.Save();
|
||||||
|
MyScreenManager.CloseAllScreensNowExcept(null);
|
||||||
|
MyPlugins.Unload();
|
||||||
|
Restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Restart()
|
||||||
|
{
|
||||||
|
Application.Restart();
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This method attempts to disable JIT compiling for the assembly.
|
||||||
|
/// This method will force any member access exceptions by methods to be thrown now instead of later.
|
||||||
|
/// </summary>
|
||||||
|
public static void Precompile(Assembly a)
|
||||||
|
{
|
||||||
|
Type[] types;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
types = a.GetTypes();
|
||||||
|
}
|
||||||
|
catch (ReflectionTypeLoadException e)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("LoaderExceptions: ");
|
||||||
|
foreach (var e2 in e.LoaderExceptions)
|
||||||
|
sb.Append(e2).AppendLine();
|
||||||
|
LogFile.WriteLine(sb.ToString());
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var t in types)
|
||||||
|
{
|
||||||
|
// Static constructors allow for early code execution which can cause issues later in the game
|
||||||
|
if (HasStaticConstructor(t))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach (var m in t.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.NonPublic | BindingFlags.Public |
|
||||||
|
BindingFlags.Instance | BindingFlags.Static))
|
||||||
|
{
|
||||||
|
if (m.HasAttribute<HarmonyReversePatch>())
|
||||||
|
throw new("Harmony attribute 'HarmonyReversePatch' found on the method '" + m.Name +
|
||||||
|
"' is not compatible with Plugin Loader!");
|
||||||
|
Precompile(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Precompile(MethodInfo m)
|
||||||
|
{
|
||||||
|
if (!m.IsAbstract && !m.ContainsGenericParameters)
|
||||||
|
RuntimeHelpers.PrepareMethod(m.MethodHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasStaticConstructor(Type t)
|
||||||
|
{
|
||||||
|
return t.GetConstructors(BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic |
|
||||||
|
BindingFlags.Instance).Any(c => c.IsStatic);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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.WriteGameLog("Error while opening file dialog: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.WriteGameLog("Error while opening file dialog: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
77
PluginLoader/LogFile.cs
Normal file
77
PluginLoader/LogFile.cs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
using VRage.Utils;
|
||||||
|
|
||||||
|
namespace PluginLoader;
|
||||||
|
|
||||||
|
public static class LogFile
|
||||||
|
{
|
||||||
|
private const string fileName = "loader.log";
|
||||||
|
private static StreamWriter writer;
|
||||||
|
|
||||||
|
public static void Init(string mainPath)
|
||||||
|
{
|
||||||
|
var file = Path.Combine(mainPath, fileName);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
writer = File.CreateText(file);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
writer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes the specifed text to the log file.
|
||||||
|
/// WARNING: Not thread safe!
|
||||||
|
/// </summary>
|
||||||
|
public static void WriteLine(string text, bool gameLog = true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
writer?.WriteLine($"{DateTime.UtcNow:O} {text}");
|
||||||
|
if (gameLog)
|
||||||
|
WriteGameLog(text);
|
||||||
|
writer?.Flush();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes the specifed text to the game log file.
|
||||||
|
/// This function is thread safe.
|
||||||
|
/// </summary>
|
||||||
|
public static void WriteGameLog(string text)
|
||||||
|
{
|
||||||
|
MyLog.Default.WriteLine($"[PluginLoader] {text}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void WriteTrace(string text, bool gameLog = true)
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
writer?.WriteLine($"{DateTime.UtcNow:O} {text}");
|
||||||
|
if (gameLog)
|
||||||
|
WriteGameLog($"[PluginLoader] {text}");
|
||||||
|
writer?.Flush();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Dispose()
|
||||||
|
{
|
||||||
|
if (writer == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
writer.Flush();
|
||||||
|
writer.Close();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
writer = null;
|
||||||
|
}
|
||||||
|
}
|
243
PluginLoader/Main.cs
Normal file
243
PluginLoader/Main.cs
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
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
|
||||||
|
{
|
||||||
|
private const string HarmonyVersion = "2.2.1.0";
|
||||||
|
|
||||||
|
public static Main Instance;
|
||||||
|
|
||||||
|
private readonly List<PluginInstance> plugins = new();
|
||||||
|
|
||||||
|
private bool init;
|
||||||
|
|
||||||
|
public Main()
|
||||||
|
{
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
Splash = new();
|
||||||
|
|
||||||
|
Instance = this;
|
||||||
|
|
||||||
|
var temp = Cursor.Current;
|
||||||
|
Cursor.Current = Cursors.AppStarting;
|
||||||
|
|
||||||
|
var pluginsDir = LoaderTools.PluginsDir;
|
||||||
|
Directory.CreateDirectory(pluginsDir);
|
||||||
|
|
||||||
|
LogFile.Init(pluginsDir);
|
||||||
|
LogFile.WriteLine("Starting - v" + 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.WriteLine("An error occurred while setting up networking, web requests will probably fail: " + e);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.WriteLine("Patching");
|
||||||
|
|
||||||
|
// Check harmony version
|
||||||
|
var expectedHarmony = new Version(HarmonyVersion);
|
||||||
|
var actualHarmony = typeof(Harmony).Assembly.GetName().Version;
|
||||||
|
if (expectedHarmony != actualHarmony)
|
||||||
|
LogFile.WriteLine(
|
||||||
|
$"WARNING: Unexpected Harmony version, plugins may be unstable. Expected {expectedHarmony} but found {actualHarmony}");
|
||||||
|
|
||||||
|
new Harmony("avaness.PluginLoader").PatchAll(Assembly.GetExecutingAssembly());
|
||||||
|
|
||||||
|
Splash.SetText("Instantiating plugins...");
|
||||||
|
LogFile.WriteLine("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.WriteLine($"Finished startup. Took {sw.ElapsedMilliseconds}ms");
|
||||||
|
|
||||||
|
Cursor.Current = temp;
|
||||||
|
|
||||||
|
Splash.Delete();
|
||||||
|
Splash = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PluginList List { get; }
|
||||||
|
public PluginConfig Config { get; }
|
||||||
|
public SplashScreen Splash { get; }
|
||||||
|
|
||||||
|
/// <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.WriteLine($"Initializing {plugins.Count} plugins");
|
||||||
|
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;
|
||||||
|
LogFile.Dispose();
|
||||||
|
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 ReportEnabledPlugins()
|
||||||
|
{
|
||||||
|
if (!PlayerConsent.ConsentGiven)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Splash.SetText("Reporting plugin usage...");
|
||||||
|
LogFile.WriteLine("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.WriteLine("List of enabled plugins has been sent to the statistics server");
|
||||||
|
else
|
||||||
|
LogFile.WriteLine("Failed to send the list of enabled plugins to the statistics server");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterComponents()
|
||||||
|
{
|
||||||
|
LogFile.WriteLine($"Registering {plugins.Count} components");
|
||||||
|
foreach (var plugin in plugins)
|
||||||
|
plugin.RegisterSession(MySession.Static);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DisablePlugins()
|
||||||
|
{
|
||||||
|
Config.Disable();
|
||||||
|
plugins.Clear();
|
||||||
|
LogFile.WriteLine("Disabled all plugins");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InstantiatePlugins()
|
||||||
|
{
|
||||||
|
LogFile.WriteLine($"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();
|
||||||
|
if (args.Name.Contains("0Harmony"))
|
||||||
|
{
|
||||||
|
if (assembly != null)
|
||||||
|
LogFile.WriteLine("Resolving 0Harmony for " + assembly);
|
||||||
|
else
|
||||||
|
LogFile.WriteLine("Resolving 0Harmony");
|
||||||
|
return typeof(Harmony).Assembly;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.Name.Contains("SEPluginManager"))
|
||||||
|
{
|
||||||
|
if (assembly != null)
|
||||||
|
LogFile.WriteLine("Resolving SEPluginManager for " + assembly);
|
||||||
|
else
|
||||||
|
LogFile.WriteLine("Resolving SEPluginManager");
|
||||||
|
return typeof(SEPMPlugin).Assembly;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
48
PluginLoader/Network/GitHub.cs
Normal file
48
PluginLoader/Network/GitHub.cs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
using System.Net;
|
||||||
|
|
||||||
|
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}/";
|
||||||
|
|
||||||
|
public static Stream DownloadRepo(string name, string commit, out string fileName)
|
||||||
|
{
|
||||||
|
var uri = new Uri(string.Format(repoZipUrl, name, commit), UriKind.Absolute);
|
||||||
|
LogFile.WriteLine("Downloading " + uri);
|
||||||
|
var request = WebRequest.CreateHttp(uri);
|
||||||
|
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||||
|
request.Timeout = Main.Instance.Config.NetworkTimeout;
|
||||||
|
|
||||||
|
var response = (HttpWebResponse)request.GetResponse();
|
||||||
|
fileName = response.Headers["Content-Disposition"];
|
||||||
|
if (fileName != null)
|
||||||
|
{
|
||||||
|
var index = fileName.IndexOf("filename=");
|
||||||
|
if (index >= 0)
|
||||||
|
{
|
||||||
|
index += "filename=".Length;
|
||||||
|
fileName = fileName.Substring(index).Trim('"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.GetResponseStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Stream DownloadFile(string name, string commit, string path)
|
||||||
|
{
|
||||||
|
var uri = new Uri(string.Format(rawUrl, name, commit) + path.TrimStart('/'), UriKind.Absolute);
|
||||||
|
LogFile.WriteLine("Downloading " + uri);
|
||||||
|
var request = WebRequest.CreateHttp(uri);
|
||||||
|
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||||
|
request.Timeout = Main.Instance.Config.NetworkTimeout;
|
||||||
|
|
||||||
|
var response = (HttpWebResponse)request.GetResponse();
|
||||||
|
return response.GetResponseStream();
|
||||||
|
}
|
||||||
|
}
|
18
PluginLoader/Patch/Patch_ComponentRegistered.cs
Normal file
18
PluginLoader/Patch/Patch_ComponentRegistered.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
60
PluginLoader/Patch/Patch_CreateMenu.cs
Normal file
60
PluginLoader/Patch/Patch_CreateMenu.cs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
26
PluginLoader/Patch/Patch_DisableConfig.cs
Normal file
26
PluginLoader/Patch/Patch_DisableConfig.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
38
PluginLoader/Patch/Patch_IngameRestart.cs
Normal file
38
PluginLoader/Patch/Patch_IngameRestart.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
37
PluginLoader/Patch/Patch_MyDefinitionManager.cs
Normal file
37
PluginLoader/Patch/Patch_MyDefinitionManager.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
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.WriteLine("Loading client mod definitions for " + mod.WorkshopId);
|
||||||
|
newMods.Add(mod.GetModItem());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mods = newMods;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
LogFile.WriteLine("An error occured while loading client mods: " + e);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
48
PluginLoader/Patch/Patch_MyScriptManager.cs
Normal file
48
PluginLoader/Patch/Patch_MyScriptManager.cs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
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.WriteLine("Loading client mod scripts for " + mod.WorkshopId);
|
||||||
|
loadScripts(__instance, mod.ModLocation, mod.GetModContext());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
LogFile.WriteLine("An error occured while loading client mods: " + e);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
181
PluginLoader/PluginConfig.cs
Normal file
181
PluginLoader/PluginConfig.cs
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
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 NetworkTimeout
|
||||||
|
{
|
||||||
|
get => networkTimeout;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value < 100)
|
||||||
|
networkTimeout = 100;
|
||||||
|
else if (value > 60000)
|
||||||
|
networkTimeout = 60000;
|
||||||
|
else
|
||||||
|
networkTimeout = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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: ");
|
||||||
|
foreach (var id in EnabledPlugins)
|
||||||
|
if (!plugins.Contains(id))
|
||||||
|
{
|
||||||
|
LogFile.WriteLine($"{id} was in the config but is no longer available");
|
||||||
|
toRemove.Add(id);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.Append(id).Append(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EnabledPlugins.Count > 0)
|
||||||
|
sb.Length -= 2;
|
||||||
|
else
|
||||||
|
sb.Append("None");
|
||||||
|
LogFile.WriteLine(sb.ToString());
|
||||||
|
|
||||||
|
|
||||||
|
foreach (var id in toRemove)
|
||||||
|
EnabledPlugins.Remove(id);
|
||||||
|
|
||||||
|
if (toRemove.Count > 0)
|
||||||
|
Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Disable()
|
||||||
|
{
|
||||||
|
EnabledPlugins.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Save()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
LogFile.WriteLine("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.WriteLine("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.WriteLine("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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
176
PluginLoader/PluginInstance.cs
Normal file
176
PluginLoader/PluginInstance.cs
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
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.WriteLine($"Unable to find OpenConfigDialog() in {data} due to an error: {e}");
|
||||||
|
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.WriteLine($"Registered {count} session components from: {mainAssembly.FullName}");
|
||||||
|
}
|
||||||
|
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.WriteLine($"Failed to dispose {data} because of an error: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThrowError(string error)
|
||||||
|
{
|
||||||
|
LogFile.WriteLine(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 => typeof(IPlugin).IsAssignableFrom(t));
|
||||||
|
if (pluginType == null)
|
||||||
|
{
|
||||||
|
LogFile.WriteLine($"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();
|
||||||
|
}
|
||||||
|
}
|
369
PluginLoader/PluginList.cs
Normal file
369
PluginLoader/PluginList.cs
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
using System.Collections;
|
||||||
|
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.WriteLine("WARNING: No plugins in the plugin list. Plugin list will contain local plugins only.");
|
||||||
|
HasError = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
FindWorkshopPlugins(config);
|
||||||
|
FindLocalPlugins(config, mainDirectory);
|
||||||
|
LogFile.WriteLine($"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.WriteLine($"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, out PluginData[] list)
|
||||||
|
{
|
||||||
|
list = null;
|
||||||
|
|
||||||
|
if (File.Exists(file) && new FileInfo(file).Length > 0)
|
||||||
|
{
|
||||||
|
LogFile.WriteLine("Reading whitelist from cache");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (Stream binFile = File.OpenRead(file))
|
||||||
|
{
|
||||||
|
list = Serializer.Deserialize<PluginData[]>(binFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
LogFile.WriteLine("Whitelist retrieved from disk");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
LogFile.WriteLine("Error while reading whitelist: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LogFile.WriteLine("No whitelist cache exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryDownloadWhitelistFile(string file, string hash, PluginConfig config, out PluginData[] list)
|
||||||
|
{
|
||||||
|
list = null;
|
||||||
|
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())
|
||||||
|
using (var entryReader = new StreamReader(entryStream))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var data = (PluginData)xml.Deserialize(entryReader);
|
||||||
|
newPlugins[data.Id] = data;
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException e)
|
||||||
|
{
|
||||||
|
LogFile.WriteLine("An error occurred while reading the plugin xml: " +
|
||||||
|
(e.InnerException ?? e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
list = newPlugins.Values.ToArray();
|
||||||
|
return TrySaveWhitelist(file, list, hash, config);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
LogFile.WriteLine("Error while downloading whitelist: " + e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TrySaveWhitelist(string file, PluginData[] list, string hash, PluginConfig config)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
LogFile.WriteLine("Saving whitelist to disk");
|
||||||
|
using (var mem = new MemoryStream())
|
||||||
|
{
|
||||||
|
Serializer.Serialize(mem, list);
|
||||||
|
using (Stream binFile = File.Create(file))
|
||||||
|
{
|
||||||
|
mem.WriteTo(binFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.ListHash = hash;
|
||||||
|
config.Save();
|
||||||
|
|
||||||
|
LogFile.WriteLine("Whitelist updated");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
LogFile.WriteLine("Error while saving whitelist: " + e);
|
||||||
|
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.WriteLine("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.WriteLine($"The plugin '{steam}' is missing and cannot be loaded.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
LogFile.WriteLine($"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;
|
||||||
|
}
|
||||||
|
}
|
21
PluginLoader/PluginLoader.csproj
Normal file
21
PluginLoader/PluginLoader.csproj
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<Project Sdk="Microsoft.Net.SDK">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net6.0-windows10.0.19041.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>true</ImplicitUsings>
|
||||||
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Lib.Harmony" Version="2.2.2" />
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="2.9.0" IncludeAssets="compile" PrivateAssets="all" />
|
||||||
|
<PackageReference Include="protobuf-net" Version="2.4.7" />
|
||||||
|
<PackageReference Include="SpaceEngineersDedicated.ReferenceAssemblies" Version="1.201.13" IncludeAssets="compile" PrivateAssets="all" />
|
||||||
|
<PackageReference Include="Steamworks.NET" Version="15.0.1" IncludeAssets="compile" PrivateAssets="all" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="splash.gif" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
24
PluginLoader/Profile.cs
Normal file
24
PluginLoader/Profile.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
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; }
|
||||||
|
}
|
8
PluginLoader/SEPM/Logger.cs
Normal file
8
PluginLoader/SEPM/Logger.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace PluginLoader.SEPM;
|
||||||
|
|
||||||
|
public class Logger
|
||||||
|
{
|
||||||
|
public void Log(string text)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
9
PluginLoader/SEPM/SEPMPlugin.cs
Normal file
9
PluginLoader/SEPM/SEPMPlugin.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using HarmonyLib;
|
||||||
|
using VRage.Plugins;
|
||||||
|
|
||||||
|
namespace PluginLoader.SEPM;
|
||||||
|
|
||||||
|
public interface SEPMPlugin : IPlugin
|
||||||
|
{
|
||||||
|
void Main(Harmony harmony, Logger log);
|
||||||
|
}
|
14
PluginLoader/Stats/Model/ConsentRequest.cs
Normal file
14
PluginLoader/Stats/Model/ConsentRequest.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// 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; }
|
||||||
|
}
|
24
PluginLoader/Stats/Model/PluginStat.cs
Normal file
24
PluginLoader/Stats/Model/PluginStat.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
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; }
|
||||||
|
}
|
11
PluginLoader/Stats/Model/PluginStats.cs
Normal file
11
PluginLoader/Stats/Model/PluginStats.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
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; }
|
||||||
|
}
|
17
PluginLoader/Stats/Model/TrackRequest.cs
Normal file
17
PluginLoader/Stats/Model/TrackRequest.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
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; }
|
||||||
|
}
|
20
PluginLoader/Stats/Model/VoteRequest.cs
Normal file
20
PluginLoader/Stats/Model/VoteRequest.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
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; }
|
||||||
|
}
|
101
PluginLoader/Stats/StatsClient.cs
Normal file
101
PluginLoader/Stats/StatsClient.cs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
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.WriteLine("Registering player consent on the statistics server");
|
||||||
|
else
|
||||||
|
LogFile.WriteLine("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.WriteGameLog("Downloading plugin statistics anonymously...");
|
||||||
|
votingToken = null;
|
||||||
|
return SimpleHttpClient.Get<PluginStats>(StatsUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
LogFile.WriteGameLog("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.WriteLine("Voting token is not available, cannot vote");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogFile.WriteLine($"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;
|
||||||
|
}
|
||||||
|
}
|
83
PluginLoader/SteamAPI.cs
Normal file
83
PluginLoader/SteamAPI.cs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using HarmonyLib;
|
||||||
|
using Sandbox.Engine.Networking;
|
||||||
|
using Steamworks;
|
||||||
|
using VRage.Game;
|
||||||
|
using VRage.Utils;
|
||||||
|
using Parallel = ParallelTasks.Parallel;
|
||||||
|
|
||||||
|
namespace PluginLoader;
|
||||||
|
|
||||||
|
public static class SteamAPI
|
||||||
|
{
|
||||||
|
private static MethodInfo DownloadModsBlocking;
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
if (!ids.Any())
|
||||||
|
return;
|
||||||
|
|
||||||
|
var modItems =
|
||||||
|
new List<MyObjectBuilder_Checkpoint.ModItem>(
|
||||||
|
ids.Select(x => new MyObjectBuilder_Checkpoint.ModItem(x, "Steam")));
|
||||||
|
LogFile.WriteLine($"Updating {modItems.Count} workshop items");
|
||||||
|
|
||||||
|
// Source: MyWorkshop.DownloadWorldModsBlocking
|
||||||
|
var result = new MyWorkshop.ResultData();
|
||||||
|
var task = Parallel.Start(delegate { result = UpdateInternal(modItems); });
|
||||||
|
while (!task.IsComplete)
|
||||||
|
{
|
||||||
|
MyGameService.Update();
|
||||||
|
Thread.Sleep(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
var exceptions = task.Exceptions;
|
||||||
|
if (exceptions != null && exceptions.Length > 0)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("An error occurred while updating workshop items:");
|
||||||
|
foreach (var e in exceptions)
|
||||||
|
sb.Append(e);
|
||||||
|
LogFile.WriteLine(sb.ToString());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LogFile.WriteLine("Unable to update workshop items");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MyWorkshop.ResultData UpdateInternal(List<MyObjectBuilder_Checkpoint.ModItem> mods)
|
||||||
|
{
|
||||||
|
// Source: MyWorkshop.DownloadWorldModsBlockingInternal
|
||||||
|
|
||||||
|
MyLog.Default.IncreaseIndent();
|
||||||
|
|
||||||
|
var list = new List<WorkshopId>(mods.Select(x => new WorkshopId(x.PublishedFileId, x.PublishedServiceName)));
|
||||||
|
|
||||||
|
if (DownloadModsBlocking == null)
|
||||||
|
DownloadModsBlocking = AccessTools.Method(typeof(MyWorkshop), "DownloadModsBlocking");
|
||||||
|
|
||||||
|
var resultData = (MyWorkshop.ResultData)DownloadModsBlocking.Invoke(mods, new object[]
|
||||||
|
{
|
||||||
|
mods, new MyWorkshop.ResultData { Success = true }, list, new MyWorkshop.CancelToken()
|
||||||
|
});
|
||||||
|
|
||||||
|
MyLog.Default.DecreaseIndent();
|
||||||
|
return resultData;
|
||||||
|
}
|
||||||
|
}
|
25
PluginLoader/Tools/PostHttpContent.cs
Normal file
25
PluginLoader/Tools/PostHttpContent.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
190
PluginLoader/Tools/SimpleHttpClient.cs
Normal file
190
PluginLoader/Tools/SimpleHttpClient.cs
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
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.WriteGameLog($"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.WriteGameLog($"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.WriteGameLog($"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.WriteGameLog($"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.WriteGameLog($"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.WriteGameLog($"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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
52
PluginLoader/Tools/Tools.cs
Normal file
52
PluginLoader/Tools/Tools.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
BIN
PluginLoader/splash.gif
Normal file
BIN
PluginLoader/splash.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 870 KiB |
Reference in New Issue
Block a user