Editor/Core/Protocol.cs
using System.Text.Json.Nodes;
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using System.Linq;
namespace TeamCreate;
public sealed record ObjectState(string Id, string Parent, JsonObject Data);
public sealed record Change(string Id, ObjectState Before, ObjectState After);
public sealed class Packet
{
public string Kind { get; set; }
public string Id { get; set; } = Guid.NewGuid().ToString();
public string Peer { get; set; }
public string Name { get; set; }
public string Project { get; set; }
public string Baseline { get; set; }
public string Token { get; set; }
public string Session { get; set; }
public string Scene { get; set; }
public string Error { get; set; }
public long Revision { get; set; }
public bool Final { get; set; }
public List<ObjectState> Objects { get; set; }
public List<Change> Changes { get; set; }
public JsonObject Presence { get; set; }
public int? ProtocolVersion { get; set; }
public CollaborationMode? Mode { get; set; }
public string RuntimeRunId { get; set; }
public long? RuntimeTick { get; set; }
public long? RuntimeSequence { get; set; }
public string RuntimeOperationKind { get; set; }
public string SteamId { get; set; }
public string DisplayName { get; set; }
public string SteamToken { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string OrgIdent { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string LibraryVersion { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string ProjectManifest { get; set; }
}
public static class Protocol
{
public const int CurrentVersion = 4;
public const string CurrentVersionName = "teamcreate-4";
public const string VersionName = "teamcreate-4";
public const string OlderLibraryMessage = "This peer runs an older collaboration library. Update the collaboration library on both machines and reconnect.";
public const string MissingProjectRevisionMessage = "This peer did not report its project revision. Update the collaboration library on both machines and reconnect.";
public static bool IsValidMode(CollaborationMode? mode)
{
return mode == CollaborationMode.Source || mode == CollaborationMode.RuntimeExperimental;
}
public static bool ValidateSourceHandshake(Packet p)
{
return GetHandshakeError(p) == null && p != null && p.Mode == CollaborationMode.Source;
}
public static string GetHandshakeError(Packet p, bool requireValidPeer = false)
{
if (p == null || p.Kind != "hello" || (requireValidPeer && !Guid.TryParse(p.Peer, out var _)))
{
return "Invalid collaboration handshake";
}
if (p.ProtocolVersion != 4)
{
return $"Unsupported collaboration protocol version. This client requires protocol {4} ({"teamcreate-4"}).";
}
if (!IsValidMode(p.Mode))
{
return "Invalid collaboration mode in hello. Source or runtime collaboration mode is required.";
}
if (p.Mode == CollaborationMode.RuntimeExperimental && !RuntimeStateRules.IsValidRunId(p.RuntimeRunId))
{
return "Runtime collaboration handshake requires a valid runtime run ID.";
}
if (!SteamIdentityRules.TryParse(p.SteamId, out var _))
{
return "Collaboration handshake requires a valid Steam account ID. Sign in to Steam and reconnect.";
}
if (!SteamTokenRules.IsPlausibleToken(p.SteamToken))
{
return "Collaboration handshake requires a Steam identity token. Reconnect to obtain one.";
}
if (string.IsNullOrWhiteSpace(p.OrgIdent) || string.IsNullOrWhiteSpace(p.LibraryVersion) || !Version.TryParse(p.LibraryVersion, out Version _))
{
return "This peer runs an older collaboration library. Update the collaboration library on both machines and reconnect.";
}
if (string.IsNullOrWhiteSpace(p.ProjectManifest))
{
return "This peer did not report its project revision. Update the collaboration library on both machines and reconnect.";
}
return null;
}
public static bool ValidateHandshake(Packet p)
{
return GetHandshakeError(p) == null;
}
public static bool ValidateRuntimeEnvelope(Packet p, string activeRunId, string activeScenePath = null, long? lastAcceptedSequence = null)
{
if (p == null || p.ProtocolVersion != 4 || p.Mode != CollaborationMode.RuntimeExperimental)
{
return false;
}
if (!IsValidMode(p.Mode) || !RuntimeStateRules.IsValidRunId(activeRunId) || !RuntimeStateRules.IsValidRunId(p.RuntimeRunId))
{
return false;
}
if (!string.Equals(p.RuntimeRunId, activeRunId, StringComparison.Ordinal))
{
return false;
}
if (!p.RuntimeSequence.HasValue || p.RuntimeSequence.Value <= 0)
{
return false;
}
if (lastAcceptedSequence.HasValue && p.RuntimeSequence.Value <= lastAcceptedSequence.Value)
{
return false;
}
return activeScenePath == null || string.Equals(p.Scene, activeScenePath, StringComparison.Ordinal);
}
}
public static class StateRules
{
public const string Version = "teamcreate-1";
public static readonly HashSet<string> ObjectFields = new HashSet<string> { "Name", "Position", "Rotation", "Scale", "Enabled", "Tags" };
public static readonly Dictionary<string, HashSet<string>> ComponentFields = new Dictionary<string, HashSet<string>>
{
["Sandbox.ModelRenderer"] = new HashSet<string> { "Tint" },
["Sandbox.CameraComponent"] = new HashSet<string> { "FieldOfView", "BackgroundColor", "Orthographic", "OrthographicHeight", "ZNear", "ZFar" },
["Sandbox.DirectionalLight"] = new HashSet<string> { "LightColor", "SkyColor", "Shadows" },
["Sandbox.PointLight"] = new HashSet<string> { "LightColor", "Radius", "Shadows" },
["Sandbox.BoxCollider"] = new HashSet<string> { "Center", "Scale", "IsTrigger" }
};
public static bool Equal(ObjectState a, ObjectState b)
{
return ((object)a == null) ? ((object)b == null) : ((object)b != null && a.Id == b.Id && a.Parent == b.Parent && JsonNode.DeepEquals(a.Data, b.Data));
}
public static Dictionary<string, ObjectState> Map(IEnumerable<ObjectState> objects)
{
return objects.ToDictionary((ObjectState x) => x.Id);
}
public static List<Change> Diff(Dictionary<string, ObjectState> before, Dictionary<string, ObjectState> after)
{
return (from id in before.Keys.Union(after.Keys)
orderby id
select new Change(id, before.GetValueOrDefault(id), after.GetValueOrDefault(id)) into x
where !Equal(x.Before, x.After)
select x).ToList();
}
public static void ValidateChange(Change c)
{
if ((object)c.Before == null && (object)c.After == null)
{
throw new InvalidOperationException("Empty change");
}
foreach (ObjectState item in new ObjectState[2] { c.Before, c.After }.Where((ObjectState x) => (object)x != null))
{
if (!Guid.TryParse(c.Id, out var _) || item.Id != c.Id || item.Data["__guid"]?.GetValue<string>() != c.Id)
{
throw new InvalidOperationException("Invalid object identity");
}
if (item.Data.Any((KeyValuePair<string, JsonNode> p) => p.Key.Contains("Prefab", StringComparison.OrdinalIgnoreCase)))
{
throw new InvalidOperationException("Prefab edits are not supported");
}
}
if ((object)c.Before == null || (object)c.After == null)
{
ObjectState objectState = c.Before ?? c.After;
if (!(objectState.Data["Components"] is JsonArray jsonArray))
{
return;
}
{
foreach (JsonNode item2 in jsonArray)
{
JsonObject jsonObject = item2.AsObject();
string type = jsonObject["__type"].GetValue<string>();
if (!ComponentFields.ContainsKey(type))
{
throw new InvalidOperationException("Creation/deletion of " + type + " is not supported");
}
if (jsonObject.Any((KeyValuePair<string, JsonNode> x) => !SafeRenderOptions(x) && (x.Value is JsonObject { Count: >0 } || (x.Value is JsonArray jsonArray4 && jsonArray4.Count > 0))))
{
throw new InvalidOperationException("Creation/deletion of components with embedded graphs or object references is not supported");
}
bool SafeRenderOptions(KeyValuePair<string, JsonNode> field)
{
return type == "Sandbox.ModelRenderer" && field.Key == "RenderOptions" && field.Value is JsonObject source && source.All((KeyValuePair<string, JsonNode> x) => new ReadOnlySpan<string>(new string[4] { "GameLayer", "OverlayLayer", "BloomLayer", "AfterUILayer" }).Contains(x.Key) && x.Value is JsonValue jsonValue && jsonValue.TryGetValue<bool>(out var _));
}
}
return;
}
}
foreach (string item3 in c.Before.Data.Select((KeyValuePair<string, JsonNode> x) => x.Key).Union(c.After.Data.Select((KeyValuePair<string, JsonNode> x) => x.Key)))
{
if (JsonNode.DeepEquals(c.Before.Data[item3], c.After.Data[item3]) || ObjectFields.Contains(item3))
{
continue;
}
if (item3 != "Components")
{
throw new InvalidOperationException("Unsupported GameObject property: " + item3);
}
JsonArray jsonArray2 = c.Before.Data[item3].AsArray();
JsonArray jsonArray3 = c.After.Data[item3].AsArray();
if (jsonArray2.Count != jsonArray3.Count)
{
throw new InvalidOperationException("Component addition/removal is not supported");
}
for (int num = 0; num < jsonArray2.Count; num++)
{
JsonObject jsonObject2 = jsonArray2[num].AsObject();
JsonObject jsonObject3 = jsonArray3[num].AsObject();
string value = jsonObject2["__type"].GetValue<string>();
foreach (string item4 in jsonObject2.Select((KeyValuePair<string, JsonNode> x) => x.Key).Union(jsonObject3.Select((KeyValuePair<string, JsonNode> x) => x.Key)))
{
if (!JsonNode.DeepEquals(jsonObject2[item4], jsonObject3[item4]))
{
if (!ComponentFields.TryGetValue(value, out var value2) || !value2.Contains(item4))
{
throw new InvalidOperationException("Unsupported inspector property: " + value + "." + item4);
}
if (!(jsonObject3[item4] is JsonValue))
{
throw new InvalidOperationException("Only scalar inspector values are supported");
}
}
}
}
}
}
public static void ValidateGraph(Dictionary<string, ObjectState> objects)
{
HashSet<string> hashSet = new HashSet<string>(objects.Keys);
foreach (ObjectState value in objects.Values)
{
if (value.Data["Components"] is JsonArray jsonArray)
{
foreach (JsonNode item in jsonArray)
{
string text = item?["__guid"]?.GetValue<string>();
if (text == null || !Guid.TryParse(text, out var _) || !hashSet.Add(text))
{
throw new InvalidOperationException("Duplicate/invalid component identity");
}
}
}
HashSet<string> hashSet2 = new HashSet<string> { value.Id };
string parent = value.Parent;
while (!string.IsNullOrEmpty(parent))
{
if (!objects.ContainsKey(parent))
{
throw new InvalidOperationException("Parent does not exist");
}
if (!hashSet2.Add(parent))
{
throw new InvalidOperationException("Parenting cycle");
}
parent = objects[parent].Parent;
}
}
}
}
public sealed class Authority
{
private readonly Dictionary<string, Packet> receipts = new Dictionary<string, Packet>();
private readonly Action<Packet> persist;
public Dictionary<string, Dictionary<string, ObjectState>> Scenes { get; } = new Dictionary<string, Dictionary<string, ObjectState>>();
public long Revision { get; private set; }
public Authority(Action<Packet> persist)
{
this.persist = persist;
}
public Packet Subscribe(Packet p)
{
if (!Scenes.ContainsKey(p.Scene))
{
Dictionary<string, ObjectState> dictionary = StateRules.Map(p.Objects);
StateRules.ValidateGraph(dictionary);
persist(new Packet
{
Kind = "seed",
Scene = p.Scene,
Objects = p.Objects
});
Scenes.Add(p.Scene, dictionary);
}
return new Packet
{
Kind = "snapshot",
Scene = p.Scene,
Objects = Scenes[p.Scene].Values.ToList(),
Revision = Revision
};
}
public Packet Apply(Packet p)
{
string key = p.Peer + ":" + p.Id;
if (receipts.TryGetValue(key, out var value))
{
return value;
}
try
{
if (!Scenes.TryGetValue(p.Scene, out var value2))
{
throw new InvalidOperationException("Subscribe to scene first");
}
if (p.Changes == null || (p.Changes.Count == 0 && !p.Final))
{
throw new InvalidOperationException("Empty operation");
}
if (p.Changes.Select((Change x) => x.Id).Distinct().Count() != p.Changes.Count)
{
throw new InvalidOperationException("Duplicate object in operation");
}
Dictionary<string, ObjectState> dictionary = new Dictionary<string, ObjectState>(value2);
foreach (Change change in p.Changes)
{
StateRules.ValidateChange(change);
if (!StateRules.Equal(value2.GetValueOrDefault(change.Id), change.Before))
{
throw new InvalidOperationException("Conflict on " + change.Id + ": first accepted edit wins. Your edit is retained in recovery.");
}
if ((object)change.After == null)
{
dictionary.Remove(change.Id);
}
else
{
dictionary[change.Id] = change.After;
}
}
StateRules.ValidateGraph(dictionary);
value = new Packet
{
Kind = "accepted",
Id = p.Id,
Peer = p.Peer,
Scene = p.Scene,
Changes = p.Changes,
Revision = Revision + 1,
Final = p.Final
};
persist(value);
Revision++;
Scenes[p.Scene] = dictionary;
}
catch (Exception ex)
{
value = new Packet
{
Kind = "reject",
Id = p.Id,
Scene = p.Scene,
Error = ex.Message
};
}
receipts[key] = value;
return value;
}
}