Merge pull request #575 from daemitus/devImages

Load dev plugin icons from disk, load all icons as needed instead of one by one
This commit is contained in:
goaaats 2021-09-22 10:15:19 +02:00 committed by GitHub
commit d04604935b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 224 additions and 143 deletions

View file

@ -25,7 +25,6 @@ using Dalamud.Plugin.Internal.Types;
using Dalamud.Utility; using Dalamud.Utility;
using ImGuiNET; using ImGuiNET;
using ImGuiScene; using ImGuiScene;
using Microsoft.VisualBasic;
namespace Dalamud.Interface.Internal.Windows namespace Dalamud.Interface.Internal.Windows
{ {
@ -56,7 +55,7 @@ namespace Dalamud.Interface.Internal.Windows
private string[] testerImagePaths = new string[5]; private string[] testerImagePaths = new string[5];
private string testerIconPath = string.Empty; private string testerIconPath = string.Empty;
private TextureWrap?[]? testerImages; private TextureWrap?[] testerImages;
private TextureWrap? testerIcon; private TextureWrap? testerIcon;
private bool testerError = false; private bool testerError = false;
@ -76,9 +75,8 @@ namespace Dalamud.Interface.Internal.Windows
private List<AvailablePluginUpdate> pluginListUpdatable = new(); private List<AvailablePluginUpdate> pluginListUpdatable = new();
private bool hasDevPlugins = false; private bool hasDevPlugins = false;
private bool downloadingIcons = false; private Dictionary<string, TextureWrap?> pluginIconMap = new();
private Dictionary<string, (bool IsDownloaded, TextureWrap?[]? Textures)> pluginImagesMap = new(); private Dictionary<string, TextureWrap?[]> pluginImagesMap = new();
private Dictionary<string, (bool IsDownloaded, TextureWrap? Texture)> pluginIconMap = new();
private string searchText = string.Empty; private string searchText = string.Empty;
@ -197,8 +195,6 @@ namespace Dalamud.Interface.Internal.Windows
{ {
this.pluginIconMap.Clear(); this.pluginIconMap.Clear();
this.pluginImagesMap.Clear(); this.pluginImagesMap.Clear();
this.DownloadPluginIcons();
} }
private static string? GetPluginIconUrl(PluginManifest manifest, bool isThirdParty, bool isTesting) private static string? GetPluginIconUrl(PluginManifest manifest, bool isThirdParty, bool isTesting)
@ -209,10 +205,18 @@ namespace Dalamud.Interface.Internal.Windows
return MainRepoImageUrl.Format(isTesting ? "testing" : "plugins", manifest.InternalName, "icon.png"); return MainRepoImageUrl.Format(isTesting ? "testing" : "plugins", manifest.InternalName, "icon.png");
} }
private static List<string>? GetPluginImageUrls(PluginManifest manifest, bool isThirdParty, bool isTesting) private static List<string?> GetPluginImageUrls(PluginManifest manifest, bool isThirdParty, bool isTesting)
{ {
if (isThirdParty) if (isThirdParty)
{
if (manifest.ImageUrls.Count > 5)
{
Log.Warning($"Plugin {manifest.InternalName} has too many images");
return manifest.ImageUrls.Take(5).ToList();
}
return manifest.ImageUrls; return manifest.ImageUrls;
}
var output = new List<string>(); var output = new List<string>();
for (var i = 1; i <= 5; i++) for (var i = 1; i <= 5; i++)
@ -223,6 +227,36 @@ namespace Dalamud.Interface.Internal.Windows
return output; return output;
} }
private static FileInfo? GetPluginIconFileInfo(LocalPlugin? plugin)
{
var pluginDir = plugin.DllFile.Directory;
var devUrl = new FileInfo(Path.Combine(pluginDir.FullName, "images", "icon.png"));
if (devUrl.Exists)
return devUrl;
return null;
}
private static List<FileInfo?> GetPluginImageFileInfos(LocalPlugin? plugin)
{
var pluginDir = plugin.DllFile.Directory;
var output = new List<FileInfo>();
for (var i = 1; i <= 5; i++)
{
var devUrl = new FileInfo(Path.Combine(pluginDir.FullName, "images", $"image{i}.png"));
if (devUrl.Exists)
{
output.Add(devUrl);
continue;
}
output.Add(null);
}
return output;
}
private void DrawHeader() private void DrawHeader()
{ {
var style = ImGui.GetStyle(); var style = ImGui.GetStyle();
@ -555,7 +589,6 @@ namespace Dalamud.Interface.Internal.Windows
} }
} }
// TODO: Technically, we really should just load images from a devplugin folder
private void DrawImageTester() private void DrawImageTester()
{ {
var sectionSize = ImGuiHelpers.GlobalScale * 66; var sectionSize = ImGuiHelpers.GlobalScale * 66;
@ -800,7 +833,7 @@ namespace Dalamud.Interface.Internal.Windows
return ready; return ready;
} }
private bool DrawPluginCollapsingHeader(string label, PluginManifest manifest, bool trouble, bool updateAvailable, bool isNew, Action drawContextMenuAction, int index) private bool DrawPluginCollapsingHeader(string label, LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty, bool trouble, bool updateAvailable, bool isNew, Action drawContextMenuAction, int index)
{ {
ImGui.Separator(); ImGui.Separator();
@ -837,12 +870,20 @@ namespace Dalamud.Interface.Internal.Windows
ImGui.SetCursorPos(startCursor); ImGui.SetCursorPos(startCursor);
var hasIcon = this.pluginIconMap.TryGetValue(manifest.InternalName, out var icon);
var iconTex = this.defaultIcon; var iconTex = this.defaultIcon;
if (hasIcon && icon.IsDownloaded && icon.Texture != null) var hasIcon = this.pluginIconMap.TryGetValue(manifest.InternalName, out var cachedIconTex);
if (!hasIcon)
{ {
iconTex = icon.Texture; this.pluginIconMap.Add(manifest.InternalName, null);
Task.Run(async () => await this.DownloadPluginIconAsync(plugin, manifest, isThirdParty));
}
else if (cachedIconTex != null)
{
iconTex = cachedIconTex;
}
else
{
// nothing
} }
var iconSize = ImGuiHelpers.ScaledVector2(64, 64); var iconSize = ImGuiHelpers.ScaledVector2(64, 64);
@ -873,8 +914,8 @@ namespace Dalamud.Interface.Internal.Windows
// Download count // Download count
var downloadCountText = manifest.DownloadCount > 0 var downloadCountText = manifest.DownloadCount > 0
? Locs.PluginBody_AuthorWithDownloadCount(manifest.Author, manifest.DownloadCount) ? Locs.PluginBody_AuthorWithDownloadCount(manifest.Author, manifest.DownloadCount)
: Locs.PluginBody_AuthorWithDownloadCountUnavailable(manifest.Author); : Locs.PluginBody_AuthorWithDownloadCountUnavailable(manifest.Author);
ImGui.SameLine(); ImGui.SameLine();
ImGui.TextColored(ImGuiColors.DalamudGrey3, downloadCountText); ImGui.TextColored(ImGuiColors.DalamudGrey3, downloadCountText);
@ -927,7 +968,8 @@ namespace Dalamud.Interface.Internal.Windows
ImGui.PushID($"available{index}{manifest.InternalName}"); ImGui.PushID($"available{index}{manifest.InternalName}");
if (this.DrawPluginCollapsingHeader(label, manifest, false, false, !wasSeen, () => this.DrawAvailablePluginContextMenu(manifest), index)) var isThirdParty = manifest.SourceRepo.IsThirdParty;
if (this.DrawPluginCollapsingHeader(label, null, manifest, isThirdParty, false, false, !wasSeen, () => this.DrawAvailablePluginContextMenu(manifest), index))
{ {
if (!wasSeen) if (!wasSeen)
configuration.SeenPluginInternalName.Add(manifest.InternalName); configuration.SeenPluginInternalName.Add(manifest.InternalName);
@ -996,7 +1038,7 @@ namespace Dalamud.Interface.Internal.Windows
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
if (this.DrawPluginImages(manifest, index, manifest.SourceRepo.IsThirdParty)) if (this.DrawPluginImages(null, manifest, isThirdParty, index))
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
ImGui.Unindent(); ImGui.Unindent();
@ -1132,7 +1174,7 @@ namespace Dalamud.Interface.Internal.Windows
ImGui.PushID($"installed{index}{plugin.Manifest.InternalName}"); ImGui.PushID($"installed{index}{plugin.Manifest.InternalName}");
if (this.DrawPluginCollapsingHeader(label, plugin.Manifest, trouble, availablePluginUpdate != default, false, () => this.DrawInstalledPluginContextMenu(plugin), index)) if (this.DrawPluginCollapsingHeader(label, plugin, plugin.Manifest, plugin.Manifest.IsThirdParty, trouble, availablePluginUpdate != default, false, () => this.DrawInstalledPluginContextMenu(plugin), index))
{ {
if (!this.WasPluginSeen(plugin.Manifest.InternalName)) if (!this.WasPluginSeen(plugin.Manifest.InternalName))
configuration.SeenPluginInternalName.Add(plugin.Manifest.InternalName); configuration.SeenPluginInternalName.Add(plugin.Manifest.InternalName);
@ -1154,7 +1196,7 @@ namespace Dalamud.Interface.Internal.Windows
ImGui.SameLine(); ImGui.SameLine();
ImGui.TextColored(ImGuiColors.DalamudGrey3, downloadText); ImGui.TextColored(ImGuiColors.DalamudGrey3, downloadText);
var isThirdParty = !string.IsNullOrEmpty(manifest.InstalledFromUrl); var isThirdParty = manifest.IsThirdParty;
// Installed from // Installed from
if (plugin.IsDev) if (plugin.IsDev)
@ -1217,9 +1259,8 @@ namespace Dalamud.Interface.Internal.Windows
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
this.DrawPluginImages(manifest, index, isThirdParty); if (this.DrawPluginImages(plugin, manifest, isThirdParty, index))
ImGuiHelpers.ScaledDummy(5);
ImGuiHelpers.ScaledDummy(5);
ImGui.Unindent(); ImGui.Unindent();
} }
@ -1510,18 +1551,18 @@ namespace Dalamud.Interface.Internal.Windows
} }
} }
private bool DrawPluginImages(PluginManifest manifest, int index, bool isThirdParty) private bool DrawPluginImages(LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty, int index)
{ {
if (!this.pluginImagesMap.TryGetValue(manifest.InternalName, out var images)) var hasImages = this.pluginImagesMap.TryGetValue(manifest.InternalName, out var imageTextures);
if (!hasImages)
{ {
Task.Run(() => this.DownloadPluginImagesAsync(manifest, isThirdParty)); this.pluginImagesMap.Add(manifest.InternalName, Array.Empty<TextureWrap>());
Task.Run(async () => await this.DownloadPluginImagesAsync(plugin, manifest, isThirdParty));
return false; return false;
} }
if (!images.IsDownloaded) if (imageTextures.Length == 0)
return false;
if (images.Textures == null)
return false; return false;
const float thumbFactor = 2.7f; const float thumbFactor = 2.7f;
@ -1534,42 +1575,39 @@ namespace Dalamud.Interface.Internal.Windows
if (ImGui.BeginChild($"plugin{index}ImageScrolling", new Vector2(width - (70 * ImGuiHelpers.GlobalScale), (PluginImageHeight / thumbFactor) + scrollBarSize), false, ImGuiWindowFlags.HorizontalScrollbar | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoBackground)) if (ImGui.BeginChild($"plugin{index}ImageScrolling", new Vector2(width - (70 * ImGuiHelpers.GlobalScale), (PluginImageHeight / thumbFactor) + scrollBarSize), false, ImGuiWindowFlags.HorizontalScrollbar | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoBackground))
{ {
if (images.Textures != null && images.Textures is { Length: > 0 }) for (var i = 0; i < imageTextures.Length; i++)
{ {
for (var i = 0; i < images.Textures.Length; i++) var image = imageTextures[i];
if (image == null)
continue;
ImGui.PushStyleVar(ImGuiStyleVar.PopupBorderSize, 0);
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero);
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero);
var popupId = $"plugin{index}image{i}";
if (ImGui.BeginPopup(popupId))
{ {
var popupId = $"plugin{index}image{i}"; if (ImGui.ImageButton(image.ImGuiHandle, new Vector2(image.Width, image.Height)))
var image = images.Textures[i]; ImGui.CloseCurrentPopup();
if (image == null)
continue;
ImGui.PushStyleVar(ImGuiStyleVar.PopupBorderSize, 0); ImGui.EndPopup();
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero); }
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero);
if (ImGui.BeginPopup(popupId)) ImGui.PopStyleVar(3);
{
if (ImGui.ImageButton(image.ImGuiHandle, new Vector2(image.Width, image.Height)))
ImGui.CloseCurrentPopup();
ImGui.EndPopup(); ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero);
}
ImGui.PopStyleVar(3); if (ImGui.ImageButton(image.ImGuiHandle, ImGuiHelpers.ScaledVector2(image.Width / thumbFactor, image.Height / thumbFactor)))
ImGui.OpenPopup(popupId);
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); ImGui.PopStyleVar();
if (ImGui.ImageButton(image.ImGuiHandle, ImGuiHelpers.ScaledVector2(image.Width / thumbFactor, image.Height / thumbFactor))) if (i < imageTextures.Length - 1)
ImGui.OpenPopup(popupId); {
ImGui.SameLine();
ImGui.PopStyleVar(); ImGuiHelpers.ScaledDummy(5);
ImGui.SameLine();
if (i < images.Textures.Length - 1)
{
ImGui.SameLine();
ImGuiHelpers.ScaledDummy(5);
ImGui.SameLine();
}
} }
} }
} }
@ -1612,8 +1650,6 @@ namespace Dalamud.Interface.Internal.Windows
.ToList(); .ToList();
this.pluginListUpdatable = pluginManager.UpdatablePlugins.ToList(); this.pluginListUpdatable = pluginManager.UpdatablePlugins.ToList();
this.ResortPlugins(); this.ResortPlugins();
this.DownloadPluginIcons();
} }
private void OnInstalledPluginsChanged() private void OnInstalledPluginsChanged()
@ -1624,8 +1660,6 @@ namespace Dalamud.Interface.Internal.Windows
this.pluginListUpdatable = pluginManager.UpdatablePlugins.ToList(); this.pluginListUpdatable = pluginManager.UpdatablePlugins.ToList();
this.hasDevPlugins = this.pluginListInstalled.Any(plugin => plugin.IsDev); this.hasDevPlugins = this.pluginListInstalled.Any(plugin => plugin.IsDev);
this.ResortPlugins(); this.ResortPlugins();
this.DownloadPluginIcons();
} }
private void ResortPlugins() private void ResortPlugins()
@ -1705,133 +1739,173 @@ namespace Dalamud.Interface.Internal.Windows
this.errorModalOnNextFrame = true; this.errorModalOnNextFrame = true;
} }
private void DownloadPluginIcons() private async Task DownloadPluginIconAsync(LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty)
{ {
if (this.downloadingIcons) var interfaceManager = Service<InterfaceManager>.Get();
var pluginManager = Service<PluginManager>.Get();
static bool ValidateIcon(TextureWrap icon, string loc)
{ {
Log.Error("Already downloading icons, skipping..."); if (icon == null)
return false;
if (icon.Height > PluginIconHeight || icon.Width > PluginIconWidth)
{
Log.Error($"Icon at {loc} was not of the correct resolution.");
return false;
}
if (icon.Height != icon.Width)
{
Log.Error($"Icon at {loc} was not square.");
return false;
}
return true;
}
if (plugin != null && plugin.IsDev)
{
var file = GetPluginIconFileInfo(plugin);
if (file != null)
{
Log.Verbose($"Fetching icon for {manifest.InternalName} from {file.FullName}");
var icon = interfaceManager.LoadImage(file.FullName);
if (!ValidateIcon(icon, file.FullName))
return;
this.pluginIconMap[manifest.InternalName] = icon;
Log.Verbose($"Plugin icon for {manifest.InternalName} loaded from disk");
}
return; return;
} }
this.downloadingIcons = true; var useTesting = pluginManager.UseTesting(manifest);
var url = GetPluginIconUrl(manifest, isThirdParty, useTesting);
var pluginManager = Service<PluginManager>.Get();
Log.Verbose("Start downloading plugin icons...");
Task.Run(async () =>
{
var plugins = pluginManager.AvailablePlugins.Select(x => x);
foreach (var pluginManifest in plugins)
{
var useTesting = pluginManager.UseTesting(pluginManifest);
if (!this.pluginIconMap.ContainsKey(pluginManifest.InternalName))
await this.DownloadPluginIconAsync(pluginManifest, useTesting);
}
}).ContinueWith(t =>
{
Log.Verbose($"Icon download finished, faulted: {t.IsFaulted}");
this.downloadingIcons = false;
});
}
private async Task DownloadPluginIconAsync(RemotePluginManifest manifest, bool isTesting)
{
var interfaceManager = Service<InterfaceManager>.Get();
Log.Verbose($"Downloading icon for {manifest.InternalName}");
this.pluginIconMap.Add(manifest.InternalName, (false, null));
var url = GetPluginIconUrl(manifest, manifest.SourceRepo.IsThirdParty, isTesting);
Log.Verbose($"Icon from {url}");
if (url != null) if (url != null)
{ {
Log.Verbose($"Downloading icon for {manifest.InternalName} from {url}");
var data = await this.httpClient.GetAsync(url); var data = await this.httpClient.GetAsync(url);
if (data.StatusCode == HttpStatusCode.NotFound) if (data.StatusCode == HttpStatusCode.NotFound)
return; return;
data.EnsureSuccessStatusCode(); data.EnsureSuccessStatusCode();
var icon = interfaceManager.LoadImage(await data.Content.ReadAsByteArrayAsync()); var icon = interfaceManager.LoadImage(await data.Content.ReadAsByteArrayAsync());
if (icon != null) if (!ValidateIcon(icon, url))
{ return;
if (icon.Height > PluginIconHeight || icon.Width > PluginIconWidth)
{
Log.Error($"Icon at {manifest.IconUrl} was not of the correct resolution.");
return;
}
if (icon.Height != icon.Width) this.pluginIconMap[manifest.InternalName] = icon;
{ Log.Verbose($"Plugin icon for {manifest.InternalName} downloaded");
Log.Error($"Icon at {manifest.IconUrl} was not square.");
return;
}
this.pluginIconMap[manifest.InternalName] = (true, icon); return;
}
} }
Log.Verbose($"Icon for {manifest.InternalName} is not available");
} }
private async Task DownloadPluginImagesAsync(PluginManifest manifest, bool isThirdParty) private async Task DownloadPluginImagesAsync(LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty)
{ {
var interfaceManager = Service<InterfaceManager>.Get(); var interfaceManager = Service<InterfaceManager>.Get();
var pluginManager = Service<PluginManager>.Get(); var pluginManager = Service<PluginManager>.Get();
Log.Verbose($"Downloading images for {manifest.InternalName}"); static bool ValidateImage(TextureWrap image, string loc)
this.pluginImagesMap.Add(manifest.InternalName, (false, null));
var urls = GetPluginImageUrls(manifest, isThirdParty, pluginManager.UseTesting(manifest));
var didAny = false;
if (urls != null)
{ {
if (urls.Count > 5) if (image == null)
return false;
if (image.Height != PluginImageHeight || image.Width != PluginImageWidth)
{ {
Log.Error($"Plugin {manifest.InternalName} has too many images."); Log.Error($"Image at {loc} was not of the correct resolution.");
return; return false;
} }
return true;
}
if (plugin != null && plugin.IsDev)
{
var files = GetPluginImageFileInfos(plugin);
if (files != null)
{
var didAny = false;
var pluginImages = new TextureWrap[files.Count];
for (var i = 0; i < files.Count; i++)
{
var file = files[i];
if (file == null)
continue;
Log.Verbose($"Loading image{i + 1} for {manifest.InternalName} from {file.FullName}");
var image = interfaceManager.LoadImage(await File.ReadAllBytesAsync(file.FullName));
if (!ValidateImage(image, file.FullName))
continue;
Log.Verbose($"Plugin image{i + 1} for {manifest.InternalName} loaded from disk");
pluginImages[i] = image;
didAny = true;
}
if (didAny)
{
Log.Verbose($"Plugin images for {manifest.InternalName} loaded from disk");
this.pluginImagesMap[manifest.InternalName] = pluginImages;
return;
}
}
// Dev plugins are loaded from disk only
return;
}
var useTesting = pluginManager.UseTesting(manifest);
var urls = GetPluginImageUrls(manifest, isThirdParty, useTesting);
if (urls != null)
{
var didAny = false;
var pluginImages = new TextureWrap[urls.Count]; var pluginImages = new TextureWrap[urls.Count];
for (var i = 0; i < urls.Count; i++) for (var i = 0; i < urls.Count; i++)
{ {
var data = await this.httpClient.GetAsync(urls[i]); var url = urls[i];
Serilog.Log.Information($"Download from {urls[i]}"); Log.Verbose($"Downloading image{i + 1} for {manifest.InternalName} from {url}");
var data = await this.httpClient.GetAsync(url);
if (data.StatusCode == HttpStatusCode.NotFound) if (data.StatusCode == HttpStatusCode.NotFound)
continue; continue;
data.EnsureSuccessStatusCode(); data.EnsureSuccessStatusCode();
var image = interfaceManager.LoadImage(await data.Content.ReadAsByteArrayAsync()); var image = interfaceManager.LoadImage(await data.Content.ReadAsByteArrayAsync());
if (image == null) if (!ValidateImage(image, url))
{ continue;
return;
}
if (image.Height != PluginImageHeight || image.Width != PluginImageWidth)
{
Log.Error($"Image at {urls[i]} was not of the correct resolution.");
return;
}
didAny = true;
Log.Verbose($"Plugin image{i + 1} for {manifest.InternalName} downloaded");
pluginImages[i] = image; pluginImages[i] = image;
didAny = true;
} }
if (didAny) if (didAny)
{ {
this.pluginImagesMap[manifest.InternalName] = (true, pluginImages); Log.Verbose($"Plugin images for {manifest.InternalName} downloaded");
this.pluginImagesMap[manifest.InternalName] = pluginImages;
return;
} }
} }
Log.Verbose($"Plugin images for {manifest.InternalName} downloaded"); Log.Verbose($"Images for {manifest.InternalName} are not available");
} }
[SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "Disregard here")] [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "Disregard here")]

View file

@ -24,10 +24,17 @@ namespace Dalamud.Plugin.Internal.Types
/// <summary> /// <summary>
/// Gets or sets the 3rd party repo URL that this plugin was installed from. Used to display where the plugin was /// Gets or sets the 3rd party repo URL that this plugin was installed from. Used to display where the plugin was
/// sourced from on the installed plugin view. This should not be included in the plugin master. /// sourced from on the installed plugin view. This should not be included in the plugin master. This value is null
/// when installed from the main repo.
/// </summary> /// </summary>
public string InstalledFromUrl { get; set; } public string InstalledFromUrl { get; set; }
/// <summary>
/// Gets a value indicating whether this manifest is associated with a plugin that was installed from a third party
/// repo. Unless the manifest has been manually modified, this is determined by the InstalledFromUrl being null.
/// </summary>
public bool IsThirdParty => !string.IsNullOrEmpty(this.InstalledFromUrl);
/// <summary> /// <summary>
/// Save a plugin manifest to file. /// Save a plugin manifest to file.
/// </summary> /// </summary>