#if UNITY_EDITOR #nullable enable using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using UnityEditor; using UnityEngine; public static class ShrinkInRepoModTemplateGenerator { private const string GenerateMenuPath = "ShrinkSDK/模组/生成仓库内模板(推荐)"; private const string TemplateRelativePath = "Assets/Modules/ShrinkModFramework/Editor/Scaffolding/InRepoModTemplate"; private const string DefaultOutputRootRelativePath = "Assets/GeneratedMods"; private const string TemplateStampFileName = ".shrink-inrepo-mod-template.json"; private const string TemplateManifestFileName = ".shrink-inrepo-mod-template-files.txt"; [MenuItem(GenerateMenuPath)] public static void GenerateTemplate() { try { var context = BuildContext(); if (context == null) return; CopyTemplateProject(context.Value.TemplateRoot, context.Value.OutputRoot, context.Value.Replacements); AssetDatabase.Refresh(); UnityEngine.Debug.Log($"[ShrinkModFramework] 已生成仓库内模组模板:{context.Value.OutputRoot}"); } catch (Exception ex) { UnityEngine.Debug.LogError($"[ShrinkModFramework] 生成仓库内模组模板失败:{ex.Message}"); UnityEngine.Debug.LogException(ex); } } private static (string TemplateRoot, string OutputRoot, Dictionary Replacements)? BuildContext() { var projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); var templateRoot = Path.Combine(projectRoot, TemplateRelativePath.Replace('/', Path.DirectorySeparatorChar)); if (!Directory.Exists(templateRoot)) throw new DirectoryNotFoundException($"未找到仓库内模组模板目录:{templateRoot}"); var defaultOutputRoot = Path.Combine(projectRoot, DefaultOutputRootRelativePath.Replace('/', Path.DirectorySeparatorChar)); Directory.CreateDirectory(defaultOutputRoot); var outputRoot = EditorUtility.SaveFolderPanel( "选择仓库内模组模板输出目录", defaultOutputRoot, "SampleShrinkMod"); if (string.IsNullOrWhiteSpace(outputRoot)) return null; outputRoot = Path.GetFullPath(outputRoot); var assetsRoot = Path.GetFullPath(Application.dataPath); if (!outputRoot.StartsWith(assetsRoot, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("仓库内模组模板必须生成到当前项目的 Assets 目录下。"); var directoryName = new DirectoryInfo(outputRoot).Name; var projectName = SanitizeProjectName(directoryName); var rootNamespace = projectName; var displayName = projectName; var modId = BuildDefaultModId(projectName); var commandRoot = ToCommandToken(projectName); var asmdefName = projectName + ".Runtime"; var assetRelativeOutput = "Assets" + outputRoot.Substring(assetsRoot.Length).Replace('\\', '/'); var replacements = new Dictionary(StringComparer.Ordinal) { ["__PROJECT_NAME__"] = projectName, ["__ROOT_NAMESPACE__"] = rootNamespace, ["__DISPLAY_NAME__"] = displayName, ["__MOD_ID__"] = modId, ["__COMMAND_ROOT__"] = commandRoot, ["__ASMDEF_NAME__"] = asmdefName, ["__ASSET_RELATIVE_OUTPUT__"] = assetRelativeOutput }; return (templateRoot, outputRoot, replacements); } private static void CopyTemplateProject(string templateRoot, string outputRoot, IReadOnlyDictionary replacements) { Directory.CreateDirectory(outputRoot); var stampPath = Path.Combine(outputRoot, TemplateStampFileName); if (Directory.EnumerateFileSystemEntries(outputRoot).Any() && !File.Exists(stampPath)) { throw new InvalidOperationException( $"目标目录已存在且不是 ShrinkModFramework 自动生成的仓库内模组模板,为避免覆盖人工修改,已中止:{outputRoot}"); } var managedFiles = new List(); foreach (var templateFile in Directory.EnumerateFiles(templateRoot, "*", SearchOption.AllDirectories)) { var relativePath = Path.GetRelativePath(templateRoot, templateFile); relativePath = ReplaceTokens(relativePath, replacements); var outputRelativePath = relativePath.EndsWith(".txt", StringComparison.Ordinal) ? relativePath[..^".txt".Length] : relativePath; managedFiles.Add(NormalizePath(outputRelativePath)); var outputFile = Path.Combine(outputRoot, outputRelativePath); var outputDir = Path.GetDirectoryName(outputFile); if (!string.IsNullOrWhiteSpace(outputDir)) Directory.CreateDirectory(outputDir); var content = File.ReadAllText(templateFile, Encoding.UTF8); content = ReplaceTokens(content, replacements); File.WriteAllText(outputFile, content, new UTF8Encoding(false)); } var manifestPath = Path.Combine(outputRoot, TemplateManifestFileName); File.WriteAllLines(manifestPath, managedFiles.OrderBy(item => item, StringComparer.Ordinal), new UTF8Encoding(false)); var stamp = new StringBuilder(); stamp.AppendLine("{"); stamp.AppendLine(@" ""generatedBy"": ""ShrinkInRepoModTemplateGenerator"","); stamp.AppendLine($@" ""generatedAt"": ""{DateTime.UtcNow:O}"""); stamp.AppendLine("}"); File.WriteAllText(stampPath, stamp.ToString(), new UTF8Encoding(false)); } private static string ReplaceTokens(string input, IReadOnlyDictionary replacements) { var result = input; foreach (var pair in replacements) result = result.Replace(pair.Key, pair.Value); return result; } private static string SanitizeProjectName(string rawName) { if (string.IsNullOrWhiteSpace(rawName)) return "SampleShrinkMod"; var builder = new StringBuilder(); var capitalizeNext = true; foreach (var ch in rawName) { if (char.IsLetterOrDigit(ch)) { builder.Append(capitalizeNext ? char.ToUpperInvariant(ch) : ch); capitalizeNext = false; } else { capitalizeNext = true; } } if (builder.Length == 0) builder.Append("SampleShrinkMod"); if (char.IsDigit(builder[0])) builder.Insert(0, 'M'); return builder.ToString(); } private static string BuildDefaultModId(string projectName) { var token = ToKebabCase(projectName); return string.IsNullOrWhiteSpace(token) ? "example.sample-mod" : $"example.{token}"; } private static string ToCommandToken(string projectName) { var token = ToKebabCase(projectName).Replace("-", "_"); return string.IsNullOrWhiteSpace(token) ? "sample_mod" : token; } private static string ToKebabCase(string value) { if (string.IsNullOrWhiteSpace(value)) return string.Empty; var builder = new StringBuilder(); for (var i = 0; i < value.Length; i++) { var ch = value[i]; if (!char.IsLetterOrDigit(ch)) { if (builder.Length > 0 && builder[^1] != '-') builder.Append('-'); continue; } if (char.IsUpper(ch) && i > 0 && builder.Length > 0 && builder[^1] != '-') builder.Append('-'); builder.Append(char.ToLowerInvariant(ch)); } return builder.ToString().Trim('-'); } private static string NormalizePath(string path) => path.Replace('\\', '/'); } #endif