Invalid ghost buffer data with nosend ghost fields

There is a ghost with data in the buffer. Blob reference can’t be GhostFIeld, ok, disable sending. And everything should be fine. But apparently because of the specifics of serialization of buffer changes something goes wrong and on the client (only on the client) buffer differs from the version on the server, it is the data that differs. In the example there are 3 fields - ghost id, non-ghost id and id stored in blob. Everything is fine on the server, but on the client, in the end there is an element with a correct ghost id, but all other fields have completely incorrect values.

If I understand correctly, then due to serialization of ghosts unity on the client can use on quite the same data in the local buffer when receiving changes from the server.
The doc says that [GhostField(SendData=false)] is allowed to use buffers, with the note that I have to set the value initially - initially it is set, but I have to manually control these values in case of changes on the server?

Am I missing something? Some option or the only way out is to store such data in a separate repository

using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.NetCode;
using UnityEngine;
using Random = Unity.Mathematics.Random;

namespace DotsInventory
{
    [GhostComponent()]
    public partial struct TestBufferElement : IBufferElementData
    {
        public struct TestBlob
        {
            public int id;
        }

        [GhostField] public int id;
        [GhostField(SendData = false)] public int sameId;
        [GhostField(SendData = false)] public BlobAssetReference<TestBlob> blob;
    }

    public class TestAuthoring : MonoBehaviour
    {
        private class TestBaker : Baker<TestAuthoring>
        {
            public override void Bake(TestAuthoring authoring)
            {
                var e = GetEntity(TransformUsageFlags.None);
                var els = AddBuffer<TestBufferElement>(e);

                for (int i = 0; i < 100; i++)
                {
                    var blob = new BlobBuilder(Allocator.Temp);
                    ref var root = ref blob.ConstructRoot<TestBufferElement.TestBlob>();
                    root.id = i;
                    var asset = blob.CreateBlobAssetReference<TestBufferElement.TestBlob>(Allocator.Persistent);
                    blob.Dispose();
                    AddBlobAsset(ref asset, out _);
                    els.Add(new TestBufferElement() { id = i, sameId = i, blob = asset });
                }
            }
        }
    }

    [BurstCompile]
    [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
    [UpdateInGroup(typeof(SimulationSystemGroup))]
    partial struct TestSystem : ISystem
    {
        private Random _rnd;

        [BurstCompile]
        public void OnCreate(ref SystemState state)
        {
            _rnd = Random.CreateFromIndex(234234234);
        }

        [BurstCompile]
        public void OnDestroy(ref SystemState state)
        {
        }

        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
           
            foreach (var els in SystemAPI.Query<DynamicBuffer<TestBufferElement>>())
            {
                if (els.Length != 1)
                    els.RemoveAt(_rnd.NextInt(0, els.Length));
            }
        }
    }
}

You are randomly removing elements by index from the buffer. This action is not something that is explicitly replicated, but you are changing what data is synced from the element removed to last index (which is removed). This basically means on client side your original non synced data is fixed to its original indices, but the synced data will change what element index it is stored on.

Multiple ways to solve this, but would be better to know the full use case. As mentioned, doing removal by index is probably not the best option to modify large buffers as it will trigger syncronizing large amount of values.

This is expected unfortunately, see Ghost snapshots and synchronization | Netcode for Entities | 1.3.6.

Simplified example:

  1. Lets say you have 0 elements in the list on both client and server.
  2. The server adds 3 new elements.
  3. When the server replicates these 3 elements, it’s actually only replicating the new DynamicBuffer<TestBufferElement>.Length, and the id field for index 0, 1 and 2.
  4. Thus, on the client, we set the .Length property to 4, and you’ll notice .Length calls ResizeUninitialized(value) internally, which populates index 0, 1 and 2 with uninitialized data (i.e. the memory is not even zeroed).
  5. We then write in the GhostField values to each index - in your case, the id field.
  6. Thus sameId and blob will be uninitialized memory, which means they could have any value.

There is a small false positive chance of sameId == id with this, so we’re considering changing uninitialized dynamic buffer memory to be zero’d instead when replicating new elements (but TMK, that change/fix has not landed yet).

EDIT: Also note, if you use any variation of RemoveAt (or remove less elements than you add), we’ll only shift the GhostFields on all elements above. Thus you’ll also get false negatives if using sameId == id, but that can be fine depending on your use-case. In the short term, I’d recommend using IBufferElementData like a ring buffer if you need per-element-changed tracking.