using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using UnityEditor; using UnityEngine; using Object = UnityEngine.Object; #if H3VR_IMPORTED using Valve.Newtonsoft.Json; using Valve.Newtonsoft.Json.Linq; #endif namespace MeatKit { [CreateAssetMenu(menuName = "MeatKit/Build Profile")] public class BuildProfile : ScriptableObject, IValidatable { private const string BaseOutputPath = "AssetBundles/"; [Header("Thunderstore Metadata")] public string PackageName = ""; public string Author = ""; public string Version = ""; public Texture2D Icon; public Object ReadMe; public string WebsiteURL = ""; public string Description = ""; public string[] AdditionalDependencies = new string[0]; [Header("Script Options")] public bool StripNamespaces = true; public string[] AdditionalNamespaces = new string[0]; public bool ApplyHarmonyPatches = true; [Header("Export Options")] public BuildItem[] BuildItems = new BuildItem[0]; public BuildAction BuildAction = BuildAction.JustBuildFiles; [HideInInspector] public string OutputProfile = ""; public Dictionary Validate() { var messages = new Dictionary(); // Package name needs to match regex if (!Regex.IsMatch(PackageName, @"^[a-zA-Z_0-9]+$")) messages["PackageName"] = BuildMessage.Error("Package name can only contain letters, numbers, and underscores."); // Author needs to match regex if (!Regex.IsMatch(Author, @"^[a-zA-Z_0-9]+$")) messages["Author"] = BuildMessage.Error("Author can only contain letters, numbers, and underscores."); // Make sure the version number is a valid x.x.x if (!Regex.IsMatch(Version, @"^\d+\.\d+\.\d+$")) messages["Version"] = BuildMessage.Error("Version number must be in format 'x.x.x'."); // Description must be no longer than 250 chars if (Description.Length > 250) messages["Description"] = BuildMessage.Error("Description cannot be longer than 250 characters."); // Icon must exist and be 256 x 256 if (!Icon) messages["Icon"] = BuildMessage.Error("Missing icon."); else if (Icon.width != 256 || Icon.height != 256) messages["Icon"] = BuildMessage.Info("Icon will be resized to 256x256."); if (!ReadMe) messages["ReadMe"] = BuildMessage.Error("Missing readme."); else if (!AssetDatabase.GetAssetPath(ReadMe).EndsWith(".md", StringComparison.InvariantCultureIgnoreCase)) messages["ReadMe"] = BuildMessage.Warning("Are you sure this is a Markdown file? It doesn't have the .md file extension."); switch (BuildAction) { case BuildAction.JustBuildFiles: messages["BuildAction"] = BuildMessage.Info( "This will just create the files for a Thunderstore package in your AssetBundles folder."); break; case BuildAction.CopyToProfile: messages["BuildAction"] = BuildMessage.Info( "This will copy the built files into the plugins folder of the selected profile."); if (string.IsNullOrEmpty(OutputProfile)) messages["OutputProfile"] = BuildMessage.Error("Please set the output profile."); else if (!Directory.Exists(OutputProfile)) messages["OutputProfile"] = BuildMessage.Error("Selected profile no longer exists."); else if (!File.Exists(Path.Combine(OutputProfile, "mods.yml"))) messages["OutputProfile"] = BuildMessage.Error( "Selected folder is not a r2mm profile. Please select the folder which contains the plugins folder."); break; case BuildAction.CreateThunderstorePackage: messages["BuildAction"] = BuildMessage.Info( "This will zip up the built files as a final build for importing into r2mm / uploading to Thunderstore."); break; } return messages; } public bool EnsureValidForEditor() { BuildLog.WriteLine("Starting build profile check..."); // Check ourselves bool hasErrors = false, hasWarnings = false; foreach (var message in Validate().Values) { // Log them switch (message.Type) { case MessageType.Error: Debug.LogError(AssetDatabase.GetAssetPath(this) + ": " + message.Message); hasErrors = true; break; case MessageType.Warning: Debug.LogWarning(AssetDatabase.GetAssetPath(this) + ": " + message.Message); hasWarnings = true; break; } BuildLog.WriteLine(" " + message.Type + ": " + this + ": " + message.Message); } // Go over each build item and check for any validation messages foreach (var item in BuildItems.Where(x => x != null)) foreach (var message in item.Validate().Values) { // Log them switch (message.Type) { case MessageType.Error: Debug.LogError(AssetDatabase.GetAssetPath(item) + ": " + message.Message); hasErrors = true; break; case MessageType.Warning: Debug.LogWarning(AssetDatabase.GetAssetPath(item) + ": " + message.Message); hasWarnings = true; break; } BuildLog.WriteLine(" " + message.Type + ": " + item + ": " + message.Message); } // If there's errors don't let anything continue if (hasErrors) { EditorUtility.DisplayDialog("Build errors", "There were errors validating your build items. Please check the console for more info.", "Ok."); BuildLog.SetCompletionStatus(true, "Build profile failed one or more checks", null); return false; } // If there's only warnings, let the user decide if they want to continue if (hasWarnings) { var continueAnyway = EditorUtility.DisplayDialog("Build warnings", "Some build items validated with warnings. Continue with build anyway?", "Yes", "No"); BuildLog.WriteLine("Build profile has one or more warnings."); BuildLog.WriteLine(" Continue anyway? " + continueAnyway); BuildLog.SetCompletionStatus(true, "User canceled build", null); return continueAnyway; } else { // Otherwise continue BuildLog.WriteLine(" Build profile passed all checks!"); return true; } } public string[] GetRequiredDependencies() { return BuildItems .Where(x => x != null) .SelectMany(x => x.RequiredDependencies) .Distinct().ToArray(); } public string MainNamespace { get { return Author + "." + PackageName; } } public string[] GetRequiredNamespaces() { return new[] {MainNamespace}; } public string[] GetAllAllowedNamespaces() { return GetRequiredNamespaces().Concat(AdditionalNamespaces).ToArray(); } public string ExportPath { get { return Path.Combine(BaseOutputPath, Path.Combine(PackageName, Version)) + Path.DirectorySeparatorChar; } } public void WriteThunderstoreManifest(string location) { #if H3VR_IMPORTED var obj = new JObject(); obj["name"] = PackageName; obj["author"] = Author; obj["version_number"] = Version; obj["description"] = Description; obj["website_url"] = string.IsNullOrEmpty(WebsiteURL) ? "" : WebsiteURL; // ReSharper disable once CoVariantArrayConversion obj["dependencies"] = new JArray(GetRequiredDependencies().Concat(AdditionalDependencies).ToArray()); File.WriteAllText(location, JsonConvert.SerializeObject(obj,Formatting.Indented)); #endif } } public enum BuildAction { JustBuildFiles, CopyToProfile, CreateThunderstorePackage } }