From b07a3affbaa99460d6ffb95c7c789670ea8300a0 Mon Sep 17 00:00:00 2001 From: "George L. Albany" Date: Sat, 30 Apr 2022 20:47:06 -0400 Subject: [PATCH 1/2] Added Plugin List (#137) Added GUI.PluginListScreen for drawing the plugin list Adds Plugins button to Main Menu Draws plugins based on GUI.PluginInfo Needed to add SemanticVersion 2.0.0.0 to PathfinderAPI project GUI.PluginInfo reads from BepInPlugin, Meta.PluginInfoAttribute, Meta.PluginWebsiteAttribute, BepInDependency, and BepInIncompatibility for plugin information Displays load order, is enabled, and allows displaying of plugin image (either set in PluginInfoAttribute.ImagePath or .) Does not draw undefined/null elements Added Event.Menu.DrawMainMenuButtonEvent for detecting and manipulating main menu button draw events. Added Meta.PluginInfoAttribute for extra plugin metadata Added Meta.PluginWebsiteAttribute for plugin website listing (is automatically sorted by key in PluginInfo) Added PluginInfo and PluginWebsite attributes to PathfinderAPIPlugin --- .../Event/Menu/DrawMainMenuButtonEvent.cs | 213 ++++++++++++++ PathfinderAPI/GUI/PluginInfo.cs | 263 ++++++++++++++++++ PathfinderAPI/GUI/PluginListScreen.cs | 86 ++++++ PathfinderAPI/Meta/PluginInfoAttribute.cs | 16 ++ PathfinderAPI/Meta/PluginWebsiteAttribute.cs | 14 + PathfinderAPI/PathfinderAPI.csproj | 4 + PathfinderAPI/PathfinderAPIPlugin.cs | 9 + 7 files changed, 605 insertions(+) create mode 100644 PathfinderAPI/Event/Menu/DrawMainMenuButtonEvent.cs create mode 100644 PathfinderAPI/GUI/PluginInfo.cs create mode 100644 PathfinderAPI/GUI/PluginListScreen.cs create mode 100644 PathfinderAPI/Meta/PluginInfoAttribute.cs create mode 100644 PathfinderAPI/Meta/PluginWebsiteAttribute.cs diff --git a/PathfinderAPI/Event/Menu/DrawMainMenuButtonEvent.cs b/PathfinderAPI/Event/Menu/DrawMainMenuButtonEvent.cs new file mode 100644 index 00000000..345268a3 --- /dev/null +++ b/PathfinderAPI/Event/Menu/DrawMainMenuButtonEvent.cs @@ -0,0 +1,213 @@ +using Hacknet; +using Hacknet.Gui; +using HarmonyLib; +using Microsoft.Xna.Framework; +using Mono.Cecil.Cil; +using MonoMod.Cil; + +namespace Pathfinder.Event.Menu; + +public enum MainMenuButtonType : int +{ + Undefined = -1, + Unknown = 0, + NewSession = 1, + Continue = 1102, + Login = 11, + Settings = 3, + StartRelayServer = 4, + Extensions = 5, + NewLabyrinthSession = 7, + Exit = 15, +} + +[HarmonyPatch] +public class DrawMainMenuButtonEvent : MainMenuEvent +{ + delegate bool ButtonDrawDelegate(int id, int x, int y, int width, int height, string text, Color color, MainMenu self); + + public ButtonData Data { get; } + + public DrawMainMenuButtonEvent(MainMenu mainMenu, ButtonData data) : base(mainMenu) + { + Data = data; + } + + [HarmonyILManipulator] + [HarmonyPatch(typeof(MainMenu), nameof(MainMenu.drawMainMenuButtons))] + private static void AfterMainMenuDraw(ILContext il) + { + ILCursor c = new ILCursor(il); + + c.GotoNext(MoveType.After, + x => x.MatchLdcI4(450), + x => x.MatchLdcI4(50), + x => x.MatchLdstr("New Session"), + x => x.MatchCall(typeof(LocaleTerms), nameof(LocaleTerms.Loc)), + x => x.MatchLdsfld(typeof(MainMenu), nameof(MainMenu.buttonColor)) + ); + c.Remove(); + c.Remove(); + c.Emit(OpCodes.Ldarg_0); + c.Emit(OpCodes.Ldloca_S, (byte)0); + c.Emit(OpCodes.Ldloca_S, (byte)4); + c.EmitDelegate(ButtonDrawExecution); + + // Continue Session Button + c.GotoNext(MoveType.After, + x => x.MatchLdfld(typeof(MainMenu), "canLoad"), + x => x.MatchBrtrue(out var _), + x => x.MatchCall(typeof(Color), "get_Black"), + x => x.MatchBr(out var _), + x => x.MatchLdsfld(typeof(MainMenu), nameof(MainMenu.buttonColor)), + x => x.MatchNop() + ); + c.Remove(); + c.Remove(); + c.Emit(OpCodes.Ldarg_0); + c.Emit(OpCodes.Ldloca_S, (byte)0); + c.Emit(OpCodes.Ldloca_S, (byte)4); + c.EmitDelegate(ButtonDrawExecution); + + c.GotoNext(MoveType.After, + x => x.MatchLdstr("Login"), + x => x.MatchCall(typeof(LocaleTerms), nameof(LocaleTerms.Loc)), + x => x.MatchLdloc(1), + x => x.MatchBrtrue(out var _), + x => x.MatchCall(typeof(Color), "get_Black"), + x => x.MatchBr(out var _), + x => x.MatchLdsfld(typeof(MainMenu), nameof(MainMenu.buttonColor)), + x => x.MatchNop() + ); + c.Remove(); + c.Remove(); + c.Emit(OpCodes.Ldarg_0); + c.Emit(OpCodes.Ldloca_S, (byte)0); + c.Emit(OpCodes.Ldloca_S, (byte)4); + c.EmitDelegate(ButtonDrawExecution); + + c.GotoNext(MoveType.After, + x => x.MatchLdstr("Settings"), + x => x.MatchCall(typeof(LocaleTerms), nameof(LocaleTerms.Loc)), + x => x.MatchLdsfld(typeof(MainMenu), nameof(MainMenu.buttonColor)) + ); + c.Remove(); + c.Remove(); + c.Emit(OpCodes.Ldarg_0); + c.Emit(OpCodes.Ldloca_S, (byte)0); + c.Emit(OpCodes.Ldloca_S, (byte)4); + c.EmitDelegate(ButtonDrawExecution); + + c.GotoNext(MoveType.After, + x => x.MatchLdstr("Start Relay Server"), + x => x.MatchLdsfld(typeof(MainMenu), nameof(MainMenu.buttonColor)) + ); + c.Remove(); + c.Remove(); + c.Emit(OpCodes.Ldarg_0); + c.Emit(OpCodes.Ldloca_S, (byte)0); + c.Emit(OpCodes.Ldloca_S, (byte)4); + c.EmitDelegate(ButtonDrawExecution); + + c.GotoNext(MoveType.After, + x => x.MatchLdstr("Extensions"), + x => x.MatchLdsfld(typeof(MainMenu), nameof(MainMenu.buttonColor)) + ); + c.Remove(); + c.Remove(); + c.Emit(OpCodes.Ldarg_0); + c.Emit(OpCodes.Ldloca_S, (byte)0); + c.Emit(OpCodes.Ldloca_S, (byte)4); + c.EmitDelegate(ButtonDrawExecution); + + // New Labyrinth Session Button + c.GotoNext(MoveType.After, + x => x.MatchCall(typeof(Utils), nameof(Utils.rand)), + x => x.MatchSub(), + x => x.MatchCall(typeof(Color), nameof(Color.Lerp)) + ); + c.Remove(); + c.Remove(); + c.Emit(OpCodes.Ldarg_0); + c.Emit(OpCodes.Ldloca_S, (byte)0); + c.Emit(OpCodes.Ldloca_S, (byte)4); + c.EmitDelegate(ButtonDrawExecution); + + c.GotoNext(MoveType.After, + x => x.MatchLdstr("Exit"), + x => x.MatchCall(typeof(LocaleTerms), nameof(LocaleTerms.Loc)), + x => x.MatchLdsfld(typeof(MainMenu), nameof(MainMenu.exitButtonColor)) + ); + c.Remove(); + c.Remove(); + c.Emit(OpCodes.Ldarg_0); + c.Emit(OpCodes.Ldloca_S, (byte)0); + c.Emit(OpCodes.Ldloca_S, (byte)4); + c.EmitDelegate(ButtonDrawExecution); + } + + // I have no idea why it was implemented with two Y position indexers and fixing it would kind of be pointless, just gonna keep them equal + // If I don't do this at least two buttons could overlap + public static bool ButtonDrawExecution(int id, int x, int y, int width, int height, string text, Color color, MainMenu self, ref int yPosIndexOne, ref int yPostIndexTwo) + { + var loadButtonData = new ButtonData(id, x, y, width, height, text, color, Math.Max(yPosIndexOne, yPostIndexTwo)); + var drawMainMenuButton = new DrawMainMenuButtonEvent(self, loadButtonData); + EventManager.InvokeAll(drawMainMenuButton); + yPosIndexOne = yPostIndexTwo = loadButtonData.YPositionIndex; + + if(!drawMainMenuButton.Cancelled) + return Button.doButton( + loadButtonData.Id, + loadButtonData.X, + loadButtonData.Y, + loadButtonData.Width, + loadButtonData.Height, + loadButtonData.Text, + loadButtonData.Color + ); + return false; + } + + public class ButtonData + { + public const int DefaultButtonTopPadding = 15; + + public int Id; + public int X; + public int Y; + public int Width; + public int Height; + public string Text; + public Color Color; + public int YPositionIndex; + + private MainMenuButtonType _buttonType = MainMenuButtonType.Undefined; + public MainMenuButtonType ButtonType + { + get + { + if(_buttonType == MainMenuButtonType.Undefined) + { + if(Enum.IsDefined(typeof(MainMenuButtonType), Id)) + _buttonType = (MainMenuButtonType)Id; + else _buttonType = MainMenuButtonType.Unknown; + } + return _buttonType; + } + } + + public ButtonData(int id, int x, int y, int width, int height, string text, Color color, int yPosIndex) + { + Id = id; + X = x; + Y = y; + Width = width; + Height = height; + Text = text; + Color = color; + YPositionIndex = yPosIndex; + } + + public bool Is(MainMenuButtonType button) => ButtonType == button; + } +} diff --git a/PathfinderAPI/GUI/PluginInfo.cs b/PathfinderAPI/GUI/PluginInfo.cs new file mode 100644 index 00000000..85560d7d --- /dev/null +++ b/PathfinderAPI/GUI/PluginInfo.cs @@ -0,0 +1,263 @@ +using System.Collections.ObjectModel; +using System.Diagnostics; +using BepInEx; +using BepInEx.Hacknet; +using Hacknet; +using Hacknet.Gui; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Pathfinder.Meta; + +namespace Pathfinder.GUI; + +public class PluginInfo +{ + private static List _loadOrderCache; + private static ReadOnlyCollection _readonlyLoadOrderCache; + protected static ReadOnlyCollection LoadOrderCache => _readonlyLoadOrderCache ??= new ReadOnlyCollection(_loadOrderCache); + + public readonly HacknetPlugin Plugin; + public readonly string Name; + public readonly string Guid; + public readonly SemanticVersioning.Version Version; + public readonly bool Enabled; + public readonly int LoadOrder; + public readonly string ConfigPath; + private bool _configPathExists; + public bool ConfigPathExists { get => _configPathExists; protected set => _configPathExists = value; } + public readonly string Description; + public readonly ReadOnlyCollection SoftDependencies; + public readonly ReadOnlyCollection HardDependencies; + public readonly ReadOnlyCollection Incompatibles; + public readonly ReadOnlyCollection Authors; + public readonly string TeamName; + private ReadOnlyDictionary _readonlyWebsites; + public ReadOnlyDictionary Websites => _readonlyWebsites ??= new ReadOnlyDictionary(_websites); + private readonly SortedDictionary _websites = new SortedDictionary(); + private readonly Texture2D ImageTexture; + + public PluginInfo(PluginListScreen screen, HacknetPlugin plugin) + { + Plugin = plugin; + var metadata = MetadataHelper.GetMetadata(Plugin); + if(metadata == null) + throw new InvalidOperationException($"Plugin {Plugin.GetType().FullName} lacks a BepInPlugin attribute."); + Name = metadata.Name; + Guid = metadata.GUID; + Version = metadata.Version; + ConfigPath = Plugin.Config.ConfigFilePath; + ConfigPathExists = File.Exists(ConfigPath); + var pluginDataAttrib = MetadataHelper.GetAttributes(Plugin).FirstOrDefault(); + if(pluginDataAttrib != null) + { + Authors = new ReadOnlyCollection(pluginDataAttrib.Authors); + TeamName = pluginDataAttrib.TeamName; + if(pluginDataAttrib.ImageName != null) + ImageTexture = LoadTexture(screen.ScreenManager.GraphicsDevice, Path.Combine(Paths.PluginPath, pluginDataAttrib.ImageName)); + } + if(ImageTexture == null) + { + ImageTexture = LoadTexture(screen.ScreenManager.GraphicsDevice, Path.Combine(Paths.PluginPath, Guid)); + } + foreach(var website in MetadataHelper.GetAttributes(Plugin)) + _websites[website.WebsiteName] = website.WebsiteUrl; + var softDeps = new List(); + var hardDeps = new List(); + foreach(var deps in MetadataHelper.GetDependencies(Plugin.GetType())) + { + if(deps.Flags == BepInDependency.DependencyFlags.SoftDependency) + softDeps.Add($"{deps.DependencyGUID}{(deps.VersionRange?.ToString()?.Length > 0 ? $" ({deps.VersionRange})" : "")}"); + else + hardDeps.Add($"{deps.DependencyGUID}{(deps.VersionRange?.ToString()?.Length > 0 ? $" ({deps.VersionRange})" : "")}"); + } + SoftDependencies = new ReadOnlyCollection(softDeps); + HardDependencies = new ReadOnlyCollection(hardDeps); + var incompats = new List(); + foreach(var incomp in MetadataHelper.GetAttributes(Plugin)) + incompats.Add(incomp.IncompatibilityGUID); + Incompatibles = new ReadOnlyCollection(incompats); + + if(_loadOrderCache == null) + { + _loadOrderCache = new List(); + foreach(var pluginPair in HacknetChainloader.Instance.Plugins) + _loadOrderCache.Add(pluginPair.Key); + } + + LoadOrder = _loadOrderCache.IndexOf(Guid); + if(LoadOrder != -1) + { + LoadOrder++; + Enabled = true; + } + + Button = new PFButton(0, 0, 0, 0, $"{(Enabled ? $"{LoadOrder}. " : "")}{Name} ({Guid} v{Version})") + { + }; + + ConfigPathButton = new PFButton(0,0,0,0,""); + } + + public virtual int Width { get; } = 300; + public virtual int Height { get; } = 100; + + protected PFButton Button; + protected PFButton ConfigPathButton; + + public virtual bool DrawListElement(GameTime time, PluginListScreen screen, Point offset, bool isSelected) + { + var result = isSelected; + Button.X = offset.X; + Button.Y = offset.Y; + Button.Width = Width; + Button.Height = Height; + + if(isSelected) + Button.Color = GuiData.Default_Trans_Grey_Strong; + else + Button.Color = null; + + if(Button.Do()) + result = true; + + if(isSelected) + DrawData(time, screen, offset); + + return result; + } + + private int _panelId = -1; + + public virtual void DrawData(GameTime time, PluginListScreen screen, Point offset) + { + var viewWidth = screen.ScreenManager.GraphicsDevice.Viewport.Width; + var viewHeight = screen.ScreenManager.GraphicsDevice.Viewport.Height; + var xPos = offset.X + Width + 10; + var yPos = 10; + var rect = new Rectangle(xPos, yPos, viewWidth - xPos - 10, viewHeight - (yPos*2)); + ScrollablePanel.beginPanel( + _panelId > -1 + ? _panelId + : _panelId = PFButton.GetNextID(), + rect, + Vector2.Zero + ); + + RenderedRectangle.doRectangle(0, 0, rect.Width, rect.Height, new Color(0, 0, 0, 175)); + RenderedRectangle.doRectangleOutline(0, 0, rect.Width, rect.Height, 2, Color.Gray); + + var labelYPos = 10; + if(ImageTexture != null) + { + DrawTexture(new Vector2(10, labelYPos), ImageTexture); + labelYPos += ImageTexture.Height + 10; + } + const int yPosAdd = 40; + TextItem.doLabel(new Vector2(10, labelYPos), $"Name: {Name}", null); + labelYPos += yPosAdd; + TextItem.doLabel(new Vector2(10, labelYPos), $"Guid: {Guid}", null); + labelYPos += yPosAdd; + TextItem.doLabel(new Vector2(10, labelYPos), $"Version: {Version}", null); + labelYPos += yPosAdd; + TextItem.doLabel(new Vector2(10, labelYPos), $"Enabled: {Enabled}", null); + labelYPos += yPosAdd; + if(Enabled) + { + TextItem.doLabel(new Vector2(10, labelYPos), $"Load Order: {LoadOrder}", null); + labelYPos += yPosAdd; + } + const string configPathText = "Config Path: "; + TextItem.doLabel(new Vector2(10, labelYPos), configPathText, null); + var configTextSize = GuiData.font.MeasureString(configPathText); + ConfigPathButton.X = (int)configTextSize.X + 10; + ConfigPathButton.Y = labelYPos; + ConfigPathButton.Width = 900; + ConfigPathButton.Height = (int)configTextSize.Y; + ConfigPathButton.Text = ConfigPath; + if(ConfigPathButton.Do()) + { + if(!ConfigPathExists) File.Create(ConfigPath); + new Thread(() => new Process + { + StartInfo = new ProcessStartInfo(ConfigPath) + { + UseShellExecute = true + } + }.Start()).Start(); + } + labelYPos += yPosAdd; + TextItem.doLabel(new Vector2(10, labelYPos), $"Config Exists: {ConfigPathExists}", null); + labelYPos += yPosAdd; + if(Description != null) + { + TextItem.doLabel(new Vector2(10, labelYPos), $"Description: {Description ?? ""}", null); + labelYPos += yPosAdd; + } + if(Authors != null) + { + TextItem.doLabel(new Vector2(10, labelYPos), $"Author{(Authors.Count > 1 ? "s" : "")}: {string.Join(", ", Authors)}", null); + labelYPos += yPosAdd; + } + if(TeamName != null) + { + TextItem.doLabel(new Vector2(10, labelYPos), $"Team Name: {TeamName}", null); + labelYPos += yPosAdd; + } + if(HardDependencies.Count > 0) + { + TextItem.doLabel(new Vector2(10, labelYPos), $"Hard Dependenc{(HardDependencies.Count > 1 ? "ies" : "y")}: {string.Join(", ", HardDependencies)}", null); + labelYPos += yPosAdd; + } + if(SoftDependencies.Count > 0) + { + TextItem.doLabel(new Vector2(10, labelYPos), $"Soft Dependenc{(SoftDependencies.Count > 1 ? "ies" : "y")}: {string.Join(", ", SoftDependencies)}", null); + labelYPos += yPosAdd; + } + if(Incompatibles.Count > 0) + { + TextItem.doLabel(new Vector2(10, labelYPos), $"Incompatible{(Incompatibles.Count > 1 ? "s" : "")}: {string.Join(", ", Incompatibles)}", null); + labelYPos += yPosAdd; + } + if(Websites.Count > 1) + { + TextItem.doLabel(new Vector2(10, labelYPos), $"Websites: ", null); + labelYPos += yPosAdd; + } + foreach(var webPair in Websites) + { + TextItem.doLabel(new Vector2(40, labelYPos), $"{webPair.Key}: {webPair.Value}", null); + labelYPos += yPosAdd; + } + + + ScrollablePanel.endPanel(_panelId, Vector2.Zero, rect, 0); + } + + protected void DrawTexture(Vector2 position, Texture2D texture, Color? color = null) + { + GuiData.spriteBatch.Draw(texture, position, color ?? Color.White); + } + + protected Texture2D LoadTexture(GraphicsDevice device, string fileName) + { + // TODO: validate if file is actually an image file perhaps? https://stackoverflow.com/a/49683945 + if(!File.Exists(fileName)) foreach(var ext in new[] { ".bmp", ".gif", ".jpg", ".jpeg", ".png", ".tga", ".tif", ".tiff" }) + { + var imgPath = Path.Combine(Paths.PluginPath, Guid) + ext; + if(File.Exists(imgPath)) + { + fileName = imgPath; + break; + } + } + try + { + using(FileStream fs = File.Open(fileName, FileMode.Open)) + return Texture2D.FromStream(device, fs); + } + catch(Exception ex) + { + throw ex; + } + } +} \ No newline at end of file diff --git a/PathfinderAPI/GUI/PluginListScreen.cs b/PathfinderAPI/GUI/PluginListScreen.cs new file mode 100644 index 00000000..9320d08c --- /dev/null +++ b/PathfinderAPI/GUI/PluginListScreen.cs @@ -0,0 +1,86 @@ +using BepInEx.Hacknet; +using Hacknet; +using Hacknet.Gui; +using Microsoft.Xna.Framework; +using Pathfinder.Event; +using Pathfinder.Event.Menu; +using Pathfinder.Meta.Load; +using Pathfinder.Util; + +namespace Pathfinder.GUI; + +public class PluginListScreen : GameScreen +{ + private List _pluginDataList = new List(); + + public string SelectedGuid; + + private PFButton BackButton = new PFButton(10, 10, 220, 30, $"<- {LocaleTerms.Loc("Back")}", Color.Gray); + + public override void Draw(GameTime gameTime) + { + base.Draw(gameTime); + PostProcessor.begin(); + ScreenManager.FadeBackBufferToBlack(255); + GuiData.startDraw(); + PatternDrawer.draw( + new Rectangle(0, 0, ScreenManager.GraphicsDevice.Viewport.Width, ScreenManager.GraphicsDevice.Viewport.Height), + 0.5f, + Color.Black, + new Color(2, 2, 2), + GuiData.spriteBatch + ); + if (BackButton.Do()) + ExitScreen(); + + int defX = 10, defY = 50; + foreach(var pluginData in _pluginDataList) + { + if(pluginData.DrawListElement(gameTime, this, new Point(defX, defY), pluginData.Guid == SelectedGuid)) + SelectedGuid = pluginData.Guid; + defY += pluginData.Height + 10; + } + + + GuiData.endDraw(); + PostProcessor.end(); + } + + public override void HandleInput(InputState input) + { + base.HandleInput(input); + GuiData.doInput(input); + } + + public override void LoadContent() + { + base.LoadContent(); + foreach(var plugin in HacknetChainloader.Instance.Plugins) + _pluginDataList.Add(new PluginInfo(this, (HacknetPlugin)plugin.Value.Instance)); + } + + private static PFButton _pluginListButton = new PFButton(0,0,0,0, "Plugins"); + + [Initialize] + internal static void Initialize() + { + EventManager.AddHandler(OnMainMenuButtonDraw); + } + + public static void OnMainMenuButtonDraw(DrawMainMenuButtonEvent evt) + { + if(!evt.Data.Is(MainMenuButtonType.Extensions)) return; + + _pluginListButton.Color = MainMenu.buttonColor; + _pluginListButton.X = evt.Data.X; + _pluginListButton.Y = evt.Data.Y; + _pluginListButton.Width = evt.Data.Width; + _pluginListButton.Height = evt.Data.Height; + if(_pluginListButton.Do()) + evt.MainMenu.ScreenManager.AddScreen(new PluginListScreen()); + + evt.Data.Y += evt.Data.Height + DrawMainMenuButtonEvent.ButtonData.DefaultButtonTopPadding; + evt.Data.YPositionIndex = evt.Data.Y; + + } +} \ No newline at end of file diff --git a/PathfinderAPI/Meta/PluginInfoAttribute.cs b/PathfinderAPI/Meta/PluginInfoAttribute.cs new file mode 100644 index 00000000..040a7ede --- /dev/null +++ b/PathfinderAPI/Meta/PluginInfoAttribute.cs @@ -0,0 +1,16 @@ +namespace Pathfinder.Meta; + +public class PluginInfoAttribute : System.Attribute +{ + public string Description; + public string ImageName; + public string[] Authors; + public string TeamName; + + public PluginInfoAttribute(string description, string teamName = null, string author = null) + { + Description = description; + if(author != null) Authors = new []{ author }; + TeamName = teamName; + } +} \ No newline at end of file diff --git a/PathfinderAPI/Meta/PluginWebsiteAttribute.cs b/PathfinderAPI/Meta/PluginWebsiteAttribute.cs new file mode 100644 index 00000000..87bd2d18 --- /dev/null +++ b/PathfinderAPI/Meta/PluginWebsiteAttribute.cs @@ -0,0 +1,14 @@ +namespace Pathfinder.Meta; + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +public class PluginWebsiteAttribute : System.Attribute +{ + public string WebsiteName; + public string WebsiteUrl; + + public PluginWebsiteAttribute(string websiteName, string websiteUrl) + { + WebsiteName = websiteName; + WebsiteUrl = websiteUrl; + } +} \ No newline at end of file diff --git a/PathfinderAPI/PathfinderAPI.csproj b/PathfinderAPI/PathfinderAPI.csproj index 5ac179a6..92218395 100644 --- a/PathfinderAPI/PathfinderAPI.csproj +++ b/PathfinderAPI/PathfinderAPI.csproj @@ -31,6 +31,10 @@ ..\libs\MonoMod.Utils.dll False + + ..\libs\SemanticVersioning.dll + False + diff --git a/PathfinderAPI/PathfinderAPIPlugin.cs b/PathfinderAPI/PathfinderAPIPlugin.cs index 372b7e69..ec36e729 100644 --- a/PathfinderAPI/PathfinderAPIPlugin.cs +++ b/PathfinderAPI/PathfinderAPIPlugin.cs @@ -10,6 +10,15 @@ namespace Pathfinder; [BepInPlugin(ModGUID, ModName, HacknetChainloader.VERSION)] [BepInDependency("com.Pathfinder.Updater", BepInDependency.DependencyFlags.SoftDependency)] +[PluginInfo("An extensive modding API for Hacknet that enables practically limitless programable extensions to the game.", + Authors = new string[] + { + "Windows10CE (Araon)", "Spartan322 (George)", "Fayti1703", "Arkhist", "SoundOfScooting", + "CanadaHonk", "Seeker1437", "MaowImpl (Aidan)" + } +)] +[PluginWebsite("Github", "https://github.com/Arkhist/Hacknet-Pathfinder")] +[PluginWebsite("Documentation", "https://arkhist.github.io/Hacknet-Pathfinder/")] [Updater( "https://api.github.com/repos/Arkhist/Hacknet-Pathfinder/releases", "Pathfinder.Release.zip", From e57089593921f2fdbc954c9c506496dc9f4829b9 Mon Sep 17 00:00:00 2001 From: "George L. Albany" Date: Sun, 1 May 2022 00:50:30 -0400 Subject: [PATCH 2/2] Added updater buttons to plugin list Also added PluginInfo and PluginWebsite to PathfinderUpdaterPlugin Reworked PathfinderUpdater.MainMenuOverride Check and Perform update async methods to allow PFButton assignment --- PathfinderAPI/GUI/PluginInfo.cs | 47 +++++++++++++++++++- PathfinderUpdater/MainMenuOverride.cs | 27 ++++++----- PathfinderUpdater/PathfinderUpdaterPlugin.cs | 7 +++ 3 files changed, 68 insertions(+), 13 deletions(-) diff --git a/PathfinderAPI/GUI/PluginInfo.cs b/PathfinderAPI/GUI/PluginInfo.cs index 85560d7d..80f165b3 100644 --- a/PathfinderAPI/GUI/PluginInfo.cs +++ b/PathfinderAPI/GUI/PluginInfo.cs @@ -1,5 +1,6 @@ using System.Collections.ObjectModel; using System.Diagnostics; +using System.Reflection; using BepInEx; using BepInEx.Hacknet; using Hacknet; @@ -36,6 +37,13 @@ public class PluginInfo private readonly SortedDictionary _websites = new SortedDictionary(); private readonly Texture2D ImageTexture; + private readonly PFButton CheckForUpdateButton; + private readonly PFButton UpdateButton; + private readonly Func CheckForUpdateAction; + private readonly Func UpdateAction; + private readonly Func CanPerformUpdate; + private readonly Func CanCheckForUpdate; + public PluginInfo(PluginListScreen screen, HacknetPlugin plugin) { Plugin = plugin; @@ -50,6 +58,7 @@ public PluginInfo(PluginListScreen screen, HacknetPlugin plugin) var pluginDataAttrib = MetadataHelper.GetAttributes(Plugin).FirstOrDefault(); if(pluginDataAttrib != null) { + Description = pluginDataAttrib.Description; Authors = new ReadOnlyCollection(pluginDataAttrib.Authors); TeamName = pluginDataAttrib.TeamName; if(pluginDataAttrib.ImageName != null) @@ -96,6 +105,22 @@ public PluginInfo(PluginListScreen screen, HacknetPlugin plugin) }; ConfigPathButton = new PFButton(0,0,0,0,""); + + var updaterAttrb = MetadataHelper.GetAttributes(Plugin).FirstOrDefault(); + if(updaterAttrb != null && HacknetChainloader.Instance.Plugins.TryGetValue("com.Pathfinder.Updater", out var updaterInfo)) + { + var mainMenuOverrideType = updaterInfo.Instance.GetType().Assembly.GetType("PathfinderUpdater.MainMenuOverride"); + var methInfo = mainMenuOverrideType.GetMethod("PerformCheckAndUpdateButtonAsync", BindingFlags.NonPublic | BindingFlags.Static); + if(methInfo.GetParameters().FirstOrDefault() != null) + { + CheckForUpdateAction = (Func)methInfo.CreateDelegate(typeof(Func)); + UpdateAction = (Func)mainMenuOverrideType.GetMethod("PerformUpdateAndUpdateButtonAsync", BindingFlags.NonPublic | BindingFlags.Static).CreateDelegate(typeof(Func)); + CanPerformUpdate = (Func)mainMenuOverrideType.GetProperty("CanPerformUpdate", BindingFlags.NonPublic | BindingFlags.Static).GetGetMethod(true).CreateDelegate(typeof(Func)); + CanCheckForUpdate = (Func)mainMenuOverrideType.GetProperty("CanCheckForUpdate", BindingFlags.NonPublic | BindingFlags.Static).GetGetMethod(true).CreateDelegate(typeof(Func)); + CheckForUpdateButton = new PFButton(0,0,0,0, "Check For Update", new Color(255, 255, 87)); + UpdateButton = new PFButton(0,0,0,0, "Update"); + } + } } public virtual int Width { get; } = 300; @@ -159,6 +184,25 @@ public virtual void DrawData(GameTime time, PluginListScreen screen, Point offse labelYPos += yPosAdd; TextItem.doLabel(new Vector2(10, labelYPos), $"Version: {Version}", null); labelYPos += yPosAdd; + if(CheckForUpdateButton != null && UpdateButton != null) + { + CheckForUpdateButton.X = 10; + CheckForUpdateButton.Y = labelYPos; + CheckForUpdateButton.Width = 160; + CheckForUpdateButton.Height = 30; + UpdateButton.X = CheckForUpdateButton.X + CheckForUpdateButton.Width + 10; + UpdateButton.Y = labelYPos; + UpdateButton.Width = 160; + UpdateButton.Height = 30; + labelYPos += CheckForUpdateButton.Height + 10; + + if(CheckForUpdateButton.Do() && CanCheckForUpdate()) + Task.Run(async () => await CheckForUpdateAction(UpdateButton, CheckForUpdateButton)); + + if(UpdateButton.Do() && CanPerformUpdate()) + Task.Run(async () => await UpdateAction(screen, UpdateButton)); + + } TextItem.doLabel(new Vector2(10, labelYPos), $"Enabled: {Enabled}", null); labelYPos += yPosAdd; if(Enabled) @@ -190,7 +234,7 @@ public virtual void DrawData(GameTime time, PluginListScreen screen, Point offse labelYPos += yPosAdd; if(Description != null) { - TextItem.doLabel(new Vector2(10, labelYPos), $"Description: {Description ?? ""}", null); + TextItem.doLabel(new Vector2(10, labelYPos), $"Description: {Description}", null); labelYPos += yPosAdd; } if(Authors != null) @@ -250,6 +294,7 @@ protected Texture2D LoadTexture(GraphicsDevice device, string fileName) break; } } + if(!File.Exists(fileName)) return null; try { using(FileStream fs = File.Open(fileName, FileMode.Open)) diff --git a/PathfinderUpdater/MainMenuOverride.cs b/PathfinderUpdater/MainMenuOverride.cs index dd94b81e..66f99ddb 100644 --- a/PathfinderUpdater/MainMenuOverride.cs +++ b/PathfinderUpdater/MainMenuOverride.cs @@ -42,36 +42,39 @@ internal static void OptionsSaved(CustomOptionsSaveEvent args) popupScreen.NoRestartPrompt.Value = PathfinderUpdaterPlugin.NoRestartPrompt.Value; } - internal static async Task PerformCheckAndUpdateButtonAsync() + internal static async Task PerformCheckAndUpdateButtonAsync(PFButton updateButton = null, PFButton checkButton = null) { - PerformUpdate.Text = UPDATE_STRING; + if(updateButton == null) updateButton = PerformUpdate; + if(checkButton == null) checkButton = CheckForUpdate; + updateButton.Text = UPDATE_STRING; CanCheckForUpdate = false; - var oldText = CheckForUpdate.Text; - CheckForUpdate.Text = "Finding Latest Update..."; + var oldText = checkButton.Text; + checkButton.Text = "Finding Latest Update..."; CanPerformUpdate = PathfinderUpdaterPlugin.NeedsUpdate = (await PathfinderUpdaterPlugin.PerformCheckAsync(true)).Length > 0; - CheckForUpdate.Text = oldText; + checkButton.Text = oldText; CanCheckForUpdate = true; - PerformUpdate.Text += $" (v{PathfinderUpdaterPlugin.PathfinderUpdater.LatestVersion})"; + updateButton.Text += $" (v{PathfinderUpdaterPlugin.PathfinderUpdater.LatestVersion})"; } - private static async Task PerformUpdateAndUpdateButtonAsync(MainMenu menu) + private static async Task PerformUpdateAndUpdateButtonAsync(GameScreen menu, PFButton updateButton = null) { + if(updateButton == null) updateButton = PerformUpdate; var couldCheckForUpdate = CanCheckForUpdate; CanCheckForUpdate = false; CanPerformUpdate = false; - var oldText = PerformUpdate.Text; - PerformUpdate.Text = "Currently Updating..."; + var oldText = updateButton.Text; + updateButton.Text = "Currently Updating..."; await PathfinderUpdaterPlugin.PerformUpdateAsync(); - PerformUpdate.Text = oldText; + updateButton.Text = oldText; if(!menu.ScreenManager.screens.Contains(popupScreen) && !PathfinderUpdaterPlugin.NoRestartPrompt.Value) menu.ScreenManager.AddScreen(popupScreen ??= new RestartPopupScreen()); CanPerformUpdate = !menu.ScreenManager.screens.Contains(popupScreen); CanCheckForUpdate = couldCheckForUpdate; } - private static bool CanCheckForUpdate = true; - private static bool CanPerformUpdate; + private static bool CanCheckForUpdate { get; set; } = true; + private static bool CanPerformUpdate { get; set; } internal static void OnDrawMainMenu(MainMenuEvent args) { if(CheckForUpdate.Do() && CanCheckForUpdate) diff --git a/PathfinderUpdater/PathfinderUpdaterPlugin.cs b/PathfinderUpdater/PathfinderUpdaterPlugin.cs index 71eb798e..30bfd5f8 100644 --- a/PathfinderUpdater/PathfinderUpdaterPlugin.cs +++ b/PathfinderUpdater/PathfinderUpdaterPlugin.cs @@ -13,6 +13,13 @@ namespace PathfinderUpdater; [BepInPlugin(ModGUID, ModName, HacknetChainloader.VERSION)] +[PluginInfo("An extension to Pathfinder which automatically updates BepInEx.Hacknet, PathfinderAPI, and PathfinderUpdater.", + Authors = new string[] + { + "Windows10CE (Araon)", "Spartan322 (George)" + } +)] +[PluginWebsite("Github", "https://github.com/Arkhist/Hacknet-Pathfinder")] [Updater( "https://api.github.com/repos/Arkhist/Hacknet-Pathfinder/releases", "Pathfinder.Release.zip",