Files
ShrinkModFramework/Editor/Scaffolding/ShrinkInRepoModTemplateGenerator.cs
T
cneicy 39f6f97707
Publish UPM package / publish (push) Successful in 2s
chore(editor): localize mod framework menus
2026-08-26 12:48:32 +08:00

204 lines
7.9 KiB
C#

#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<string, string> 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<string, string>(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<string, string> 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<string>();
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<string, string> 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