using System;
using System.Globalization;
using System.IO;
using System.Text;
namespace Uigenx.Editor
{
///
/// Writes and validates `.meta` files so a re-export never changes an asset's GUID and never
/// breaks a scene reference.
///
///
/// The ScriptedImporter ids below were read from real `.meta` files shipped with Unity 6000.5
/// and from a production project: 452 `.uxml` metas all use 13804, 547 `.uss` metas all use
/// 12385 and 15 `.tss` metas all use 12388, every one of them with the same script GUID.
/// `.uss` and `.tss` carry a trailing `disableValidation: 0` line, `.uxml` does not.
///
public static class UgxGuidPolicy
{
public const string ScriptedImporterScriptGuid = "0000000000000000e000000000000000";
public const int UxmlImporterFileId = 13804;
public const int UssImporterFileId = 12385;
public const int TssImporterFileId = 12388;
public const int FileFormatVersion = 2;
/// Unity writes a trailing space after these keys; matching it keeps diffs empty.
const string EmptyValueSuffix = " ";
public static string MetaPathFor(string assetPath) => assetPath + ".meta";
public static string NewGuid() => Guid.NewGuid().ToString("N");
/// Unity writes lowercase hex. Uppercase is accepted and normalised on the way in.
public static bool IsValidGuid(string guid)
{
if (string.IsNullOrEmpty(guid) || guid.Length != 32) return false;
foreach (var c in guid)
{
bool hex = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f');
if (!hex) return false;
}
return true;
}
/// Lowercases a GUID the daemon may have sent in upper case. Null when unusable.
public static string NormalizeGuid(string guid)
{
if (string.IsNullOrEmpty(guid) || guid.Length != 32) return null;
var lower = guid.ToLowerInvariant();
return IsValidGuid(lower) ? lower : null;
}
public static bool TryReadGuid(string metaPath, out string guid)
{
guid = null;
if (!File.Exists(metaPath)) return false;
foreach (var line in File.ReadLines(metaPath))
{
if (!line.StartsWith("guid:", StringComparison.Ordinal)) continue;
guid = line.Substring(5).Trim();
return IsValidGuid(guid);
}
return false;
}
///
/// Makes sure the asset has a `.meta` with a stable GUID. An existing meta always wins, so
/// re-importing never renumbers an asset that scenes and prefabs already reference.
/// Returns the GUID now in force.
///
public static string EnsureMeta(string assetPath, string desiredGuid)
{
var metaPath = MetaPathFor(assetPath);
if (TryReadGuid(metaPath, out var existing)) return existing;
var guid = NormalizeGuid(desiredGuid) ?? NewGuid();
File.WriteAllText(metaPath, BuildMeta(Path.GetExtension(assetPath), guid), new UTF8Encoding(false));
return guid;
}
///
/// The exact text Unity writes for the importer that owns this extension. Deterministic:
/// the same extension and GUID always produce byte identical output.
///
public static string BuildMeta(string extension, string guid)
{
if (!IsValidGuid(guid)) throw new ArgumentException("guid must be 32 lowercase hex digits", nameof(guid));
var builder = new StringBuilder(320);
builder.Append("fileFormatVersion: ").Append(FileFormatVersion.ToString(CultureInfo.InvariantCulture))
.Append('\n');
builder.Append("guid: ").Append(guid).Append('\n');
switch (Normalize(extension))
{
case ".uxml":
AppendScriptedImporter(builder, UxmlImporterFileId, false);
break;
case ".uss":
AppendScriptedImporter(builder, UssImporterFileId, true);
break;
case ".tss":
AppendScriptedImporter(builder, TssImporterFileId, true);
break;
default:
// Unity fills in the importer block on first import. A two line meta is what
// real project tooling writes and Unity accepts it.
break;
}
return builder.ToString();
}
static void AppendScriptedImporter(StringBuilder builder, int fileId, bool disableValidation)
{
builder.Append("ScriptedImporter:\n");
builder.Append(" internalIDToNameTable: []\n");
builder.Append(" externalObjects: {}\n");
builder.Append(" serializedVersion: 2\n");
builder.Append(" userData:").Append(EmptyValueSuffix).Append('\n');
builder.Append(" assetBundleName:").Append(EmptyValueSuffix).Append('\n');
builder.Append(" assetBundleVariant:").Append(EmptyValueSuffix).Append('\n');
builder.Append(" script: {fileID: ").Append(fileId.ToString(CultureInfo.InvariantCulture))
.Append(", guid: ").Append(ScriptedImporterScriptGuid).Append(", type: 0}\n");
if (disableValidation) builder.Append(" disableValidation: 0\n");
}
///
/// Rewrites the GUID of an asset Unity has already imported. Only for an asset this
/// package just created or recreated: forcing a GUID onto anything else renames a
/// reference the rest of the project may already hold. Returns false when nothing changed.
///
public static bool ForceGuid(string assetPath, string desiredGuid)
{
var guid = NormalizeGuid(desiredGuid);
if (guid == null) return false;
var metaPath = MetaPathFor(assetPath);
if (!File.Exists(metaPath)) return false;
if (TryReadGuid(metaPath, out var existing) && string.Equals(existing, guid, StringComparison.Ordinal))
return false;
var lines = File.ReadAllLines(metaPath);
bool written = false;
for (int i = 0; i < lines.Length; i++)
{
if (!lines[i].StartsWith("guid:", StringComparison.Ordinal)) continue;
lines[i] = "guid: " + guid;
written = true;
break;
}
if (!written) return false;
File.WriteAllText(metaPath, string.Join("\n", lines) + "\n", new UTF8Encoding(false));
return true;
}
/// Writes a folder meta, which Unity needs before it will import the folder's contents.
public static string EnsureFolderMeta(string folderPath, string desiredGuid = null)
{
var metaPath = MetaPathFor(folderPath);
if (TryReadGuid(metaPath, out var existing)) return existing;
var guid = NormalizeGuid(desiredGuid) ?? NewGuid();
var builder = new StringBuilder(160);
builder.Append("fileFormatVersion: ").Append(FileFormatVersion.ToString(CultureInfo.InvariantCulture))
.Append('\n');
builder.Append("guid: ").Append(guid).Append('\n');
builder.Append("folderAsset: yes\n");
builder.Append("DefaultImporter:\n");
builder.Append(" externalObjects: {}\n");
builder.Append(" userData:").Append(EmptyValueSuffix).Append('\n');
builder.Append(" assetBundleName:").Append(EmptyValueSuffix).Append('\n');
builder.Append(" assetBundleVariant:").Append(EmptyValueSuffix).Append('\n');
File.WriteAllText(metaPath, builder.ToString(), new UTF8Encoding(false));
return guid;
}
/// True when the meta names the importer Unity uses for this extension.
public static bool Validate(string assetPath, out string problem)
{
problem = null;
var metaPath = MetaPathFor(assetPath);
if (!File.Exists(metaPath))
{
problem = "no .meta file";
return false;
}
if (!TryReadGuid(metaPath, out _))
{
problem = "the .meta has no valid guid line";
return false;
}
int expected = ExpectedImporterFileId(Path.GetExtension(assetPath));
if (expected == 0) return true;
var text = File.ReadAllText(metaPath);
if (text.Contains("fileID: " + expected.ToString(CultureInfo.InvariantCulture))) return true;
problem = "the .meta does not use ScriptedImporter fileID " + expected;
return false;
}
public static int ExpectedImporterFileId(string extension)
{
switch (Normalize(extension))
{
case ".uxml": return UxmlImporterFileId;
case ".uss": return UssImporterFileId;
case ".tss": return TssImporterFileId;
default: return 0;
}
}
static string Normalize(string extension)
=> string.IsNullOrEmpty(extension) ? string.Empty : extension.ToLowerInvariant();
}
}