Deterministic Script Guids. Resolves #691

This commit is contained in:
Jeremy Pritts 2023-01-21 10:46:06 -05:00
parent 7e4e1591ea
commit 8ff9be22d5
3 changed files with 58 additions and 19 deletions

View File

@ -21,13 +21,8 @@ namespace AssetRipper.Export.UnityProjects.Scripts
// find copies in whole project and skip them
Dictionary<MonoScriptInfo, IMonoScript> uniqueDictionary = new();
foreach (IUnityObjectBase asset in script.Collection.Bundle.FetchAssetsInHierarchy())
foreach (IMonoScript assetScript in script.Collection.Bundle.FetchAssetsInHierarchy().OfType<IMonoScript>())
{
if (asset is not IMonoScript assetScript)
{
continue;
}
MonoScriptInfo info = MonoScriptInfo.From(assetScript);
if (uniqueDictionary.TryGetValue(info, out IMonoScript? uniqueScript))
{
@ -80,7 +75,7 @@ namespace AssetRipper.Export.UnityProjects.Scripts
{
if (MonoScriptExtensions.HasNamespace(script.Collection.Version))
{
int fileID = Compute(script.Namespace_C115.String, script.ClassName_C115.String);
int fileID = ComputeScriptFileID(script.Namespace_C115.String, script.ClassName_C115.String);
return new MetaPtr(fileID, UnityEngineGUID, AssetExporter.ToExportType(asset));
}
else
@ -88,18 +83,37 @@ namespace AssetRipper.Export.UnityProjects.Scripts
ScriptIdentifier scriptInfo = script.GetScriptID(AssetExporter.AssemblyManager);
if (!scriptInfo.IsDefault)
{
int fileID = Compute(scriptInfo.Namespace, scriptInfo.Name);
int fileID = ComputeScriptFileID(scriptInfo.Namespace, scriptInfo.Name);
return new MetaPtr(fileID, UnityEngineGUID, AssetExporter.ToExportType(asset));
}
}
}
long exportID = GetExportID(asset);
UnityGUID uniqueGUID = script.GUID;
UnityGUID uniqueGUID = ComputeScriptGuid(script);
return new MetaPtr(exportID, uniqueGUID, AssetExporter.ToExportType(asset));
}
private static int Compute(string @namespace, string name)
/// <summary>
/// Compute a unique hash of a script and use that as the Guid for the script.
/// </summary>
/// <remarks>
/// This is for consistency. Script guid's are random when created in Unity.
/// </remarks>
private static UnityGUID ComputeScriptGuid(IMonoScript script)
{
//The assembly file name without any extension.
ReadOnlySpan<byte> assemblyName = Encoding.UTF8.GetBytes(script.GetAssemblyNameFixed());
return UnityGUID.Md5Hash(assemblyName, script.Namespace_C115.Data, script.ClassName_C115.Data);
}
/// <summary>
/// Compute the FileID of a script inside an assembly.
/// </summary>
/// <remarks>
/// This is a Unity algorithm.
/// </remarks>
private static int ComputeScriptFileID(string @namespace, string name)
{
string toBeHashed = $"s\0\0\0{@namespace}{name}";
using MD4 hash = new();
@ -120,7 +134,7 @@ namespace AssetRipper.Export.UnityProjects.Scripts
IMonoScript script = (IMonoScript)asset;
IMonoImporter importer = MonoImporterFactory.CreateAsset(container.ExportVersion);
importer.ExecutionOrder_C1035 = (short)script.ExecutionOrder_C115;
Meta meta = new Meta(script.GUID, importer);
Meta meta = new Meta(ComputeScriptGuid(script), importer);
ExportMeta(container, meta, path);
}

View File

@ -1,8 +1,8 @@
using AssetRipper.IO.Files;
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
namespace AssetRipper.Tests
namespace AssetRipper.IO.Files.Tests
{
public class GuidTests
{
@ -72,6 +72,21 @@ namespace AssetRipper.Tests
Assert.That(equivalentGuid, Is.EqualTo(originalGuid));
}
[Test]
public void UnityGuidIsTheSameSizeAsSystemGuid()
{
Assert.That(Unsafe.SizeOf<UnityGUID>(), Is.EqualTo(Unsafe.SizeOf<Guid>()));
}
[Test]
public void ToBytesMethod()
{
Guid originalGuid = Guid.NewGuid();
UnityGUID tobyteArrayGuid = new UnityGUID(originalGuid.ToByteArray());
UnityGUID memoryMarshallGuid = new UnityGUID(MemoryMarshal.Cast<Guid, byte>(new ReadOnlySpan<Guid>(in originalGuid)));
Assert.That(memoryMarshallGuid, Is.EqualTo(tobyteArrayGuid));
}
private static string GetLongRandomString(int numSetsOf32Characters = 4)
{
StringBuilder sb = new(numSetsOf32Characters * 32);

View File

@ -143,14 +143,24 @@ namespace AssetRipper.IO.Files
/// <remarks>
/// The returned guid is most likely not "valid" by official standards. However, Unity doesn't seem to care.
/// </remarks>
/// <param name="inputBytes">Input byte array. Can be any length</param>
/// <returns>A stable guid corresponding to the input bytes</returns>
public static UnityGUID Md5Hash(byte[] inputBytes)
/// <param name="input">Input data. Can be any length</param>
/// <returns>A stable guid corresponding to the <paramref name="input"/>.</returns>
public static UnityGUID Md5Hash(ReadOnlySpan<byte> input)
{
byte[] hashBytes = MD5.HashData(inputBytes);
byte[] hashBytes = MD5.HashData(input);
return new UnityGUID(ConvertSystemOrUnityBytes(hashBytes));
}
public static UnityGUID Md5Hash(ReadOnlySpan<byte> assemblyName, ReadOnlySpan<byte> @namespace, ReadOnlySpan<byte> className)
{
int length = assemblyName.Length + @namespace.Length + className.Length;
Span<byte> input = length < 1024 ? stackalloc byte[length] : GC.AllocateUninitializedArray<byte>(length);
assemblyName.CopyTo(input);
@namespace.CopyTo(input.Slice(assemblyName.Length));
className.CopyTo(input.Slice(assemblyName.Length + @namespace.Length));
return Md5Hash(input);
}
public bool IsZero => Data0 == 0 && Data1 == 0 && Data2 == 0 && Data3 == 0;
public uint Data0 { get; set; }
@ -166,7 +176,7 @@ namespace AssetRipper.IO.Files
public static UnityGUID Zero => default;
[ThreadStatic]
private static StringBuilder? s_sb = null;
private static StringBuilder? s_sb;
private static StringBuilder GetStringBuilder()
{