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(); } }