├── .gitattributes ├── .github └── FUNDING.yml ├── PluginLoader ├── splash.gif ├── Data │ ├── PluginStatus.cs │ ├── ISteamItem.cs │ ├── ObsoletePlugin.cs │ ├── GitHubPlugin.AssetFile.cs │ ├── LocalPlugin.cs │ ├── ModPlugin.cs │ ├── GitHubPlugin.CacheManifest.cs │ └── PluginData.cs ├── Config │ ├── GitHubPluginConfig.cs │ ├── LocalFolderConfig.cs │ ├── PluginDataConfig.cs │ └── PluginConfig.cs ├── Stats │ ├── Model │ │ ├── ConsentRequest.cs │ │ ├── VoteRequest.cs │ │ ├── PluginStats.cs │ │ ├── TrackRequest.cs │ │ └── PluginStat.cs │ └── StatsClient.cs ├── Patch │ ├── Patch_ComponentRegistered.cs │ ├── Patch_DisableConfig.cs │ ├── Patch_MyDefinitionManager.cs │ ├── Patch_MyScriptManager.cs │ ├── Patch_CreateMenu.cs │ └── Patch_IngameShortcuts.cs ├── Network │ ├── NuGetPackageList.cs │ ├── NuGetPackageId.cs │ ├── NuGetLogger.cs │ ├── GitHub.cs │ ├── NuGetPackage.cs │ └── NuGetClient.cs ├── deploy.bat ├── Properties │ └── AssemblyInfo.cs ├── Tools │ ├── Tools.cs │ └── SimpleHttpClient.cs ├── Profile.cs ├── GUI │ ├── TextInputDialog.cs │ ├── GuiControls │ │ ├── ParentButton.cs │ │ └── RatingControl.cs │ ├── ConfigurePlugin.cs │ ├── SplashScreen.cs │ ├── PlayerConsent.cs │ ├── ProfilesMenu.cs │ ├── PluginDetailMenu.cs │ └── PluginScreen.cs ├── LogFile.cs ├── SteamAPI.cs ├── AssemblyResolver.cs ├── Compiler │ ├── RoslynCompiler.cs │ └── RoslynReferences.cs ├── PluginInstance.cs ├── PluginLoader.csproj ├── Main.cs └── PluginList.cs ├── Edit-and-run-before-opening-solution.bat ├── README.md ├── LICENSE ├── PluginLoader.sln └── .gitignore /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [sepluginloader] 4 | -------------------------------------------------------------------------------- /PluginLoader/splash.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sepluginloader/PluginLoader/HEAD/PluginLoader/splash.gif -------------------------------------------------------------------------------- /PluginLoader/Data/PluginStatus.cs: -------------------------------------------------------------------------------- 1 | namespace avaness.PluginLoader.Data 2 | { 3 | public enum PluginStatus 4 | { 5 | None, PendingUpdate, Updated, Error, Blocked 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /PluginLoader/Data/ISteamItem.cs: -------------------------------------------------------------------------------- 1 | namespace avaness.PluginLoader.Data 2 | { 3 | public interface ISteamItem 4 | { 5 | string Id { get; } 6 | ulong WorkshopId { get; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /PluginLoader/Config/GitHubPluginConfig.cs: -------------------------------------------------------------------------------- 1 | namespace avaness.PluginLoader.Config 2 | { 3 | public class GitHubPluginConfig : PluginDataConfig 4 | { 5 | public string SelectedVersion { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /PluginLoader/Config/LocalFolderConfig.cs: -------------------------------------------------------------------------------- 1 | namespace avaness.PluginLoader.Config 2 | { 3 | public class LocalFolderConfig : PluginDataConfig 4 | { 5 | public string DataFile { get; set; } 6 | public bool DebugBuild { get; set; } = true; 7 | 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Edit-and-run-before-opening-solution.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | REM Location of your SpaceEngineers.exe 4 | mklink /J Bin64 "C:\Program Files (x86)\Steam\steamapps\common\SpaceEngineers\Bin64" 5 | 6 | REM Location of your workshop 7 | mklink /J workshop "C:\Program Files (x86)\Steam\steamapps\workshop" 8 | 9 | pause 10 | -------------------------------------------------------------------------------- /PluginLoader/Config/PluginDataConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Xml.Serialization; 2 | 3 | namespace avaness.PluginLoader.Config 4 | { 5 | [XmlInclude(typeof(LocalFolderConfig))] 6 | [XmlInclude(typeof(GitHubPluginConfig))] 7 | public abstract class PluginDataConfig 8 | { 9 | public string Id { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /PluginLoader/Data/ObsoletePlugin.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace avaness.PluginLoader.Data 4 | { 5 | internal class ObsoletePlugin : PluginData 6 | { 7 | public override string Source => "Obsolete"; 8 | public override bool IsLocal => false; 9 | public override bool IsCompiled => false; 10 | 11 | public override Assembly GetAssembly() 12 | { 13 | return null; 14 | } 15 | 16 | public override void Show() 17 | { 18 | 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /PluginLoader/Stats/Model/ConsentRequest.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable UnusedAutoPropertyAccessor.Global 2 | 3 | namespace avaness.PluginLoader.Stats.Model 4 | { 5 | // Request data received from the Plugin Loader to store user consent or withdrawal, 6 | // this request is NOT sent if the user does not give consent in the first place 7 | public class ConsentRequest 8 | { 9 | // Hash of the player's Steam ID 10 | public string PlayerHash { get; set; } 11 | 12 | // True if the consent has just given, false if has just withdrawn 13 | public bool Consent { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /PluginLoader/Patch/Patch_ComponentRegistered.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Sandbox.Game.World; 3 | using System.Reflection; 4 | using VRage.Game; 5 | using VRage.Plugins; 6 | 7 | namespace avaness.PluginLoader.Patch 8 | { 9 | [HarmonyPatch(typeof(MySession), "RegisterComponentsFromAssembly")] 10 | [HarmonyPatch(new[] { typeof(Assembly), typeof(bool), typeof(MyModContext) })] 11 | public static class Patch_ComponentRegistered 12 | { 13 | public static void Prefix(Assembly assembly) 14 | { 15 | if(assembly == MyPlugins.GameAssembly) 16 | Main.Instance?.RegisterComponents(); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /PluginLoader/Network/NuGetPackageList.cs: -------------------------------------------------------------------------------- 1 | using ProtoBuf; 2 | using System.Xml.Serialization; 3 | 4 | namespace avaness.PluginLoader.Network 5 | { 6 | [ProtoContract] 7 | public class NuGetPackageList 8 | { 9 | [ProtoMember(1)] 10 | public string Config { get; set; } 11 | 12 | [ProtoMember(2)] 13 | [XmlElement("PackageReference")] 14 | public NuGetPackageId[] PackageIds { get; set; } 15 | 16 | public string PackagesConfigNormalized => Config?.Replace('\\', '/').TrimStart('/'); 17 | 18 | public bool HasPackages => !string.IsNullOrWhiteSpace(Config) || (PackageIds != null && PackageIds.Length > 0); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Discord](https://img.shields.io/discord/816445932693487678)](https://discord.gg/VJGjzdgnjf) 2 | 3 | > [!Warning] 4 | > **Plugin Loader has been shut down and will no longer receive any updates.** 5 | 6 | # PluginLoader 7 | A tool to load plugins for Space Engineers automatically. 8 | 9 | ## Installation 10 | To install Plugin Loader, install the special [game launcher](https://github.com/sepluginloader/SpaceEngineersLauncher). It will download and keep Plugin Loader up to date automatically. 11 | 12 | ## Plugin List 13 | The plugin list can only be found in game, but the data is retreived from the [PluginHub repository](https://github.com/sepluginloader/PluginHub). 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD Zero Clause License 2 | 3 | Copyright (c) 2025 SE Plugin Loader 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 10 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 11 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 12 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 13 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 14 | PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /PluginLoader/Stats/Model/VoteRequest.cs: -------------------------------------------------------------------------------- 1 | namespace avaness.PluginLoader.Stats.Model 2 | { 3 | // Request data sent to the StatsServer each time the user changes his/her vote on a plugin 4 | public class VoteRequest 5 | { 6 | // Id of the plugin 7 | public string PluginId { get; set; } 8 | 9 | // Obfuscated player identifier, see Track.PlayerHash 10 | public string PlayerHash { get; set; } 11 | 12 | // Voting token returned with the plugin stats 13 | public string VotingToken { get; set; } 14 | 15 | // Vote to store 16 | // +1: Upvote 17 | // 0: Clear vote 18 | // -1: Downvote 19 | public int Vote { get; set; } 20 | } 21 | } -------------------------------------------------------------------------------- /PluginLoader/deploy.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | Rem Run this script with the full file path as argument 3 | 4 | if [%1] == [] goto eof 5 | 6 | set se_folder=%~dp0..\Bin64 7 | set counter=1 8 | 9 | Rem Wait for the file to be ready 10 | :waitfile 11 | 2>nul ( 12 | >>%se_folder%\%~nx1 (call ) 13 | ) && (goto copyfile) || (echo File is in use.) 14 | set /a counter=counter+1 15 | echo Trying attempt #%counter% 16 | ping -n 6 127.0.0.1 >nul 17 | goto waitfile 18 | 19 | Rem Copy the file to the target location 20 | :copyfile 21 | echo Copying DLLs 22 | copy /y /b PluginLoader.dll "%se_folder%\" 23 | copy /y /b 0Harmony.dll "%se_folder%\" 24 | copy /y /b Newtonsoft.Json.dll "%se_folder%\" 25 | copy /y /b NuGet.*.dll "%se_folder%\" 26 | 27 | echo DONE deploying 28 | :eof 29 | -------------------------------------------------------------------------------- /PluginLoader/Stats/Model/PluginStats.cs: -------------------------------------------------------------------------------- 1 | using avaness.PluginLoader.Data; 2 | using System.Collections.Generic; 3 | 4 | namespace avaness.PluginLoader.Stats.Model 5 | { 6 | // Statistics for all plugins 7 | public class PluginStats 8 | { 9 | // Key: pluginId 10 | public Dictionary Stats { get; set; } = new Dictionary(); 11 | 12 | // Token the player is required to present for voting (making it harder to spoof votes) 13 | public string VotingToken { get; set; } 14 | 15 | public PluginStat GetStatsForPlugin(PluginData data) 16 | { 17 | if (Stats.TryGetValue(data.Id, out PluginStat result)) 18 | return result; 19 | return new PluginStat(); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /PluginLoader/Stats/Model/TrackRequest.cs: -------------------------------------------------------------------------------- 1 | namespace avaness.PluginLoader.Stats.Model 2 | { 3 | // Request data sent to the StatsServer each time the game is started 4 | public class TrackRequest 5 | { 6 | // Hash of the player's Steam ID 7 | // Hexdump of the first 80 bits of SHA1($"{steamId}") 8 | // The client determines the ID of the player, never the server. 9 | // Using a hash is required for data protection and privacy. 10 | // Using a hash makes it impractical to track back usage or votes to 11 | // individual players, while still allowing for near-perfect deduplication. 12 | // It also prevents stealing all the Steam IDs from the server's database. 13 | public string PlayerHash { get; set; } 14 | 15 | // Ids of enabled plugins when the game started 16 | public string[] EnabledPluginIds { get; set; } 17 | } 18 | } -------------------------------------------------------------------------------- /PluginLoader/Stats/Model/PluginStat.cs: -------------------------------------------------------------------------------- 1 | namespace avaness.PluginLoader.Stats.Model 2 | { 3 | // Statistics for a single plugin 4 | public class PluginStat 5 | { 6 | // Number of players who successfully started SE with this plugin enabled anytime during the past 30 days 7 | public int Players { get; set; } 8 | 9 | // Total number of upvotes and downvotes since the beginning (votes do not expire) 10 | public int Upvotes { get; set; } 11 | public int Downvotes { get; set; } 12 | 13 | // Whether the requesting player tried the plugin 14 | public bool Tried { get; set; } 15 | 16 | // Current vote of the requesting player 17 | // +1: Upvoted 18 | // 0: No vote (or cleared it) 19 | // -1: Downvoted 20 | public int Vote { get; set; } 21 | 22 | // Number of half stars [1-10] based on the upvote ratio, zero if there are not enough votes on the plugin yet 23 | public int Rating { get; set; } 24 | } 25 | } -------------------------------------------------------------------------------- /PluginLoader/Patch/Patch_DisableConfig.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Sandbox; 3 | using System.Windows.Forms; 4 | using VRage.Input; 5 | using avaness.PluginLoader.Config; 6 | 7 | namespace avaness.PluginLoader.Patch 8 | { 9 | [HarmonyPatch(typeof(MySandboxGame), "LoadData")] 10 | public static class Patch_DisableConfig 11 | { 12 | public static void Postfix() 13 | { 14 | // This is the earliest point in which I can use MyInput.Static 15 | if (Main.Instance == null) 16 | return; 17 | 18 | Main main = Main.Instance; 19 | PluginConfig config = main.Config; 20 | if(config != null && config.Count > 0 && MyInput.Static is MyVRageInput && MyInput.Static.IsKeyPress(MyKeys.Escape) 21 | && LoaderTools.ShowMessageBox("Escape pressed. Start the game with all plugins disabled?", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) 22 | { 23 | main.DisablePlugins(); 24 | MyInput.Static.ClearStates(); 25 | } 26 | else 27 | { 28 | main.InstantiatePlugins(); 29 | } 30 | } 31 | 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /PluginLoader.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.33423.256 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginLoader", "PluginLoader\PluginLoader.csproj", "{A7C22A74-56EA-4DC2-89AA-A1134BFB8497}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|x64 = Debug|x64 11 | Release|x64 = Release|x64 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {A7C22A74-56EA-4DC2-89AA-A1134BFB8497}.Debug|x64.ActiveCfg = Debug|Any CPU 15 | {A7C22A74-56EA-4DC2-89AA-A1134BFB8497}.Debug|x64.Build.0 = Debug|Any CPU 16 | {A7C22A74-56EA-4DC2-89AA-A1134BFB8497}.Release|x64.ActiveCfg = Release|Any CPU 17 | {A7C22A74-56EA-4DC2-89AA-A1134BFB8497}.Release|x64.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {967E9774-8CCE-457F-9261-7F880A0A1777} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /PluginLoader/Network/NuGetPackageId.cs: -------------------------------------------------------------------------------- 1 | using NuGet.Packaging.Core; 2 | using NuGet.Versioning; 3 | using ProtoBuf; 4 | using System.Xml.Serialization; 5 | 6 | namespace avaness.PluginLoader.Network 7 | { 8 | [ProtoContract] 9 | public class NuGetPackageId 10 | { 11 | [ProtoMember(1)] 12 | [XmlElement] 13 | public string Name { get; set; } 14 | 15 | [ProtoIgnore] 16 | [XmlAttribute("Include")] 17 | public string NameAttribute 18 | { 19 | get => Name; 20 | set => Name = value; 21 | } 22 | 23 | [ProtoMember(2)] 24 | [XmlElement] 25 | public string Version { get; set; } 26 | 27 | [ProtoIgnore] 28 | [XmlAttribute("Version")] 29 | public string VersionAttribute 30 | { 31 | get => Version; 32 | set => Version = value; 33 | } 34 | 35 | public bool TryGetIdentity(out PackageIdentity id) 36 | { 37 | id = null; 38 | if(string.IsNullOrWhiteSpace(Name) || string.IsNullOrWhiteSpace(Version)) 39 | return false; 40 | 41 | NuGetVersion version; 42 | if (!NuGetVersion.TryParse(Version, out version)) 43 | return false; 44 | 45 | id = new PackageIdentity(Name, version); 46 | return true; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /PluginLoader/Patch/Patch_MyDefinitionManager.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using System; 3 | using VRage.Game; 4 | using System.Linq; 5 | using avaness.PluginLoader.Data; 6 | using System.Collections.Generic; 7 | using Sandbox.Definitions; 8 | 9 | namespace avaness.PluginLoader.Patch 10 | { 11 | [HarmonyPatch(typeof(MyDefinitionManager), "LoadData")] 12 | public static class Patch_MyDefinitionManager 13 | { 14 | 15 | public static void Prefix(ref List mods) 16 | { 17 | 18 | try 19 | { 20 | HashSet currentMods = new HashSet(mods.Select(x => x.PublishedFileId)); 21 | List newMods = new List(mods); 22 | 23 | foreach (PluginData data in Main.Instance.Config.EnabledPlugins) 24 | { 25 | if (data is ModPlugin mod && !currentMods.Contains(mod.WorkshopId) && mod.Exists) 26 | { 27 | LogFile.WriteLine("Loading client mod definitions for " + mod.WorkshopId); 28 | newMods.Add(mod.GetModItem()); 29 | } 30 | } 31 | 32 | mods = newMods; 33 | } 34 | catch (Exception e) 35 | { 36 | LogFile.Error("An error occured while loading client mods: " + e); 37 | throw; 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /PluginLoader/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("PluginLoader")] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("")] 11 | [assembly: AssemblyProduct("PluginLoader")] 12 | [assembly: AssemblyCopyright("Copyright © 2021")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("a7c22a74-56ea-4dc2-89aa-a1134bfb8497")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion("1.12.8.0")] 35 | [assembly: AssemblyFileVersion("1.12.8.0")] 36 | -------------------------------------------------------------------------------- /PluginLoader/Tools/Tools.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Security.Cryptography; 4 | using System.Text; 5 | using Steamworks; 6 | 7 | namespace avaness.PluginLoader.Tools 8 | { 9 | public static class Tools 10 | { 11 | public static readonly UTF8Encoding Utf8 = new UTF8Encoding(); 12 | 13 | public static string Sha1HexDigest(string text) 14 | { 15 | using var sha1 = new SHA1Managed(); 16 | var buffer = Utf8.GetBytes(text); 17 | var digest = sha1.ComputeHash(buffer); 18 | return BytesToHex(digest); 19 | } 20 | 21 | private static string BytesToHex(IReadOnlyCollection ba) 22 | { 23 | var hex = new StringBuilder(2 * ba.Count); 24 | 25 | foreach (var t in ba) 26 | hex.Append(t.ToString("x2")); 27 | 28 | return hex.ToString(); 29 | } 30 | 31 | public static string FormatDateIso8601(DateTime dt) => dt.ToString("s").Substring(0, 10); 32 | 33 | public static ulong GetSteamId() 34 | { 35 | return SteamUser.GetSteamID().m_SteamID; 36 | } 37 | 38 | // FIXME: Replace this with the proper library call, I could not find one 39 | public static string FormatUriQueryString(Dictionary parameters) 40 | { 41 | var query = new StringBuilder(); 42 | foreach (var p in parameters) 43 | { 44 | if (query.Length > 0) 45 | query.Append('&'); 46 | query.Append($"{Uri.EscapeDataString(p.Key)}={Uri.EscapeDataString(p.Value)}"); 47 | } 48 | return query.ToString(); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /PluginLoader/Profile.cs: -------------------------------------------------------------------------------- 1 | using avaness.PluginLoader.Data; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace avaness.PluginLoader 6 | { 7 | public class Profile 8 | { 9 | // Unique key of the profile 10 | public string Key { get; set; } 11 | 12 | // Name of the profile 13 | public string Name { get; set; } 14 | 15 | // Plugin IDs 16 | public string[] Plugins { get; set; } 17 | 18 | public Profile() 19 | { 20 | } 21 | 22 | public Profile(string name, string[] plugins) 23 | { 24 | Key = Guid.NewGuid().ToString(); 25 | Name = name; 26 | Plugins = plugins; 27 | } 28 | 29 | public IEnumerable GetPlugins() 30 | { 31 | foreach (string id in Plugins) 32 | { 33 | if (Main.Instance.List.TryGetPlugin(id, out PluginData plugin)) 34 | yield return plugin; 35 | } 36 | } 37 | 38 | public string GetDescription() 39 | { 40 | int locals = 0; 41 | int plugins = 0; 42 | int mods = 0; 43 | foreach (PluginData plugin in GetPlugins()) 44 | { 45 | if (plugin.IsLocal) 46 | locals++; 47 | else if (plugin is ModPlugin) 48 | mods++; 49 | else 50 | plugins++; 51 | } 52 | 53 | List infoItems = new List(); 54 | if (locals > 0) 55 | infoItems.Add(locals > 1 ? $"{locals} local plugins" : "1 local plugin"); 56 | if (plugins > 0) 57 | infoItems.Add(plugins > 1 ? $"{plugins} plugins" : "1 plugin"); 58 | if (mods > 0) 59 | infoItems.Add(mods > 1 ? $"{mods} mods" : "1 mod"); 60 | 61 | return string.Join(", ", infoItems); 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /PluginLoader/GUI/TextInputDialog.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using Sandbox.Graphics.GUI; 4 | using VRage.Utils; 5 | using VRageMath; 6 | 7 | namespace avaness.PluginLoader.GUI 8 | { 9 | public class TextInputDialog : PluginScreen 10 | { 11 | private readonly string title; 12 | private string text; 13 | private readonly Action onComplete; 14 | 15 | public TextInputDialog(string title, string defaultText = null, Action onComplete = null) : base(size: new Vector2(0.45f, 0.25f)) 16 | { 17 | this.title = title; 18 | text = defaultText; 19 | this.onComplete = onComplete; 20 | } 21 | 22 | public override string GetFriendlyName() 23 | { 24 | return typeof(TextInputDialog).FullName; 25 | } 26 | 27 | public override void RecreateControls(bool constructor) 28 | { 29 | base.RecreateControls(constructor); 30 | 31 | MyGuiControlLabel caption = AddCaption(title); 32 | 33 | Vector2 bottomMid = new Vector2(0, m_size.Value.Y / 2); 34 | MyGuiControlButton btnApply = new MyGuiControlButton(position: bottomMid - GuiSpacing, text: new StringBuilder("Ok"), originAlign: MyGuiDrawAlignEnum.HORISONTAL_RIGHT_AND_VERTICAL_BOTTOM, onButtonClick: OnOkClick); 35 | MyGuiControlButton btnCancel = new MyGuiControlButton(text: new StringBuilder("Cancel"), onButtonClick: OnCancelClick); 36 | PositionToRight(btnApply, btnCancel, spacing: GuiSpacing * 2); 37 | Controls.Add(btnApply); 38 | Controls.Add(btnCancel); 39 | 40 | MyGuiControlTextbox textbox = new MyGuiControlTextbox(defaultText: text); 41 | textbox.TextChanged += OnTextChanged; 42 | Controls.Add(textbox); 43 | textbox.SelectAll(); 44 | FocusedControl = textbox; 45 | } 46 | 47 | private void OnTextChanged(MyGuiControlTextbox textbox) 48 | { 49 | text = textbox.Text; 50 | } 51 | 52 | private void OnCancelClick(MyGuiControlButton btn) 53 | { 54 | CloseScreen(); 55 | } 56 | 57 | private void OnOkClick(MyGuiControlButton obj) 58 | { 59 | if (!string.IsNullOrWhiteSpace(text)) 60 | onComplete?.Invoke(text); 61 | CloseScreen(); 62 | } 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /PluginLoader/Data/GitHubPlugin.AssetFile.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Xml.Serialization; 3 | 4 | namespace avaness.PluginLoader.Data 5 | { 6 | public partial class GitHubPlugin 7 | { 8 | public class AssetFile 9 | { 10 | public enum AssetType { Asset, Lib, LibContent } 11 | 12 | public string Name { get; set; } 13 | public string Hash { get; set; } 14 | public long Length { get; set; } 15 | public AssetType Type { get; set; } 16 | [XmlIgnore] 17 | public string BaseDir { get; set; } 18 | 19 | public string NormalizedFileName => Name.Replace('\\', '/').TrimStart('/'); 20 | 21 | public string FullPath => Path.GetFullPath(Path.Combine(BaseDir, Name)); 22 | 23 | public AssetFile() 24 | { 25 | 26 | } 27 | 28 | public AssetFile(string file, AssetType type) 29 | { 30 | Name = file; 31 | Type = type; 32 | } 33 | 34 | public void GetFileInfo() 35 | { 36 | string file = FullPath; 37 | if (!File.Exists(file)) 38 | return; 39 | 40 | FileInfo info = new FileInfo(file); 41 | Length = info.Length; 42 | Hash = LoaderTools.GetHash256(file); 43 | } 44 | 45 | public bool IsValid() 46 | { 47 | string file = FullPath; 48 | if (!File.Exists(file)) 49 | return false; 50 | 51 | FileInfo info = new FileInfo(file); 52 | if (info.Length != Length) 53 | return false; 54 | 55 | string newHash = LoaderTools.GetHash256(file); 56 | if (newHash != Hash) 57 | return false; 58 | 59 | return true; 60 | } 61 | 62 | public void Save(Stream stream) 63 | { 64 | string newFile = FullPath; 65 | Directory.CreateDirectory(Path.GetDirectoryName(newFile)); 66 | using (FileStream file = File.Create(newFile)) 67 | { 68 | stream.CopyTo(file); 69 | } 70 | 71 | GetFileInfo(); 72 | } 73 | 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /PluginLoader/Data/LocalPlugin.cs: -------------------------------------------------------------------------------- 1 | using Sandbox.Graphics.GUI; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Reflection; 5 | using VRage; 6 | 7 | namespace avaness.PluginLoader.Data 8 | { 9 | public class LocalPlugin : PluginData 10 | { 11 | public override string Source => MyTexts.GetString(MyCommonTexts.Local); 12 | public override bool IsLocal => true; 13 | public override bool IsCompiled => false; 14 | 15 | public override string Id 16 | { 17 | get 18 | { 19 | return base.Id; 20 | } 21 | set 22 | { 23 | base.Id = value; 24 | if (File.Exists(value)) 25 | FriendlyName = Path.GetFileName(value); 26 | } 27 | } 28 | 29 | private AssemblyResolver resolver; 30 | 31 | private LocalPlugin() 32 | { 33 | 34 | } 35 | 36 | public LocalPlugin(string dll) 37 | { 38 | Id = dll; 39 | Status = PluginStatus.None; 40 | } 41 | 42 | public override Assembly GetAssembly() 43 | { 44 | if(File.Exists(Id)) 45 | { 46 | resolver = new AssemblyResolver(); 47 | resolver.AddSourceFolder(Path.GetDirectoryName(Id)); 48 | resolver.AddAllowedAssemblyFile(Id); 49 | resolver.AssemblyResolved += AssemblyResolved; 50 | Assembly a = Assembly.LoadFile(Id); 51 | Version = a.GetName().Version; 52 | return a; 53 | } 54 | return null; 55 | } 56 | 57 | public override string ToString() 58 | { 59 | return Id; 60 | } 61 | 62 | public override void Show() 63 | { 64 | string file = Path.GetFullPath(Id); 65 | if (File.Exists(file)) 66 | Process.Start("explorer.exe", $"/select, \"{file}\""); 67 | } 68 | 69 | private void AssemblyResolved(string assemblyPath) 70 | { 71 | Main main = Main.Instance; 72 | if (!main.Config.IsEnabled(assemblyPath)) 73 | main.List.Remove(assemblyPath); 74 | } 75 | 76 | public override void GetDescriptionText(MyGuiControlMultilineText textbox) 77 | { 78 | textbox.Visible = false; 79 | textbox.Clear(); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /PluginLoader/LogFile.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using VRage.Utils; 3 | using NLog; 4 | using NLog.Config; 5 | using NLog.Layouts; 6 | 7 | namespace avaness.PluginLoader 8 | { 9 | public static class LogFile 10 | { 11 | private const string fileName = "loader.log"; 12 | private static Logger logger; 13 | private static LogFactory logFactory; 14 | 15 | public static void Init(string mainPath) 16 | { 17 | string file = Path.Combine(mainPath, fileName); 18 | LoggingConfiguration config = new LoggingConfiguration(); 19 | config.AddRuleForAllLevels(new NLog.Targets.FileTarget() 20 | { 21 | DeleteOldFileOnStartup = true, 22 | FileName = file, 23 | Layout = new SimpleLayout("${longdate} [${level:uppercase=true}] (${threadid}) ${message:withexception=true}") 24 | }); 25 | logFactory = new LogFactory(config); 26 | logFactory.ThrowExceptions = false; 27 | 28 | try 29 | { 30 | logger = logFactory.GetLogger("PluginLoader"); 31 | } 32 | catch 33 | { 34 | logger = null; 35 | } 36 | } 37 | 38 | public static void Error(string text, bool gameLog = true) 39 | { 40 | WriteLine(text, LogLevel.Error, gameLog); 41 | } 42 | 43 | public static void Warn(string text, bool gameLog = true) 44 | { 45 | WriteLine(text, LogLevel.Warn, gameLog); 46 | } 47 | 48 | public static void WriteLine(string text, LogLevel level = null, bool gameLog = true) 49 | { 50 | try 51 | { 52 | if (level == null) 53 | level = LogLevel.Info; 54 | logger?.Log(level, text); 55 | if(gameLog) 56 | MyLog.Default?.WriteLine($"[PluginLoader] [{level.Name}] {text}"); 57 | } 58 | catch 59 | { 60 | Dispose(); 61 | } 62 | } 63 | 64 | 65 | public static void Dispose() 66 | { 67 | if (logger == null) 68 | return; 69 | 70 | try 71 | { 72 | logFactory.Flush(); 73 | logFactory.Dispose(); 74 | } 75 | catch { } 76 | logger = null; 77 | logFactory = null; 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /PluginLoader/Network/NuGetLogger.cs: -------------------------------------------------------------------------------- 1 | using NuGet.Common; 2 | using System.Threading.Tasks; 3 | 4 | namespace avaness.PluginLoader.Network 5 | { 6 | public class NuGetLogger : ILogger 7 | { 8 | public void Log(LogLevel level, string data) 9 | { 10 | LogFile.WriteLine($"[NuGet] {data}", ConvertLogLevel(level)); 11 | } 12 | 13 | public void Log(ILogMessage message) 14 | { 15 | Log(message.Level, message.Message); 16 | } 17 | 18 | private NLog.LogLevel ConvertLogLevel(LogLevel level) 19 | { 20 | switch (level) 21 | { 22 | case LogLevel.Debug: 23 | return NLog.LogLevel.Debug; 24 | case LogLevel.Verbose: 25 | return NLog.LogLevel.Debug; 26 | case LogLevel.Information: 27 | return NLog.LogLevel.Info; 28 | case LogLevel.Minimal: 29 | return NLog.LogLevel.Info; 30 | case LogLevel.Warning: 31 | return NLog.LogLevel.Warn; 32 | case LogLevel.Error: 33 | return NLog.LogLevel.Error; 34 | } 35 | 36 | return NLog.LogLevel.Info; 37 | } 38 | 39 | public Task LogAsync(LogLevel level, string data) 40 | { 41 | Log(level, data); 42 | return Task.CompletedTask; 43 | } 44 | 45 | public Task LogAsync(ILogMessage message) 46 | { 47 | Log(message); 48 | return Task.CompletedTask; 49 | } 50 | 51 | public void LogDebug(string data) 52 | { 53 | Log(LogLevel.Debug, data); 54 | } 55 | 56 | public void LogError(string data) 57 | { 58 | Log(LogLevel.Error, data); 59 | } 60 | 61 | public void LogInformation(string data) 62 | { 63 | Log(LogLevel.Information, data); 64 | } 65 | 66 | public void LogInformationSummary(string data) 67 | { 68 | Log(LogLevel.Information, data); 69 | } 70 | 71 | public void LogMinimal(string data) 72 | { 73 | Log(LogLevel.Minimal, data); 74 | } 75 | 76 | public void LogVerbose(string data) 77 | { 78 | Log(LogLevel.Verbose, data); 79 | } 80 | 81 | public void LogWarning(string data) 82 | { 83 | Log(LogLevel.Warning, data); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /PluginLoader/GUI/GuiControls/ParentButton.cs: -------------------------------------------------------------------------------- 1 | using Sandbox; 2 | using Sandbox.Graphics.GUI; 3 | using System; 4 | using VRage.Audio; 5 | using VRage.Input; 6 | using VRageMath; 7 | 8 | namespace avaness.PluginLoader.GUI.GuiControls 9 | { 10 | class ParentButton : MyGuiControlParent 11 | { 12 | private bool mouseOver = false; 13 | private bool mouseClick = false; 14 | 15 | public event Action OnButtonClicked; 16 | 17 | public ParentButton() 18 | { } 19 | 20 | public ParentButton(Vector2? position = null, Vector2? size = null, Vector4? backgroundColor = null, string toolTip = null) 21 | : base(position, size, backgroundColor, toolTip) 22 | { 23 | CanPlaySoundOnMouseOver = false; 24 | HighlightType = MyGuiControlHighlightType.WHEN_CURSOR_OVER; 25 | CanHaveFocus = true; 26 | IsActiveControl = true; 27 | OnMouseOverChanged(mouseOver); 28 | } 29 | 30 | public override MyGuiControlBase HandleInput() 31 | { 32 | bool actualMouseOver = CheckMouseOver(); // Do NOT trust Keen's IsMouseOver 33 | if (actualMouseOver != mouseOver) 34 | { 35 | mouseOver = actualMouseOver; 36 | OnMouseOverChanged(mouseOver); 37 | } 38 | 39 | if (mouseOver) 40 | { 41 | if (MyInput.Static.IsNewPrimaryButtonPressed() || MyInput.Static.IsNewSecondaryButtonPressed()) 42 | { 43 | mouseClick = true; 44 | } 45 | else if (mouseClick && (MyInput.Static.IsNewPrimaryButtonReleased() || MyInput.Static.IsNewSecondaryButtonReleased())) 46 | { 47 | mouseClick = false; 48 | if (OnButtonClicked != null) 49 | OnButtonClicked.Invoke(this); 50 | } 51 | } 52 | else 53 | { 54 | mouseClick = false; 55 | } 56 | 57 | return base.HandleInput(); 58 | } 59 | 60 | private void OnMouseOverChanged(bool mouseOver) 61 | { 62 | BorderEnabled = mouseOver; 63 | if(mouseOver) 64 | BackgroundTexture = MyGuiConstants.TEXTURE_RECTANGLE_NEUTRAL; 65 | else 66 | BackgroundTexture = MyGuiConstants.TEXTURE_RECTANGLE_DARK; 67 | } 68 | 69 | public void PlayClickSound() 70 | { 71 | MyGuiSoundManager.PlaySound(GuiSounds.MouseClick); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /PluginLoader/Patch/Patch_MyScriptManager.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Sandbox.Game.World; 3 | using System; 4 | using VRage.Game; 5 | using System.Linq; 6 | using System.Reflection; 7 | using avaness.PluginLoader.Data; 8 | using System.Collections.Generic; 9 | using VRage.Scripting; 10 | 11 | namespace avaness.PluginLoader.Patch 12 | { 13 | [HarmonyPatch(typeof(MyScriptManager), "LoadData")] 14 | public static class Patch_MyScripManager 15 | { 16 | private static Action loadScripts; 17 | private static FieldInfo conditionalSymbols; 18 | private const string ConditionalSymbol = "PLUGIN_LOADER"; 19 | 20 | private static HashSet ConditionalSymbols => (HashSet)conditionalSymbols.GetValue(MyScriptCompiler.Static); 21 | 22 | static Patch_MyScripManager() 23 | { 24 | loadScripts = (Action)Delegate.CreateDelegate(typeof(Action), typeof(MyScriptManager).GetMethod("LoadScripts", BindingFlags.Instance | BindingFlags.NonPublic)); 25 | conditionalSymbols = typeof(MyScriptCompiler).GetField("m_conditionalCompilationSymbols", BindingFlags.Instance | BindingFlags.NonPublic); 26 | } 27 | 28 | public static void Postfix(MyScriptManager __instance) 29 | { 30 | try 31 | { 32 | HashSet currentMods; 33 | if (MySession.Static.Mods != null) 34 | currentMods = new HashSet(MySession.Static.Mods.Select(x => x.PublishedFileId)); 35 | else 36 | currentMods = new HashSet(); 37 | 38 | HashSet conditionalSymbols = ConditionalSymbols; 39 | conditionalSymbols.Add(ConditionalSymbol); 40 | foreach (PluginData data in Main.Instance.Config.EnabledPlugins) 41 | { 42 | if (data is ModPlugin mod && !currentMods.Contains(mod.WorkshopId) && mod.Exists) 43 | { 44 | LogFile.WriteLine("Loading client mod scripts for " + mod.WorkshopId); 45 | loadScripts(__instance, mod.ModLocation, mod.GetModContext()); 46 | } 47 | } 48 | conditionalSymbols.Remove(ConditionalSymbol); 49 | } 50 | catch (Exception e) 51 | { 52 | LogFile.Error("An error occured while loading client mods: " + e); 53 | throw; 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /PluginLoader/Patch/Patch_CreateMenu.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Sandbox.Graphics.GUI; 3 | using SpaceEngineers.Game.GUI; 4 | using System.Text; 5 | using avaness.PluginLoader.GUI; 6 | using VRage.Game; 7 | using VRage.Utils; 8 | using VRageMath; 9 | using System; 10 | 11 | // ReSharper disable InconsistentNaming 12 | 13 | namespace avaness.PluginLoader.Patch 14 | { 15 | [HarmonyPatch(typeof(MyGuiScreenMainMenu), "CreateMainMenu")] 16 | public static class Patch_CreateMainMenu 17 | { 18 | private static bool usedAutoRejoin = false; 19 | 20 | public static void Postfix(MyGuiScreenMainMenu __instance, Vector2 leftButtonPositionOrigin, ref Vector2 lastButtonPosition, MyGuiControlButton ___m_continueButton) 21 | { 22 | MyGuiControlButton lastBtn = null; 23 | foreach (var control in __instance.Controls) 24 | { 25 | if (control is MyGuiControlButton btn && btn.Position == lastButtonPosition) 26 | { 27 | lastBtn = btn; 28 | break; 29 | } 30 | } 31 | 32 | Vector2 position; 33 | if (lastBtn == null) 34 | { 35 | position = lastButtonPosition + MyGuiConstants.MENU_BUTTONS_POSITION_DELTA; 36 | } 37 | else 38 | { 39 | position = lastBtn.Position; 40 | lastBtn.Position = lastButtonPosition + MyGuiConstants.MENU_BUTTONS_POSITION_DELTA; 41 | } 42 | 43 | MyGuiControlButton openBtn = new MyGuiControlButton(position, MyGuiControlButtonStyleEnum.StripeLeft, originAlign: MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_BOTTOM, text: new StringBuilder("Plugins"), onButtonClick: _ => MainPluginMenu.Open()) 44 | { 45 | BorderEnabled = false, 46 | BorderSize = 0, 47 | BorderHighlightEnabled = false, 48 | BorderColor = Vector4.Zero 49 | }; 50 | __instance.Controls.Add(openBtn); 51 | 52 | if (___m_continueButton != null && ___m_continueButton.Visible && !usedAutoRejoin && Environment.GetCommandLineArgs().Contains(LoaderTools.AutoRejoinArg)) 53 | { 54 | ___m_continueButton.PressButton(); 55 | usedAutoRejoin = true; 56 | } 57 | } 58 | } 59 | 60 | 61 | [HarmonyPatch(typeof(MyGuiScreenMainMenu), "CreateInGameMenu")] 62 | public static class Patch_CreateInGameMenu 63 | { 64 | public static void Postfix(MyGuiScreenMainMenu __instance, Vector2 leftButtonPositionOrigin, ref Vector2 lastButtonPosition) 65 | { 66 | Patch_CreateMainMenu.Postfix(__instance, leftButtonPositionOrigin, ref lastButtonPosition, null); 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /PluginLoader/Network/GitHub.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net; 4 | 5 | namespace avaness.PluginLoader.Network 6 | { 7 | public static class GitHub 8 | { 9 | 10 | public const string listRepoName = "sepluginloader/PluginHub"; 11 | public const string listRepoCommit = "main"; 12 | public const string listRepoHash = "plugins.sha1"; 13 | 14 | private const string repoZipUrl = "https://github.com/{0}/archive/{1}.zip"; 15 | private const string rawUrl = "https://raw.githubusercontent.com/{0}/{1}/"; 16 | 17 | public static void Init() 18 | { 19 | // Fix tls 1.2 not supported on Windows 7 - github.com is tls 1.2 only 20 | try 21 | { 22 | ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; 23 | } 24 | catch (NotSupportedException e) 25 | { 26 | LogFile.Error("An error occurred while setting up networking, web requests will probably fail: " + e); 27 | } 28 | } 29 | 30 | public static Stream GetStream(Uri uri) 31 | { 32 | HttpWebRequest request = WebRequest.CreateHttp(uri); 33 | request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; 34 | Config.PluginConfig config = Main.Instance.Config; 35 | request.Timeout = config.NetworkTimeout; 36 | if(!config.AllowIPv6) 37 | request.ServicePoint.BindIPEndPointDelegate = BlockIPv6; 38 | 39 | HttpWebResponse response = (HttpWebResponse)request.GetResponse(); 40 | MemoryStream output = new MemoryStream(); 41 | using (Stream responseStream = response.GetResponseStream()) 42 | responseStream.CopyTo(output); 43 | output.Position = 0; 44 | return output; 45 | } 46 | 47 | private static IPEndPoint BlockIPv6(ServicePoint servicePoint, IPEndPoint remoteEndPoint, int retryCount) 48 | { 49 | if (remoteEndPoint.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) 50 | return new IPEndPoint(IPAddress.Any, 0); 51 | 52 | throw new InvalidOperationException("No IPv4 address"); 53 | } 54 | 55 | public static Stream DownloadRepo(string name, string commit) 56 | { 57 | Uri uri = new Uri(string.Format(repoZipUrl, name, commit), UriKind.Absolute); 58 | LogFile.WriteLine("Downloading " + uri); 59 | return GetStream(uri); 60 | } 61 | 62 | public static Stream DownloadFile(string name, string commit, string path) 63 | { 64 | Uri uri = new Uri(string.Format(rawUrl, name, commit) + path.TrimStart('/'), UriKind.Absolute); 65 | LogFile.WriteLine("Downloading " + uri); 66 | return GetStream(uri); 67 | } 68 | 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /PluginLoader/GUI/ConfigurePlugin.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Text; 4 | using Sandbox.Graphics.GUI; 5 | using VRage.Utils; 6 | using VRageMath; 7 | 8 | namespace avaness.PluginLoader.GUI; 9 | 10 | public class ConfigurePlugin : PluginScreen 11 | { 12 | private readonly List pluginInstances; 13 | 14 | private MyGuiControlTable table; 15 | 16 | public override string GetFriendlyName() 17 | { 18 | return typeof(PluginDetailMenu).FullName; 19 | } 20 | 21 | public ConfigurePlugin() : base(size: new Vector2(0.7f, 0.9f)) 22 | { 23 | pluginInstances = Main.Instance.Plugins.Where(p => p.HasConfigDialog).ToList(); 24 | } 25 | 26 | public override void RecreateControls(bool constructor) 27 | { 28 | base.RecreateControls(constructor); 29 | 30 | MyGuiControlLabel caption = AddCaption("Configure a plugin", captionScale: 1); 31 | AddBarBelow(caption); 32 | 33 | RectangleF area = GetAreaBelow(caption, GuiSpacing * 2); 34 | table = new MyGuiControlTable() 35 | { 36 | Size = area.Size, 37 | Position = area.Position, 38 | OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_LEFT_AND_VERTICAL_TOP, 39 | }; 40 | 41 | table.ColumnsCount = 1; 42 | table.SetCustomColumnWidths([1.0f]); 43 | table.SetColumnName(0, new StringBuilder("Name")); 44 | table.SetColumnComparison(0, CellTextComparison); 45 | 46 | table.ItemDoubleClicked += OnItemSelected; 47 | table.ItemSelected += OnItemSelected; 48 | 49 | SetTableHeight(table, area.Height - GuiSpacing); 50 | 51 | AddTableRows(); 52 | table.SortByColumn(0, MyGuiControlTable.SortStateEnum.Ascending); 53 | table.SelectedRowIndex = -1; 54 | 55 | Controls.Add(table); 56 | } 57 | 58 | private void AddTableRows() 59 | { 60 | foreach (PluginInstance p in pluginInstances) 61 | { 62 | if (p.HasConfigDialog) 63 | table.Add(CreateRow(p)); 64 | } 65 | } 66 | 67 | private static MyGuiControlTable.Row CreateRow(PluginInstance pluginInstance) 68 | { 69 | MyGuiControlTable.Row row = new MyGuiControlTable.Row(pluginInstance); 70 | row.AddCell(new MyGuiControlTable.Cell(text: pluginInstance?.FriendlyName ?? "", userData: pluginInstance)); 71 | return row; 72 | } 73 | 74 | private int CellTextComparison(MyGuiControlTable.Cell x, MyGuiControlTable.Cell y) 75 | { 76 | if (x == null) 77 | return y == null ? 0 : 1; 78 | 79 | return y == null ? -1 : TextComparison(x.Text, y.Text); 80 | } 81 | 82 | private int TextComparison(StringBuilder x, StringBuilder y) 83 | { 84 | if (x == null) 85 | return y == null ? 0 : 1; 86 | 87 | return y == null ? -1 : x.CompareTo(y); 88 | } 89 | 90 | private void OnItemSelected(MyGuiControlTable arg1, MyGuiControlTable.EventArgs arg2) 91 | { 92 | PluginInstance selectedPluginInstance = table.SelectedRow?.GetCell(0).UserData as PluginInstance; 93 | if (selectedPluginInstance == null) 94 | return; 95 | 96 | selectedPluginInstance.OpenConfig(); 97 | CloseScreen(); 98 | } 99 | } -------------------------------------------------------------------------------- /PluginLoader/Patch/Patch_IngameShortcuts.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Sandbox.Game.Gui; 3 | using Sandbox.Game.World; 4 | using Sandbox.Graphics.GUI; 5 | using System.Diagnostics; 6 | using System.IO; 7 | using System.Text; 8 | using avaness.PluginLoader.GUI; 9 | using VRage; 10 | using VRage.Input; 11 | using VRage.Utils; 12 | 13 | namespace avaness.PluginLoader.Patch 14 | { 15 | [HarmonyPatch(typeof(MyGuiScreenGamePlay), "HandleUnhandledInput")] 16 | public static class Patch_IngameShortcuts 17 | { 18 | public static bool Prefix() 19 | { 20 | IMyInput input = MyInput.Static; 21 | if (MySession.Static == null || input == null) 22 | return true; 23 | 24 | if (input.IsAnyAltKeyPressed() && input.IsAnyCtrlKeyPressed()) 25 | { 26 | if(input.IsNewKeyPressed(MyKeys.F5)) 27 | { 28 | ShowRestartMenu(); 29 | return false; 30 | } 31 | 32 | if(input.IsNewKeyPressed(MyKeys.L)) 33 | { 34 | ShowLogMenu(); 35 | return false; 36 | } 37 | 38 | if (input.IsNewKeyPressed(MyKeys.OemQuestion)) 39 | { 40 | ShowConfigurePlugin(); 41 | return false; 42 | } 43 | } 44 | 45 | return true; 46 | } 47 | 48 | private static void ShowConfigurePlugin() 49 | { 50 | MyGuiSandbox.AddScreen(new ConfigurePlugin()); 51 | } 52 | 53 | public static void ShowLogMenu() 54 | { 55 | var box = MyGuiSandbox.CreateMessageBox(MyMessageBoxStyleEnum.Error, MyMessageBoxButtonsType.YES_NO, new StringBuilder("Plugin Loader: Show game log?"), MyTexts.Get(MyCommonTexts.MessageBoxCaptionPleaseConfirm), callback: OnLogMessageClosed); 56 | box.SkipTransition = true; 57 | box.CloseBeforeCallback = true; 58 | MyGuiSandbox.AddScreen(box); 59 | } 60 | 61 | private static void OnLogMessageClosed(MyGuiScreenMessageBox.ResultEnum @enum) 62 | { 63 | if (@enum != MyGuiScreenMessageBox.ResultEnum.YES) 64 | return; 65 | 66 | string file = MyLog.Default?.GetFilePath(); 67 | if (File.Exists(file)) 68 | Process.Start("explorer.exe", $"\"{file}\""); 69 | } 70 | 71 | public static void ShowRestartMenu() 72 | { 73 | var box = MyGuiSandbox.CreateMessageBox(MyMessageBoxStyleEnum.Error, MyMessageBoxButtonsType.YES_NO, new StringBuilder("Plugin Loader: Are you sure you want to restart the game?"), MyTexts.Get(MyCommonTexts.MessageBoxCaptionPleaseConfirm), callback: OnRestartMessageClosed); 74 | box.SkipTransition = true; 75 | box.CloseBeforeCallback = true; 76 | MyGuiSandbox.AddScreen(box); 77 | } 78 | 79 | private static void OnRestartMessageClosed(MyGuiScreenMessageBox.ResultEnum result) 80 | { 81 | if(result == MyGuiScreenMessageBox.ResultEnum.YES) 82 | { 83 | LoaderTools.Unload(); 84 | LoaderTools.Restart(true); 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /PluginLoader/SteamAPI.cs: -------------------------------------------------------------------------------- 1 | using Sandbox.Engine.Networking; 2 | using Steamworks; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using ParallelTasks; 7 | using VRage.Game; 8 | using System.Threading; 9 | using VRage.Utils; 10 | using HarmonyLib; 11 | using System.Reflection; 12 | using System.Text; 13 | 14 | namespace avaness.PluginLoader 15 | { 16 | public static class SteamAPI 17 | { 18 | private static MethodInfo DownloadModsBlocking; 19 | 20 | public static bool IsSubscribed(ulong id) 21 | { 22 | EItemState state = (EItemState)SteamUGC.GetItemState(new PublishedFileId_t(id)); 23 | return (state & EItemState.k_EItemStateSubscribed) == EItemState.k_EItemStateSubscribed; 24 | } 25 | 26 | public static void SubscribeToItem(ulong id) 27 | { 28 | SteamUGC.SubscribeItem(new PublishedFileId_t(id)); 29 | } 30 | 31 | public static void Update(IEnumerable ids) 32 | { 33 | var modItems = new List(ids.Select(x => new MyObjectBuilder_Checkpoint.ModItem(x, "Steam"))); 34 | if (modItems.Count == 0) 35 | return; 36 | LogFile.WriteLine($"Updating {modItems.Count} workshop items"); 37 | 38 | // Source: MyWorkshop.DownloadWorldModsBlocking 39 | MyWorkshop.ResultData result = new MyWorkshop.ResultData(); 40 | Task task = Parallel.Start(delegate 41 | { 42 | result = UpdateInternal(modItems); 43 | }); 44 | while (!task.IsComplete) 45 | { 46 | MyGameService.Update(); 47 | Thread.Sleep(10); 48 | } 49 | 50 | if (result.Result != VRage.GameServices.MyGameServiceCallResult.OK) 51 | { 52 | Exception[] exceptions = task.Exceptions; 53 | if(exceptions != null && exceptions.Length > 0) 54 | { 55 | StringBuilder sb = new StringBuilder(); 56 | sb.AppendLine("An error occurred while updating workshop items:"); 57 | foreach (Exception e in exceptions) 58 | sb.Append(e); 59 | LogFile.Error(sb.ToString()); 60 | } 61 | else 62 | { 63 | LogFile.Error("Unable to update workshop items. Result: " + result.Result); 64 | } 65 | 66 | } 67 | } 68 | 69 | public static MyWorkshop.ResultData UpdateInternal(List mods) 70 | { 71 | // Source: MyWorkshop.DownloadWorldModsBlockingInternal 72 | 73 | MyLog.Default.IncreaseIndent(); 74 | 75 | List list = new List(mods.Select(x => new WorkshopId(x.PublishedFileId, x.PublishedServiceName))); 76 | 77 | if (DownloadModsBlocking == null) 78 | DownloadModsBlocking = AccessTools.Method(typeof(MyWorkshop), "DownloadModsBlocking"); 79 | 80 | MyWorkshop.ResultData resultData = (MyWorkshop.ResultData)DownloadModsBlocking.Invoke(mods, new object[] { 81 | mods, new MyWorkshop.ResultData(), list, new MyWorkshop.CancelToken() 82 | }); 83 | 84 | MyLog.Default.DecreaseIndent(); 85 | return resultData; 86 | } 87 | 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /PluginLoader/Network/NuGetPackage.cs: -------------------------------------------------------------------------------- 1 | using NuGet.Frameworks; 2 | using NuGet.Packaging; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | 8 | namespace avaness.PluginLoader.Network 9 | { 10 | public class NuGetPackage 11 | { 12 | private readonly string installPath; 13 | private readonly NuGetFramework targetFramework; 14 | 15 | public Item[] LibFiles { get; private set; } 16 | public Item[] ContentFiles { get; private set; } 17 | 18 | public NuGetPackage(string installPath, NuGetFramework targetFramework) 19 | { 20 | this.installPath = installPath; 21 | this.targetFramework = targetFramework; 22 | GetFileLists(); 23 | } 24 | 25 | private void GetFileLists() 26 | { 27 | PackageFolderReader packageReader = new PackageFolderReader(installPath); 28 | FrameworkReducer frameworkReducer = new FrameworkReducer(); 29 | LibFiles = GetItems(packageReader.GetLibItems(), frameworkReducer, targetFramework, false); 30 | ContentFiles = GetItems(packageReader.GetContentItems(), frameworkReducer, targetFramework, true); 31 | } 32 | 33 | private Item[] GetItems(IEnumerable itemGroups, FrameworkReducer frameworkReducer, NuGetFramework targetFramework, bool contentItems) 34 | { 35 | NuGetFramework nearest = frameworkReducer.GetNearest(targetFramework, itemGroups.Select(x => x.TargetFramework)); 36 | if (nearest != null) 37 | { 38 | List libFiles = new List(); 39 | foreach (FrameworkSpecificGroup group in itemGroups.Where(x => x.TargetFramework.Equals(nearest))) 40 | libFiles.AddRange(group.Items.Select(x => GetPackageItem(x, group.TargetFramework, contentItems)).Where(x => x != null)); 41 | return libFiles.ToArray(); 42 | } 43 | 44 | return Array.Empty(); 45 | } 46 | 47 | private Item GetPackageItem(string path, NuGetFramework framework, bool content) 48 | { 49 | string fullPath = Path.GetFullPath(Path.Combine(installPath, path)); 50 | if (!File.Exists(fullPath)) 51 | return null; 52 | 53 | string folder; 54 | string file; 55 | if (TrySplitPath(fullPath, framework.GetShortFolderName(), out folder, out file)) 56 | return new Item(file, folder); 57 | 58 | if (TrySplitPath(fullPath, content ? "content" : "lib", out folder, out file)) 59 | return new Item(file, folder); 60 | 61 | return null; 62 | } 63 | 64 | private bool TrySplitPath(string fullPath, string lastFolderName, out string folder, out string file) 65 | { 66 | folder = null; 67 | file = null; 68 | 69 | int index = fullPath.IndexOf(lastFolderName); 70 | if (index < 0 || fullPath.Length <= index + lastFolderName.Length + 2) 71 | return false; 72 | 73 | folder = fullPath.Substring(0, index + lastFolderName.Length); 74 | file = fullPath.Substring(folder.Length + 1); 75 | return true; 76 | } 77 | 78 | 79 | public class Item 80 | { 81 | public Item(string path, string folder) 82 | { 83 | FilePath = path; 84 | Folder = folder; 85 | FullPath = Path.Combine(Folder, FilePath); 86 | } 87 | 88 | public string FilePath { get; set; } 89 | public string Folder { get; set; } 90 | public string FullPath { get; set; } 91 | 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /PluginLoader/Stats/StatsClient.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using avaness.PluginLoader.GUI; 3 | using avaness.PluginLoader.Stats.Model; 4 | using avaness.PluginLoader.Tools; 5 | 6 | namespace avaness.PluginLoader.Stats 7 | { 8 | public static class StatsClient 9 | { 10 | // API address 11 | private static string baseUri = "https://pluginstats.ferenczi.eu"; 12 | 13 | public static void OverrideBaseUrl(string uri) 14 | { 15 | if (string.IsNullOrEmpty(uri)) 16 | return; 17 | 18 | baseUri = uri; 19 | } 20 | 21 | // API endpoints 22 | private static string ConsentUri => $"{baseUri}/Consent"; 23 | private static string StatsUri => $"{baseUri}/Stats"; 24 | private static string TrackUri => $"{baseUri}/Track"; 25 | private static string VoteUri => $"{baseUri}/Vote"; 26 | 27 | // Hashed Steam ID of the player 28 | private static string PlayerHash => playerHash ??= Tools.Tools.Sha1HexDigest($"{Tools.Tools.GetSteamId()}").Substring(0, 20); 29 | private static string playerHash; 30 | 31 | // Latest voting token received 32 | private static string votingToken; 33 | 34 | public static bool Consent(bool consent) 35 | { 36 | if (consent) 37 | LogFile.WriteLine($"Registering player consent on the statistics server"); 38 | else 39 | LogFile.WriteLine($"Withdrawing player consent, removing user data from the statistics server"); 40 | 41 | var consentRequest = new ConsentRequest() 42 | { 43 | PlayerHash = PlayerHash, 44 | Consent = consent 45 | }; 46 | 47 | return SimpleHttpClient.Post(ConsentUri, consentRequest); 48 | } 49 | 50 | // This function may be called from another thread. 51 | public static PluginStats DownloadStats() 52 | { 53 | if (!PlayerConsent.ConsentGiven) 54 | { 55 | LogFile.WriteLine("Downloading plugin statistics anonymously..."); 56 | votingToken = null; 57 | return SimpleHttpClient.Get(StatsUri); 58 | } 59 | 60 | LogFile.WriteLine("Downloading plugin statistics, ratings and votes for " + PlayerHash); 61 | 62 | var parameters = new Dictionary { ["playerHash"] = PlayerHash }; 63 | var pluginStats = SimpleHttpClient.Get(StatsUri, parameters); 64 | 65 | votingToken = pluginStats?.VotingToken; 66 | 67 | return pluginStats; 68 | } 69 | 70 | public static bool Track(string[] pluginIds) 71 | { 72 | var trackRequest = new TrackRequest 73 | { 74 | PlayerHash = PlayerHash, 75 | EnabledPluginIds = pluginIds 76 | }; 77 | 78 | return SimpleHttpClient.Post(TrackUri, trackRequest); 79 | } 80 | 81 | public static PluginStat Vote(string pluginId, int vote) 82 | { 83 | if (votingToken == null) 84 | { 85 | LogFile.Error($"Voting token is not available, cannot vote"); 86 | return null; 87 | } 88 | 89 | LogFile.WriteLine($"Voting {vote} on plugin {pluginId}"); 90 | var voteRequest = new VoteRequest 91 | { 92 | PlayerHash = PlayerHash, 93 | PluginId = pluginId, 94 | VotingToken = votingToken, 95 | Vote = vote 96 | }; 97 | 98 | var stat = SimpleHttpClient.Post(VoteUri, voteRequest); 99 | return stat; 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /PluginLoader/GUI/SplashScreen.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Forms; 2 | using System.Drawing; 3 | using System.Reflection; 4 | using System.IO; 5 | using VRage; 6 | using Sandbox.Game; 7 | 8 | namespace avaness.PluginLoader.GUI 9 | { 10 | public class SplashScreen : Form 11 | { 12 | private const float barWidth = 0.98f; // 98% of width 13 | private const float barHeight = 0.06f; // 6% of height 14 | private static readonly Color backgroundColor = Color.FromArgb(4, 4, 4); 15 | 16 | private readonly bool invalid; 17 | private readonly Label lbl; 18 | private readonly PictureBox gifBox; 19 | private readonly RectangleF bar; 20 | 21 | private float barValue = float.NaN; 22 | 23 | public object GameInfo { get; private set; } 24 | 25 | public SplashScreen() 26 | { 27 | Image gif; 28 | if (Application.OpenForms.Count == 0 || !TryLoadImage(out gif)) 29 | { 30 | invalid = true; 31 | return; 32 | } 33 | 34 | Form defaultSplash = Application.OpenForms[0]; 35 | Size = defaultSplash.Size; 36 | ClientSize = defaultSplash.ClientSize; 37 | MyVRage.Platform.Windows.HideSplashScreen(); 38 | 39 | Name = "SplashScreenPluginLoader"; 40 | FormBorderStyle = FormBorderStyle.None; 41 | 42 | SizeF barSize = new SizeF(Size.Width * barWidth, Size.Height * barHeight); 43 | float padding = (1 - barWidth) * Size.Width * 0.5f; 44 | PointF barStart = new PointF(padding, Size.Height - barSize.Height - padding); 45 | bar = new RectangleF(barStart, barSize); 46 | 47 | Font lblFont = new Font(FontFamily.GenericSansSerif, 12, FontStyle.Bold); 48 | lbl = new Label 49 | { 50 | Name = "PluginLoaderInfo", 51 | Font = lblFont, 52 | BackColor = backgroundColor, 53 | ForeColor = Color.White, 54 | MaximumSize = Size, 55 | Size = new Size(Size.Width, lblFont.Height), 56 | TextAlign = ContentAlignment.MiddleCenter, 57 | Location = new Point(0, (int)(barStart.Y - lblFont.Height - 1)), 58 | }; 59 | Controls.Add(lbl); 60 | 61 | gifBox = new PictureBox() 62 | { 63 | Name = "PluginLoaderAnimation", 64 | Image = gif, 65 | Size = Size, 66 | AutoSize = false, 67 | SizeMode = PictureBoxSizeMode.StretchImage, 68 | }; 69 | Controls.Add(gifBox); 70 | 71 | gifBox.Paint += OnPictureBoxDraw; 72 | 73 | CenterToScreen(); 74 | Show(); 75 | ForceUpdate(); 76 | } 77 | 78 | private bool TryLoadImage(out Image img) 79 | { 80 | try 81 | { 82 | Assembly myAssembly = Assembly.GetExecutingAssembly(); 83 | Stream myStream = myAssembly.GetManifestResourceStream("avaness.PluginLoader.splash.gif"); 84 | img = new Bitmap(myStream); 85 | return true; 86 | } 87 | catch 88 | { 89 | img = null; 90 | return false; 91 | } 92 | } 93 | 94 | public void SetText(string msg) 95 | { 96 | if (invalid) 97 | return; 98 | 99 | lbl.Text = msg; 100 | barValue = float.NaN; 101 | gifBox.Invalidate(); 102 | ForceUpdate(); 103 | } 104 | 105 | public void SetBarValue(float percent = float.NaN) 106 | { 107 | if (invalid) 108 | return; 109 | 110 | barValue = percent; 111 | gifBox.Invalidate(); 112 | ForceUpdate(); 113 | } 114 | 115 | private void ForceUpdate() 116 | { 117 | Application.DoEvents(); 118 | } 119 | 120 | private void OnPictureBoxDraw(object sender, PaintEventArgs e) 121 | { 122 | if (!float.IsNaN(barValue)) 123 | { 124 | Graphics graphics = e.Graphics; 125 | graphics.FillRectangle(Brushes.DarkSlateGray, bar); 126 | graphics.FillRectangle(Brushes.White, new RectangleF(bar.Location, new SizeF(bar.Width * barValue, bar.Height))); 127 | } 128 | } 129 | 130 | public void Delete() 131 | { 132 | if (invalid) 133 | return; 134 | 135 | gifBox.Paint -= OnPictureBoxDraw; 136 | Close(); 137 | Dispose(); 138 | ForceUpdate(); 139 | MyVRage.Platform.Windows.ShowSplashScreen(MyPerGameSettings.BasicGameInfo.SplashScreenImage, new VRageMath.Vector2(0.7f, 0.7f)); 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /PluginLoader/GUI/GuiControls/RatingControl.cs: -------------------------------------------------------------------------------- 1 | using Sandbox.Graphics; 2 | using Sandbox.Graphics.GUI; 3 | using VRage.Utils; 4 | using VRageMath; 5 | 6 | namespace avaness.PluginLoader.GUI.GuiControls 7 | { 8 | // From Sandbox.Game.Screens.Helpers.MyGuiControlRating 9 | internal class RatingControl : MyGuiControlBase 10 | { 11 | private Vector2 m_textureSize = new Vector2(32f); 12 | 13 | private readonly float m_space = 8f; 14 | 15 | private int m_value; 16 | 17 | private int m_maxValue; 18 | 19 | public string EmptyTexture = "Textures\\GUI\\Icons\\Rating\\NoStar.png"; 20 | 21 | public string FilledTexture = "Textures\\GUI\\Icons\\Rating\\FullStar.png"; 22 | 23 | public string HalfFilledTexture = "Textures\\GUI\\Icons\\Rating\\HalfStar.png"; 24 | 25 | public int MaxValue 26 | { 27 | get => m_maxValue; 28 | set 29 | { 30 | m_maxValue = value; 31 | RecalculateSize(); 32 | } 33 | } 34 | 35 | public int Value 36 | { 37 | get => m_value; 38 | set => m_value = value; 39 | } 40 | 41 | public RatingControl(int value = 0, int maxValue = 10) 42 | { 43 | m_value = value; 44 | m_maxValue = maxValue; 45 | BackgroundTexture = null; 46 | base.ColorMask = Vector4.One; 47 | } 48 | 49 | private void RecalculateSize() 50 | { 51 | Vector2 vector = MyGuiManager.GetHudNormalizedSizeFromPixelSize(m_textureSize) * new Vector2(0.75f, 1f); 52 | Vector2 hudNormalizedSizeFromPixelSize = MyGuiManager.GetHudNormalizedSizeFromPixelSize(new Vector2(m_space * 0.75f, 0f)); 53 | base.Size = new Vector2((vector.X + hudNormalizedSizeFromPixelSize.X) * m_maxValue, vector.Y); 54 | } 55 | 56 | public float GetWidth() 57 | { 58 | float num = MyGuiManager.GetHudNormalizedSizeFromPixelSize(m_textureSize).X * 0.75f; 59 | float num2 = MyGuiManager.GetHudNormalizedSizeFromPixelSize(new Vector2(m_space * 0.75f, 0f)).X; 60 | return (num + num2) * MaxValue / 2f; 61 | } 62 | 63 | public override void Draw(float transitionAlpha, float backgroundTransitionAlpha) 64 | { 65 | base.Draw(transitionAlpha, backgroundTransitionAlpha); 66 | if (MaxValue <= 0) 67 | { 68 | return; 69 | } 70 | Vector2 normalizedSize = MyGuiManager.GetHudNormalizedSizeFromPixelSize(m_textureSize) * new Vector2(0.75f, 1f); 71 | Vector2 hudNormalizedSizeFromPixelSize = MyGuiManager.GetHudNormalizedSizeFromPixelSize(new Vector2(m_space * 0.75f, 0f)); 72 | Vector2 vector = GetPositionAbsoluteTopLeft() + new Vector2(0f, (base.Size.Y - normalizedSize.Y) / 2f); 73 | Vector2 vector2 = new Vector2((normalizedSize.X + hudNormalizedSizeFromPixelSize.X) * 0.5f, normalizedSize.Y); 74 | for (int i = 0; i < MaxValue; i += 2) 75 | { 76 | Vector2 normalizedCoord = vector + new Vector2(vector2.X * i, 0f); 77 | if (i == Value - 1) 78 | { 79 | MyGuiManager.DrawSpriteBatch(HalfFilledTexture, normalizedCoord, normalizedSize, ApplyColorMaskModifiers(ColorMask, Enabled, transitionAlpha), MyGuiDrawAlignEnum.HORISONTAL_LEFT_AND_VERTICAL_TOP, useFullClientArea: false, waitTillLoaded: false); 80 | } 81 | else if (i < Value) 82 | { 83 | MyGuiManager.DrawSpriteBatch(FilledTexture, normalizedCoord, normalizedSize, ApplyColorMaskModifiers(ColorMask, Enabled, transitionAlpha), MyGuiDrawAlignEnum.HORISONTAL_LEFT_AND_VERTICAL_TOP, useFullClientArea: false, waitTillLoaded: false); 84 | } 85 | else 86 | { 87 | MyGuiManager.DrawSpriteBatch(EmptyTexture, normalizedCoord, normalizedSize, ApplyColorMaskModifiers(ColorMask, Enabled, transitionAlpha), MyGuiDrawAlignEnum.HORISONTAL_LEFT_AND_VERTICAL_TOP, useFullClientArea: false, waitTillLoaded: false); 88 | } 89 | } 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /PluginLoader/AssemblyResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | 7 | namespace avaness.PluginLoader 8 | { 9 | public class AssemblyResolver 10 | { 11 | private readonly HashSet allowedAssemblyNames = new HashSet(); 12 | private readonly HashSet allowedAssemblyFiles = new HashSet(); 13 | private readonly List sourceFolders = new List(); 14 | private readonly Dictionary assemblies = new Dictionary(); 15 | private bool enabled; 16 | 17 | public event Action AssemblyResolved; 18 | 19 | public AssemblyResolver() 20 | { 21 | 22 | } 23 | 24 | /// 25 | /// Adds an assembly to the list of assemblies that are allowed to request from this resolver. 26 | /// 27 | public void AddAllowedAssemblyName(string assemblyName) 28 | { 29 | allowedAssemblyNames.Add(assemblyName); 30 | } 31 | 32 | /// 33 | /// Adds an assembly to the list of assemblies that are allowed to request from this resolver. 34 | /// 35 | public void AddAllowedAssemblyFile(string assemblyFile) 36 | { 37 | allowedAssemblyFiles.Add(Path.GetFullPath(assemblyFile)); 38 | } 39 | 40 | /// 41 | /// Adds a folder of assemblies to resolve. 42 | /// 43 | public void AddSourceFolder(string folder, SearchOption fileSearch = SearchOption.TopDirectoryOnly) 44 | { 45 | if (!Directory.Exists(folder)) 46 | return; 47 | 48 | sourceFolders.Add(Path.GetFullPath(folder)); 49 | foreach (string name in Directory.EnumerateFiles(folder, "*.dll", fileSearch)) 50 | { 51 | if (!Path.GetExtension(name).Equals(".dll", StringComparison.OrdinalIgnoreCase)) 52 | continue; 53 | string assemblyName = Path.GetFileNameWithoutExtension(name); 54 | if (!assemblies.ContainsKey(assemblyName)) 55 | { 56 | assemblies.Add(assemblyName, name); 57 | if(!enabled) 58 | { 59 | AppDomain.CurrentDomain.AssemblyResolve += Resolve; 60 | enabled = true; 61 | } 62 | } 63 | } 64 | } 65 | 66 | private Assembly Resolve(object sender, ResolveEventArgs args) 67 | { 68 | if (!IsAllowedRequest(args.RequestingAssembly)) 69 | return null; 70 | 71 | AssemblyName targetAssembly = new AssemblyName(args.Name); 72 | if (assemblies.TryGetValue(targetAssembly.Name, out string targetPath) && File.Exists(targetPath)) 73 | { 74 | Assembly a = Assembly.LoadFile(targetPath); 75 | if (AssemblyResolved != null) 76 | AssemblyResolved.Invoke(targetPath); 77 | LogFile.WriteLine($"Resolved {targetAssembly} as {a.GetName()} for {args.RequestingAssembly.GetName()}"); 78 | return a; 79 | } 80 | return null; 81 | } 82 | 83 | private bool IsAllowedRequest(Assembly requestingAssembly) 84 | { 85 | if (requestingAssembly == null) 86 | return false; 87 | 88 | string name = requestingAssembly.GetName().Name; 89 | 90 | if (string.IsNullOrWhiteSpace(requestingAssembly.Location)) 91 | return allowedAssemblyNames.Contains(name); 92 | 93 | string location = Path.GetFullPath(requestingAssembly.Location); 94 | 95 | if (allowedAssemblyFiles.Contains(location)) 96 | return true; 97 | 98 | if (sourceFolders.Any(x => location.StartsWith(x, StringComparison.OrdinalIgnoreCase))) 99 | return true; 100 | 101 | return allowedAssemblyNames.Contains(name); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /PluginLoader/Data/ModPlugin.cs: -------------------------------------------------------------------------------- 1 | using ProtoBuf; 2 | using Sandbox.Graphics.GUI; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Xml.Serialization; 8 | using VRage.Game; 9 | 10 | namespace avaness.PluginLoader.Data 11 | { 12 | [ProtoContract] 13 | public class ModPlugin : PluginData, ISteamItem 14 | { 15 | public override string Source => "Mod"; 16 | public override bool IsLocal => false; 17 | public override bool IsCompiled => false; 18 | 19 | [XmlIgnore] 20 | public ulong WorkshopId { get; private set; } 21 | 22 | public override string Id 23 | { 24 | get 25 | { 26 | return base.Id; 27 | } 28 | set 29 | { 30 | base.Id = value; 31 | WorkshopId = ulong.Parse(Id); 32 | } 33 | 34 | } 35 | 36 | [ProtoMember(1)] 37 | [XmlArray] 38 | [XmlArrayItem("Id")] 39 | public ulong[] DependencyIds { get; set; } = new ulong[0]; 40 | 41 | [XmlIgnore] 42 | public ModPlugin[] Dependencies { get; set; } = new ModPlugin[0]; 43 | 44 | public ModPlugin() 45 | { } 46 | 47 | public override Assembly GetAssembly() 48 | { 49 | return null; 50 | } 51 | 52 | public override bool TryLoadAssembly(out Assembly a) 53 | { 54 | a = null; 55 | return false; 56 | } 57 | 58 | public override void Show() 59 | { 60 | MyGuiSandbox.OpenUrl("https://steamcommunity.com/workshop/filedetails/?id=" + Id, UrlOpenMode.SteamOrExternalWithConfirm); 61 | } 62 | 63 | private string modLocation; 64 | private bool isLegacy; 65 | public string ModLocation 66 | { 67 | get 68 | { 69 | if (modLocation != null) 70 | return modLocation; 71 | modLocation = Path.Combine(Path.GetFullPath(@"..\..\..\workshop\content\244850\"), WorkshopId.ToString()); 72 | if (Directory.Exists(modLocation) && !Directory.Exists(Path.Combine(modLocation, "Data"))) 73 | { 74 | string legacyFile = Directory.EnumerateFiles(modLocation, "*_legacy.bin").FirstOrDefault(); 75 | if(legacyFile != null) 76 | { 77 | isLegacy = true; 78 | modLocation = legacyFile; 79 | } 80 | } 81 | return modLocation; 82 | } 83 | } 84 | 85 | public bool Exists => Directory.Exists(ModLocation) || (isLegacy && File.Exists(modLocation)); 86 | 87 | public MyObjectBuilder_Checkpoint.ModItem GetModItem() 88 | { 89 | var modItem = new MyObjectBuilder_Checkpoint.ModItem(WorkshopId, "Steam"); 90 | modItem.SetModData(new WorkshopItem(ModLocation)); 91 | return modItem; 92 | } 93 | 94 | class WorkshopItem : VRage.GameServices.MyWorkshopItem 95 | { 96 | public WorkshopItem(string folder) 97 | { 98 | Folder = folder; 99 | } 100 | } 101 | 102 | public MyModContext GetModContext() 103 | { 104 | MyModContext modContext = new MyModContext(); 105 | modContext.Init(GetModItem()); 106 | modContext.Init(WorkshopId.ToString(), null, ModLocation); 107 | return modContext; 108 | } 109 | 110 | 111 | public override bool UpdateEnabledPlugins(HashSet enabledPlugins, bool enable) 112 | { 113 | bool changed = base.UpdateEnabledPlugins(enabledPlugins, enable); 114 | 115 | if(enable) 116 | { 117 | foreach (ModPlugin other in Dependencies) 118 | { 119 | if (enabledPlugins.Add(other.Id)) 120 | changed = true; 121 | } 122 | } 123 | 124 | return changed; 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /PluginLoader/GUI/PlayerConsent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using avaness.PluginLoader.Stats; 4 | using Sandbox.Graphics.GUI; 5 | using VRageMath; 6 | 7 | namespace avaness.PluginLoader.GUI 8 | { 9 | public static class PlayerConsent 10 | { 11 | public static event Action OnConsentChanged; 12 | 13 | public static void ShowDialog(Action continuation = null) 14 | { 15 | MyGuiScreenMessageBox dialog = MyGuiSandbox.CreateMessageBox(buttonType: MyMessageBoxButtonsType.YES_NO_CANCEL, 16 | messageText: new StringBuilder( 17 | " Would you like to rate plugins and inform developers?\r\n" + 18 | "\r\n" + 19 | "\r\n" + 20 | "YES: Plugin Loader will send the list of enabled plugins to our server\r\n" + 21 | " each time the game starts. Your Steam ID is sent only in hashed form,\r\n" + 22 | " which makes it hard to identify you. Plugin usage statistics is kept\r\n" + 23 | " for up to 90 days. Votes on plugins are preserved indefinitely.\r\n" + 24 | " Server log files and database backups may be kept up to 90 days.\r\n" + 25 | " Location of data storage: European Union\r\n" + 26 | "\r\n" + 27 | "\r\n" + 28 | "NO: None of your data will be sent to nor stored on our statistics server.\r\n" + 29 | " Plugin Loader will still connect to download the statistics shown.\r\n"), 30 | size: new Vector2(0.6f, 0.6f), 31 | messageCaption: new StringBuilder("Consent"), 32 | callback: result => GetConfirmation(result, continuation)); 33 | 34 | if (dialog.Controls.GetControlByName("MyGuiControlMultilineText") is MyGuiControlMultilineText text) 35 | text.TextAlign = VRage.Utils.MyGuiDrawAlignEnum.HORISONTAL_LEFT_AND_VERTICAL_CENTER; 36 | 37 | MyGuiSandbox.AddScreen(dialog); 38 | } 39 | 40 | public static bool ConsentRequested => !string.IsNullOrEmpty(Main.Instance.Config.DataHandlingConsentDate); 41 | 42 | public static bool ConsentGiven => Main.Instance.Config.DataHandlingConsent; 43 | 44 | private static void GetConfirmation(MyGuiScreenMessageBox.ResultEnum result, Action continuation) 45 | { 46 | if (result == MyGuiScreenMessageBox.ResultEnum.CANCEL) 47 | return; 48 | 49 | var consent = result == MyGuiScreenMessageBox.ResultEnum.YES; 50 | 51 | var consentWithdrawn = ConsentRequested && ConsentGiven && !consent; 52 | if (consentWithdrawn) 53 | { 54 | MyGuiSandbox.AddScreen(MyGuiSandbox.CreateMessageBox(MyMessageBoxStyleEnum.Info, MyMessageBoxButtonsType.YES_NO_CANCEL, new StringBuilder("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 StringBuilder("Confirm consent withdrawal"), callback: res => StoreConsent(res, false, continuation))); 55 | return; 56 | } 57 | 58 | StoreConsent(MyGuiScreenMessageBox.ResultEnum.YES, consent, continuation); 59 | } 60 | 61 | private static void StoreConsent(MyGuiScreenMessageBox.ResultEnum confirmationResult, bool consent, Action continuation) 62 | { 63 | if (confirmationResult != MyGuiScreenMessageBox.ResultEnum.YES) 64 | return; 65 | 66 | if (ConsentRequested && consent == ConsentGiven) 67 | { 68 | continuation?.Invoke(); 69 | return; 70 | } 71 | 72 | if (!StatsClient.Consent(consent)) 73 | { 74 | LogFile.Error("Failed to register player consent on statistics server"); 75 | return; 76 | } 77 | 78 | var config = Main.Instance.Config; 79 | config.DataHandlingConsentDate = Tools.Tools.FormatDateIso8601(DateTime.Today); 80 | config.DataHandlingConsent = consent; 81 | config.Save(); 82 | 83 | if (consent) 84 | StatsClient.Track(Main.Instance.TrackablePluginIds); 85 | 86 | OnConsentChanged?.Invoke(); 87 | 88 | continuation?.Invoke(); 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /PluginLoader/Compiler/RoslynCompiler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.CodeAnalysis.Emit; 4 | using Microsoft.CodeAnalysis.Text; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Text; 10 | 11 | namespace avaness.PluginLoader.Compiler 12 | { 13 | public class RoslynCompiler 14 | { 15 | private readonly List source = new List(); 16 | private readonly List customReferences = new List(); 17 | private bool debugBuild; 18 | 19 | public RoslynCompiler(bool debugBuild = false) 20 | { 21 | this.debugBuild = debugBuild; 22 | } 23 | 24 | public void Load(Stream s, string name) 25 | { 26 | MemoryStream mem = new MemoryStream(); 27 | using (mem) 28 | { 29 | s.CopyTo(mem); 30 | source.Add(new Source(mem, name, debugBuild)); 31 | } 32 | } 33 | 34 | public byte[] Compile(string assemblyName, out byte[] symbols) 35 | { 36 | symbols = null; 37 | 38 | CSharpCompilation compilation = CSharpCompilation.Create( 39 | assemblyName, 40 | syntaxTrees: source.Select(x => x.Tree), 41 | references: RoslynReferences.EnumerateAllReferences().Concat(customReferences), 42 | options: new CSharpCompilationOptions( 43 | OutputKind.DynamicallyLinkedLibrary, 44 | optimizationLevel: debugBuild ? OptimizationLevel.Debug : OptimizationLevel.Release, 45 | allowUnsafe: true)); 46 | 47 | using (MemoryStream pdb = new MemoryStream()) 48 | using (MemoryStream ms = new MemoryStream()) 49 | { 50 | // write IL code into memory 51 | EmitResult result; 52 | if (debugBuild) 53 | { 54 | result = compilation.Emit(ms, pdb, 55 | embeddedTexts: source.Select(x => x.Text), 56 | options: new EmitOptions(debugInformationFormat: DebugInformationFormat.PortablePdb, pdbFilePath: Path.ChangeExtension(assemblyName, "pdb"))); 57 | } 58 | else 59 | { 60 | result = compilation.Emit(ms); 61 | } 62 | 63 | if (!result.Success) 64 | { 65 | // handle exceptions 66 | IEnumerable failures = result.Diagnostics.Where(diagnostic => 67 | diagnostic.IsWarningAsError || 68 | diagnostic.Severity == DiagnosticSeverity.Error); 69 | 70 | foreach (Diagnostic diagnostic in failures) 71 | { 72 | Location location = diagnostic.Location; 73 | Source source = this.source.FirstOrDefault(x => x.Tree == location.SourceTree); 74 | LinePosition pos = location.GetLineSpan().StartLinePosition; 75 | LogFile.Error($"{diagnostic.Id}: {diagnostic.GetMessage()} in file:\n{source?.Name ?? "null"} ({pos.Line + 1},{pos.Character + 1})"); 76 | } 77 | throw new Exception("Compilation failed!"); 78 | } 79 | else 80 | { 81 | if(debugBuild) 82 | { 83 | pdb.Seek(0, SeekOrigin.Begin); 84 | symbols = pdb.ToArray(); 85 | } 86 | 87 | ms.Seek(0, SeekOrigin.Begin); 88 | return ms.ToArray(); 89 | } 90 | } 91 | 92 | } 93 | 94 | public void TryAddDependency(string dll) 95 | { 96 | if(Path.HasExtension(dll) 97 | && Path.GetExtension(dll).Equals(".dll", StringComparison.OrdinalIgnoreCase) 98 | && File.Exists(dll)) 99 | { 100 | try 101 | { 102 | MetadataReference reference = MetadataReference.CreateFromFile(dll); 103 | if (reference != null) 104 | { 105 | LogFile.WriteLine("Custom compiler reference: " + (reference.Display ?? dll)); 106 | customReferences.Add(reference); 107 | } 108 | } 109 | catch 110 | { } 111 | } 112 | } 113 | 114 | private class Source 115 | { 116 | public string Name { get; } 117 | public SyntaxTree Tree { get; } 118 | public EmbeddedText Text { get; } 119 | 120 | public Source(Stream s, string name, bool includeText) 121 | { 122 | Name = name; 123 | SourceText source = SourceText.From(s, canBeEmbedded: includeText); 124 | if (includeText) 125 | { 126 | Text = EmbeddedText.FromSource(name, source); 127 | Tree = CSharpSyntaxTree.ParseText(source, new CSharpParseOptions(LanguageVersion.Latest), name); 128 | } 129 | else 130 | { 131 | Tree = CSharpSyntaxTree.ParseText(source, new CSharpParseOptions(LanguageVersion.Latest)); 132 | } 133 | } 134 | } 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /PluginLoader/Compiler/RoslynReferences.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using NLog; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Reflection; 8 | using System.Text; 9 | 10 | namespace avaness.PluginLoader.Compiler 11 | { 12 | public static class RoslynReferences 13 | { 14 | private static Dictionary allReferences = new Dictionary(); 15 | private static readonly HashSet referenceBlacklist = new HashSet(new[] { "System.ValueTuple" }); 16 | 17 | public static void GenerateAssemblyList() 18 | { 19 | if (allReferences.Count > 0) 20 | return; 21 | 22 | AssemblyName harmonyInfo = typeof(HarmonyLib.Harmony).Assembly.GetName(); 23 | 24 | Stack loadedAssemblies = new Stack(AppDomain.CurrentDomain.GetAssemblies().Where(IsValidReference)); 25 | 26 | StringBuilder sb = new StringBuilder(); 27 | 28 | sb.AppendLine(); 29 | string line = "==================================="; 30 | sb.AppendLine(line); 31 | sb.AppendLine("Assembly References"); 32 | sb.AppendLine(line); 33 | 34 | LogLevel level = LogLevel.Info; 35 | try 36 | { 37 | foreach (Assembly a in loadedAssemblies) 38 | { 39 | // Prevent other Harmony versions from being loaded 40 | AssemblyName name = a.GetName(); 41 | if (name.Name == harmonyInfo.Name && name.Version != harmonyInfo.Version) 42 | { 43 | LogFile.Warn($"Multiple Harmony assemblies are loaded. Plugin Loader is using {harmonyInfo} but found {name}"); 44 | continue; 45 | } 46 | 47 | AddAssemblyReference(a); 48 | sb.AppendLine(a.FullName); 49 | } 50 | foreach(Assembly a in GetOtherReferences()) 51 | { 52 | AddAssemblyReference(a); 53 | sb.AppendLine(a.FullName); 54 | } 55 | sb.AppendLine(line); 56 | while (loadedAssemblies.Count > 0) 57 | { 58 | Assembly a = loadedAssemblies.Pop(); 59 | 60 | foreach (AssemblyName name in a.GetReferencedAssemblies()) 61 | { 62 | // Prevent other Harmony versions from being loaded 63 | if (name.Name == harmonyInfo.Name && name.Version != harmonyInfo.Version) 64 | { 65 | LogFile.Warn($"Multiple Harmony assemblies are loaded. Plugin Loader is using {harmonyInfo} but found {name}"); 66 | continue; 67 | } 68 | 69 | if (!ContainsReference(name) && TryLoadAssembly(name, out Assembly aRef) && IsValidReference(aRef)) 70 | { 71 | AddAssemblyReference(aRef); 72 | sb.AppendLine(name.FullName); 73 | loadedAssemblies.Push(aRef); 74 | } 75 | } 76 | } 77 | sb.AppendLine(line); 78 | } 79 | catch (Exception e) 80 | { 81 | sb.Append("Error: ").Append(e).AppendLine(); 82 | level = LogLevel.Error; 83 | } 84 | 85 | LogFile.WriteLine(sb.ToString(), level, gameLog: false); 86 | } 87 | 88 | /// 89 | /// This method is used to load references that otherwise would not exist or be optimized out 90 | /// 91 | private static IEnumerable GetOtherReferences() 92 | { 93 | yield return typeof(Microsoft.CSharp.RuntimeBinder.Binder).Assembly; 94 | yield return typeof(System.Windows.Forms.DataVisualization.Charting.Chart).Assembly; 95 | } 96 | 97 | private static bool ContainsReference(AssemblyName name) 98 | { 99 | return allReferences.ContainsKey(name.Name); 100 | } 101 | 102 | private static bool TryLoadAssembly(AssemblyName name, out Assembly aRef) 103 | { 104 | try 105 | { 106 | aRef = Assembly.Load(name); 107 | return true; 108 | } 109 | catch (IOException) 110 | { 111 | aRef = null; 112 | return false; 113 | } 114 | } 115 | 116 | private static void AddAssemblyReference(Assembly a) 117 | { 118 | string name = a.GetName().Name; 119 | if (!allReferences.ContainsKey(name)) 120 | allReferences.Add(name, MetadataReference.CreateFromFile(a.Location)); 121 | } 122 | 123 | public static IEnumerable EnumerateAllReferences() 124 | { 125 | return allReferences.Values; 126 | } 127 | 128 | private static bool IsValidReference(Assembly a) 129 | { 130 | return !a.IsDynamic && !string.IsNullOrWhiteSpace(a.Location) && !referenceBlacklist.Contains(a.GetName().Name); 131 | } 132 | 133 | public static void LoadReference(string name) 134 | { 135 | try 136 | { 137 | AssemblyName aName = new AssemblyName(name); 138 | if (!allReferences.ContainsKey(aName.Name)) 139 | { 140 | Assembly a = Assembly.Load(aName); 141 | LogFile.WriteLine("Reference added at runtime: " + a.FullName); 142 | MetadataReference aRef = MetadataReference.CreateFromFile(a.Location); 143 | allReferences[a.GetName().Name] = aRef; 144 | } 145 | } 146 | catch (IOException) 147 | { 148 | LogFile.Warn("Unable to find the assembly '" + name + "'!"); 149 | } 150 | } 151 | 152 | public static bool Contains(string id) 153 | { 154 | return allReferences.ContainsKey(id); 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /PluginLoader/Tools/SimpleHttpClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Net; 5 | using System.Net.Http; 6 | using System.Text; 7 | using LitJson; 8 | 9 | namespace avaness.PluginLoader.Tools 10 | { 11 | public static class SimpleHttpClient 12 | { 13 | // REST API request timeout in milliseconds 14 | private const int TimeoutMs = 3000; 15 | 16 | public static TV Get(string url) 17 | where TV : class, new() 18 | { 19 | try 20 | { 21 | using var response = (HttpWebResponse)CreateRequest(HttpMethod.Get, url).GetResponse(); 22 | 23 | using var responseStream = response.GetResponseStream(); 24 | if (responseStream == null) 25 | return null; 26 | 27 | using var streamReader = new StreamReader(responseStream, Encoding.UTF8); 28 | return JsonMapper.ToObject(streamReader.ReadToEnd()); 29 | } 30 | catch (WebException e) 31 | { 32 | LogFile.Error($"REST API request failed: GET {url} [{e.Message}]"); 33 | return null; 34 | } 35 | } 36 | 37 | public static TV Get(string url, Dictionary parameters) 38 | where TV : class, new() 39 | { 40 | var uriBuilder = new StringBuilder(url); 41 | AppendQueryParameters(uriBuilder, parameters); 42 | var uri = uriBuilder.ToString(); 43 | 44 | try 45 | { 46 | using var response = (HttpWebResponse)CreateRequest(HttpMethod.Get, uri).GetResponse(); 47 | 48 | using var responseStream = response.GetResponseStream(); 49 | if (responseStream == null) 50 | return null; 51 | 52 | using var streamReader = new StreamReader(responseStream, Encoding.UTF8); 53 | return JsonMapper.ToObject(streamReader.ReadToEnd()); 54 | } 55 | catch (WebException e) 56 | { 57 | LogFile.Error($"REST API request failed: GET {uri} [{e.Message}]"); 58 | return null; 59 | } 60 | } 61 | 62 | public static TV Post(string url) 63 | where TV : class, new() 64 | { 65 | try 66 | { 67 | var request = CreateRequest(HttpMethod.Post, url); 68 | request.ContentLength = 0L; 69 | return PostRequest(request); 70 | } 71 | catch (WebException e) 72 | { 73 | LogFile.Error($"REST API request failed: POST {url} [{e.Message}]"); 74 | return null; 75 | } 76 | } 77 | 78 | public static TV Post(string url, Dictionary parameters) 79 | where TV : class, new() 80 | { 81 | var uriBuilder = new StringBuilder(url); 82 | AppendQueryParameters(uriBuilder, parameters); 83 | var uri = uriBuilder.ToString(); 84 | 85 | try 86 | { 87 | var request = CreateRequest(HttpMethod.Post, uri); 88 | request.ContentType = "application/x-www-form-urlencoded"; 89 | request.ContentLength = 0; 90 | return PostRequest(request); 91 | } 92 | catch (WebException e) 93 | { 94 | LogFile.Error($"REST API request failed: POST {uri} [{e.Message}]"); 95 | return null; 96 | } 97 | } 98 | 99 | public static TV Post(string url, TR body) 100 | where TR : class, new() 101 | where TV : class, new() 102 | { 103 | try 104 | { 105 | var request = CreateRequest(HttpMethod.Post, url); 106 | var requestJson = JsonMapper.ToJson(body); 107 | var requestBytes = Encoding.UTF8.GetBytes(requestJson); 108 | request.ContentType = "application/json"; 109 | request.ContentLength = requestBytes.Length; 110 | return PostRequest(request, requestBytes); 111 | } 112 | catch (WebException e) 113 | { 114 | LogFile.Error($"REST API request failed: POST {url} [{e.Message}]"); 115 | return null; 116 | } 117 | } 118 | 119 | public static bool Post(string url, TR body) 120 | where TR : class, new() 121 | { 122 | try 123 | { 124 | var request = CreateRequest(HttpMethod.Post, url); 125 | var requestJson = JsonMapper.ToJson(body); 126 | var requestBytes = Encoding.UTF8.GetBytes(requestJson); 127 | request.ContentType = "application/json"; 128 | request.ContentLength = requestBytes.Length; 129 | return PostRequest(request, requestBytes); 130 | } 131 | catch (WebException e) 132 | { 133 | LogFile.Error($"REST API request failed: POST {url} [{e.Message}]"); 134 | return false; 135 | } 136 | } 137 | 138 | private static TV PostRequest(HttpWebRequest request, byte[] body = null) where TV : class, new() 139 | { 140 | if (body != null) 141 | { 142 | using var requestStream = request.GetRequestStream(); 143 | requestStream.Write(body, 0, body.Length); 144 | requestStream.Close(); 145 | } 146 | 147 | using var response = (HttpWebResponse)request.GetResponse(); 148 | using var responseStream = response.GetResponseStream(); 149 | if (responseStream == null) 150 | return null; 151 | 152 | using var streamReader = new StreamReader(responseStream, Encoding.UTF8); 153 | var data = JsonMapper.ToObject(streamReader.ReadToEnd()); 154 | return data; 155 | } 156 | 157 | private static bool PostRequest(HttpWebRequest request, byte[] body = null) 158 | { 159 | if (body != null) 160 | { 161 | using var requestStream = request.GetRequestStream(); 162 | requestStream.Write(body, 0, body.Length); 163 | requestStream.Close(); 164 | } 165 | 166 | using var response = (HttpWebResponse)request.GetResponse(); 167 | 168 | return response.StatusCode == HttpStatusCode.OK; 169 | } 170 | 171 | private static HttpWebRequest CreateRequest(HttpMethod method, string url) 172 | { 173 | var http = WebRequest.CreateHttp(url); 174 | http.Method = method.ToString().ToUpper(); 175 | http.Timeout = TimeoutMs; 176 | return http; 177 | } 178 | 179 | private static void AppendQueryParameters(StringBuilder stringBuilder, Dictionary parameters) 180 | { 181 | if (parameters == null || parameters.Count == 0) 182 | return; 183 | 184 | var first = true; 185 | foreach (var p in parameters) 186 | { 187 | stringBuilder.Append(first ? '?' : '&'); 188 | first = false; 189 | stringBuilder.Append(Uri.EscapeDataString(p.Key)); 190 | stringBuilder.Append('='); 191 | stringBuilder.Append(Uri.EscapeDataString(p.Value)); 192 | } 193 | } 194 | } 195 | } -------------------------------------------------------------------------------- /PluginLoader/GUI/ProfilesMenu.cs: -------------------------------------------------------------------------------- 1 | using avaness.PluginLoader.Data; 2 | using Sandbox.Graphics.GUI; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using VRage.Utils; 7 | using VRageMath; 8 | 9 | namespace avaness.PluginLoader.GUI 10 | { 11 | public class ProfilesMenu : PluginScreen 12 | { 13 | private MyGuiControlTable profilesTable; 14 | private MyGuiControlButton btnUpdate, btnLoad, btnRename, btnDelete; 15 | private Dictionary profiles; 16 | private readonly HashSet enabledPlugins; 17 | private bool profilesModified = false; 18 | 19 | public ProfilesMenu(HashSet enabledPlugins) : base(size: new Vector2(0.85f, 0.52f)) 20 | { 21 | this.enabledPlugins = enabledPlugins; 22 | profiles = Main.Instance.Config.ProfileMap; 23 | Closed += OnScreenClosed; 24 | } 25 | 26 | private void OnScreenClosed(MyGuiScreenBase source, bool isUnloading) 27 | { 28 | if(profilesModified) 29 | Main.Instance.Config.Save(); 30 | Closed -= OnScreenClosed; 31 | } 32 | 33 | public override string GetFriendlyName() 34 | { 35 | return typeof(ProfilesMenu).FullName; 36 | } 37 | 38 | public override void RecreateControls(bool constructor) 39 | { 40 | base.RecreateControls(constructor); 41 | 42 | // Top 43 | MyGuiControlLabel caption = AddCaption("Profiles", captionScale: 1); 44 | AddBarBelow(caption); 45 | 46 | // Bottom: New/Update, Load, Rename, Delete 47 | Vector2 bottomMid = new Vector2(0, m_size.Value.Y / 2); 48 | btnLoad = new MyGuiControlButton(position: new Vector2(bottomMid.X - (GuiSpacing / 2), bottomMid.Y - GuiSpacing), text: new StringBuilder("Load"), originAlign: MyGuiDrawAlignEnum.HORISONTAL_RIGHT_AND_VERTICAL_BOTTOM, onButtonClick: OnLoadClick); 49 | 50 | btnUpdate = new MyGuiControlButton(text: new StringBuilder("New"), onButtonClick: OnUpdateClick); 51 | PositionToLeft(btnLoad, btnUpdate); 52 | 53 | btnRename = new MyGuiControlButton(text: new StringBuilder("Rename"), onButtonClick: OnRenameClick); 54 | PositionToRight(btnLoad, btnRename); 55 | 56 | btnDelete = new MyGuiControlButton(text: new StringBuilder("Delete"), onButtonClick: OnDeleteClick); 57 | PositionToRight(btnRename, btnDelete); 58 | 59 | Controls.Add(btnUpdate); 60 | Controls.Add(btnLoad); 61 | Controls.Add(btnRename); 62 | Controls.Add(btnDelete); 63 | AddBarAbove(btnLoad); 64 | 65 | // Table 66 | RectangleF area = GetAreaBetween(caption, btnRename, GuiSpacing * 2); 67 | 68 | profilesTable = new MyGuiControlTable() 69 | { 70 | Size = area.Size, 71 | Position = area.Position, 72 | OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_LEFT_AND_VERTICAL_TOP, 73 | }; 74 | profilesTable.ColumnsCount = 2; 75 | profilesTable.SetCustomColumnWidths(new[] 76 | { 77 | 0.4f, 78 | 0.6f, 79 | }); 80 | profilesTable.SetColumnName(0, new StringBuilder("Name")); 81 | profilesTable.SetColumnName(1, new StringBuilder("Enabled Count")); 82 | profilesTable.ItemDoubleClicked += OnItemDoubleClicked; 83 | profilesTable.ItemSelected += OnItemSelected; 84 | SetTableHeight(profilesTable, area.Size.Y); 85 | Controls.Add(profilesTable); 86 | foreach (Profile p in profiles.Values) 87 | profilesTable.Add(CreateProfileRow(p)); 88 | UpdateButtons(); 89 | } 90 | 91 | private void OnItemSelected(MyGuiControlTable table, MyGuiControlTable.EventArgs args) 92 | { 93 | UpdateButtons(); 94 | } 95 | 96 | private void OnItemDoubleClicked(MyGuiControlTable table, MyGuiControlTable.EventArgs args) 97 | { 98 | int rowIndex = args.RowIndex; 99 | if (rowIndex >= 0 && rowIndex < table.RowsCount && table.GetRow(rowIndex)?.UserData is Profile p) 100 | LoadProfile(p); 101 | } 102 | 103 | private void LoadProfile(Profile p) 104 | { 105 | enabledPlugins.Clear(); 106 | foreach(PluginData plugin in p.GetPlugins()) 107 | enabledPlugins.Add(plugin.Id); 108 | CloseScreen(); 109 | } 110 | 111 | private static MyGuiControlTable.Row CreateProfileRow(Profile p) 112 | { 113 | MyGuiControlTable.Row row = new MyGuiControlTable.Row(p); 114 | 115 | row.AddCell(new MyGuiControlTable.Cell(text: p.Name, toolTip: p.Name)); 116 | string desc = p.GetDescription(); 117 | row.AddCell(new MyGuiControlTable.Cell(text: desc, toolTip: desc)); 118 | return row; 119 | } 120 | 121 | private void UpdateButtons() 122 | { 123 | bool selected = profilesTable.SelectedRow != null; 124 | btnUpdate.Text = selected ? "Update" : "New"; 125 | btnLoad.Enabled = selected; 126 | btnRename.Enabled = selected; 127 | btnDelete.Enabled = selected; 128 | } 129 | 130 | private void OnDeleteClick(MyGuiControlButton btn) 131 | { 132 | MyGuiControlTable.Row row = profilesTable.SelectedRow; 133 | if (row?.UserData is Profile p) 134 | { 135 | profiles.Remove(p.Key); 136 | profilesTable.Remove(row); 137 | profilesModified = true; 138 | UpdateButtons(); 139 | } 140 | } 141 | 142 | private void OnRenameClick(MyGuiControlButton btn) 143 | { 144 | MyGuiControlTable.Row row = profilesTable.SelectedRow; 145 | if (row?.UserData is Profile p) 146 | { 147 | MyScreenManager.AddScreen(new TextInputDialog("Profile Name", p.Name, onComplete: (name) => 148 | { 149 | p.Name = name; 150 | row.GetCell(0).Text.Clear().Append(name); 151 | profilesModified = true; 152 | })); 153 | } 154 | } 155 | 156 | private void OnLoadClick(MyGuiControlButton btn) 157 | { 158 | if (profilesTable.SelectedRow?.UserData is Profile p) 159 | LoadProfile(p); 160 | } 161 | 162 | 163 | private void OnUpdateClick(MyGuiControlButton btn) 164 | { 165 | MyGuiControlTable.Row row = profilesTable.SelectedRow; 166 | if(row == null) 167 | { 168 | // New profile 169 | MyScreenManager.AddScreen(new TextInputDialog("Profile Name", onComplete: CreateProfile)); 170 | } 171 | else if(row.UserData is Profile p) 172 | { 173 | // Update profile 174 | p.Plugins = enabledPlugins.ToArray(); 175 | row.GetCell(1).Text.Clear().Append(p.GetDescription()); 176 | profilesModified = true; 177 | } 178 | } 179 | 180 | private void CreateProfile(string name) 181 | { 182 | if (string.IsNullOrWhiteSpace(name)) 183 | return; 184 | 185 | Profile newProfile = new Profile(name, enabledPlugins.ToArray()); 186 | profiles[newProfile.Key] = newProfile; 187 | MyGuiControlTable.Row row = CreateProfileRow(newProfile); 188 | profilesTable.Add(row); 189 | profilesTable.SelectedRow = row; 190 | UpdateButtons(); 191 | profilesModified = true; 192 | } 193 | 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # JustCode is a .NET coding add-in 131 | .JustCode 132 | 133 | # TeamCity is a build add-in 134 | _TeamCity* 135 | 136 | # DotCover is a Code Coverage Tool 137 | *.dotCover 138 | 139 | # AxoCover is a Code Coverage Tool 140 | .axoCover/* 141 | !.axoCover/settings.json 142 | 143 | # Visual Studio code coverage results 144 | *.coverage 145 | *.coveragexml 146 | 147 | # NCrunch 148 | _NCrunch_* 149 | .*crunch*.local.xml 150 | nCrunchTemp_* 151 | 152 | # MightyMoose 153 | *.mm.* 154 | AutoTest.Net/ 155 | 156 | # Web workbench (sass) 157 | .sass-cache/ 158 | 159 | # Installshield output folder 160 | [Ee]xpress/ 161 | 162 | # DocProject is a documentation generator add-in 163 | DocProject/buildhelp/ 164 | DocProject/Help/*.HxT 165 | DocProject/Help/*.HxC 166 | DocProject/Help/*.hhc 167 | DocProject/Help/*.hhk 168 | DocProject/Help/*.hhp 169 | DocProject/Help/Html2 170 | DocProject/Help/html 171 | 172 | # Click-Once directory 173 | publish/ 174 | 175 | # Publish Web Output 176 | *.[Pp]ublish.xml 177 | *.azurePubxml 178 | # Note: Comment the next line if you want to checkin your web deploy settings, 179 | # but database connection strings (with potential passwords) will be unencrypted 180 | *.pubxml 181 | *.publishproj 182 | 183 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 184 | # checkin your Azure Web App publish settings, but sensitive information contained 185 | # in these scripts will be unencrypted 186 | PublishScripts/ 187 | 188 | # NuGet Packages 189 | *.nupkg 190 | # NuGet Symbol Packages 191 | *.snupkg 192 | # The packages folder can be ignored because of Package Restore 193 | **/[Pp]ackages/* 194 | # except build/, which is used as an MSBuild target. 195 | !**/[Pp]ackages/build/ 196 | # Uncomment if necessary however generally it will be regenerated when needed 197 | #!**/[Pp]ackages/repositories.config 198 | # NuGet v3's project.json files produces more ignorable files 199 | *.nuget.props 200 | *.nuget.targets 201 | 202 | # Microsoft Azure Build Output 203 | csx/ 204 | *.build.csdef 205 | 206 | # Microsoft Azure Emulator 207 | ecf/ 208 | rcf/ 209 | 210 | # Windows Store app package directories and files 211 | AppPackages/ 212 | BundleArtifacts/ 213 | Package.StoreAssociation.xml 214 | _pkginfo.txt 215 | *.appx 216 | *.appxbundle 217 | *.appxupload 218 | 219 | # Visual Studio cache files 220 | # files ending in .cache can be ignored 221 | *.[Cc]ache 222 | # but keep track of directories ending in .cache 223 | !?*.[Cc]ache/ 224 | 225 | # Others 226 | ClientBin/ 227 | ~$* 228 | *~ 229 | *.dbmdl 230 | *.dbproj.schemaview 231 | *.jfm 232 | *.pfx 233 | *.publishsettings 234 | orleans.codegen.cs 235 | 236 | # Including strong name files can present a security risk 237 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 238 | #*.snk 239 | 240 | # Since there are multiple workflows, uncomment next line to ignore bower_components 241 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 242 | #bower_components/ 243 | 244 | # RIA/Silverlight projects 245 | Generated_Code/ 246 | 247 | # Backup & report files from converting an old project file 248 | # to a newer Visual Studio version. Backup files are not needed, 249 | # because we have git ;-) 250 | _UpgradeReport_Files/ 251 | Backup*/ 252 | UpgradeLog*.XML 253 | UpgradeLog*.htm 254 | ServiceFabricBackup/ 255 | *.rptproj.bak 256 | 257 | # SQL Server files 258 | *.mdf 259 | *.ldf 260 | *.ndf 261 | 262 | # Business Intelligence projects 263 | *.rdl.data 264 | *.bim.layout 265 | *.bim_*.settings 266 | *.rptproj.rsuser 267 | *- [Bb]ackup.rdl 268 | *- [Bb]ackup ([0-9]).rdl 269 | *- [Bb]ackup ([0-9][0-9]).rdl 270 | 271 | # Microsoft Fakes 272 | FakesAssemblies/ 273 | 274 | # GhostDoc plugin setting file 275 | *.GhostDoc.xml 276 | 277 | # Node.js Tools for Visual Studio 278 | .ntvs_analysis.dat 279 | node_modules/ 280 | 281 | # Visual Studio 6 build log 282 | *.plg 283 | 284 | # Visual Studio 6 workspace options file 285 | *.opt 286 | 287 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 288 | *.vbw 289 | 290 | # Visual Studio LightSwitch build output 291 | **/*.HTMLClient/GeneratedArtifacts 292 | **/*.DesktopClient/GeneratedArtifacts 293 | **/*.DesktopClient/ModelManifest.xml 294 | **/*.Server/GeneratedArtifacts 295 | **/*.Server/ModelManifest.xml 296 | _Pvt_Extensions 297 | 298 | # Paket dependency manager 299 | .paket/paket.exe 300 | paket-files/ 301 | 302 | # FAKE - F# Make 303 | .fake/ 304 | 305 | # CodeRush personal settings 306 | .cr/personal 307 | 308 | # Python Tools for Visual Studio (PTVS) 309 | __pycache__/ 310 | *.pyc 311 | 312 | # Cake - Uncomment if you are using it 313 | # tools/** 314 | # !tools/packages.config 315 | 316 | # Tabs Studio 317 | *.tss 318 | 319 | # Telerik's JustMock configuration file 320 | *.jmconfig 321 | 322 | # BizTalk build output 323 | *.btp.cs 324 | *.btm.cs 325 | *.odx.cs 326 | *.xsd.cs 327 | 328 | # OpenCover UI analysis results 329 | OpenCover/ 330 | 331 | # Azure Stream Analytics local run output 332 | ASALocalRun/ 333 | 334 | # MSBuild Binary and Structured Log 335 | *.binlog 336 | 337 | # NVidia Nsight GPU debugger configuration file 338 | *.nvuser 339 | 340 | # MFractors (Xamarin productivity tool) working folder 341 | .mfractor/ 342 | 343 | # Local History for Visual Studio 344 | .localhistory/ 345 | 346 | # BeatPulse healthcheck temp database 347 | healthchecksdb 348 | 349 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 350 | MigrationBackup/ 351 | 352 | # Ionide (cross platform F# VS Code tools) working folder 353 | .ionide/ 354 | 355 | # SE's Bin64 folder symlinked for DLL dependencies 356 | /Bin64/ 357 | 358 | # Steam's workshop folder symlinked for DLL dependencies 359 | /workshop/ 360 | 361 | # JetBrains Rider project files 362 | .idea/ 363 | 364 | .editorconfig 365 | -------------------------------------------------------------------------------- /PluginLoader/Data/GitHubPlugin.CacheManifest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Xml.Serialization; 6 | 7 | namespace avaness.PluginLoader.Data 8 | { 9 | public partial class GitHubPlugin 10 | { 11 | public class CacheManifest 12 | { 13 | private const string pluginFile = "plugin.dll"; 14 | private const string manifestFile = "manifest.xml"; 15 | private const string commitFile = "commit.sha1"; 16 | private const string assetFolder = "Assets"; 17 | private const string libFolder = "Bin"; 18 | 19 | private string cacheDir; 20 | private string assetDir; 21 | private string libDir; 22 | private Dictionary assetFiles = new Dictionary(); 23 | 24 | [XmlIgnore] 25 | public string DllFile { get; private set; } 26 | public string AssetFolder => assetDir; 27 | public string LibDir => libDir; 28 | 29 | public string Commit { get; set; } 30 | public int GameVersion { get; set; } 31 | [XmlArray] 32 | [XmlArrayItem("File")] 33 | public AssetFile[] AssetFiles 34 | { 35 | get 36 | { 37 | return assetFiles.Values.ToArray(); 38 | } 39 | set 40 | { 41 | assetFiles = value.ToDictionary(GetAssetKey); 42 | } 43 | } 44 | 45 | public CacheManifest() 46 | { 47 | 48 | } 49 | 50 | private void Init(string cacheDir) 51 | { 52 | this.cacheDir = cacheDir; 53 | assetDir = Path.Combine(cacheDir, assetFolder); 54 | libDir = Path.Combine(cacheDir, libFolder); 55 | DllFile = Path.Combine(cacheDir, pluginFile); 56 | 57 | foreach (AssetFile file in assetFiles.Values) 58 | SetBaseDir(file); 59 | 60 | // Backwards compatibility 61 | string oldCommitFile = Path.Combine(cacheDir, commitFile); 62 | if(File.Exists(oldCommitFile)) 63 | { 64 | try 65 | { 66 | Commit = File.ReadAllText(oldCommitFile).Trim(); 67 | File.Delete(oldCommitFile); 68 | Save(); 69 | } 70 | catch (Exception e) 71 | { 72 | LogFile.WriteLine("Error while reading old commit file: " + e); 73 | } 74 | } 75 | } 76 | 77 | public static CacheManifest Load (string userName, string repoName) 78 | { 79 | string cacheDir = Path.Combine(LoaderTools.PluginsDir, "GitHub", userName, repoName); 80 | Directory.CreateDirectory(cacheDir); 81 | 82 | CacheManifest manifest; 83 | 84 | string manifestLocation = Path.Combine(cacheDir, manifestFile); 85 | if(!File.Exists(manifestLocation)) 86 | { 87 | manifest = new CacheManifest(); 88 | } 89 | else 90 | { 91 | XmlSerializer serializer = new XmlSerializer(typeof(CacheManifest)); 92 | try 93 | { 94 | using (Stream file = File.OpenRead(manifestLocation)) 95 | manifest = (CacheManifest)serializer.Deserialize(file); 96 | } 97 | catch (Exception e) 98 | { 99 | LogFile.WriteLine("Error while loading manifest file: " + e); 100 | manifest = new CacheManifest(); 101 | } 102 | } 103 | 104 | manifest.Init(cacheDir); 105 | return manifest; 106 | } 107 | 108 | public bool IsCacheValid(string currentCommit, int currentGameVersion, bool requiresAssets, bool requiresPackages) 109 | { 110 | if(!File.Exists(DllFile) || Commit != currentCommit) 111 | return false; 112 | 113 | if(currentGameVersion != 0) 114 | { 115 | if (GameVersion == 0 || GameVersion != currentGameVersion) 116 | return false; 117 | } 118 | 119 | if (requiresAssets && !assetFiles.Values.Any(x => x.Type == AssetFile.AssetType.Asset)) 120 | return false; 121 | 122 | if (requiresPackages && !assetFiles.Values.Any(x => x.Type != AssetFile.AssetType.Asset)) 123 | return false; 124 | 125 | foreach (AssetFile file in assetFiles.Values) 126 | { 127 | if (!file.IsValid()) 128 | return false; 129 | } 130 | 131 | return true; 132 | } 133 | 134 | public void ClearAssets() 135 | { 136 | assetFiles.Clear(); 137 | } 138 | 139 | public AssetFile CreateAsset(string file, AssetFile.AssetType type = AssetFile.AssetType.Asset) 140 | { 141 | file = file.Replace('\\', '/').TrimStart('/'); 142 | AssetFile asset = new AssetFile(file, type); 143 | SetBaseDir(asset); 144 | asset.GetFileInfo(); 145 | assetFiles[GetAssetKey(asset)] = asset; 146 | return asset; 147 | } 148 | 149 | private string GetAssetKey(AssetFile asset) 150 | { 151 | if (asset.Type == AssetFile.AssetType.Asset) 152 | return assetFolder + "/" + asset.NormalizedFileName; 153 | return libFolder + "/" + asset.NormalizedFileName; 154 | } 155 | 156 | private void SetBaseDir(AssetFile asset) 157 | { 158 | asset.BaseDir = asset.Type == AssetFile.AssetType.Asset ? assetDir : libDir; 159 | } 160 | 161 | public bool IsAssetValid(AssetFile asset) 162 | { 163 | return asset.IsValid(); 164 | } 165 | 166 | public void SaveAsset(AssetFile asset, Stream stream) 167 | { 168 | asset.Save(stream); 169 | } 170 | 171 | public void Save() 172 | { 173 | string manifestLocation = Path.Combine(cacheDir, manifestFile); 174 | XmlSerializer serializer = new XmlSerializer(typeof(CacheManifest)); 175 | try 176 | { 177 | using (Stream file = File.Create(manifestLocation)) 178 | serializer.Serialize(file, this); 179 | } 180 | catch (Exception e) 181 | { 182 | LogFile.WriteLine("Error while saving manifest file: " + e); 183 | } 184 | } 185 | 186 | public void DeleteUnknownFiles() 187 | { 188 | DeleteUnknownFiles(assetDir); 189 | DeleteUnknownFiles(libDir); 190 | } 191 | 192 | public void DeleteUnknownFiles(string assetDir) 193 | { 194 | if (!Directory.Exists(assetDir)) 195 | return; 196 | 197 | foreach(string file in Directory.EnumerateFiles(assetDir, "*", SearchOption.AllDirectories)) 198 | { 199 | string relativePath = file.Substring(cacheDir.Length).Replace('\\', '/').TrimStart('/'); 200 | if (!assetFiles.ContainsKey(relativePath)) 201 | File.Delete(file); 202 | } 203 | } 204 | 205 | public void Invalidate() 206 | { 207 | Commit = null; 208 | Save(); 209 | } 210 | } 211 | } 212 | } -------------------------------------------------------------------------------- /PluginLoader/PluginInstance.cs: -------------------------------------------------------------------------------- 1 | using avaness.PluginLoader.Data; 2 | using Sandbox.Game.World; 3 | using System; 4 | using System.Linq; 5 | using System.Reflection; 6 | using HarmonyLib; 7 | using VRage.Game.Components; 8 | using VRage.Plugins; 9 | using System.IO; 10 | using System.Text; 11 | 12 | namespace avaness.PluginLoader 13 | { 14 | public class PluginInstance 15 | { 16 | private readonly Type mainType; 17 | private readonly PluginData data; 18 | private readonly Assembly mainAssembly; 19 | private MethodInfo openConfigDialog; 20 | private IPlugin plugin; 21 | private IHandleInputPlugin inputPlugin; 22 | 23 | public string Id => data.Id; 24 | public string FriendlyName => data.FriendlyName; 25 | public string Author => data.Author; 26 | public bool HasConfigDialog => openConfigDialog != null; 27 | 28 | private PluginInstance(PluginData data, Assembly mainAssembly, Type mainType) 29 | { 30 | this.data = data; 31 | this.mainAssembly = mainAssembly; 32 | this.mainType = mainType; 33 | } 34 | 35 | /// 36 | /// To be called when a is thrown. Returns true if the exception was thrown from this plugin. 37 | /// 38 | public bool ContainsExceptionSite(MemberAccessException exception) 39 | { 40 | // Note: this wont find exceptions thrown within transpiled methods or some kinds of patches 41 | Assembly a = exception.TargetSite?.DeclaringType?.Assembly; 42 | if (a != null && a == mainAssembly) 43 | { 44 | data.InvalidateCache(); 45 | ThrowError($"ERROR: Plugin {data} threw an exception: {exception}"); 46 | return true; 47 | } 48 | return false; 49 | } 50 | 51 | public bool Instantiate() 52 | { 53 | try 54 | { 55 | plugin = (IPlugin)Activator.CreateInstance(mainType); 56 | inputPlugin = plugin as IHandleInputPlugin; 57 | LoadAssets(); 58 | } 59 | catch (Exception e) 60 | { 61 | ThrowError($"Failed to instantiate {data} because of an error: {e}"); 62 | return false; 63 | } 64 | 65 | 66 | try 67 | { 68 | openConfigDialog = AccessTools.DeclaredMethod(mainType, "OpenConfigDialog", Array.Empty()); 69 | } 70 | catch (Exception e) 71 | { 72 | LogFile.Error($"Unable to find OpenConfigDialog() in {data} due to an error: {e}"); 73 | openConfigDialog = null; 74 | } 75 | return true; 76 | } 77 | 78 | private void LoadAssets() 79 | { 80 | string assetFolder = data.GetAssetPath(); 81 | if (string.IsNullOrEmpty(assetFolder) || !Directory.Exists(assetFolder)) 82 | return; 83 | 84 | LogFile.WriteLine($"Loading assets for {data} from {assetFolder}"); 85 | MethodInfo loadAssets = AccessTools.DeclaredMethod(mainType, "LoadAssets", new[] { typeof(string) }); 86 | if (loadAssets != null) 87 | loadAssets.Invoke(plugin, new[] { assetFolder }); 88 | } 89 | 90 | public void OpenConfig() 91 | { 92 | if (plugin == null || openConfigDialog == null) 93 | return; 94 | 95 | try 96 | { 97 | openConfigDialog.Invoke(plugin, Array.Empty()); 98 | } 99 | catch (Exception e) 100 | { 101 | ThrowError($"Failed to open plugin config for {data} because of an error: {e}"); 102 | } 103 | } 104 | 105 | public bool Init(object gameInstance) 106 | { 107 | if (plugin == null) 108 | return false; 109 | 110 | try 111 | { 112 | 113 | plugin.Init(gameInstance); 114 | return true; 115 | } 116 | catch (Exception e) 117 | { 118 | ThrowError($"Failed to initialize {data} because of an error: {e}"); 119 | return false; 120 | } 121 | } 122 | 123 | public void RegisterSession(MySession session) 124 | { 125 | if (plugin != null) 126 | { 127 | try 128 | { 129 | Type descType = typeof(MySessionComponentDescriptor); 130 | int count = 0; 131 | foreach (Type t in mainAssembly.GetTypes().Where(t => Attribute.IsDefined(t, descType))) 132 | { 133 | MySessionComponentBase comp = (MySessionComponentBase)Activator.CreateInstance(t); 134 | session.RegisterComponent(comp, comp.UpdateOrder, comp.Priority); 135 | count++; 136 | } 137 | if(count > 0) 138 | LogFile.WriteLine($"Registered {count} session components from: {mainAssembly.FullName}"); 139 | } 140 | catch (Exception e) 141 | { 142 | ThrowError($"Failed to register {data} because of an error: {e}"); 143 | } 144 | } 145 | 146 | } 147 | 148 | public bool Update() 149 | { 150 | if (plugin == null) 151 | return false; 152 | 153 | plugin.Update(); 154 | return true; 155 | } 156 | 157 | public bool HandleInput() 158 | { 159 | if (plugin == null) 160 | return false; 161 | 162 | inputPlugin?.HandleInput(); 163 | return true; 164 | } 165 | 166 | public void Dispose() 167 | { 168 | if(plugin != null) 169 | { 170 | try 171 | { 172 | plugin.Dispose(); 173 | plugin = null; 174 | inputPlugin = null; 175 | } 176 | catch (Exception e) 177 | { 178 | data.Status = PluginStatus.Error; 179 | LogFile.Error($"Failed to dispose {data} because of an error: {e}"); 180 | } 181 | } 182 | } 183 | 184 | private void ThrowError(string error) 185 | { 186 | LogFile.Error(error); 187 | data.Error(); 188 | Dispose(); 189 | } 190 | 191 | public static bool TryGet(PluginData data, out PluginInstance instance) 192 | { 193 | instance = null; 194 | if (data.Status == PluginStatus.Error || !data.TryLoadAssembly(out Assembly a)) 195 | return false; 196 | 197 | try 198 | { 199 | Type pluginType = a.GetTypes().FirstOrDefault(t => typeof(IPlugin).IsAssignableFrom(t)); 200 | 201 | if (pluginType == null) 202 | { 203 | LogFile.Error($"Failed to load {data} because it does not contain an IPlugin"); 204 | data.Error(); 205 | return false; 206 | } 207 | 208 | instance = new PluginInstance(data, a, pluginType); 209 | return true; 210 | } 211 | catch(Exception e) 212 | { 213 | StringBuilder sb = new StringBuilder(); 214 | sb.Append("Failed to load ").Append(data).Append(" because of an exception: ").Append(e).AppendLine(); 215 | if (e is ReflectionTypeLoadException typeLoadEx && typeLoadEx.LoaderExceptions != null) 216 | { 217 | sb.Append("Exception details: ").AppendLine(); 218 | foreach (Exception loaderException in typeLoadEx.LoaderExceptions) 219 | sb.Append(loaderException).AppendLine(); 220 | } 221 | LogFile.Error(sb.ToString()); 222 | data.Error(); 223 | return false; 224 | } 225 | 226 | } 227 | 228 | public override string ToString() 229 | { 230 | return data.ToString(); 231 | } 232 | 233 | } 234 | } -------------------------------------------------------------------------------- /PluginLoader/GUI/PluginDetailMenu.cs: -------------------------------------------------------------------------------- 1 | using avaness.PluginLoader.Data; 2 | using avaness.PluginLoader.Stats; 3 | using avaness.PluginLoader.Stats.Model; 4 | using Sandbox.Graphics.GUI; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Text; 8 | using VRage.Game; 9 | using VRage.Utils; 10 | using VRageMath; 11 | 12 | namespace avaness.PluginLoader.GUI 13 | { 14 | public class PluginDetailMenu : PluginScreen 15 | { 16 | private HashSet enabledPlugins; 17 | private PluginData plugin; 18 | private PluginInstance pluginInstance; 19 | private PluginStat stats; 20 | private MyGuiControlParent votingPanel; 21 | 22 | /// 23 | /// Called when a development folder plugin is removed 24 | /// 25 | public event Action OnPluginRemoved; 26 | 27 | public event Action OnRestartRequired; 28 | 29 | public PluginDetailMenu(PluginData plugin, HashSet enabledPlugins) : base(size: new Vector2(0.5f, 0.8f)) 30 | { 31 | this.plugin = plugin; 32 | this.enabledPlugins = enabledPlugins; 33 | if (Main.Instance.TryGetPluginInstance(plugin.Id, out PluginInstance instance)) 34 | pluginInstance = instance; 35 | PluginStats stats = Main.Instance.Stats ?? new PluginStats(); 36 | this.stats = stats.GetStatsForPlugin(plugin); 37 | } 38 | 39 | public override string GetFriendlyName() 40 | { 41 | return typeof(PluginDetailMenu).FullName; 42 | } 43 | 44 | public override void RecreateControls(bool constructor) 45 | { 46 | base.RecreateControls(constructor); 47 | 48 | // Top 49 | MyGuiControlLabel caption = AddCaption(plugin is ModPlugin ? "Mod Details" : "Plugin Details", captionScale: 1); 50 | AddBarBelow(caption); 51 | 52 | // Bottom 53 | Vector2 halfSize = m_size.Value / 2; 54 | MyGuiControlLabel lblSource = new MyGuiControlLabel(text: plugin.Source, position: new Vector2(GuiSpacing - halfSize.X, halfSize.Y - GuiSpacing), originAlign: MyGuiDrawAlignEnum.HORISONTAL_LEFT_AND_VERTICAL_BOTTOM); 55 | Controls.Add(lblSource); 56 | 57 | Vector2 buttonPos = new Vector2(0, halfSize.Y - (lblSource.Size.Y + GuiSpacing)); 58 | MyGuiControlButton btnInfo = new MyGuiControlButton(position: new Vector2(buttonPos.X - (GuiSpacing / 2), buttonPos.Y - GuiSpacing), text: new StringBuilder("More Info"), originAlign: MyGuiDrawAlignEnum.HORISONTAL_RIGHT_AND_VERTICAL_BOTTOM, onButtonClick: OnPluginOpenClick); 59 | Controls.Add(btnInfo); 60 | 61 | MyGuiControlButton btnSettings = new MyGuiControlButton(text: new StringBuilder("Settings"), onButtonClick: OnPluginSettingsClick); 62 | btnSettings.Enabled = pluginInstance != null && pluginInstance.HasConfigDialog; 63 | PositionToRight(btnInfo, btnSettings); 64 | Controls.Add(btnSettings); 65 | 66 | plugin.AddDetailControls(this, btnInfo, out MyGuiControlBase bottomControl); 67 | if (bottomControl == null) 68 | bottomControl = btnInfo; 69 | 70 | // Center 71 | MyLayoutTable layout = GetLayoutTableBetween(caption, bottomControl, verticalSpacing: GuiSpacing * 2); 72 | layout.SetColumnWidthsNormalized(0.5f, 0.5f); 73 | layout.SetRowHeightsNormalized(0.05f, 0.05f, 0.05f, 0.85f); 74 | 75 | layout.Add(new MyGuiControlLabel(text: plugin.FriendlyName, textScale: 0.9f), MyAlignH.Left, MyAlignV.Bottom, 0, 0); 76 | layout.Add(new MyGuiControlLabel(text: plugin.Author), MyAlignH.Left, MyAlignV.Top, 1, 0); 77 | 78 | MyGuiControlMultilineText descriptionText = new MyGuiControlMultilineText(textAlign: MyGuiDrawAlignEnum.HORISONTAL_LEFT_AND_VERTICAL_TOP, textBoxAlign: MyGuiDrawAlignEnum.HORISONTAL_LEFT_AND_VERTICAL_TOP) 79 | { 80 | VisualStyle = MyGuiControlMultilineStyleEnum.BackgroundBordered 81 | }; 82 | layout.AddWithSize(descriptionText, MyAlignH.Center, MyAlignV.Center, 3, 0, colSpan: 2); 83 | descriptionText.OnLinkClicked += (x, url) => MyGuiSandbox.OpenUrl(url, UrlOpenMode.SteamOrExternalWithConfirm); 84 | plugin.GetDescriptionText(descriptionText); 85 | 86 | MyGuiControlCheckbox enabledCheckbox = new MyGuiControlCheckbox(toolTip: "Enabled", isChecked: enabledPlugins.Contains(plugin.Id)); 87 | enabledCheckbox.IsCheckedChanged += OnEnabledChanged; 88 | layout.Add(enabledCheckbox, MyAlignH.Right, MyAlignV.Top, 0, 1); 89 | 90 | if (!plugin.IsLocal) 91 | { 92 | layout.Add(new MyGuiControlLabel(text: stats.Players + " users"), MyAlignH.Left, MyAlignV.Center, 2, 0); 93 | 94 | votingPanel = new MyGuiControlParent(); 95 | layout.AddWithSize(votingPanel, MyAlignH.Center, MyAlignV.Center, 1, 1, 2); 96 | CreateVotingPanel(votingPanel); 97 | } 98 | 99 | } 100 | 101 | private void OnEnabledChanged(MyGuiControlCheckbox checkbox) 102 | { 103 | plugin.UpdateEnabledPlugins(enabledPlugins, checkbox.IsChecked); 104 | } 105 | 106 | private void CreateVotingPanel(MyGuiControlParent parent) 107 | { 108 | bool canVote = plugin.Enabled || stats.Tried; 109 | 110 | MyLayoutHorizontal layout = new MyLayoutHorizontal(parent, 0); 111 | 112 | MyGuiControlButton btnVoteUp = new MyGuiControlButton(visualStyle: MyGuiControlButtonStyleEnum.SquareSmall) 113 | { 114 | Checked = stats.Vote > 0, 115 | }; 116 | if (canVote) 117 | btnVoteUp.ButtonClicked += OnRateUpClicked; 118 | else 119 | btnVoteUp.Enabled = false; 120 | AddImageToButton(btnVoteUp, @"Textures\GUI\Icons\Blueprints\like_test.png", 0.8f); 121 | layout.Add(btnVoteUp, MyAlignV.Bottom); 122 | 123 | MyGuiControlLabel lblVoteUp = new MyGuiControlLabel(text: stats.Upvotes.ToString()); 124 | PositionToRight(btnVoteUp, lblVoteUp, spacing: GuiSpacing / 5); 125 | AdvanceLayout(ref layout, lblVoteUp.Size.X + GuiSpacing); 126 | parent.Controls.Add(lblVoteUp); 127 | 128 | MyGuiControlButton btnVoteDown = new MyGuiControlButton(visualStyle: MyGuiControlButtonStyleEnum.SquareSmall) 129 | { 130 | Checked = stats.Vote < 0, 131 | }; 132 | if (canVote) 133 | btnVoteDown.ButtonClicked += OnRateDownClicked; 134 | else 135 | btnVoteDown.Enabled = false; 136 | AddImageToButton(btnVoteDown, @"Textures\GUI\\Icons\Blueprints\dislike_test.png", 0.8f); 137 | layout.Add(btnVoteDown, MyAlignV.Bottom); 138 | 139 | MyGuiControlLabel lblVoteDown = new MyGuiControlLabel(text: stats.Downvotes.ToString()); 140 | PositionToRight(btnVoteDown, lblVoteDown, spacing: GuiSpacing / 5); 141 | parent.Controls.Add(lblVoteDown); 142 | } 143 | 144 | private void OnRateDownClicked(MyGuiControlButton btn) 145 | { 146 | Vote(-1); 147 | } 148 | 149 | private void OnRateUpClicked(MyGuiControlButton btn) 150 | { 151 | Vote(1); 152 | } 153 | 154 | private void Vote(int vote) 155 | { 156 | if (PlayerConsent.ConsentGiven) 157 | StoreVote(vote); 158 | else 159 | PlayerConsent.ShowDialog(() => StoreVote(vote)); 160 | } 161 | 162 | private void StoreVote(int vote) 163 | { 164 | if (!PlayerConsent.ConsentGiven) 165 | return; 166 | 167 | if (stats.Vote == vote) 168 | vote = 0; 169 | 170 | PluginStat updatedStat = StatsClient.Vote(plugin.Id, vote); 171 | if (updatedStat == null) 172 | return; 173 | 174 | PluginStats allStats = Main.Instance.Stats; 175 | if (allStats != null) 176 | allStats.Stats[plugin.Id] = updatedStat; 177 | 178 | stats = updatedStat; 179 | RefreshVotingPanel(); 180 | } 181 | 182 | private void RefreshVotingPanel() 183 | { 184 | if (votingPanel == null) 185 | return; 186 | votingPanel.Controls.Clear(); 187 | CreateVotingPanel(votingPanel); 188 | } 189 | 190 | private void OnPluginSettingsClick(MyGuiControlButton btn) 191 | { 192 | if (pluginInstance != null) 193 | pluginInstance.OpenConfig(); 194 | } 195 | 196 | private void OnPluginOpenClick(MyGuiControlButton btn) 197 | { 198 | plugin.Show(); 199 | } 200 | 201 | public void InvokeOnPluginRemoved(PluginData plugin) 202 | { 203 | OnPluginRemoved?.Invoke(plugin); 204 | } 205 | 206 | public void InvokeOnRestartRequired() 207 | { 208 | OnRestartRequired?.Invoke(); 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /PluginLoader/Network/NuGetClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using avaness.PluginLoader.Compiler; 8 | using NuGet.Common; 9 | using NuGet.Configuration; 10 | using NuGet.Frameworks; 11 | using NuGet.Packaging; 12 | using NuGet.Packaging.Core; 13 | using NuGet.Packaging.Signing; 14 | using NuGet.Protocol; 15 | using NuGet.Protocol.Core.Types; 16 | using NuGet.Resolver; 17 | using VRage.FileSystem; 18 | 19 | namespace avaness.PluginLoader.Network 20 | { 21 | public class NuGetClient 22 | { 23 | const string NugetServiceIndex = "https://api.nuget.org/v3/index.json"; 24 | private static readonly NuGetFramework ProjectFramework = NuGetFramework.Parse("net48"); 25 | 26 | private static readonly ILogger logger = new NuGetLogger(); 27 | 28 | private readonly string packageFolder; 29 | private readonly SourceRepository sourceRepository; 30 | private readonly PackagePathResolver pathResolver; 31 | private readonly PackageExtractionContext extractionContext; 32 | private readonly ISettings nugetSettings; 33 | 34 | public NuGetClient() 35 | { 36 | nugetSettings = Settings.LoadDefaultSettings(root: null); 37 | extractionContext = new PackageExtractionContext(PackageSaveMode.Defaultv3, XmlDocFileSaveMode.Skip, ClientPolicyContext.GetClientPolicy(nugetSettings, logger), logger); 38 | sourceRepository = Repository.Factory.GetCoreV3(NugetServiceIndex); 39 | 40 | packageFolder = Path.GetFullPath(Path.Combine(MyFileSystem.ExePath, "NuGet", "packages")); 41 | Directory.CreateDirectory(packageFolder); 42 | pathResolver = new PackagePathResolver(packageFolder); 43 | } 44 | 45 | public NuGetPackage[] DownloadFromConfig(Stream packagesConfig) 46 | { 47 | return Task.Run(() => DownloadFromConfigAsync(packagesConfig)).GetAwaiter().GetResult(); 48 | } 49 | 50 | public async Task DownloadFromConfigAsync(Stream packagesConfig) 51 | { 52 | logger.LogInformation("Downloading packages from packages.config"); 53 | 54 | PackagesConfigReader reader = new PackagesConfigReader(packagesConfig, true); 55 | List packages = new List(); 56 | using (SourceCacheContext cacheContext = new SourceCacheContext()) 57 | { 58 | foreach (PackageReference package in reader.GetPackages(false)) 59 | { 60 | NuGetPackage installedPackage = await DownloadPackage(cacheContext, package.PackageIdentity, package.TargetFramework); 61 | if (installedPackage != null) 62 | packages.Add(installedPackage); 63 | } 64 | } 65 | 66 | return packages.ToArray(); 67 | } 68 | 69 | public NuGetPackage[] DownloadPackages(IEnumerable packageIds, bool getDependencies = true) 70 | { 71 | return Task.Run(() => DownloadPackagesAsync(packageIds, getDependencies)).GetAwaiter().GetResult(); 72 | } 73 | 74 | public async Task DownloadPackagesAsync(IEnumerable packageIds, bool getDependencies = true) 75 | { 76 | List packages = new List(); 77 | foreach (NuGetPackageId id in packageIds) 78 | { 79 | if (id.TryGetIdentity(out PackageIdentity nugetId)) 80 | packages.Add(nugetId); 81 | } 82 | 83 | if (packages.Count == 0) 84 | return Array.Empty(); 85 | 86 | logger.LogInformation($"Downloading {packages.Count} packages with dependencies"); 87 | 88 | List result = new List(); 89 | using (SourceCacheContext cacheContext = new SourceCacheContext()) 90 | { 91 | IEnumerable downloadPackages = packages.Where(x => !CheckAlreadyInstalled(x.Id)); 92 | if (getDependencies) 93 | downloadPackages = await ResolveDependencies(downloadPackages, cacheContext); 94 | 95 | foreach (PackageIdentity id in downloadPackages) 96 | { 97 | NuGetPackage installedPackage = await DownloadPackage(cacheContext, id); 98 | if (installedPackage != null) 99 | result.Add(installedPackage); 100 | } 101 | } 102 | 103 | return result.ToArray(); 104 | } 105 | 106 | private async Task> ResolveDependencies(IEnumerable packages, SourceCacheContext context) 107 | { 108 | PackageResolverContext resolverContext = new PackageResolverContext( 109 | dependencyBehavior: DependencyBehavior.Lowest, 110 | targetIds: packages.Select(x => x.Id), 111 | requiredPackageIds: Enumerable.Empty(), 112 | packagesConfig: Enumerable.Empty(), 113 | preferredVersions: Enumerable.Empty(), 114 | availablePackages: await GetDependencies(packages, context), 115 | packageSources: new[] { sourceRepository.PackageSource }, 116 | log: logger); 117 | 118 | return new PackageResolver().Resolve(resolverContext, CancellationToken.None); 119 | } 120 | 121 | private async Task> GetDependencies( 122 | IEnumerable packages, 123 | SourceCacheContext context) 124 | { 125 | Dictionary result = new Dictionary(); 126 | 127 | DependencyInfoResource dependencyInfoResource = await sourceRepository.GetResourceAsync(); 128 | if (dependencyInfoResource == null) 129 | return result.Values; 130 | 131 | Stack stack = new Stack(packages); 132 | while (stack.Count > 0) 133 | { 134 | PackageIdentity package = stack.Pop(); 135 | 136 | if (!result.ContainsKey(package)) 137 | { 138 | SourcePackageDependencyInfo dependencyInfo = await dependencyInfoResource.ResolvePackage(package, ProjectFramework, context, logger, CancellationToken.None); 139 | result.Add(package, dependencyInfo); 140 | if (dependencyInfo == null) 141 | continue; 142 | foreach (PackageDependency dependency in dependencyInfo.Dependencies) 143 | stack.Push(new PackageIdentity(dependency.Id, dependency.VersionRange.MinVersion)); 144 | } 145 | } 146 | 147 | return result.Values.Where(x => x != null); 148 | } 149 | 150 | public async Task DownloadPackage(SourceCacheContext cacheContext, PackageIdentity package, NuGetFramework framework = null) 151 | { 152 | if (CheckAlreadyInstalled(package.Id)) 153 | return null; 154 | 155 | if (framework == null || framework.IsAny || framework.IsAgnostic || framework.IsUnsupported) 156 | framework = ProjectFramework; 157 | 158 | string installedPath = pathResolver.GetInstalledPath(package); 159 | if (installedPath == null) 160 | { 161 | DownloadResource downloadResource = await sourceRepository.GetResourceAsync(CancellationToken.None); 162 | 163 | DownloadResourceResult downloadResult = await downloadResource.GetDownloadResourceResultAsync( 164 | package, 165 | new PackageDownloadContext(cacheContext), 166 | SettingsUtility.GetGlobalPackagesFolder(nugetSettings), 167 | logger, CancellationToken.None); 168 | 169 | await PackageExtractor.ExtractPackageAsync( 170 | downloadResult.PackageSource, 171 | downloadResult.PackageStream, 172 | pathResolver, 173 | extractionContext, 174 | CancellationToken.None); 175 | 176 | installedPath = pathResolver.GetInstalledPath(package); 177 | if (installedPath == null) 178 | return null; 179 | 180 | logger.LogInformation($"Package downloaded: {package.Id}"); 181 | } 182 | else 183 | { 184 | logger.LogInformation($"Package located in cache: {package.Id}"); 185 | } 186 | 187 | return new NuGetPackage(installedPath, framework); 188 | } 189 | 190 | private bool CheckAlreadyInstalled(string id) 191 | { 192 | if (id.Equals("Lib.Harmony", StringComparison.InvariantCultureIgnoreCase)) 193 | { 194 | logger.LogInformation("Package " + id + " not downloaded because it is in Bin64"); 195 | return true; 196 | } 197 | return false; 198 | } 199 | 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /PluginLoader/Data/PluginData.cs: -------------------------------------------------------------------------------- 1 | using ProtoBuf; 2 | using avaness.PluginLoader.GUI; 3 | using Sandbox.Graphics.GUI; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Diagnostics; 7 | using System.IO; 8 | using System.Reflection; 9 | using System.Text.RegularExpressions; 10 | using System.Windows.Forms; 11 | using System.Xml.Serialization; 12 | using VRage.Utils; 13 | using avaness.PluginLoader.Config; 14 | 15 | namespace avaness.PluginLoader.Data 16 | { 17 | [XmlInclude(typeof(GitHubPlugin))] 18 | [XmlInclude(typeof(ModPlugin))] 19 | [ProtoContract] 20 | [ProtoInclude(100, typeof(ObsoletePlugin))] 21 | [ProtoInclude(103, typeof(GitHubPlugin))] 22 | [ProtoInclude(104, typeof(ModPlugin))] 23 | public abstract class PluginData : IEquatable 24 | { 25 | public abstract string Source { get; } 26 | public abstract bool IsLocal { get; } 27 | public abstract bool IsCompiled { get; } 28 | 29 | [XmlIgnore] 30 | public Version Version { get; protected set; } 31 | 32 | [XmlIgnore] 33 | public virtual PluginStatus Status { get; set; } = PluginStatus.None; 34 | public virtual string StatusString 35 | { 36 | get 37 | { 38 | switch (Status) 39 | { 40 | case PluginStatus.PendingUpdate: 41 | return "Pending Update"; 42 | case PluginStatus.Updated: 43 | return "Updated"; 44 | case PluginStatus.Error: 45 | return "Error!"; 46 | case PluginStatus.Blocked: 47 | return "Not whitelisted!"; 48 | default: 49 | return ""; 50 | } 51 | } 52 | } 53 | 54 | [ProtoMember(1)] 55 | public virtual string Id { get; set; } 56 | 57 | [ProtoMember(2)] 58 | public string FriendlyName { get; set; } = "Unknown"; 59 | 60 | [ProtoMember(3)] 61 | public bool Hidden { get; set; } = false; 62 | 63 | [ProtoMember(4)] 64 | public string GroupId { get; set; } 65 | 66 | [ProtoMember(5)] 67 | public string Tooltip { get; set; } 68 | 69 | [ProtoMember(6)] 70 | public string Author { get; set; } 71 | 72 | [ProtoMember(7)] 73 | public string Description { get; set; } 74 | 75 | [XmlIgnore] 76 | public List Group { get; } = new List(); 77 | 78 | [XmlIgnore] 79 | public bool Enabled => Main.Instance.Config.IsEnabled(Id); 80 | 81 | 82 | protected PluginData() 83 | { 84 | } 85 | 86 | /// 87 | /// Loads the user settings into the plugin. Returns true if the config was modified. 88 | /// 89 | public virtual bool LoadData(ref PluginDataConfig config, bool enabled) 90 | { 91 | return false; 92 | } 93 | 94 | public abstract Assembly GetAssembly(); 95 | 96 | public virtual bool TryLoadAssembly(out Assembly a) 97 | { 98 | if (Status == PluginStatus.Error) 99 | { 100 | a = null; 101 | return false; 102 | } 103 | 104 | try 105 | { 106 | // Get the file path 107 | a = GetAssembly(); 108 | if (Status == PluginStatus.Blocked) 109 | return false; 110 | 111 | if (a == null) 112 | { 113 | LogFile.Error("Failed to load " + ToString()); 114 | Error(); 115 | return false; 116 | } 117 | 118 | // Precompile the entire assembly in order to force any missing method exceptions 119 | //LogFile.WriteLine("Precompiling " + a); 120 | //LoaderTools.Precompile(a); 121 | return true; 122 | } 123 | catch (Exception e) 124 | { 125 | string name = ToString(); 126 | LogFile.Error($"Failed to load {name} because of an error: " + e); 127 | if (e is MemberAccessException) 128 | { 129 | LogFile.Error($"Is {name} up to date?"); 130 | InvalidateCache(); 131 | } 132 | 133 | if (e is NotSupportedException && e.Message.Contains("loadFromRemoteSources")) 134 | Error($"The plugin {name} was blocked by windows. Please unblock the file in the dll file properties."); 135 | else 136 | Error(); 137 | a = null; 138 | return false; 139 | } 140 | } 141 | 142 | 143 | public override bool Equals(object obj) 144 | { 145 | return Equals(obj as PluginData); 146 | } 147 | 148 | public bool Equals(PluginData other) 149 | { 150 | return other != null && 151 | Id == other.Id; 152 | } 153 | 154 | public override int GetHashCode() 155 | { 156 | return 2108858624 + EqualityComparer.Default.GetHashCode(Id); 157 | } 158 | 159 | public static bool operator ==(PluginData left, PluginData right) 160 | { 161 | return EqualityComparer.Default.Equals(left, right); 162 | } 163 | 164 | public static bool operator !=(PluginData left, PluginData right) 165 | { 166 | return !(left == right); 167 | } 168 | 169 | public override string ToString() 170 | { 171 | return Id + '|' + FriendlyName; 172 | } 173 | 174 | public void Error(string msg = null) 175 | { 176 | Status = PluginStatus.Error; 177 | if (Main.Instance.DebugCompileAll) 178 | return; 179 | if (msg == null) 180 | 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."; 181 | string file = MyLog.Default.GetFilePath(); 182 | if(File.Exists(file) && file.EndsWith(".log")) 183 | { 184 | MyLog.Default.Flush(); 185 | msg += "\n\nWould you like to open the game log?"; 186 | DialogResult result = LoaderTools.ShowMessageBox(msg, MessageBoxButtons.YesNo, MessageBoxIcon.Error); 187 | if (result == DialogResult.Yes) 188 | Process.Start(file); 189 | } 190 | else 191 | { 192 | LoaderTools.ShowMessageBox(msg, MessageBoxButtons.OK, MessageBoxIcon.Error); 193 | } 194 | } 195 | 196 | public abstract void Show(); 197 | 198 | public virtual void GetDescriptionText(MyGuiControlMultilineText textbox) 199 | { 200 | textbox.Visible = true; 201 | textbox.Clear(); 202 | if (string.IsNullOrEmpty(Description)) 203 | { 204 | if (string.IsNullOrEmpty(Tooltip)) 205 | textbox.AppendText("No description"); 206 | else 207 | textbox.AppendText(CapLength(Tooltip, 1000)); 208 | return; 209 | } 210 | else 211 | { 212 | string text = CapLength(Description, 1000); 213 | int textStart = 0; 214 | foreach (Match m in Regex.Matches(text, @"https?:\/\/(www\.)?[\w-.]{2,256}\.[a-z]{2,4}\b[\w-.@:%\+~#?&//=]*")) 215 | { 216 | int textLen = m.Index - textStart; 217 | if (textLen > 0) 218 | textbox.AppendText(text.Substring(textStart, textLen)); 219 | 220 | textbox.AppendLink(m.Value, m.Value); 221 | textStart = m.Index + m.Length; 222 | } 223 | 224 | if (textStart < text.Length) 225 | textbox.AppendText(text.Substring(textStart)); 226 | } 227 | } 228 | 229 | private string CapLength(string s, int len) 230 | { 231 | if (s.Length > len) 232 | return s.Substring(0, len); 233 | return s; 234 | } 235 | 236 | public virtual bool UpdateEnabledPlugins(HashSet enabledPlugins, bool enable) 237 | { 238 | bool changed; 239 | 240 | if (enable) 241 | { 242 | changed = enabledPlugins.Add(Id); 243 | 244 | foreach (PluginData other in Group) 245 | { 246 | if (!ReferenceEquals(other, this) && other.UpdateEnabledPlugins(enabledPlugins, false)) 247 | changed = true; 248 | } 249 | } 250 | else 251 | { 252 | changed = enabledPlugins.Remove(Id); 253 | } 254 | 255 | return changed; 256 | } 257 | 258 | /// 259 | /// Invalidate any compiled assemblies on the disk 260 | /// 261 | public virtual void InvalidateCache() 262 | { 263 | 264 | } 265 | 266 | public virtual void AddDetailControls(PluginDetailMenu screen, MyGuiControlBase bottomControl, out MyGuiControlBase topControl) 267 | { 268 | topControl = null; 269 | } 270 | 271 | public virtual string GetAssetPath() 272 | { 273 | return null; 274 | } 275 | } 276 | } -------------------------------------------------------------------------------- /PluginLoader/PluginLoader.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {A7C22A74-56EA-4DC2-89AA-A1134BFB8497} 8 | Library 9 | Properties 10 | avaness.PluginLoader 11 | PluginLoader 12 | v4.8 13 | 512 14 | true 15 | latest 16 | 17 | 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | x64 26 | 27 | 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | x64 35 | 36 | 37 | 38 | ..\Bin64\Microsoft.CodeAnalysis.dll 39 | False 40 | 41 | 42 | ..\Bin64\Microsoft.CodeAnalysis.CSharp.dll 43 | False 44 | 45 | 46 | False 47 | 48 | 49 | 50 | ..\Bin64\NLog.dll 51 | False 52 | 53 | 54 | ..\Bin64\ProtoBuf.Net.dll 55 | False 56 | 57 | 58 | False 59 | ..\Bin64\ProtoBuf.Net.Core.dll 60 | False 61 | 62 | 63 | False 64 | ..\Bin64\Sandbox.Game.dll 65 | False 66 | 67 | 68 | False 69 | ..\Bin64\Sandbox.Graphics.dll 70 | False 71 | 72 | 73 | False 74 | ..\Bin64\SpaceEngineers.Game.dll 75 | False 76 | 77 | 78 | ..\Bin64\Steamworks.NET.dll 79 | False 80 | 81 | 82 | 83 | False 84 | ..\Bin64\System.Collections.Immutable.dll 85 | False 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | False 101 | ..\Bin64\VRage.dll 102 | False 103 | 104 | 105 | False 106 | ..\Bin64\VRage.Game.dll 107 | False 108 | 109 | 110 | False 111 | ..\Bin64\VRage.Input.dll 112 | False 113 | 114 | 115 | False 116 | ..\Bin64\VRage.Library.dll 117 | False 118 | 119 | 120 | False 121 | ..\Bin64\VRage.Math.dll 122 | False 123 | 124 | 125 | ..\Bin64\VRage.Scripting.dll 126 | False 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | Form 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 2.3.3 197 | 198 | 199 | 4.7.0 200 | 201 | 202 | 13.0.3 203 | 204 | 205 | 6.7.1 206 | 207 | 208 | 209 | 210 | call "$(ProjectDir)\deploy.bat" "$(TargetPath) " 211 | 212 | 213 | -------------------------------------------------------------------------------- /PluginLoader/GUI/PluginScreen.cs: -------------------------------------------------------------------------------- 1 | using Sandbox; 2 | using Sandbox.Graphics.GUI; 3 | using System; 4 | using VRage.Utils; 5 | using VRageMath; 6 | 7 | namespace avaness.PluginLoader.GUI 8 | { 9 | public abstract class PluginScreen : MyGuiScreenBase 10 | { 11 | public const float GuiSpacing = 0.0175f; 12 | 13 | public PluginScreen(Vector2? position = null, Vector2? size = null) : 14 | base(position ?? new Vector2(0.5f), MyGuiConstants.SCREEN_BACKGROUND_COLOR, size ?? new Vector2(0.5f), 15 | backgroundTransition: MySandboxGame.Config.UIBkOpacity, guiTransition: MySandboxGame.Config.UIOpacity) 16 | { 17 | EnabledBackgroundFade = true; 18 | m_closeOnEsc = true; 19 | m_drawEvenWithoutFocus = true; 20 | CanHideOthers = true; 21 | CanBeHidden = true; 22 | CloseButtonEnabled = true; 23 | } 24 | 25 | public override void LoadContent() 26 | { 27 | base.LoadContent(); 28 | RecreateControls(true); 29 | } 30 | 31 | protected RectangleF GetAreaBetween(MyGuiControlBase top, MyGuiControlBase bottom, float verticalSpacing = GuiSpacing, float horizontalSpacing = GuiSpacing) 32 | { 33 | Vector2 halfSize = m_size.Value / 2; 34 | 35 | float topPosY = GetCoordTopLeftFromAligned(top).Y; 36 | Vector2 topPos = new Vector2(horizontalSpacing - halfSize.X, topPosY + top.Size.Y + verticalSpacing); 37 | 38 | float bottomPosY = GetCoordTopLeftFromAligned(bottom).Y; 39 | Vector2 bottomPos = new Vector2(halfSize.X - horizontalSpacing, bottomPosY - verticalSpacing); 40 | 41 | Vector2 size = bottomPos - topPos; 42 | size.X = Math.Abs(size.X); 43 | size.Y = Math.Abs(size.Y); 44 | 45 | return new RectangleF(topPos, size); 46 | } 47 | 48 | protected RectangleF GetAreaBelow(MyGuiControlBase top, float verticalSpacing = GuiSpacing, float horizontalSpacing = GuiSpacing) 49 | { 50 | Vector2 halfSize = m_size.Value / 2; 51 | 52 | float topPosY = GetCoordTopLeftFromAligned(top).Y; 53 | Vector2 topPos = new Vector2(horizontalSpacing - halfSize.X, topPosY + top.Size.Y + verticalSpacing); 54 | 55 | Vector2 bottomPos = new Vector2(halfSize.X - horizontalSpacing, halfSize.Y); 56 | 57 | Vector2 size = bottomPos - topPos; 58 | size.X = Math.Abs(size.X); 59 | size.Y = Math.Abs(size.Y); 60 | 61 | return new RectangleF(topPos, size); 62 | } 63 | 64 | protected MyLayoutTable GetLayoutTableBetween(MyGuiControlBase top, MyGuiControlBase bottom, float verticalSpacing = GuiSpacing, float horizontalSpacing = GuiSpacing) 65 | { 66 | RectangleF rect = GetAreaBetween(top, bottom, verticalSpacing, horizontalSpacing); 67 | return new MyLayoutTable(this, rect.Position, rect.Size); 68 | } 69 | 70 | protected void AddBarBelow(MyGuiControlBase control, float barWidth = 0.8f, float spacing = GuiSpacing) 71 | { 72 | MyGuiControlSeparatorList bar = new MyGuiControlSeparatorList(); 73 | barWidth *= m_size.Value.X; 74 | float controlTop = GetCoordTopLeftFromAligned(control).Y; 75 | bar.AddHorizontal(new Vector2(barWidth * -0.5f, controlTop + spacing + control.Size.Y), barWidth); 76 | Controls.Add(bar); 77 | } 78 | 79 | protected void AddBarAbove(MyGuiControlBase control, float barWidth = 0.8f, float spacing = GuiSpacing) 80 | { 81 | MyGuiControlSeparatorList bar = new MyGuiControlSeparatorList(); 82 | barWidth *= m_size.Value.X; 83 | float controlTop = GetCoordTopLeftFromAligned(control).Y; 84 | bar.AddHorizontal(new Vector2(barWidth * -0.5f, controlTop - spacing), barWidth); 85 | Controls.Add(bar); 86 | } 87 | 88 | protected void AdvanceLayout(ref MyLayoutVertical layout, float amount = GuiSpacing) 89 | { 90 | layout.Advance(amount * MyGuiConstants.GUI_OPTIMAL_SIZE.Y); 91 | } 92 | 93 | protected void AdvanceLayout(ref MyLayoutHorizontal layout, float amount = GuiSpacing) 94 | { 95 | layout.Advance(amount * MyGuiConstants.GUI_OPTIMAL_SIZE.Y); 96 | } 97 | 98 | protected Vector2 GetCoordTopLeftFromAligned(MyGuiControlBase control) 99 | { 100 | return MyUtils.GetCoordTopLeftFromAligned(control.Position, control.Size, control.OriginAlign); 101 | } 102 | 103 | /// 104 | /// Positions to the right of with a spacing of . 105 | /// 106 | public void PositionToRight(MyGuiControlBase currentControl, MyGuiControlBase newControl, MyAlignV align = MyAlignV.Center, float spacing = GuiSpacing) 107 | { 108 | Vector2 currentTopLeft = GetCoordTopLeftFromAligned(currentControl); 109 | currentTopLeft.X += currentControl.Size.X + spacing; 110 | switch (align) 111 | { 112 | case MyAlignV.Top: 113 | newControl.OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_LEFT_AND_VERTICAL_TOP; 114 | break; 115 | case MyAlignV.Center: 116 | currentTopLeft.Y += currentControl.Size.Y / 2; 117 | newControl.OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_LEFT_AND_VERTICAL_CENTER; 118 | break; 119 | case MyAlignV.Bottom: 120 | currentTopLeft.Y += currentControl.Size.Y; 121 | newControl.OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_LEFT_AND_VERTICAL_BOTTOM; 122 | break; 123 | default: 124 | return; 125 | } 126 | newControl.Position = currentTopLeft; 127 | } 128 | 129 | /// 130 | /// Positions to the left of with a spacing of . 131 | /// 132 | public void PositionToLeft(MyGuiControlBase currentControl, MyGuiControlBase newControl, MyAlignV align = MyAlignV.Center, float spacing = GuiSpacing) 133 | { 134 | Vector2 currentTopLeft = GetCoordTopLeftFromAligned(currentControl); 135 | currentTopLeft.X -= spacing; 136 | switch (align) 137 | { 138 | case MyAlignV.Top: 139 | newControl.OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_RIGHT_AND_VERTICAL_TOP; 140 | break; 141 | case MyAlignV.Center: 142 | currentTopLeft.Y += currentControl.Size.Y / 2; 143 | newControl.OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_RIGHT_AND_VERTICAL_CENTER; 144 | break; 145 | case MyAlignV.Bottom: 146 | currentTopLeft.Y += currentControl.Size.Y; 147 | newControl.OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_RIGHT_AND_VERTICAL_BOTTOM; 148 | break; 149 | default: 150 | return; 151 | } 152 | newControl.Position = currentTopLeft; 153 | } 154 | 155 | /// 156 | /// Positions above with a spacing of . 157 | /// 158 | public void PositionAbove(MyGuiControlBase currentControl, MyGuiControlBase newControl, MyAlignH align = MyAlignH.Center, float spacing = GuiSpacing) 159 | { 160 | Vector2 currentTopLeft = GetCoordTopLeftFromAligned(currentControl); 161 | currentTopLeft.Y -= spacing; 162 | switch (align) 163 | { 164 | case MyAlignH.Left: 165 | newControl.OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_LEFT_AND_VERTICAL_BOTTOM; 166 | break; 167 | case MyAlignH.Center: 168 | currentTopLeft.X += currentControl.Size.X / 2; 169 | newControl.OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_BOTTOM; 170 | break; 171 | case MyAlignH.Right: 172 | currentTopLeft.X += currentControl.Size.X; 173 | newControl.OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_RIGHT_AND_VERTICAL_BOTTOM; 174 | break; 175 | default: 176 | return; 177 | } 178 | newControl.Position = currentTopLeft; 179 | } 180 | 181 | /// 182 | /// Positions below with a spacing of . 183 | /// 184 | public void PositionBelow(MyGuiControlBase currentControl, MyGuiControlBase newControl, MyAlignH align = MyAlignH.Center, float spacing = GuiSpacing) 185 | { 186 | Vector2 currentTopLeft = GetCoordTopLeftFromAligned(currentControl); 187 | currentTopLeft.Y += currentControl.Size.Y + spacing; 188 | switch (align) 189 | { 190 | case MyAlignH.Left: 191 | newControl.OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_LEFT_AND_VERTICAL_TOP; 192 | break; 193 | case MyAlignH.Center: 194 | currentTopLeft.X += currentControl.Size.X / 2; 195 | newControl.OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_TOP; 196 | break; 197 | case MyAlignH.Right: 198 | currentTopLeft.X += currentControl.Size.X; 199 | newControl.OriginAlign = MyGuiDrawAlignEnum.HORISONTAL_RIGHT_AND_VERTICAL_TOP; 200 | break; 201 | default: 202 | return; 203 | } 204 | newControl.Position = currentTopLeft; 205 | } 206 | 207 | protected void AddImageToButton(MyGuiControlButton button, string iconTexture, float iconSize = 1) 208 | { 209 | MyGuiControlImage icon = new MyGuiControlImage(size: button.Size * iconSize, textures: new[] { iconTexture }); 210 | icon.Enabled = button.Enabled; 211 | icon.HasHighlight = button.HasHighlight; 212 | button.Elements.Add(icon); 213 | } 214 | 215 | protected void SetTableHeight(MyGuiControlTable table, float height) 216 | { 217 | float numRows = height / table.RowHeight; 218 | table.VisibleRowsCount = Math.Max((int)numRows - 1, 1); 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /PluginLoader/Config/PluginConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Xml.Serialization; 5 | using System.Linq; 6 | using System.Text; 7 | using VRage.Game; 8 | using avaness.PluginLoader.Data; 9 | 10 | namespace avaness.PluginLoader.Config 11 | { 12 | public class PluginConfig 13 | { 14 | private const string fileName = "config.xml"; 15 | 16 | private string filePath; 17 | private PluginList list; 18 | 19 | [XmlArray] 20 | [XmlArrayItem("Id")] 21 | public string[] Plugins 22 | { 23 | get { return enabledPlugins.Keys.ToArray(); } 24 | set 25 | { 26 | enabledPlugins.Clear(); 27 | foreach (string id in value) 28 | enabledPlugins[id] = null; 29 | } 30 | } 31 | public IEnumerable EnabledPlugins => enabledPlugins.Values; 32 | private readonly Dictionary enabledPlugins = new Dictionary(); 33 | 34 | [XmlArray] 35 | [XmlArrayItem("Id")] 36 | public string[] LocalFolderPlugins 37 | { 38 | get { return pluginFolders.ToArray(); } 39 | set 40 | { 41 | pluginFolders.Clear(); 42 | foreach (string folder in value) 43 | pluginFolders.Add(folder); 44 | } 45 | } 46 | private readonly HashSet pluginFolders = new HashSet(); 47 | 48 | [XmlArray] 49 | [XmlArrayItem("Profile")] 50 | public Profile[] Profiles 51 | { 52 | get { return ProfileMap.Values.ToArray(); } 53 | set 54 | { 55 | ProfileMap.Clear(); 56 | foreach (var profile in value.Where(x => x?.Key != null)) 57 | ProfileMap[profile.Key] = profile; 58 | } 59 | } 60 | 61 | [XmlIgnore] 62 | public readonly Dictionary ProfileMap = new(); 63 | 64 | [XmlArray] 65 | [XmlArrayItem("Config")] 66 | public PluginDataConfig[] PluginSettings 67 | { 68 | get { return pluginSettings.Values.ToArray(); } 69 | set 70 | { 71 | pluginSettings.Clear(); 72 | foreach (PluginDataConfig config in value.Where(x => x?.Id != null)) 73 | pluginSettings[config.Id] = config; 74 | } 75 | } 76 | private readonly Dictionary pluginSettings = new Dictionary(); 77 | 78 | public string ListHash { get; set; } 79 | 80 | public int GameVersion { get; set; } 81 | [XmlIgnore] 82 | public bool GameVersionChanged { get; private set; } 83 | 84 | // Base URL for the statistics server, change to http://localhost:5000 in config.xml for local development 85 | // ReSharper disable once UnassignedGetOnlyAutoProperty 86 | public string StatsServerBaseUrl { get; } 87 | 88 | // User consent to use the StatsServer 89 | public bool DataHandlingConsent { get; set; } 90 | public string DataHandlingConsentDate { get; set; } 91 | 92 | private int networkTimeout = 5000; 93 | public int NetworkTimeout 94 | { 95 | get 96 | { 97 | return networkTimeout; 98 | } 99 | set 100 | { 101 | if (value < 100) 102 | networkTimeout = 100; 103 | else if (value > 60000) 104 | networkTimeout = 60000; 105 | else 106 | networkTimeout = value; 107 | } 108 | } 109 | 110 | public int Count => enabledPlugins.Count; 111 | 112 | public bool AllowIPv6 { get; set; } = true; 113 | 114 | public PluginConfig() 115 | { 116 | } 117 | 118 | public void Init(PluginList plugins, bool debugCompileAll) 119 | { 120 | list = plugins; 121 | 122 | bool save = false; 123 | StringBuilder sb = new StringBuilder("Enabled plugins: "); 124 | 125 | foreach(PluginData plugin in plugins) 126 | { 127 | string id = plugin.Id; 128 | bool enabled = IsEnabled(id); 129 | 130 | if (enabled || (debugCompileAll && !plugin.IsLocal && plugin.IsCompiled)) 131 | { 132 | sb.Append(id).Append(", "); 133 | enabledPlugins[id] = plugin; 134 | } 135 | 136 | if (LoadPluginData(plugin)) 137 | save = true; 138 | } 139 | 140 | if (enabledPlugins.Count > 0) 141 | sb.Length -= 2; 142 | else 143 | sb.Append("None"); 144 | LogFile.WriteLine(sb.ToString()); 145 | 146 | foreach (KeyValuePair kv in enabledPlugins.Where(x => x.Value == null).ToArray()) 147 | { 148 | LogFile.Warn($"{kv.Key} was in the config but is no longer available"); 149 | enabledPlugins.Remove(kv.Key); 150 | save = true; 151 | } 152 | 153 | foreach (string id in pluginSettings.Keys.Where(x => !plugins.Contains(x)).ToArray()) 154 | { 155 | LogFile.Warn($"{id} had settings in the config but is no longer available"); 156 | pluginSettings.Remove(id); 157 | save = true; 158 | } 159 | 160 | if (save) 161 | Save(); 162 | } 163 | 164 | public void CheckGameVersion() 165 | { 166 | int currentGameVersion = MyFinalBuildConstants.APP_VERSION?.Version ?? 0; 167 | int storedGameVersion = GameVersion; 168 | if (currentGameVersion != 0) 169 | { 170 | if (storedGameVersion == 0) 171 | { 172 | GameVersion = currentGameVersion; 173 | Save(); 174 | } 175 | else if (storedGameVersion != currentGameVersion) 176 | { 177 | GameVersion = currentGameVersion; 178 | GameVersionChanged = true; 179 | Save(); 180 | } 181 | } 182 | } 183 | 184 | public void Disable() 185 | { 186 | enabledPlugins.Clear(); 187 | } 188 | 189 | 190 | public void Save() 191 | { 192 | try 193 | { 194 | LogFile.WriteLine("Saving config"); 195 | XmlSerializer serializer = new XmlSerializer(typeof(PluginConfig)); 196 | if (File.Exists(filePath)) 197 | File.Delete(filePath); 198 | FileStream fs = File.OpenWrite(filePath); 199 | serializer.Serialize(fs, this); 200 | fs.Flush(); 201 | fs.Close(); 202 | } 203 | catch (Exception e) 204 | { 205 | LogFile.Error($"An error occurred while saving plugin config: " + e); 206 | } 207 | } 208 | 209 | public static PluginConfig Load(string mainDirectory) 210 | { 211 | string path = Path.Combine(mainDirectory, fileName); 212 | if (File.Exists(path)) 213 | { 214 | try 215 | { 216 | XmlSerializer serializer = new XmlSerializer(typeof(PluginConfig)); 217 | PluginConfig config; 218 | using (FileStream fs = File.OpenRead(path)) 219 | config = (PluginConfig)serializer.Deserialize(fs); 220 | config.filePath = path; 221 | return config; 222 | } 223 | catch (Exception e) 224 | { 225 | LogFile.Error($"An error occurred while loading plugin config: " + e); 226 | } 227 | } 228 | 229 | return new PluginConfig 230 | { 231 | filePath = path 232 | }; 233 | } 234 | 235 | public bool IsEnabled(string id) 236 | { 237 | return enabledPlugins.ContainsKey(id); 238 | } 239 | 240 | public void SetEnabled(string id, bool enabled) 241 | { 242 | SetEnabled(list[id], enabled); 243 | } 244 | 245 | public void SetEnabled(PluginData plugin, bool enabled) 246 | { 247 | string id = plugin.Id; 248 | if (IsEnabled(id) == enabled) 249 | return; 250 | 251 | if (enabled) 252 | Enable(plugin); 253 | else 254 | Disable(id); 255 | 256 | LoadPluginData(plugin); // Must be called because the enabled state has changed 257 | } 258 | 259 | private void Enable(PluginData plugin) 260 | { 261 | string id = plugin.Id; 262 | enabledPlugins[id] = plugin; 263 | list.SubscribeToItem(id); 264 | } 265 | 266 | private void Disable(string id) 267 | { 268 | enabledPlugins.Remove(id); 269 | } 270 | 271 | /// 272 | /// Loads the stored user data into the plugin. Returns true if the config was modified. 273 | /// 274 | public bool LoadPluginData(PluginData plugin) 275 | { 276 | PluginDataConfig settings; 277 | if (!pluginSettings.TryGetValue(plugin.Id, out settings)) 278 | settings = null; 279 | if (plugin.LoadData(ref settings, IsEnabled(plugin.Id))) 280 | { 281 | if (settings == null) 282 | pluginSettings.Remove(plugin.Id); 283 | else 284 | pluginSettings[plugin.Id] = settings; 285 | return true; 286 | } 287 | return false; 288 | } 289 | 290 | /// 291 | /// Removes the stored user data for the plugin. Returns true if the config was modified. 292 | /// 293 | public bool RemovePluginData(string id) 294 | { 295 | return pluginSettings.Remove(id); 296 | } 297 | 298 | public void SavePluginData(GitHubPluginConfig settings) 299 | { 300 | pluginSettings[settings.Id] = settings; 301 | } 302 | 303 | public void AddDevelopmentFolder(string folder) 304 | { 305 | pluginFolders.Add(folder); 306 | } 307 | 308 | public void RemoveDevelopmentFolder(string folder) 309 | { 310 | pluginFolders.Remove(folder); 311 | } 312 | } 313 | } -------------------------------------------------------------------------------- /PluginLoader/Main.cs: -------------------------------------------------------------------------------- 1 | using VRage.Plugins; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using System; 5 | using System.IO; 6 | using HarmonyLib; 7 | using System.Windows.Forms; 8 | using Sandbox.Game.World; 9 | using System.Diagnostics; 10 | using System.Linq; 11 | using avaness.PluginLoader.Compiler; 12 | using avaness.PluginLoader.GUI; 13 | using avaness.PluginLoader.Data; 14 | using avaness.PluginLoader.Stats; 15 | using avaness.PluginLoader.Network; 16 | using System.Runtime.ExceptionServices; 17 | using avaness.PluginLoader.Stats.Model; 18 | using avaness.PluginLoader.Config; 19 | using VRage.Utils; 20 | using System.Text; 21 | using VRage; 22 | 23 | namespace avaness.PluginLoader 24 | { 25 | public class Main : IHandleInputPlugin 26 | { 27 | const string HarmonyVersion = "2.3.3.0"; 28 | 29 | public static Main Instance; 30 | 31 | public PluginList List { get; } 32 | public PluginConfig Config { get; } 33 | public SplashScreen Splash { get; } 34 | public PluginStats Stats {get; private set; } 35 | 36 | /// 37 | /// True if a local plugin was loaded 38 | /// 39 | public bool HasLocal { get; private set; } 40 | 41 | public bool DebugCompileAll { get; } 42 | 43 | private bool init; 44 | private readonly StringBuilder debugCompileResults = new StringBuilder(); 45 | private readonly List plugins = new List(); 46 | public List Plugins => plugins; 47 | 48 | public Main() 49 | { 50 | Stopwatch sw = Stopwatch.StartNew(); 51 | 52 | Splash = new SplashScreen(); 53 | 54 | Instance = this; 55 | 56 | Cursor temp = Cursor.Current; 57 | Cursor.Current = Cursors.AppStarting; 58 | 59 | string pluginsDir = LoaderTools.PluginsDir; 60 | Directory.CreateDirectory(pluginsDir); 61 | 62 | LogFile.Init(pluginsDir); 63 | LogFile.WriteLine("Starting - v" + Assembly.GetExecutingAssembly().GetName().Version.ToString(3)); 64 | 65 | DebugCompileAll = Environment.GetCommandLineArgs()?.Any(x => x != null && x.Equals("-testcompileall", StringComparison.InvariantCultureIgnoreCase)) == true; 66 | if (DebugCompileAll) 67 | LogFile.WriteLine("COMPILING ALL PLUGINS"); 68 | 69 | GitHub.Init(); 70 | 71 | Splash.SetText("Finding references..."); 72 | RoslynReferences.GenerateAssemblyList(); 73 | 74 | AppDomain.CurrentDomain.FirstChanceException += OnException; 75 | 76 | Splash.SetText("Starting..."); 77 | Config = PluginConfig.Load(pluginsDir); 78 | Config.CheckGameVersion(); 79 | List = new PluginList(pluginsDir, Config); 80 | 81 | Splash.SetText("Starting..."); 82 | Config.Init(List, DebugCompileAll); 83 | 84 | if (Config.GameVersionChanged) 85 | ClearGitHubCache(pluginsDir); 86 | 87 | StatsClient.OverrideBaseUrl(Config.StatsServerBaseUrl); 88 | UpdatePlayerStats(); 89 | PlayerConsent.OnConsentChanged += OnConsentChanged; 90 | 91 | Splash.SetText("Patching..."); 92 | LogFile.WriteLine("Patching"); 93 | 94 | // Check harmony version 95 | Version expectedHarmony = new Version(HarmonyVersion); 96 | Version actualHarmony = typeof(Harmony).Assembly.GetName().Version; 97 | if (expectedHarmony != actualHarmony) 98 | LogFile.Warn($"Unexpected Harmony version, plugins may be unstable. Expected {expectedHarmony} but found {actualHarmony}"); 99 | 100 | new Harmony("avaness.PluginLoader").PatchAll(Assembly.GetExecutingAssembly()); 101 | 102 | Splash.SetText("Instantiating plugins..."); 103 | LogFile.WriteLine("Instantiating plugins"); 104 | 105 | if(DebugCompileAll) 106 | debugCompileResults.Append("Plugins that failed to compile:").AppendLine(); 107 | foreach (PluginData data in Config.EnabledPlugins) 108 | { 109 | if (PluginInstance.TryGet(data, out PluginInstance p)) 110 | { 111 | plugins.Add(p); 112 | if (data.IsLocal) 113 | HasLocal = true; 114 | } 115 | else if(DebugCompileAll && data.IsCompiled) 116 | { 117 | debugCompileResults.Append(data.FriendlyName ?? "(null)").Append(" - ").Append(data.Id ?? "(null)").Append(" by ").Append(data.Author ?? "(null)").AppendLine(); 118 | } 119 | } 120 | 121 | sw.Stop(); 122 | 123 | // FIXME: It can potentially run in the background speeding up the game's startup 124 | ReportEnabledPlugins(); 125 | 126 | LogFile.WriteLine($"Finished startup. Took {sw.ElapsedMilliseconds}ms"); 127 | 128 | Cursor.Current = temp; 129 | 130 | Splash.Delete(); 131 | Splash = null; 132 | 133 | } 134 | 135 | private void OnException(object sender, FirstChanceExceptionEventArgs e) 136 | { 137 | try 138 | { 139 | MemberAccessException accessException = e.Exception as MemberAccessException; 140 | if (accessException == null) 141 | accessException = e.Exception?.InnerException as MemberAccessException; 142 | if (accessException != null) 143 | { 144 | foreach (PluginInstance plugin in plugins) 145 | { 146 | if (plugin.ContainsExceptionSite(accessException)) 147 | return; 148 | } 149 | } 150 | } 151 | catch { } // Do NOT throw exceptions inside this method! 152 | } 153 | 154 | public void UpdatePlayerStats() 155 | { 156 | ParallelTasks.Parallel.Start(() => 157 | { 158 | Stats = StatsClient.DownloadStats(); 159 | }); 160 | } 161 | 162 | private void ClearGitHubCache(string pluginsDir) 163 | { 164 | string pluginCache = Path.Combine(pluginsDir, "GitHub"); 165 | if (!Directory.Exists(pluginCache)) 166 | return; 167 | 168 | bool hasGitHub = Config.EnabledPlugins.Any(x => x is GitHubPlugin); 169 | 170 | if(hasGitHub) 171 | { 172 | LoaderTools.ShowMessageBox("Space Engineers has been updated, so all plugins that are currently enabled must be downloaded and compiled."); 173 | } 174 | 175 | try 176 | { 177 | LogFile.WriteLine("Deleting plugin cache because of an update"); 178 | Directory.Delete(pluginCache, true); 179 | } 180 | catch (Exception e) 181 | { 182 | LogFile.Error("Failed to delete plugin cache: " + e); 183 | } 184 | } 185 | 186 | public bool TryGetPluginInstance(string id, out PluginInstance instance) 187 | { 188 | instance = null; 189 | if (!init) 190 | return false; 191 | 192 | foreach (PluginInstance p in plugins) 193 | { 194 | if (p.Id == id) 195 | { 196 | instance = p; 197 | return true; 198 | } 199 | } 200 | 201 | return false; 202 | } 203 | 204 | private void ReportEnabledPlugins() 205 | { 206 | if (!PlayerConsent.ConsentGiven) 207 | return; 208 | 209 | Splash.SetText("Reporting plugin usage..."); 210 | LogFile.WriteLine("Reporting plugin usage"); 211 | 212 | // Config has already been validated at this point so all enabled plugins will have list items 213 | // FIXME: Move into a background thread 214 | if (StatsClient.Track(TrackablePluginIds)) 215 | LogFile.WriteLine("List of enabled plugins has been sent to the statistics server"); 216 | else 217 | LogFile.Error("Failed to send the list of enabled plugins to the statistics server"); 218 | } 219 | 220 | // Skip local plugins, keep only enabled ones 221 | public string[] TrackablePluginIds => Config.EnabledPlugins.Where(x => !x.IsLocal).Select(x => x.Id).ToArray(); 222 | 223 | public void RegisterComponents() 224 | { 225 | LogFile.WriteLine($"Registering {plugins.Count} components"); 226 | foreach (PluginInstance plugin in plugins) 227 | plugin.RegisterSession(MySession.Static); 228 | } 229 | 230 | public void DisablePlugins() 231 | { 232 | Config.Disable(); 233 | plugins.Clear(); 234 | LogFile.WriteLine("Disabled all plugins"); 235 | } 236 | 237 | public void InstantiatePlugins() 238 | { 239 | LogFile.WriteLine($"Loading {plugins.Count} plugins"); 240 | for (int i = plugins.Count - 1; i >= 0; i--) 241 | { 242 | PluginInstance p = plugins[i]; 243 | if (!p.Instantiate()) 244 | plugins.RemoveAtFast(i); 245 | } 246 | } 247 | 248 | public void Init(object gameInstance) 249 | { 250 | LogFile.WriteLine($"Initializing {plugins.Count} plugins"); 251 | if (DebugCompileAll) 252 | debugCompileResults.Append("Plugins that failed to Init:").AppendLine(); 253 | for (int i = plugins.Count - 1; i >= 0; i--) 254 | { 255 | PluginInstance p = plugins[i]; 256 | if (!p.Init(gameInstance)) 257 | { 258 | plugins.RemoveAtFast(i); 259 | if(DebugCompileAll) 260 | debugCompileResults.Append(p.FriendlyName ?? "(null)").Append(" - ").Append(p.Id ?? "(null)").Append(" by ").Append(p.Author ?? "(null)").AppendLine(); 261 | } 262 | } 263 | init = true; 264 | 265 | if(DebugCompileAll) 266 | { 267 | MessageBox.Show("All plugins compiled, log file will now open"); 268 | 269 | LogFile.WriteLine(debugCompileResults.ToString()); 270 | 271 | string file = MyLog.Default.GetFilePath(); 272 | if (File.Exists(file) && file.EndsWith(".log")) 273 | Process.Start(file); 274 | } 275 | } 276 | 277 | public void Update() 278 | { 279 | if (init) 280 | { 281 | for (int i = plugins.Count - 1; i >= 0; i--) 282 | { 283 | PluginInstance p = plugins[i]; 284 | if (!p.Update()) 285 | plugins.RemoveAtFast(i); 286 | } 287 | } 288 | } 289 | 290 | public void HandleInput() 291 | { 292 | if (init) 293 | { 294 | for (int i = plugins.Count - 1; i >= 0; i--) 295 | { 296 | PluginInstance p = plugins[i]; 297 | if (!p.HandleInput()) 298 | plugins.RemoveAtFast(i); 299 | } 300 | } 301 | } 302 | 303 | public void Dispose() 304 | { 305 | foreach (PluginInstance p in plugins) 306 | p.Dispose(); 307 | plugins.Clear(); 308 | 309 | PlayerConsent.OnConsentChanged -= OnConsentChanged; 310 | LogFile.Dispose(); 311 | Instance = null; 312 | } 313 | 314 | private void OnConsentChanged() 315 | { 316 | UpdatePlayerStats(); 317 | } 318 | } 319 | } -------------------------------------------------------------------------------- /PluginLoader/PluginList.cs: -------------------------------------------------------------------------------- 1 | using avaness.PluginLoader.Data; 2 | using System; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Xml.Serialization; 7 | using ProtoBuf; 8 | using System.Linq; 9 | using avaness.PluginLoader.Network; 10 | using System.IO.Compression; 11 | using avaness.PluginLoader.Config; 12 | 13 | namespace avaness.PluginLoader 14 | { 15 | public class PluginList : IEnumerable 16 | { 17 | private Dictionary plugins = new Dictionary(); 18 | 19 | public int Count => plugins.Count; 20 | 21 | public bool HasError { get; private set; } 22 | 23 | public PluginData this[string key] 24 | { 25 | get => plugins[key]; 26 | set => plugins[key] = value; 27 | } 28 | 29 | public bool Contains(string id) => plugins.ContainsKey(id); 30 | public bool TryGetPlugin(string id, out PluginData pluginData) => plugins.TryGetValue(id, out pluginData); 31 | 32 | public PluginList(string mainDirectory, PluginConfig config) 33 | { 34 | var lbl = Main.Instance.Splash; 35 | 36 | lbl.SetText("Downloading plugin list..."); 37 | DownloadList(mainDirectory, config); 38 | 39 | if(plugins.Count == 0) 40 | { 41 | LogFile.Warn("No plugins in the plugin list. Plugin list will contain local plugins only."); 42 | HasError = true; 43 | } 44 | 45 | UpdateWorkshopItems(config); 46 | 47 | FindLocalPlugins(config, mainDirectory); 48 | LogFile.WriteLine($"Found {plugins.Count} plugins"); 49 | FindPluginGroups(); 50 | FindModDependencies(); 51 | } 52 | 53 | /// 54 | /// Ensures the user is subscribed to the steam plugin. 55 | /// 56 | public void SubscribeToItem(string id) 57 | { 58 | if(plugins.TryGetValue(id, out PluginData data) && data is ISteamItem steam) 59 | SteamAPI.SubscribeToItem(steam.WorkshopId); 60 | } 61 | 62 | public bool Remove(string id) 63 | { 64 | return plugins.Remove(id); 65 | } 66 | 67 | public void Add(PluginData data) 68 | { 69 | plugins[data.Id] = data; 70 | } 71 | 72 | private void FindPluginGroups() 73 | { 74 | int groups = 0; 75 | foreach (var group in plugins.Values.Where(x => !string.IsNullOrWhiteSpace(x.GroupId)).GroupBy(x => x.GroupId)) 76 | { 77 | groups++; 78 | foreach (PluginData data in group) 79 | data.Group.AddRange(group.Where(x => x != data)); 80 | } 81 | if (groups > 0) 82 | LogFile.WriteLine($"Found {groups} plugin groups"); 83 | } 84 | 85 | private void FindModDependencies() 86 | { 87 | foreach(PluginData data in plugins.Values) 88 | { 89 | if (data is ModPlugin mod) 90 | FindModDependencies(mod); 91 | } 92 | } 93 | 94 | private void FindModDependencies(ModPlugin mod) 95 | { 96 | if (mod.DependencyIds == null) 97 | return; 98 | 99 | Dictionary dependencies = new Dictionary(); 100 | dependencies.Add(mod.WorkshopId, mod); 101 | Stack toProcess = new Stack(); 102 | toProcess.Push(mod); 103 | 104 | while (toProcess.Count > 0) 105 | { 106 | ModPlugin temp = toProcess.Pop(); 107 | 108 | if (temp.DependencyIds == null) 109 | continue; 110 | 111 | foreach (ulong id in temp.DependencyIds) 112 | { 113 | if (!dependencies.ContainsKey(id) && plugins.TryGetValue(id.ToString(), out PluginData data) && data is ModPlugin dependency) 114 | { 115 | toProcess.Push(dependency); 116 | dependencies[id] = dependency; 117 | } 118 | } 119 | } 120 | 121 | dependencies.Remove(mod.WorkshopId); 122 | mod.Dependencies = dependencies.Values.ToArray(); 123 | } 124 | 125 | private void DownloadList(string mainDirectory, PluginConfig config) 126 | { 127 | string whitelist = Path.Combine(mainDirectory, "whitelist.bin"); 128 | 129 | PluginData[] list; 130 | string currentHash = config.ListHash; 131 | string newHash; 132 | if (!TryDownloadWhitelistHash(out newHash)) 133 | { 134 | // No connection to plugin hub, read from cache 135 | if (!TryReadWhitelistFile(whitelist, out list)) 136 | return; 137 | } 138 | else if(currentHash == null || currentHash != newHash) 139 | { 140 | // Plugin list changed, try downloading new version first 141 | if (!TryDownloadWhitelistFile(whitelist, newHash, config, out list) 142 | && !TryReadWhitelistFile(whitelist, out list)) 143 | return; 144 | } 145 | else 146 | { 147 | // Plugin list did not change, try reading the current version first 148 | if (!TryReadWhitelistFile(whitelist, out list) 149 | && !TryDownloadWhitelistFile(whitelist, newHash, config, out list)) 150 | return; 151 | } 152 | 153 | if(list != null) 154 | plugins = list.ToDictionary(x => x.Id); 155 | } 156 | 157 | private bool TryReadWhitelistFile(string file, out PluginData[] list) 158 | { 159 | list = null; 160 | 161 | if (File.Exists(file) && new FileInfo(file).Length > 0) 162 | { 163 | LogFile.WriteLine("Reading whitelist from cache"); 164 | try 165 | { 166 | PluginData[] rawData; 167 | using (Stream binFile = File.OpenRead(file)) 168 | { 169 | rawData = Serializer.Deserialize(binFile); 170 | } 171 | 172 | int obsolete = 0; 173 | List tempList = new List(rawData.Length); 174 | foreach (PluginData data in rawData) 175 | { 176 | if (data is ObsoletePlugin) 177 | obsolete++; 178 | else 179 | tempList.Add(data); 180 | } 181 | LogFile.WriteLine("Whitelist retrieved from disk"); 182 | list = tempList.ToArray(); 183 | if (obsolete > 0) 184 | LogFile.Warn(obsolete + " obsolete plugins found in the whitelist file."); 185 | return true; 186 | } 187 | catch (Exception e) 188 | { 189 | LogFile.Error("Error while reading whitelist: " + e); 190 | } 191 | } 192 | else 193 | { 194 | LogFile.WriteLine("No whitelist cache exists"); 195 | } 196 | 197 | return false; 198 | } 199 | 200 | private bool TryDownloadWhitelistFile(string file, string hash, PluginConfig config, out PluginData[] list) 201 | { 202 | list = null; 203 | Dictionary newPlugins = new Dictionary(); 204 | 205 | try 206 | { 207 | using (Stream zipFileStream = GitHub.DownloadRepo(GitHub.listRepoName, GitHub.listRepoCommit)) 208 | using (ZipArchive zipFile = new ZipArchive(zipFileStream)) 209 | { 210 | XmlSerializer xml = new XmlSerializer(typeof(PluginData)); 211 | foreach (ZipArchiveEntry entry in zipFile.Entries) 212 | { 213 | if (!entry.FullName.EndsWith("xml", StringComparison.OrdinalIgnoreCase)) 214 | continue; 215 | 216 | using (Stream entryStream = entry.Open()) 217 | using (StreamReader entryReader = new StreamReader(entryStream)) 218 | { 219 | try 220 | { 221 | PluginData data = (PluginData)xml.Deserialize(entryReader); 222 | newPlugins[data.Id] = data; 223 | } 224 | catch (InvalidOperationException e) 225 | { 226 | LogFile.Error("An error occurred while reading " + entry.FullName + ": " + (e.InnerException ?? e)); 227 | } 228 | } 229 | } 230 | } 231 | 232 | list = newPlugins.Values.ToArray(); 233 | return TrySaveWhitelist(file, list, hash, config); 234 | } 235 | catch (Exception e) 236 | { 237 | LogFile.Error("Error while downloading whitelist: " + e); 238 | } 239 | 240 | return false; 241 | } 242 | 243 | private bool TrySaveWhitelist(string file, PluginData[] list, string hash, PluginConfig config) 244 | { 245 | try 246 | { 247 | LogFile.WriteLine("Saving whitelist to disk"); 248 | using (MemoryStream mem = new MemoryStream()) 249 | { 250 | Serializer.Serialize(mem, list); 251 | using (Stream binFile = File.Create(file)) 252 | { 253 | mem.WriteTo(binFile); 254 | } 255 | } 256 | 257 | config.ListHash = hash; 258 | config.Save(); 259 | 260 | LogFile.WriteLine("Whitelist updated"); 261 | return true; 262 | } 263 | catch (Exception e) 264 | { 265 | LogFile.Error("Error while saving whitelist: " + e); 266 | try 267 | { 268 | File.Delete(file); 269 | } 270 | catch { } 271 | return false; 272 | } 273 | } 274 | 275 | private bool TryDownloadWhitelistHash(out string hash) 276 | { 277 | hash = null; 278 | try 279 | { 280 | using (Stream hashStream = GitHub.DownloadFile(GitHub.listRepoName, GitHub.listRepoCommit, GitHub.listRepoHash)) 281 | using (StreamReader hashStreamReader = new StreamReader(hashStream)) 282 | { 283 | hash = hashStreamReader.ReadToEnd().Trim(); 284 | } 285 | return true; 286 | } 287 | catch (Exception e) 288 | { 289 | LogFile.Error("Error while downloading whitelist hash: " + e); 290 | return false; 291 | } 292 | } 293 | 294 | private void FindLocalPlugins(PluginConfig config, string mainDirectory) 295 | { 296 | foreach (string dll in Directory.EnumerateFiles(mainDirectory, "*.dll", SearchOption.AllDirectories)) 297 | { 298 | if(!dll.Contains(Path.DirectorySeparatorChar + "GitHub" + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)) 299 | { 300 | LocalPlugin local = new LocalPlugin(dll); 301 | string name = local.FriendlyName; 302 | if (!name.StartsWith("0Harmony") && !name.StartsWith("Microsoft")) 303 | plugins[local.Id] = local; 304 | } 305 | } 306 | 307 | foreach (string folder in config.LocalFolderPlugins) 308 | { 309 | if (Directory.Exists(folder)) 310 | { 311 | LocalFolderPlugin local = new LocalFolderPlugin(folder); 312 | plugins[local.Id] = local; 313 | } 314 | } 315 | } 316 | 317 | private void UpdateWorkshopItems(PluginConfig config) 318 | { 319 | List steamPlugins = new List(plugins.Values.Select(x => x as ISteamItem).Where(x => x != null)); 320 | 321 | Main.Instance.Splash.SetText($"Updating workshop items..."); 322 | 323 | SteamAPI.Update(steamPlugins.Where(x => config.IsEnabled(x.Id)).Select(x => x.WorkshopId)); 324 | } 325 | 326 | 327 | public IEnumerator GetEnumerator() 328 | { 329 | return plugins.Values.GetEnumerator(); 330 | } 331 | 332 | IEnumerator IEnumerable.GetEnumerator() 333 | { 334 | return plugins.Values.GetEnumerator(); 335 | } 336 | } 337 | } --------------------------------------------------------------------------------