using System.Collections.Generic;
using System.Linq;
using Abstractions.Extractors.Unity.Models;
using AssetsTools.NET;
namespace Abstractions.Extractors.Unity.Helpers;
/// Generic AssetTypeValueField tree-walking utilities.
public static class ReflectionHelper
{
private static readonly string[] NameCandidates = ["name", "m_Name", "m_ClassName", "m_ObjectHideFlags"];
/// UTF-8 decoding that silently drops invalid byte sequences instead of substituting
/// U+FFFD.
public static readonly System.Text.Encoding Utf8IgnoreErrors = System.Text.Encoding.GetEncoding(
"utf-8",
System.Text.EncoderFallback.ReplacementFallback,
new System.Text.DecoderReplacementFallback(""));
/// Converts a deserialized field tree into a plain object graph (Dictionary/List/primitives)
/// suitable for System.Text.Json.
public static object? ToPlainObject(AssetTypeValueField field)
{
var value = field.Value;
if (value is not null && value.ValueType != AssetValueType.None)
{
switch (value.ValueType)
{
case AssetValueType.Array:
return field.Children.Select(ToPlainObject).ToList();
case AssetValueType.ByteArray:
return field.AsByteArray;
case AssetValueType.String:
return field.AsString;
case AssetValueType.Bool:
return field.AsBool;
case AssetValueType.Int8:
return field.AsSByte;
case AssetValueType.UInt8:
return field.AsByte;
case AssetValueType.Int16:
return field.AsShort;
case AssetValueType.UInt16:
return field.AsUShort;
case AssetValueType.Int32:
return field.AsInt;
case AssetValueType.UInt32:
return field.AsUInt;
case AssetValueType.Int64:
return field.AsLong;
case AssetValueType.UInt64:
return field.AsULong;
case AssetValueType.Float:
return field.AsFloat;
case AssetValueType.Double:
return field.AsDouble;
default:
return field.AsObject;
}
}
if (field.Children.Count == 0) return null;
// A "vector"/"map" wrapper (List/Dictionary in the original C# class) has exactly one
// child literally named "Array" holding the elements - unwrap it so it comes out as a plain
// JSON array/list of pairs instead of an extra {"Array": [...]} nesting level.
if (field.Children.Count == 1 && field.Children[0].FieldName == "Array")
return ToPlainObject(field.Children[0]);
var dict = new Dictionary();
foreach (var child in field.Children)
dict[child.FieldName] = ToPlainObject(child);
return dict;
}
/// True if this field is a PPtr (m_FileID/m_PathID pair) pointing at a non-null object.
public static bool TryGetPPtr(AssetTypeValueField field, out long pathId, out int fileId)
{
pathId = 0;
fileId = 0;
var pathIdField = field.Get("m_PathID");
if (pathIdField.IsDummy) return false;
pathId = pathIdField.AsLong;
if (pathId == 0) return false;
var fileIdField = field.Get("m_FileID");
fileId = fileIdField.IsDummy ? 0 : fileIdField.AsInt;
return true;
}
/// Reads a top-level string field, returning false if it's absent/empty.
public static bool TryGetNonEmptyString(AssetTypeValueField field, string name, out string value)
{
value = "";
var child = field.Get(name);
if (child.IsDummy) return false;
if (child.Value?.ValueType == AssetValueType.String)
{
value = child.AsString.Trim();
}
else if (child.Value?.ValueType == AssetValueType.ByteArray)
{
value = System.Text.Encoding.UTF8.GetString(child.AsByteArray).Trim();
}
else
{
return false;
}
return !string.IsNullOrEmpty(value);
}
/// Looks at a handful of well-known name fields before falling back to
/// "{objType}_{pathId}".
public static string GetObjectName(AssetTypeValueField baseField, string objType, long pathId)
{
foreach (var candidate in NameCandidates)
{
if (TryGetNonEmptyString(baseField, candidate, out var name))
return FileNameSanitizer.Sanitize(name);
}
var selfPathId = baseField.Get("m_PathID");
if (!selfPathId.IsDummy && selfPathId.AsLong != 0)
return $"Object_{selfPathId.AsLong}";
return $"{objType}_{pathId}";
}
/// Reads the m_StreamData child if present.
public static StreamingInfo? TryGetStreamingInfo(AssetTypeValueField baseField)
{
var streamData = baseField.Get("m_StreamData");
if (streamData.IsDummy) return null;
return new StreamingInfo
{
Offset = streamData.Get("offset") is { IsDummy: false } offset ? offset.AsLong : 0,
Size = streamData.Get("size") is { IsDummy: false } size ? size.AsLong : 0,
Path = streamData.Get("path") is { IsDummy: false } path ? path.AsString : "",
};
}
}