Files

341 lines
14 KiB
C#

using System.Collections;
using System.Collections.Concurrent;
using System.Reflection;
namespace Ananta.SDK.Serialization;
/// <summary>
/// Marks a class/struct as a UX RPC contract. Inline contracts do not emit an object marker.
/// TypeMark is used by the few polymorphic 4229938 contracts whose complex marker is a concrete
/// type discriminator (for example WeaponData uses 1 instead of the normal 0xFF marker).
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public sealed class UxContractAttribute : Attribute
{
public bool Inline { get; set; }
public byte TypeMark { get; set; } = 0xFF;
}
public enum UxCountEncoding
{
Int32,
/// <summary>
/// Biased 7-bit varint: the wire carries <c>count + 1</c>. Use for gadget/native-family
/// <b>lists</b> — SyncGadgetGridAOIDecrease(68956797) only renders when its empty sector /
/// index lists are written <c>01 01</c>, and that is live-verified.
/// </summary>
Int7,
/// <summary>
/// Raw 7-bit varint: the wire carries <c>count</c> itself. Use for gadget/native-family
/// <b>dictionaries</b> — MobilePlatformSyncInfo.Players is the measured case: <c>01 01</c>
/// makes the client read one entry that is not there and drop the whole 68705244 push
/// (`ReadBuffer invalid size=8 remain=5`), while <c>01 00</c> parses. The donor handoff
/// contradicts itself on this byte (16D vs 16E/16F); the client log is the authority.
/// </summary>
Raw7
}
public enum UxObjectEncoding
{
Auto,
Complex,
Struct
}
/// <summary>Overrides object framing for one field when the same client type is used both as a struct and complex.</summary>
[AttributeUsage(AttributeTargets.Field)]
public sealed class UxObjectAttribute : Attribute
{
public UxObjectEncoding Encoding { get; set; } = UxObjectEncoding.Auto;
}
/// <summary>Overrides list/dictionary count encoding and nested object framing.</summary>
[AttributeUsage(AttributeTargets.Field)]
public sealed class UxCollectionAttribute : Attribute
{
public UxCountEncoding Count { get; set; } = UxCountEncoding.Int7;
public UxObjectEncoding ItemObjectEncoding { get; set; } = UxObjectEncoding.Auto;
public UxObjectEncoding ValueObjectEncoding { get; set; } = UxObjectEncoding.Auto;
/// <summary>
/// The single "is present" byte written before the count. The aether family treats any
/// non-zero byte as present; the gadget/native family was verified with 0x01 (handoff 16D),
/// so contracts in that family set this explicitly to stay byte-identical to the captures.
/// </summary>
public byte Marker { get; set; } = 0xFF;
}
/// <summary>UX byte buffers are nullable complex values with either Int32 or biased 7-bit lengths.</summary>
[AttributeUsage(AttributeTargets.Field)]
public sealed class UxBufferAttribute : Attribute
{
public UxCountEncoding Count { get; set; } = UxCountEncoding.Int7;
}
/// <summary>
/// Reflection serializer for dump-shaped RPC DTOs. Public fields are serialized by declaration order,
/// allowing RpcTypes to be read side-by-side with RPCSerializeAuto.lua / the IL2CPP dump.
/// </summary>
public static class UxSerializer
{
public static byte[] Serialize<T>(T value)
{
var writer = new UxWriter();
WriteValue(writer, typeof(T), value, null, UxObjectEncoding.Auto);
return writer.ToArray();
}
public static T Deserialize<T>(ReadOnlySpan<byte> body)
{
var reader = new UxReader(body.ToArray());
var value = ReadValue(reader, typeof(T), null, UxObjectEncoding.Auto);
return value is null ? default! : (T)value;
}
private static void WriteValue(
UxWriter writer,
Type type,
object? value,
FieldInfo? field,
UxObjectEncoding objectEncoding)
{
if (type == typeof(byte)) { writer.U8(value is null ? (byte)0 : (byte)value); return; }
if (type == typeof(sbyte)) { writer.U8(unchecked((byte)(value is null ? (sbyte)0 : (sbyte)value))); return; }
if (type == typeof(bool)) { writer.Bool(value is not null && (bool)value); return; }
if (type == typeof(short)) { writer.I16(value is null ? (short)0 : (short)value); return; }
if (type == typeof(ushort)) { writer.U16(value is null ? (ushort)0 : (ushort)value); return; }
if (type == typeof(int)) { writer.I32(value is null ? 0 : (int)value); return; }
if (type == typeof(uint)) { writer.U32(value is null ? 0u : (uint)value); return; }
if (type == typeof(long)) { writer.I64(value is null ? 0L : (long)value); return; }
if (type == typeof(ulong)) { writer.U64(value is null ? 0UL : (ulong)value); return; }
if (type == typeof(float)) { writer.F32(value is null ? 0f : (float)value); return; }
if (type == typeof(double)) { writer.F64(value is null ? 0d : (double)value); return; }
if (type == typeof(string)) { writer.UxString((string?)value); return; }
// byte[] is a UX buffer, not a normal List<byte>.
if (type == typeof(byte[]))
{
WriteBuffer(writer, (byte[]?)value, field);
return;
}
if (type.IsEnum)
{
var underlying = Enum.GetUnderlyingType(type);
WriteValue(
writer,
underlying,
value is null ? Activator.CreateInstance(underlying) : Convert.ChangeType(value, underlying),
field,
UxObjectEncoding.Auto);
return;
}
if (type.IsArray)
{
var collection = (IList?)value;
WriteList(writer, type.GetElementType()!, collection, field);
return;
}
if (type.IsGenericType)
{
var generic = type.GetGenericTypeDefinition();
if (generic == typeof(List<>))
{
WriteList(writer, type.GetGenericArguments()[0], (IList?)value, field);
return;
}
if (generic == typeof(Dictionary<,>))
{
WriteDictionary(writer, type, (IDictionary?)value, field);
return;
}
}
var contract = type.GetCustomAttribute<UxContractAttribute>();
if (contract is null)
throw new NotSupportedException($"{type.FullName} is not a UX contract. Add [UxContract].");
var effectiveEncoding = objectEncoding;
if (effectiveEncoding == UxObjectEncoding.Auto)
effectiveEncoding = contract.Inline ? UxObjectEncoding.Struct : UxObjectEncoding.Complex;
if (effectiveEncoding == UxObjectEncoding.Complex)
{
if (value is null) { writer.U8(0x00); return; }
writer.U8(contract.TypeMark);
}
else if (value is null)
{
// WriteStruct(nullable=false) falls back to a zero/default value in the real client.
value = Activator.CreateInstance(type, nonPublic: true);
}
foreach (var f in PublicFields(type))
{
var fieldEncoding = f.GetCustomAttribute<UxObjectAttribute>()?.Encoding ?? UxObjectEncoding.Auto;
WriteValue(writer, f.FieldType, f.GetValue(value), f, fieldEncoding);
}
}
private static object? ReadValue(
UxReader reader,
Type type,
FieldInfo? field,
UxObjectEncoding objectEncoding)
{
if (type == typeof(byte)) return reader.U8();
if (type == typeof(sbyte)) return unchecked((sbyte)reader.U8());
if (type == typeof(bool)) return reader.Bool();
if (type == typeof(short)) return reader.I16();
if (type == typeof(ushort)) return reader.U16();
if (type == typeof(int)) return reader.I32();
if (type == typeof(uint)) return reader.U32();
if (type == typeof(long)) return reader.I64();
if (type == typeof(ulong)) return reader.U64();
if (type == typeof(float)) return reader.F32();
if (type == typeof(double)) return reader.F64();
if (type == typeof(string)) return reader.UxStringNullable();
if (type == typeof(byte[])) return ReadBuffer(reader, field);
if (type.IsEnum)
return Enum.ToObject(type, ReadValue(reader, Enum.GetUnderlyingType(type), field, UxObjectEncoding.Auto)!);
if (type.IsArray)
{
var list = ReadList(reader, type.GetElementType()!, field);
if (list is null) return null;
var array = Array.CreateInstance(type.GetElementType()!, list.Count);
list.CopyTo(array, 0);
return array;
}
if (type.IsGenericType)
{
var generic = type.GetGenericTypeDefinition();
if (generic == typeof(List<>)) return ReadList(reader, type.GetGenericArguments()[0], field, type);
if (generic == typeof(Dictionary<,>)) return ReadDictionary(reader, type, field);
}
var contract = type.GetCustomAttribute<UxContractAttribute>();
if (contract is null)
throw new NotSupportedException($"{type.FullName} is not a UX contract. Add [UxContract].");
var effectiveEncoding = objectEncoding;
if (effectiveEncoding == UxObjectEncoding.Auto)
effectiveEncoding = contract.Inline ? UxObjectEncoding.Struct : UxObjectEncoding.Complex;
if (effectiveEncoding == UxObjectEncoding.Complex && reader.U8() == 0x00)
return null;
var obj = Activator.CreateInstance(type, nonPublic: true)
?? throw new InvalidOperationException($"Cannot create {type.FullName}");
foreach (var f in PublicFields(type))
{
var fieldEncoding = f.GetCustomAttribute<UxObjectAttribute>()?.Encoding ?? UxObjectEncoding.Auto;
f.SetValue(obj, ReadValue(reader, f.FieldType, f, fieldEncoding));
}
return obj;
}
private static void WriteBuffer(UxWriter writer, byte[]? data, FieldInfo? field)
{
if (data is null) { writer.U8(0x00); return; }
writer.U8(0xFF);
var encoding = field?.GetCustomAttribute<UxBufferAttribute>()?.Count ?? UxCountEncoding.Int7;
if (encoding == UxCountEncoding.Int7) writer.Int7(data.Length); else writer.I32(data.Length);
writer.Raw(data);
}
private static byte[]? ReadBuffer(UxReader reader, FieldInfo? field)
{
if (reader.U8() == 0x00) return null;
var encoding = field?.GetCustomAttribute<UxBufferAttribute>()?.Count ?? UxCountEncoding.Int7;
var count = encoding == UxCountEncoding.Int7 ? reader.Int7() : reader.I32();
return reader.Bytes(count);
}
private static void WriteList(UxWriter writer, Type itemType, IList? list, FieldInfo? field)
{
var marker = field?.GetCustomAttribute<UxCollectionAttribute>()?.Marker ?? (byte)0xFF;
if (list is null) { writer.U8(0x00); return; }
writer.U8(marker);
WriteCount(writer, list.Count, field);
var itemEncoding = field?.GetCustomAttribute<UxCollectionAttribute>()?.ItemObjectEncoding ?? UxObjectEncoding.Auto;
foreach (var item in list)
WriteValue(writer, itemType, item, null, itemEncoding);
}
private static IList? ReadList(UxReader reader, Type itemType, FieldInfo? field, Type? concreteType = null)
{
if (reader.U8() == 0x00) return null;
var count = ReadCount(reader, field);
var listType = concreteType ?? typeof(List<>).MakeGenericType(itemType);
var list = (IList)(Activator.CreateInstance(listType) ?? throw new InvalidOperationException($"Cannot create {listType}"));
var itemEncoding = field?.GetCustomAttribute<UxCollectionAttribute>()?.ItemObjectEncoding ?? UxObjectEncoding.Auto;
for (var i = 0; i < count; i++) list.Add(ReadValue(reader, itemType, null, itemEncoding));
return list;
}
private static void WriteDictionary(UxWriter writer, Type dictType, IDictionary? dict, FieldInfo? field)
{
var marker = field?.GetCustomAttribute<UxCollectionAttribute>()?.Marker ?? (byte)0xFF;
if (dict is null) { writer.U8(0x00); return; }
writer.U8(marker);
WriteCount(writer, dict.Count, field, defaultEncoding: UxCountEncoding.Int32);
var args = dictType.GetGenericArguments();
var valueEncoding = field?.GetCustomAttribute<UxCollectionAttribute>()?.ValueObjectEncoding ?? UxObjectEncoding.Auto;
foreach (DictionaryEntry item in dict)
{
WriteValue(writer, args[0], item.Key, null, UxObjectEncoding.Auto);
WriteValue(writer, args[1], item.Value, null, valueEncoding);
}
}
private static object? ReadDictionary(UxReader reader, Type dictType, FieldInfo? field)
{
if (reader.U8() == 0x00) return null;
var count = ReadCount(reader, field, defaultEncoding: UxCountEncoding.Int32);
var dict = (IDictionary)(Activator.CreateInstance(dictType) ?? throw new InvalidOperationException($"Cannot create {dictType}"));
var args = dictType.GetGenericArguments();
var valueEncoding = field?.GetCustomAttribute<UxCollectionAttribute>()?.ValueObjectEncoding ?? UxObjectEncoding.Auto;
for (var i = 0; i < count; i++)
dict.Add(
ReadValue(reader, args[0], null, UxObjectEncoding.Auto)!,
ReadValue(reader, args[1], null, valueEncoding));
return dict;
}
private static void WriteCount(UxWriter writer, int count, FieldInfo? field, UxCountEncoding defaultEncoding = UxCountEncoding.Int7)
{
var encoding = field?.GetCustomAttribute<UxCollectionAttribute>()?.Count ?? defaultEncoding;
switch (encoding)
{
case UxCountEncoding.Int7: writer.Int7(count); break;
case UxCountEncoding.Raw7: writer.Raw7(count); break;
default: writer.I32(count); break;
}
}
private static int ReadCount(UxReader reader, FieldInfo? field, UxCountEncoding defaultEncoding = UxCountEncoding.Int7)
{
var encoding = field?.GetCustomAttribute<UxCollectionAttribute>()?.Count ?? defaultEncoding;
return encoding switch
{
UxCountEncoding.Int7 => reader.Int7(),
UxCountEncoding.Raw7 => reader.Raw7(),
_ => reader.I32(),
};
}
private static readonly ConcurrentDictionary<Type, FieldInfo[]> FieldCache = new();
private static FieldInfo[] PublicFields(Type type)
=> FieldCache.GetOrAdd(type, static t =>
t.GetFields(BindingFlags.Instance | BindingFlags.Public)
.OrderBy(f => f.MetadataToken)
.ToArray());
}