mirror of
https://gitea.tendokyu.moe/Kayori/Medusa.net.git
synced 2026-09-28 01:37:59 +03:00
- Refactor handler dispatch to use a parsed GameModel instead of raw model strings (Handler.cs, HandlerService.cs, new GameModel.cs) - Add a Unity asset-extraction subsystem (Abstractions/Extractors/Unity) - Add WriteEventLogHandler, XrpcDouble serialization type - Rename EaCoin*Handler -> *EaCoinHandler for naming consistency - Add PIN/username update and forgot/reset password flows, with a console-logging IEmailSender until real email is wired up - Add a settings page and a theming pass (theme tokens, tailwind config) - Add plugin nav/route/UI-manifest frontend types
147 lines
5.5 KiB
C#
147 lines
5.5 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Abstractions.Extractors.Unity.Models;
|
|
using AssetsTools.NET;
|
|
|
|
namespace Abstractions.Extractors.Unity.Helpers;
|
|
|
|
/// <summary>Generic AssetTypeValueField tree-walking utilities.</summary>
|
|
public static class ReflectionHelper
|
|
{
|
|
private static readonly string[] NameCandidates = ["name", "m_Name", "m_ClassName", "m_ObjectHideFlags"];
|
|
|
|
/// <summary>UTF-8 decoding that silently drops invalid byte sequences instead of substituting
|
|
/// U+FFFD.</summary>
|
|
public static readonly System.Text.Encoding Utf8IgnoreErrors = System.Text.Encoding.GetEncoding(
|
|
"utf-8",
|
|
System.Text.EncoderFallback.ReplacementFallback,
|
|
new System.Text.DecoderReplacementFallback(""));
|
|
|
|
/// <summary>Converts a deserialized field tree into a plain object graph (Dictionary/List/primitives)
|
|
/// suitable for System.Text.Json.</summary>
|
|
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<T>/Dictionary<K,V> 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<string, object?>();
|
|
foreach (var child in field.Children)
|
|
dict[child.FieldName] = ToPlainObject(child);
|
|
return dict;
|
|
}
|
|
|
|
/// <summary>True if this field is a PPtr (m_FileID/m_PathID pair) pointing at a non-null object.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Reads a top-level string field, returning false if it's absent/empty.</summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>Looks at a handful of well-known name fields before falling back to
|
|
/// "{objType}_{pathId}".</summary>
|
|
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}";
|
|
}
|
|
|
|
/// <summary>Reads the m_StreamData child if present.</summary>
|
|
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 : "",
|
|
};
|
|
}
|
|
}
|