// MIT License - Copyright (c) 2025 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE
namespace WallstopStudios.UnityHelpers.Core.Serialization
{
using System;
using ProtoBuf;
using ProtoBuf.Meta;
using UnityEngine;
// Surrogates allow protobuf-net to serialize Unity structs we cannot annotate directly.
[ProtoContract]
internal struct Vector2Surrogate
{
[ProtoMember(1)]
public float x;
[ProtoMember(2)]
public float y;
public static implicit operator Vector2Surrogate(Vector2 v) => new() { x = v.x, y = v.y };
public static implicit operator Vector2(Vector2Surrogate s) => new(s.x, s.y);
}
[ProtoContract]
internal struct Vector3Surrogate
{
[ProtoMember(1)]
public float x;
[ProtoMember(2)]
public float y;
[ProtoMember(3)]
public float z;
public static implicit operator Vector3Surrogate(Vector3 v) =>
new()
{
x = v.x,
y = v.y,
z = v.z,
};
public static implicit operator Vector3(Vector3Surrogate s) => new(s.x, s.y, s.z);
}
[ProtoContract]
internal struct QuaternionSurrogate
{
[ProtoMember(1)]
public float x;
[ProtoMember(2)]
public float y;
[ProtoMember(3)]
public float z;
[ProtoMember(4)]
public float w;
public static implicit operator QuaternionSurrogate(Quaternion q) =>
new()
{
x = q.x,
y = q.y,
z = q.z,
w = q.w,
};
public static implicit operator Quaternion(QuaternionSurrogate s) =>
new(s.x, s.y, s.z, s.w);
}
[ProtoContract]
internal struct ColorSurrogate
{
[ProtoMember(1)]
public float r;
[ProtoMember(2)]
public float g;
[ProtoMember(3)]
public float b;
[ProtoMember(4)]
public float a;
public static implicit operator ColorSurrogate(Color c) =>
new()
{
r = c.r,
g = c.g,
b = c.b,
a = c.a,
};
public static implicit operator Color(ColorSurrogate s) => new(s.r, s.g, s.b, s.a);
}
[ProtoContract]
internal struct Color32Surrogate
{
[ProtoMember(1)]
public byte r;
[ProtoMember(2)]
public byte g;
[ProtoMember(3)]
public byte b;
[ProtoMember(4)]
public byte a;
public static implicit operator Color32Surrogate(Color32 c) =>
new()
{
r = c.r,
g = c.g,
b = c.b,
a = c.a,
};
public static implicit operator Color32(Color32Surrogate s) => new(s.r, s.g, s.b, s.a);
}
[ProtoContract]
internal struct RectSurrogate
{
[ProtoMember(1)]
public float x;
[ProtoMember(2)]
public float y;
[ProtoMember(3)]
public float width;
[ProtoMember(4)]
public float height;
public static implicit operator RectSurrogate(Rect r) =>
new()
{
x = r.x,
y = r.y,
width = r.width,
height = r.height,
};
public static implicit operator Rect(RectSurrogate s) => new(s.x, s.y, s.width, s.height);
}
[ProtoContract]
internal struct RectIntSurrogate
{
[ProtoMember(1)]
public int x;
[ProtoMember(2)]
public int y;
[ProtoMember(3)]
public int width;
[ProtoMember(4)]
public int height;
public static implicit operator RectIntSurrogate(RectInt r) =>
new()
{
x = r.x,
y = r.y,
width = r.width,
height = r.height,
};
public static implicit operator RectInt(RectIntSurrogate s) =>
new(s.x, s.y, s.width, s.height);
}
[ProtoContract]
internal struct BoundsSurrogate
{
[ProtoMember(1)]
public float cx;
[ProtoMember(2)]
public float cy;
[ProtoMember(3)]
public float cz;
[ProtoMember(4)]
public float sx;
[ProtoMember(5)]
public float sy;
[ProtoMember(6)]
public float sz;
public static implicit operator BoundsSurrogate(Bounds b) =>
new()
{
cx = b.center.x,
cy = b.center.y,
cz = b.center.z,
sx = b.size.x,
sy = b.size.y,
sz = b.size.z,
};
public static implicit operator Bounds(BoundsSurrogate s) =>
new(new Vector3(s.cx, s.cy, s.cz), new Vector3(s.sx, s.sy, s.sz));
}
[ProtoContract]
internal struct BoundsIntSurrogate
{
[ProtoMember(1)]
public int px;
[ProtoMember(2)]
public int py;
[ProtoMember(3)]
public int pz;
[ProtoMember(4)]
public int sx;
[ProtoMember(5)]
public int sy;
[ProtoMember(6)]
public int sz;
public static implicit operator BoundsIntSurrogate(BoundsInt b) =>
new()
{
px = b.position.x,
py = b.position.y,
pz = b.position.z,
sx = b.size.x,
sy = b.size.y,
sz = b.size.z,
};
public static implicit operator BoundsInt(BoundsIntSurrogate s) =>
new(new Vector3Int(s.px, s.py, s.pz), new Vector3Int(s.sx, s.sy, s.sz));
}
[ProtoContract]
internal struct Vector2IntSurrogate
{
[ProtoMember(1)]
public int x;
[ProtoMember(2)]
public int y;
public static implicit operator Vector2IntSurrogate(Vector2Int v) =>
new() { x = v.x, y = v.y };
public static implicit operator Vector2Int(Vector2IntSurrogate s) => new(s.x, s.y);
}
[ProtoContract]
internal struct Vector3IntSurrogate
{
[ProtoMember(1)]
public int x;
[ProtoMember(2)]
public int y;
[ProtoMember(3)]
public int z;
public static implicit operator Vector3IntSurrogate(Vector3Int v) =>
new()
{
x = v.x,
y = v.y,
z = v.z,
};
public static implicit operator Vector3Int(Vector3IntSurrogate s) => new(s.x, s.y, s.z);
}
[ProtoContract]
internal struct ResolutionSurrogate
{
[ProtoMember(1)]
public int width;
[ProtoMember(2)]
public int height;
[ProtoMember(3)]
public int refreshRate;
[Obsolete("Obsolete")]
public static implicit operator ResolutionSurrogate(Resolution r) =>
new()
{
width = r.width,
height = r.height,
refreshRate = r.refreshRate,
};
public static implicit operator Resolution(ResolutionSurrogate s)
{
Resolution r = new() { width = s.width, height = s.height };
#if !UNITY_2022_2_OR_NEWER
r.refreshRate = s.refreshRate;
#endif
return r;
}
}
// Protobuf wrapper types for serializable collections.
// These types do NOT implement IEnumerable, which prevents protobuf-net's
// collection detection from treating them as repeated fields.
// See: https://github.com/protobuf-net/protobuf-net/issues/1185
///
/// Protobuf wrapper for SerializableHashSet that avoids IEnumerable collection detection.
///
[ProtoContract]
internal sealed class SerializableHashSetProtoWrapper
{
[ProtoMember(1, OverwriteList = true)]
public T[] Items;
}
///
/// Protobuf wrapper for SerializableSortedSet that avoids IEnumerable collection detection.
///
[ProtoContract]
internal sealed class SerializableSortedSetProtoWrapper
{
[ProtoMember(1, OverwriteList = true)]
public T[] Items;
}
///
/// Protobuf wrapper for SerializableDictionary that avoids IEnumerable collection detection.
///
[ProtoContract]
internal sealed class SerializableDictionaryProtoWrapper
{
[ProtoMember(1, OverwriteList = true)]
public TKey[] Keys;
[ProtoMember(2, OverwriteList = true)]
public TValue[] Values;
}
///
/// Protobuf wrapper for SerializableSortedDictionary that avoids IEnumerable collection detection.
///
[ProtoContract]
internal sealed class SerializableSortedDictionaryProtoWrapper
{
[ProtoMember(1, OverwriteList = true)]
public TKey[] Keys;
[ProtoMember(2, OverwriteList = true)]
public TValue[] Values;
}
internal static class ProtobufUnityModel
{
static ProtobufUnityModel()
{
try
{
RuntimeTypeModel model = RuntimeTypeModel.Default;
// Register surrogates for Unity types we cannot annotate directly.
model
.Add(typeof(Vector2), applyDefaultBehaviour: false)
.SetSurrogate(typeof(Vector2Surrogate));
model
.Add(typeof(Vector3), applyDefaultBehaviour: false)
.SetSurrogate(typeof(Vector3Surrogate));
model
.Add(typeof(Quaternion), applyDefaultBehaviour: false)
.SetSurrogate(typeof(QuaternionSurrogate));
model
.Add(typeof(Color), applyDefaultBehaviour: false)
.SetSurrogate(typeof(ColorSurrogate));
model
.Add(typeof(Color32), applyDefaultBehaviour: false)
.SetSurrogate(typeof(Color32Surrogate));
model
.Add(typeof(Rect), applyDefaultBehaviour: false)
.SetSurrogate(typeof(RectSurrogate));
model
.Add(typeof(RectInt), applyDefaultBehaviour: false)
.SetSurrogate(typeof(RectIntSurrogate));
model
.Add(typeof(Bounds), applyDefaultBehaviour: false)
.SetSurrogate(typeof(BoundsSurrogate));
model
.Add(typeof(BoundsInt), applyDefaultBehaviour: false)
.SetSurrogate(typeof(BoundsIntSurrogate));
model
.Add(typeof(Vector2Int), applyDefaultBehaviour: false)
.SetSurrogate(typeof(Vector2IntSurrogate));
model
.Add(typeof(Vector3Int), applyDefaultBehaviour: false)
.SetSurrogate(typeof(Vector3IntSurrogate));
model
.Add(typeof(Resolution), applyDefaultBehaviour: false)
.SetSurrogate(typeof(ResolutionSurrogate));
// NOTE: SerializableHashSet, SerializableSortedSet, SerializableDictionary, and
// SerializableSortedDictionary are handled via wrapper-based serialization in
// Serializer.ProtoSerialize/ProtoDeserialize rather than RuntimeTypeModel configuration.
// This is necessary because protobuf-net's TryGetRepeatedProvider does not respect
// IgnoreListHandling, causing IEnumerable types to always be treated as collections.
// See: https://github.com/protobuf-net/protobuf-net/issues/1185
}
catch
{
// In restricted environments, model mutation may fail; ignore to keep JSON-only scenarios working.
}
}
internal static void EnsureInitialized() { /* triggers static ctor */
}
}
}