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
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CringeLauncher", "CringeLauncher\CringeLauncher.csproj", "{219C897E-452D-49B5-80C4-F3008718C16A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginLoader", "PluginLoader\PluginLoader.csproj", "{A7C22A74-56EA-4DC2-89AA-A1134BFB8497}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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}.Release|Any CPU.ActiveCfg = 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
|
||||
EndGlobal
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<TargetFramework>net6.0-windows10.0.19041.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
@@ -16,6 +16,11 @@
|
||||
<PackageReference Include="System.Diagnostics.PerformanceCounter" Version="6.0.1" />
|
||||
<PackageReference Include="System.Management" Version="6.0.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>
|
||||
|
||||
<Target Name="Unfuckit" AfterTargets="Build">
|
||||
|
@@ -1,4 +1,5 @@
|
||||
using HarmonyLib;
|
||||
using System.Reflection;
|
||||
using HarmonyLib;
|
||||
using Sandbox;
|
||||
using Sandbox.Engine.Multiplayer;
|
||||
using Sandbox.Engine.Networking;
|
||||
@@ -16,6 +17,7 @@ using VRage.Game;
|
||||
using VRage.GameServices;
|
||||
using VRage.Mod.Io;
|
||||
using VRage.Platform.Windows;
|
||||
using VRage.Plugins;
|
||||
using VRage.Steam;
|
||||
using VRage.UserInterface;
|
||||
using VRageRender;
|
||||
@@ -51,6 +53,10 @@ public class Launcher : IDisposable
|
||||
MyVRage.Platform.System.OnThreadpoolInitialized();
|
||||
InitRender();
|
||||
MyFileSystem.InitUserSpecific(MyGameService.UserId.ToString());
|
||||
AccessTools.MethodDelegate<Action<List<Assembly>>>(AccessTools.Method(typeof(MyPlugins), "LoadPlugins"))(new()
|
||||
{
|
||||
typeof(PluginLoader.Main).Assembly
|
||||
});
|
||||
_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