Editor/Core/PlayMirror.cs

Editor-side data model for a spectator mirror of a playback. It stores a compact, sandbox-safe copy of objects and components, applies keyframe and delta PlayFrame data in order, and tracks when a full keyframe is required.

File Access
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Nodes;

namespace TeamCreate;

public enum PlayApplyResult
{
    /// <summary>The frame was applied to the mirror.</summary>
    Applied,

    /// <summary>The frame is a repeat or older than what the mirror already holds; nothing changed.</summary>
    Ignored,

    /// <summary>The mirror has a gap (or never had a keyframe) and applied nothing; the caller asks the player for a keyframe.</summary>
    NeedsKeyframe
}

public sealed class MirrorComponent
{
    public string Guid;

    public string Type;

    public bool Enabled = true;

    public JsonObject Props = new JsonObject();

    /// <summary>Incremented whenever the type, enabled flag or properties change, so a view re-applies a component only when it must.</summary>
    public int Revision = 1;
}

public sealed class MirrorObject
{
    public int Index;

    public string Guid;

    public int Parent = -1;

    public string Name = "";

    public double[] Position = new double[3];

    public double[] Rotation = { 0, 0, 0, 1 };

    public double[] Scale = { 1, 1, 1 };

    public bool Enabled = true;

    public Dictionary<string, MirrorComponent> Components = new Dictionary<string, MirrorComponent>( StringComparer.Ordinal );
}

/// <summary>
/// The spectator's copy of a played run: plain data, no engine objects and nothing that executes. The view reads it and draws; it never asks the
/// game to run. Frames are applied only in order: a gap (a frame that never arrived, or one dropped because this spectator was behind) leaves the
/// mirror exactly as it was and asks for a keyframe, because applying a delta on top of a state it does not extend would draw something that never
/// existed.
/// </summary>
public sealed class PlayMirror
{
    /// <summary>The most objects a mirror holds. A hostile sender cannot make a spectator allocate without limit.</summary>
    public const int MaxObjects = 20000;

    public const int MaxComponentsPerObject = SpectateWhitelist.MaxComponentsPerObject;

    private readonly Dictionary<int, MirrorObject> objects = new Dictionary<int, MirrorObject>();

    public IReadOnlyDictionary<int, MirrorObject> Objects => objects;

    public long LastSeq { get; private set; }

    /// <summary>True until a keyframe has been applied, and again after a gap.</summary>
    public bool AwaitingKeyframe { get; private set; } = true;

    /// <summary>Incremented by every applied frame, so a view can tell cheaply that it must refresh.</summary>
    public long Version { get; private set; }

    /// <summary>References to objects the mirror does not hold (a delta for something never created). Normally zero.</summary>
    public int UnknownReferences { get; private set; }

    public void Reset()
    {
        objects.Clear();
        LastSeq = 0;
        AwaitingKeyframe = true;
        Version++;
    }

    public PlayApplyResult Apply( PlayFrame frame )
    {
        if ( frame == null )
        {
            return PlayApplyResult.Ignored;
        }
        if ( frame.Key )
        {
            if ( !AwaitingKeyframe && frame.Seq <= LastSeq )
            {
                return PlayApplyResult.Ignored;
            }
            objects.Clear();
            AwaitingKeyframe = false;
        }
        else
        {
            if ( AwaitingKeyframe )
            {
                return PlayApplyResult.NeedsKeyframe;
            }
            if ( frame.Seq <= LastSeq )
            {
                return PlayApplyResult.Ignored;
            }
            if ( frame.Seq != LastSeq + 1 )
            {
                AwaitingKeyframe = true;
                return PlayApplyResult.NeedsKeyframe;
            }
        }
        LastSeq = frame.Seq;
        Version++;
        int unknown = 0;
        foreach ( JsonNode node in frame.Created ) Create( node as JsonObject );
        foreach ( JsonNode node in frame.Destroyed ) objects.Remove( (int)node );
        foreach ( JsonNode node in frame.Moved ) unknown += Move( node.AsArray() ) ? 0 : 1;
        foreach ( JsonNode node in frame.Enabled )
        {
            if ( objects.TryGetValue( (int)node[0], out MirrorObject o ) ) o.Enabled = (bool)node[1];
            else unknown++;
        }
        foreach ( JsonNode node in frame.Changed ) unknown += Change( node as JsonObject ) ? 0 : 1;
        foreach ( JsonNode node in frame.RemovedComponents )
        {
            if ( objects.TryGetValue( (int)node[0], out MirrorObject o ) ) o.Components.Remove( (string)node[1] );
            else unknown++;
        }
        UnknownReferences += unknown;
        if ( unknown > 0 )
        {
            // The sender referred to something this mirror never saw: it is out of step in a way the sequence check cannot see.
            AwaitingKeyframe = true;
            return PlayApplyResult.NeedsKeyframe;
        }
        return PlayApplyResult.Applied;
    }

    private void Create( JsonObject entry )
    {
        if ( entry == null || objects.Count >= MaxObjects && !objects.ContainsKey( (int)entry["i"] ) )
        {
            return;
        }
        var o = new MirrorObject { Index = (int)entry["i"], Guid = (string)entry["u"], Parent = entry["p"] is JsonValue p && p.TryGetValue( out int pv ) ? pv : -1, Name = entry["n"] is JsonValue n && n.TryGetValue( out string name ) ? name : "" };
        if ( entry["t"] is JsonArray t && t.Count == 10 )
        {
            for ( int i = 0; i < 3; i++ ) o.Position[i] = (double)t[i];
            for ( int i = 0; i < 4; i++ ) o.Rotation[i] = (double)t[3 + i];
            for ( int i = 0; i < 3; i++ ) o.Scale[i] = (double)t[7 + i];
        }
        o.Enabled = !( entry["e"] is JsonValue e && e.TryGetValue( out bool enabled ) && !enabled );
        // The receiver sanitises again whatever the frame says: a frame built in process, or by a future code path, must not be able to bypass the list.
        foreach ( JsonNode c in SpectateWhitelist.SanitizeComponents( entry["k"], out _ ) )
        {
            JsonObject co = c.AsObject();
            var component = new MirrorComponent { Guid = (string)co["__guid"], Type = (string)co["__type"], Enabled = (bool)co["__enabled"] };
            foreach ( var prop in co )
            {
                if ( !prop.Key.StartsWith( "__", StringComparison.Ordinal ) ) component.Props[prop.Key] = prop.Value?.DeepClone();
            }
            o.Components[component.Guid] = component;
        }
        objects[o.Index] = o;
    }

    private bool Move( JsonArray entry )
    {
        if ( !objects.TryGetValue( (int)entry[0], out MirrorObject o ) )
        {
            return false;
        }
        int mask = (int)entry[1];
        int at = 2;
        if ( ( mask & 1 ) != 0 ) for ( int i = 0; i < 3; i++ ) o.Position[i] = (double)entry[at++];
        if ( ( mask & 2 ) != 0 ) for ( int i = 0; i < 4; i++ ) o.Rotation[i] = (double)entry[at++];
        if ( ( mask & 4 ) != 0 ) for ( int i = 0; i < 3; i++ ) o.Scale[i] = (double)entry[at++];
        return true;
    }

    private bool Change( JsonObject entry )
    {
        if ( entry == null || !objects.TryGetValue( (int)entry["i"], out MirrorObject o ) )
        {
            return false;
        }
        string guid = (string)entry["g"];
        if ( !o.Components.TryGetValue( guid, out MirrorComponent component ) )
        {
            if ( entry["y"] is not JsonValue y || !y.TryGetValue( out string type ) || !SpectateWhitelist.IsAllowedType( type ) )
            {
                return false;
            }
            if ( o.Components.Count >= MaxComponentsPerObject )
            {
                return true;
            }
            component = new MirrorComponent { Guid = guid, Type = type };
            o.Components[guid] = component;
        }
        component.Revision++;
        if ( entry["a"] is JsonValue a && a.TryGetValue( out bool enabled ) ) component.Enabled = enabled;
        if ( entry["p"] is JsonObject props )
        {
            foreach ( var prop in SpectateWhitelist.FilterProperties( component.Type, props ).ToList() ) component.Props[prop.Key] = prop.Value?.DeepClone();
        }
        return true;
    }
}