417 lines
16 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|