using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Server.GameStates;
// This partial class contains code for turning a list of visible entities into actual entity states.
internal sealed partial class PvsSystem
{
///
/// Generates a network entity state for the given entity.
///
/// The player to generate this state for. This may be null if the state is for replay recordings.
/// Uid of the entity to generate the state from.
/// Only provide delta changes from this tick.
/// The entity's metadata component
/// New entity State for the given entity.
private EntityState GetEntityState(ICommonSession? player, EntityUid entityUid, GameTick fromTick, MetaDataComponent meta)
{
var changed = new List();
bool sendCompList = meta.LastComponentRemoved > fromTick;
HashSet? netComps = sendCompList ? new() : null;
var stateEv = new ComponentGetState(player, fromTick);
foreach (var (netId, component) in meta.NetComponents)
{
DebugTools.Assert(component.NetSyncEnabled);
if (component.Deleted || !component.Initialized)
{
Log.Error($"Entity manager returned deleted or uninitialized component of type {component.GetType()} on entity {ToPrettyString(entityUid)} while generating entity state data for {player?.Name ?? "replay"}");
continue;
}
if (component.SendOnlyToOwner && player != null && player.AttachedEntity != entityUid)
continue;
if (component.LastModifiedTick <= fromTick)
{
if (sendCompList && (!component.SessionSpecific || player == null || EntityManager.CanGetComponentState(component, player)))
netComps!.Add(netId);
continue;
}
if (component.SessionSpecific && player != null && !EntityManager.CanGetComponentState(component, player))
continue;
var state = ComponentState(entityUid, component, netId, ref stateEv);
changed.Add(new ComponentChange(netId, state, component.LastModifiedTick));
if (state != null)
DebugTools.Assert(fromTick > component.CreationTick || state is not IComponentDeltaState);
if (sendCompList)
netComps!.Add(netId);
}
DebugTools.Assert(meta.EntityLastModifiedTick >= meta.LastComponentRemoved);
DebugTools.Assert(GetEntity(meta.NetEntity) == entityUid);
var entState = new EntityState(meta.NetEntity, changed, meta.EntityLastModifiedTick, netComps);
return entState;
}
private IComponentState? ComponentState(EntityUid uid, IComponent comp, ushort netId, ref ComponentGetState stateEv)
{
DebugTools.Assert(comp.NetSyncEnabled, $"Attempting to get component state for an un-synced component: {comp.GetType()}");
stateEv.State = null;
_getStateHandlers![netId]?.Invoke(uid, comp, ref Unsafe.As(ref stateEv));
var state = stateEv.State;
return state;
}
///
/// Variant of that includes all entity data, including data that can be inferred implicitly from the entity prototype.
///
private EntityState GetFullEntityState(ICommonSession player, EntityUid entityUid, MetaDataComponent meta)
{
var bus = EntityManager.EventBusInternal;
var changed = new List();
var stateEv = new ComponentGetState(player, GameTick.Zero);
HashSet netComps = new();
foreach (var (netId, component) in meta.NetComponents)
{
DebugTools.Assert(component.NetSyncEnabled);
if (component.SendOnlyToOwner && player.AttachedEntity != entityUid)
continue;
if (component.SessionSpecific && !EntityManager.CanGetComponentState(bus, component, player))
continue;
var state = ComponentState(entityUid, component, netId, ref stateEv);
DebugTools.Assert(state is not IComponentDeltaState);
changed.Add(new ComponentChange(netId, state, component.LastModifiedTick));
netComps.Add(netId);
}
var entState = new EntityState(meta.NetEntity, changed, meta.EntityLastModifiedTick, netComps);
return entState;
}
///
/// Gets all entity states that have been modified after and including the provided tick.
///
private void GetAllEntityStates(PvsSession pvsSession)
{
var session = pvsSession.Session;
var toTick = _gameTiming.CurTick;
var fromTick = pvsSession.FromTick;
var toSend = _uidSetPool.Get();
DebugTools.Assert(toSend.Count == 0);
bool enumerateAll = false;
DebugTools.AssertEqual(toTick, _gameTiming.CurTick);
DebugTools.Assert(toTick > fromTick);
// Null sessions imply this is a replay.
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (session == null)
{
enumerateAll = fromTick == GameTick.Zero;
}
else if (!_seenAllEnts.Contains(session))
{
enumerateAll = true;
fromTick = GameTick.Zero;
}
if (toTick.Value - fromTick.Value > DirtyBufferSize)
{
// Fall back to enumerating over all entities.
enumerateAll = true;
}
if (enumerateAll)
{
var query = AllEntityQuery();
while (query.MoveNext(out var uid, out var md))
{
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized, $"Entity {ToPrettyString(uid)} has not been initialized");
DebugTools.Assert(md.EntityLifeStage < EntityLifeStage.Terminating, $"Entity {ToPrettyString(uid)} is/has been terminated");
if (md.EntityLastModifiedTick <= fromTick)
continue;
var state = GetEntityState(session, uid, fromTick, md);
if (state.Empty)
{
Log.Error($@"{nameof(GetEntityState)} returned an empty state while enumerating entities.
Tick: {fromTick}--{toTick}
Entity: {ToPrettyString(uid)}
Last modified: {md.EntityLastModifiedTick}
Metadata last modified: {md.LastModifiedTick}
Transform last modified: {Transform(uid).LastModifiedTick}");
}
pvsSession.States.Add(state);
}
}
else
{
for (var i = fromTick.Value + 1; i <= toTick.Value; i++)
{
if (!TryGetDirtyEntities(new GameTick(i), out var add, out var dirty))
{
// This should be unreachable if `enumerateAll` is false.
throw new Exception($"Failed to get tick dirty data. tick: {i}, from: {fromTick}, to {toTick}, buffer: {DirtyBufferSize}");
}
foreach (var uid in add)
{
if (!toSend.Add(uid) || !_metaQuery.TryGetComponent(uid, out var md))
continue;
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized, $"Entity {ToPrettyString(uid)} has not been initialized");
DebugTools.Assert(md.EntityLifeStage < EntityLifeStage.Terminating, $"Entity {ToPrettyString(uid)} is/has been terminated");
DebugTools.Assert(md.EntityLastModifiedTick >= md.CreationTick, $"Entity {ToPrettyString(uid)} last modified tick is less than creation tick");
DebugTools.Assert(md.EntityLastModifiedTick > fromTick, $"Entity {ToPrettyString(uid)} last modified tick is less than from tick");
var state = GetEntityState(session, uid, fromTick, md);
if (state.Empty)
{
Log.Error($@"{nameof(GetEntityState)} returned an empty state for a new entity.
Tick: {fromTick}--{toTick}
Entity: {ToPrettyString(uid)}
Last modified: {md.EntityLastModifiedTick}
Metadata last modified: {md.LastModifiedTick}
Transform last modified: {Transform(uid).LastModifiedTick}");
continue;
}
pvsSession.States.Add(state);
}
foreach (var uid in dirty)
{
DebugTools.Assert(!add.Contains(uid));
if (!toSend.Add(uid) || !_metaQuery.TryGetComponent(uid, out var md))
continue;
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized, $"Entity {ToPrettyString(uid)} has not been initialized");
DebugTools.Assert(md.EntityLifeStage < EntityLifeStage.Terminating, $"Entity {ToPrettyString(uid)} is/has been terminated");
DebugTools.Assert(md.EntityLastModifiedTick >= md.CreationTick, $"Entity {ToPrettyString(uid)} last modified tick is less than creation tick");
DebugTools.Assert(md.EntityLastModifiedTick > fromTick, $"Entity {ToPrettyString(uid)} last modified tick is less than from tick");
var state = GetEntityState(session, uid, fromTick, md);
if (!state.Empty)
pvsSession.States.Add(state);
}
}
}
_uidSetPool.Return(toSend);
}
}