using Lua; // ReSharper disable MemberCanBePrivate.Global namespace AIStudio.Tools.PluginSystem; /// /// Represents the base of any AI Studio plugin. /// public abstract partial class PluginBase : IPluginMetadata { private static readonly Guid[] MANDATORY_INTERNAL_PLUGINS = [ new("97dfb1ba-50c4-4440-8dfa-6575daf543c8"), // Language EN-US (base language) new("43065dbc-78d0-45b7-92be-f14c2926e2dc"), // Language DE-DE ]; private readonly IReadOnlyCollection baseIssues; protected readonly LuaState state; protected readonly List pluginIssues = []; /// public string IconSVG { get; } /// public PluginType Type { get; } /// public Guid Id { get; } /// public string Name { get; } = string.Empty; /// public string Description { get; } = string.Empty; /// public PluginVersion Version { get; } /// public string[] Authors { get; } = []; /// public string SupportContact { get; } = string.Empty; /// public string SourceURL { get; } = string.Empty; /// public PluginCategory[] Categories { get; } = []; /// public PluginTargetGroup[] TargetGroups { get; } = []; /// public bool IsMaintained { get; } /// public string DeprecationMessage { get; } = string.Empty; /// public bool IsInternal { get; } /// /// The issues that occurred during the initialization of this plugin. /// public IEnumerable Issues => this.baseIssues.Concat(this.pluginIssues); /// /// True, when the plugin is valid. /// /// /// False means that there were issues during the initialization of the plugin. /// Please check the Issues property for more information. /// public bool IsValid => this is not NoPlugin && this.baseIssues.Count == 0 && this.pluginIssues.Count == 0; protected PluginBase(LuaState state, PluginType type, string parseError = "") { this.state = state; this.Type = type; var issues = new List(); if(!string.IsNullOrWhiteSpace(parseError)) issues.Add(parseError); // Notice: when no icon is specified, the default icon will be used. this.TryInitIconSVG(out _, out var iconSVG); this.IconSVG = iconSVG; if(this.TryInitId(out var issue, out var id)) { this.Id = id; this.IsInternal = MANDATORY_INTERNAL_PLUGINS.Contains(id); } else if(this is not NoPlugin) issues.Add(issue); if(this.TryInitName(out issue, out var name)) this.Name = name; else if(this is not NoPlugin) issues.Add(issue); if(this.TryInitDescription(out issue, out var description)) this.Description = description; else if(this is not NoPlugin) issues.Add(issue); if(this.TryInitVersion(out issue, out var version)) this.Version = version; else if(this is not NoPlugin) issues.Add(issue); if(this.TryInitAuthors(out issue, out var authors)) this.Authors = authors; else if(this is not NoPlugin) issues.Add(issue); if(this.TryInitSupportContact(out issue, out var contact)) this.SupportContact = contact; else if(this is not NoPlugin) issues.Add(issue); if(this.TryInitSourceURL(out issue, out var url)) this.SourceURL = url; else if(this is not NoPlugin) issues.Add(issue); if(this.TryInitCategories(out issue, out var categories)) this.Categories = categories; else if(this is not NoPlugin) issues.Add(issue); if(this.TryInitTargetGroups(out issue, out var targetGroups)) this.TargetGroups = targetGroups; else if(this is not NoPlugin) issues.Add(issue); if(this.TryInitIsMaintained(out issue, out var isMaintained)) this.IsMaintained = isMaintained; else if(this is not NoPlugin) issues.Add(issue); if(this.TryInitDeprecationMessage(out issue, out var deprecationMessage)) this.DeprecationMessage = deprecationMessage; else if(this is not NoPlugin) issues.Add(issue); this.baseIssues = issues; } #region Initialization-related methods /// /// Tries to read the ID of the plugin. /// /// The error message, when the ID could not be read. /// The read ID. /// True, when the ID could be read successfully. private bool TryInitId(out string message, out Guid id) { if (!this.state.Environment["ID"].TryRead(out var idText)) { message = "The field ID does not exist or is not a valid string."; id = Guid.Empty; return false; } if (!Guid.TryParse(idText, out id)) { message = "The field ID is not a valid GUID / UUID. The ID must be formatted in the 8-4-4-4-12 format (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)."; id = Guid.Empty; return false; } if(id == Guid.Empty) { message = "The field ID is empty. The ID must be formatted in the 8-4-4-4-12 format (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)."; return false; } message = string.Empty; return true; } /// /// Tries to read the name of the plugin. /// /// The error message, when the name could not be read. /// The read name. /// True, when the name could be read successfully. private bool TryInitName(out string message, out string name) { if (!this.state.Environment["NAME"].TryRead(out name)) { message = "The field NAME does not exist or is not a valid string."; name = string.Empty; return false; } if(string.IsNullOrWhiteSpace(name)) { message = "The field NAME is empty. The name must be a non-empty string."; return false; } message = string.Empty; return true; } /// /// Tries to read the description of the plugin. /// /// The error message, when the description could not be read. /// The read description. /// True, when the description could be read successfully. private bool TryInitDescription(out string message, out string description) { if (!this.state.Environment["DESCRIPTION"].TryRead(out description)) { message = "The field DESCRIPTION does not exist or is not a valid string."; description = string.Empty; return false; } if(string.IsNullOrWhiteSpace(description)) { message = "The field DESCRIPTION is empty. The description must be a non-empty string."; return false; } message = string.Empty; return true; } /// /// Tries to read the version of the plugin. /// /// The error message, when the version could not be read. /// The read version. /// True, when the version could be read successfully. private bool TryInitVersion(out string message, out PluginVersion version) { if (!this.state.Environment["VERSION"].TryRead(out var versionText)) { message = "The field VERSION does not exist or is not a valid string."; version = PluginVersion.NONE; return false; } if (!PluginVersion.TryParse(versionText, out version)) { message = "The field VERSION is not a valid version number. The version number must be formatted as string in the major.minor.patch format (X.X.X)."; version = PluginVersion.NONE; return false; } if(version == PluginVersion.NONE) { message = "The field VERSION is empty. The version number must be formatted as string in the major.minor.patch format (X.X.X)."; return false; } message = string.Empty; return true; } /// /// Tries to read the authors of the plugin. /// /// The error message, when the authors could not be read. /// The read authors. /// True, when the authors could be read successfully. private bool TryInitAuthors(out string message, out string[] authors) { if (!this.state.Environment["AUTHORS"].TryRead(out var authorsTable)) { authors = []; message = "The table AUTHORS does not exist or is using an invalid syntax."; return false; } var authorList = new List(); foreach(var author in authorsTable.GetArraySpan()) if(author.TryRead(out var authorName)) authorList.Add(authorName); authors = authorList.ToArray(); if(authorList.Count == 0) { message = "The table AUTHORS is empty. At least one author must be specified."; return false; } message = string.Empty; return true; } /// /// Tries to read the support contact for the plugin. /// /// The error message, when the support contact could not be read. /// The read support contact. /// True, when the support contact could be read successfully. private bool TryInitSupportContact(out string message, out string contact) { if (!this.state.Environment["SUPPORT_CONTACT"].TryRead(out contact)) { contact = string.Empty; message = "The field SUPPORT_CONTACT does not exist or is not a valid string."; return false; } if(string.IsNullOrWhiteSpace(contact)) { message = "The field SUPPORT_CONTACT is empty. The support contact must be a non-empty string."; return false; } message = string.Empty; return true; } /// /// Try to read the source URL of the plugin. /// /// The error message, when the source URL could not be read. /// The read source URL. /// True, when the source URL could be read successfully. private bool TryInitSourceURL(out string message, out string url) { if (!this.state.Environment["SOURCE_URL"].TryRead(out url)) { url = string.Empty; message = "The field SOURCE_URL does not exist or is not a valid string."; return false; } if (!url.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase) && !url.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase)) { url = string.Empty; message = "The field SOURCE_URL is not a valid URL. The URL must start with 'http://' or 'https://'."; return false; } message = string.Empty; return true; } /// /// Tries to read the categories of the plugin. /// /// The error message, when the categories could not be read. /// The read categories. /// True, when the categories could be read successfully. private bool TryInitCategories(out string message, out PluginCategory[] categories) { if (!this.state.Environment["CATEGORIES"].TryRead(out var categoriesTable)) { categories = []; message = "The table CATEGORIES does not exist or is using an invalid syntax."; return false; } var categoryList = new List(); foreach(var luaCategory in categoriesTable.GetArraySpan()) if(luaCategory.TryRead(out var categoryName)) if(Enum.TryParse(categoryName, out var category) && category != PluginCategory.NONE) categoryList.Add(category); categories = categoryList.ToArray(); if(categoryList.Count == 0) { message = $"The table CATEGORIES is empty. At least one category is necessary. Valid categories are: {CommonTools.GetAllEnumValues(PluginCategory.NONE)}."; return false; } message = string.Empty; return true; } /// /// Tries to read the intended target groups for the plugin. /// /// The error message, when the target groups could not be read. /// The read target groups. /// True, when the target groups could be read successfully. private bool TryInitTargetGroups(out string message, out PluginTargetGroup[] targetGroups) { if (!this.state.Environment["TARGET_GROUPS"].TryRead(out var targetGroupsTable)) { targetGroups = []; message = "The table TARGET_GROUPS does not exist or is using an invalid syntax."; return false; } var targetGroupList = new List(); foreach(var luaTargetGroup in targetGroupsTable.GetArraySpan()) if(luaTargetGroup.TryRead(out var targetGroupName)) if(Enum.TryParse(targetGroupName, out var targetGroup) && targetGroup != PluginTargetGroup.NONE) targetGroupList.Add(targetGroup); targetGroups = targetGroupList.ToArray(); if(targetGroups.Length == 0) { message = "The table TARGET_GROUPS is empty or is not a valid table of strings. Valid target groups are: {CommonTools.GetAllEnumValues(PluginTargetGroup.NONE)}."; return false; } message = string.Empty; return true; } /// /// Tries to read the maintenance status of the plugin. /// /// The error message, when the maintenance status could not be read. /// The read maintenance status. /// True, when the maintenance status could be read successfully. private bool TryInitIsMaintained(out string message, out bool isMaintained) { if (!this.state.Environment["IS_MAINTAINED"].TryRead(out isMaintained)) { isMaintained = false; message = "The field IS_MAINTAINED does not exist or is not a valid boolean."; return false; } message = string.Empty; return true; } /// /// Tries to read the deprecation message of the plugin. /// /// The error message, when the deprecation message could not be read. /// The read deprecation message. /// True, when the deprecation message could be read successfully. private bool TryInitDeprecationMessage(out string message, out string deprecationMessage) { if (!this.state.Environment["DEPRECATION_MESSAGE"].TryRead(out deprecationMessage)) { deprecationMessage = string.Empty; message = "The field DEPRECATION_MESSAGE does not exist, is not a valid string. This message is optional: use an empty string to indicate that the plugin is not deprecated."; return false; } message = string.Empty; return true; } /// /// Tries to initialize the UI text content of the plugin. /// /// The error message, when the UI text content could not be read. /// The read UI text content. /// True, when the UI text content could be read successfully. protected bool TryInitUITextContent(out string message, out Dictionary pluginContent) { if (!this.state.Environment["UI_TEXT_CONTENT"].TryRead(out var textTable)) { message = "The UI_TEXT_CONTENT table does not exist or is not a valid table."; pluginContent = []; return false; } this.ReadTextTable("root", textTable, out pluginContent); message = string.Empty; return true; } /// /// Reads a flat or hierarchical text table. /// /// The parent key(s). /// The table to read. /// The read table content. protected void ReadTextTable(string parent, LuaTable table, out Dictionary tableContent) { tableContent = []; var lastKey = LuaValue.Nil; while (table.TryGetNext(lastKey, out var pair)) { var keyText = pair.Key.ToString(); if (pair.Value.TryRead(out var value)) tableContent[$"{parent}::{keyText}"] = value; else if (pair.Value.TryRead(out var t)) { this.ReadTextTable($"{parent}::{keyText}", t, out var subContent); foreach (var (k, v) in subContent) tableContent[k] = v; } lastKey = pair.Key; } } #endregion }