Files
aibis-dream/Assets/Editor/Localization/LocalizedObjSpriteValidator.cs
T

417 lines
16 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AibisDream.UI;
using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Settings;
using UnityEditor.AddressableAssets.Settings.GroupSchemas;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
using UnityEngine;
namespace AibisDream.EditorTools.Localization
{
internal readonly struct LocalizedObjSpriteDirectory
{
internal LocalizedObjSpriteDirectory(
string localeCode,
string folderName,
string addressRoot,
string groupName)
{
LocaleCode = localeCode;
FolderName = folderName;
AddressRoot = addressRoot;
GroupName = groupName;
}
internal string LocaleCode { get; }
internal string FolderName { get; }
internal string AddressRoot { get; }
internal string GroupName { get; }
}
public readonly struct LocalizedObjSpriteValidationIssue
{
public LocalizedObjSpriteValidationIssue(string assetPath, string message)
{
AssetPath = assetPath;
Message = message;
}
public string AssetPath { get; }
public string Message { get; }
public override string ToString()
{
return $"{AssetPath}: {Message}";
}
}
/// <summary>
/// Validates locale-specific Obj folders without depending on Yarn references or
/// Unity Localization asset tables. A logical image becomes localized as soon as
/// it appears in any non-Chinese locale folder.
/// </summary>
public static class LocalizedObjSpriteValidator
{
internal const string DefaultArtRoot = "Assets/RawResources/Art";
internal const string DefaultFolderName = "Obj";
internal const string DefaultGroupName = "Shared_UI";
internal static readonly LocalizedObjSpriteDirectory[] LocaleDirectories =
{
new("en", "Obj_en", "Sprite/Obj_en", "Shared_ObjEn"),
new("ja-JP", "Obj_ja-JP", "Sprite/Obj_ja-JP", "Shared_ObjJaJp"),
new("ru", "Obj_ru", "Sprite/Obj_ru", "Shared_ObjRu"),
new("es", "Obj_es", "Sprite/Obj_es", "Shared_ObjEs"),
new("pt-BR", "Obj_pt-BR", "Sprite/Obj_pt-BR", "Shared_ObjPtBr")
};
private const string ValidateMenuPath =
"Tools/AIBIS/Localization/Validate Localized Obj Sprites";
private const string ConfigureMenuPath =
"Tools/AIBIS/Localization/Configure Localized Obj Addressables";
[MenuItem(ValidateMenuPath)]
public static void ValidateFromMenu()
{
IReadOnlyList<LocalizedObjSpriteValidationIssue> issues = Validate();
LogIssues(issues);
if (issues.Count == 0)
{
Debug.Log(
"[LocalizedObjSpriteValidator] Validation passed. " +
"All detected localized Obj sprite groups and Addressables are complete.");
}
}
public static IReadOnlyList<LocalizedObjSpriteValidationIssue> Validate(
string artRoot = DefaultArtRoot,
bool validateAddressables = true)
{
var issues = new List<LocalizedObjSpriteValidationIssue>();
if (!Directory.Exists(artRoot))
{
issues.Add(new LocalizedObjSpriteValidationIssue(
ToAssetPath(artRoot),
"Art root does not exist."));
return issues;
}
string defaultDirectory = Path.Combine(artRoot, DefaultFolderName);
ValidateDirectoryExists(
defaultDirectory,
LocalizedObjSpriteNaming.DefaultLocaleCode,
issues);
var localizedRelativePaths = new HashSet<string>(
StringComparer.OrdinalIgnoreCase);
foreach (LocalizedObjSpriteDirectory directory in LocaleDirectories)
{
string localeDirectory = Path.Combine(artRoot, directory.FolderName);
if (!ValidateDirectoryExists(localeDirectory, directory.LocaleCode, issues))
continue;
foreach (string filePath in Directory.EnumerateFiles(
localeDirectory,
"*.png",
SearchOption.AllDirectories))
{
localizedRelativePaths.Add(GetRelativePath(localeDirectory, filePath));
}
}
foreach (string relativePath in localizedRelativePaths.OrderBy(
path => path,
StringComparer.OrdinalIgnoreCase))
{
ValidateRequiredFile(
Path.Combine(defaultDirectory, relativePath),
LocalizedObjSpriteNaming.DefaultLocaleCode,
issues);
foreach (LocalizedObjSpriteDirectory directory in LocaleDirectories)
{
ValidateRequiredFile(
Path.Combine(artRoot, directory.FolderName, relativePath),
directory.LocaleCode,
issues);
}
}
if (validateAddressables)
ValidateAddressableConfiguration(issues);
return issues;
}
[MenuItem(ConfigureMenuPath)]
public static void ConfigureAddressables()
{
AddressableAssetSettings settings =
AddressableAssetSettingsDefaultObject.Settings;
if (settings == null)
throw new InvalidOperationException("AddressableAssetSettings does not exist.");
foreach (LocalizedObjSpriteDirectory directory in LocaleDirectories)
{
string assetPath = $"{DefaultArtRoot}/{directory.FolderName}";
EnsureAssetFolder(assetPath);
AddressableAssetGroup group = settings.FindGroup(directory.GroupName);
if (group == null)
{
group = settings.CreateGroup(
directory.GroupName,
false,
false,
true,
null,
typeof(BundledAssetGroupSchema),
typeof(ContentUpdateGroupSchema));
}
ConfigureGroup(settings, group);
string guid = AssetDatabase.AssetPathToGUID(assetPath);
AddressableAssetEntry entry =
settings.CreateOrMoveEntry(guid, group, false, false);
entry.address = directory.AddressRoot;
}
settings.SetDirty(
AddressableAssetSettings.ModificationEvent.BatchModification,
null,
true,
true);
EditorUtility.SetDirty(settings);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log(
"[LocalizedObjSpriteValidator] Localized Obj folders and " +
"Addressable groups configured.");
}
public static void ConfigureFromCommandLine()
{
ConfigureAddressables();
}
internal static void LogIssues(
IReadOnlyList<LocalizedObjSpriteValidationIssue> issues)
{
foreach (LocalizedObjSpriteValidationIssue issue in issues)
Debug.LogError($"[LocalizedObjSpriteValidator] {issue}");
}
private static void ConfigureGroup(
AddressableAssetSettings settings,
AddressableAssetGroup group)
{
BundledAssetGroupSchema bundled =
group.GetSchema<BundledAssetGroupSchema>() ??
group.AddSchema<BundledAssetGroupSchema>();
bundled.BuildPath.SetVariableByName(
settings,
AddressableAssetSettings.kLocalBuildPath);
bundled.LoadPath.SetVariableByName(
settings,
AddressableAssetSettings.kLocalLoadPath);
bundled.IncludeInBuild = true;
bundled.Compression = BundledAssetGroupSchema.BundleCompressionMode.LZ4;
bundled.BundleMode = BundledAssetGroupSchema.BundlePackingMode.PackTogether;
EditorUtility.SetDirty(bundled);
ContentUpdateGroupSchema contentUpdate =
group.GetSchema<ContentUpdateGroupSchema>() ??
group.AddSchema<ContentUpdateGroupSchema>();
contentUpdate.StaticContent = false;
EditorUtility.SetDirty(contentUpdate);
EditorUtility.SetDirty(group);
}
private static void ValidateAddressableConfiguration(
ICollection<LocalizedObjSpriteValidationIssue> issues)
{
AddressableAssetSettings settings =
AddressableAssetSettingsDefaultObject.Settings;
if (settings == null)
{
issues.Add(new LocalizedObjSpriteValidationIssue(
"Assets/AddressableAssetsData",
"AddressableAssetSettings does not exist."));
return;
}
ValidateAddressableFolder(
settings,
$"{DefaultArtRoot}/{DefaultFolderName}",
LocalizedObjSpriteNaming.DefaultAddressRoot,
DefaultGroupName,
false,
issues);
foreach (LocalizedObjSpriteDirectory directory in LocaleDirectories)
{
ValidateAddressableFolder(
settings,
$"{DefaultArtRoot}/{directory.FolderName}",
directory.AddressRoot,
directory.GroupName,
true,
issues);
}
}
private static void ValidateAddressableFolder(
AddressableAssetSettings settings,
string assetPath,
string expectedAddress,
string expectedGroup,
bool validateGroupSchema,
ICollection<LocalizedObjSpriteValidationIssue> issues)
{
string guid = AssetDatabase.AssetPathToGUID(assetPath);
AddressableAssetEntry entry = string.IsNullOrEmpty(guid)
? null
: settings.FindAssetEntry(guid);
if (entry == null)
{
issues.Add(new LocalizedObjSpriteValidationIssue(
assetPath,
"Folder is not registered as an Addressable entry."));
return;
}
if (!string.Equals(entry.address, expectedAddress, StringComparison.Ordinal))
{
issues.Add(new LocalizedObjSpriteValidationIssue(
assetPath,
$"Address is '{entry.address}', expected '{expectedAddress}'."));
}
AddressableAssetGroup group = entry.parentGroup;
if (group == null || !string.Equals(
group.Name,
expectedGroup,
StringComparison.Ordinal))
{
issues.Add(new LocalizedObjSpriteValidationIssue(
assetPath,
$"Group is '{group?.Name ?? "<null>"}', expected '{expectedGroup}'."));
return;
}
if (!validateGroupSchema)
return;
BundledAssetGroupSchema bundled = group.GetSchema<BundledAssetGroupSchema>();
if (bundled == null ||
!bundled.IncludeInBuild ||
bundled.Compression != BundledAssetGroupSchema.BundleCompressionMode.LZ4 ||
bundled.BundleMode != BundledAssetGroupSchema.BundlePackingMode.PackTogether ||
bundled.BuildPath.GetName(settings) != AddressableAssetSettings.kLocalBuildPath ||
bundled.LoadPath.GetName(settings) != AddressableAssetSettings.kLocalLoadPath)
{
issues.Add(new LocalizedObjSpriteValidationIssue(
assetPath,
"Addressable group must use local paths, LZ4, Include In Build, and Pack Together."));
}
ContentUpdateGroupSchema contentUpdate =
group.GetSchema<ContentUpdateGroupSchema>();
if (contentUpdate == null || contentUpdate.StaticContent)
{
issues.Add(new LocalizedObjSpriteValidationIssue(
assetPath,
"Addressable group must use non-static content update settings."));
}
}
private static bool ValidateDirectoryExists(
string directoryPath,
string localeCode,
ICollection<LocalizedObjSpriteValidationIssue> issues)
{
if (Directory.Exists(directoryPath))
return true;
issues.Add(new LocalizedObjSpriteValidationIssue(
ToAssetPath(directoryPath),
$"Missing required '{localeCode}' Obj directory."));
return false;
}
private static void ValidateRequiredFile(
string filePath,
string localeCode,
ICollection<LocalizedObjSpriteValidationIssue> issues)
{
if (File.Exists(filePath))
return;
issues.Add(new LocalizedObjSpriteValidationIssue(
ToAssetPath(filePath),
$"Missing required '{localeCode}' sprite variant."));
}
private static string GetRelativePath(string rootPath, string filePath)
{
string normalizedRoot = Path.GetFullPath(rootPath)
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) +
Path.DirectorySeparatorChar;
string normalizedFile = Path.GetFullPath(filePath);
return normalizedFile.Substring(normalizedRoot.Length);
}
private static void EnsureAssetFolder(string path)
{
string[] segments = path.Split('/');
string current = segments[0];
for (int index = 1; index < segments.Length; index++)
{
string next = $"{current}/{segments[index]}";
if (!AssetDatabase.IsValidFolder(next))
AssetDatabase.CreateFolder(current, segments[index]);
current = next;
}
}
private static string ToAssetPath(string path)
{
return path.Replace('\\', '/');
}
}
public sealed class LocalizedObjSpriteBuildValidator : IPreprocessBuildWithReport
{
// Run before build preprocessors that mutate version information.
public int callbackOrder => -1000;
public void OnPreprocessBuild(BuildReport report)
{
IReadOnlyList<LocalizedObjSpriteValidationIssue> issues =
LocalizedObjSpriteValidator.Validate();
if (issues.Count == 0)
return;
LocalizedObjSpriteValidator.LogIssues(issues);
string summary = string.Join(
Environment.NewLine,
issues.Take(20).Select(issue => issue.ToString()));
if (issues.Count > 20)
{
summary += Environment.NewLine +
$"... and {issues.Count - 20} more issue(s).";
}
throw new BuildFailedException(
"Localized Obj sprite validation failed:" +
Environment.NewLine + summary);
}
}
}