using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Robust.Shared.GameObjects;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Server.GameStates;
// This partial class contains contains methods for adding entities to the set of entities that are about to get sent
// to a player.
internal sealed partial class PvsSystem
{
///
/// This method adds an entity to the to-send list, updates the last-sent tick, and updates the entity's visibility.
///
private void AddToSendList(
NetEntity ent,
ref EntityData data,
List list,
GameTick fromTick,
GameTick toTick,
bool entered,
ref int dirtyEntityCount)
{
var meta = data.Entity.Comp;
DebugTools.AssertEqual(meta.NetEntity, ent);
DebugTools.Assert(fromTick < toTick);
DebugTools.AssertNotEqual(data.LastSent, toTick);
DebugTools.AssertEqual(toTick, _gameTiming.CurTick);
if (meta.EntityLifeStage >= EntityLifeStage.Terminating)
{
var rep = new EntityStringRepresentation(data.Entity);
Log.Error($"Attempted to add a deleted entity to PVS send set: '{rep}'. Deletion queued: {EntityManager.IsQueuedForDeletion(data.Entity)}. Trace:\n{Environment.StackTrace}");
// This can happen if some entity was some removed from it's parent while that parent was being deleted.
// As a result the entity was marked for deletion but was never actually properly deleted.
EntityManager.QueueDeleteEntity(data.Entity);
return;
}
data.LastSent = toTick;
list.Add(ent);
if (entered)
{
data.Visibility = PvsEntityVisibility.Entered;
dirtyEntityCount++;
return;
}
if (meta.EntityLastModifiedTick <= fromTick)
{
//entity has been sent before and hasn't been updated since
data.Visibility = PvsEntityVisibility.StayedUnchanged;
return;
}
//add us
data.Visibility = PvsEntityVisibility.StayedChanged;
dirtyEntityCount++;
}
///
/// This method figures out whether a given entity is currently entering a player's PVS range.
/// This method will also check that the player's PVS entry budget is not being exceeded.
///
private (bool Entered, bool BudgetExceeded) GetPvsEntryData(ref EntityData entity,
GameTick fromTick,
GameTick toTick,
ref int newEntityCount,
ref int enteredEntityCount,
int newEntityBudget,
int enteredEntityBudget)
{
DebugTools.AssertEqual(toTick, _gameTiming.CurTick);
var enteredSinceLastSent = fromTick == GameTick.Zero
|| entity.LastSent == GameTick.Zero
|| entity.LastSent.Value != toTick.Value - 1;
var entered = enteredSinceLastSent
|| entity.EntityLastAcked == GameTick.Zero
|| entity.EntityLastAcked < fromTick // this entity was not in the last acked state.
|| entity.LastLeftView >= fromTick; // entity left and re-entered sometime after the last acked tick
// If the entity is entering, but we already sent this entering entity in the last message, we won't add it to
// the budget. Chances are the packet will arrive in a nice and orderly fashion, and the client will stick to
// their requested budget. However this can cause issues if a packet gets dropped, because a player may create
// 2x or more times the normal entity creation budget.
if (enteredSinceLastSent)
{
if (newEntityCount >= newEntityBudget || enteredEntityCount >= enteredEntityBudget)
return (entered, true);
enteredEntityCount++;
if (entity.EntityLastAcked == GameTick.Zero)
newEntityCount++;
}
return (entered, false);
}
///
/// Recursively add an entity and all of its children to the to-send set.
///
private void RecursivelyAddTreeNode(in NetEntity nodeIndex,
RobustTree tree,
List toSend,
Dictionary entityData,
Stack stack,
GameTick fromTick,
GameTick toTick,
ref int newEntityCount,
ref int enteredEntityCount,
ref int dirtyEntityCount,
int newEntityBudget,
int enteredEntityBudget)
{
stack.Push(nodeIndex);
while (stack.TryPop(out var currentNodeIndex))
{
DebugTools.Assert(currentNodeIndex.IsValid());
// As every map is parented to uid 0 in the tree we still need to get their children, plus because we go top-down
// we may find duplicate parents with children we haven't encountered before
// on different chunks (this is especially common with direct grid children)
ref var data = ref GetOrNewEntityData(entityData, currentNodeIndex);
if (data.LastSent != toTick)
{
var (entered, budgetExceeded) = GetPvsEntryData(ref data, fromTick, toTick,
ref newEntityCount, ref enteredEntityCount, newEntityBudget, enteredEntityBudget);
if (budgetExceeded)
{
// should be false for the majority of entities
if (data.LastSent == GameTick.Zero)
entityData.Remove(currentNodeIndex);
// We continue, but do not stop iterating this or other chunks.
// This is to avoid sending bad pvs-leave messages. I.e., other entities may have just stayed in view, and we can send them without exceeding our budget.
continue;
}
AddToSendList(currentNodeIndex, ref data, toSend, fromTick, toTick, entered, ref dirtyEntityCount);
}
var node = tree[currentNodeIndex];
if (node.Children == null)
continue;
foreach (var child in node.Children)
{
stack.Push(child);
}
}
}
///
/// Recursively add an entity and all of its parents to the to-send set. This optionally also adds all children.
///
public bool RecursivelyAddOverride(in EntityUid uid,
List toSend,
Dictionary entityData,
GameTick fromTick,
GameTick toTick,
ref int newEntityCount,
ref int enteredEntityCount,
ref int dirtyEntityCount,
int newEntityBudget,
int enteredEntityBudget,
bool addChildren = false)
{
//are we valid?
//sometimes uids gets added without being valid YET (looking at you mapmanager) (mapcreate & gridcreated fire before the uids becomes valid)
if (!uid.IsValid())
return false;
var xform = _xformQuery.GetComponent(uid);
var parent = xform.ParentUid;
if (parent.IsValid() && !RecursivelyAddOverride(in parent, toSend, entityData, fromTick, toTick,
ref newEntityCount, ref enteredEntityCount, ref dirtyEntityCount, newEntityBudget, enteredEntityBudget))
{
return false;
}
var netEntity = _metaQuery.GetComponent(uid).NetEntity;
// Note that we check this AFTER adding parents. This is because while this entity may already have been added
// to the toSend set, it doesn't guarantee that its parents have been. E.g., if a player ghost just teleported
// to follow a far away entity, the player's own entity is still being sent, but we need to ensure that we also
// send the new parents, which may otherwise be delayed because of the PVS budget.
var curTick = _gameTiming.CurTick;
ref var data = ref GetOrNewEntityData(entityData, netEntity);
if (data.LastSent != curTick)
{
var (entered, _) = GetPvsEntryData(ref data, fromTick, toTick, ref newEntityCount, ref enteredEntityCount, newEntityBudget, enteredEntityBudget);
AddToSendList(netEntity, ref data, toSend, fromTick, toTick, entered, ref dirtyEntityCount);
}
if (addChildren)
{
RecursivelyAddChildren(xform, toSend, entityData, fromTick, toTick, ref newEntityCount,
ref enteredEntityCount, ref dirtyEntityCount, in newEntityBudget, in enteredEntityBudget);
}
return true;
}
///
/// Recursively add an entity and all of its children to the to-send set.
///
private void RecursivelyAddChildren(TransformComponent xform,
List toSend,
Dictionary entityData,
in GameTick fromTick,
in GameTick toTick,
ref int newEntityCount,
ref int enteredEntityCount,
ref int dirtyEntityCount,
in int newEntityBudget,
in int enteredEntityBudget)
{
foreach (var child in xform._children)
{
if (!_xformQuery.TryGetComponent(child, out var childXform))
continue;
var metadata = _metaQuery.GetComponent(child);
var netChild = metadata.NetEntity;
ref var data = ref GetOrNewEntityData(entityData, netChild);
if (data.LastSent != toTick)
{
var (entered, _) = GetPvsEntryData(ref data, fromTick, toTick, ref newEntityCount,
ref enteredEntityCount, newEntityBudget, enteredEntityBudget);
AddToSendList(netChild, ref data, toSend, fromTick, toTick, entered, ref dirtyEntityCount);
}
RecursivelyAddChildren(childXform, toSend, entityData, fromTick, toTick, ref newEntityCount,
ref enteredEntityCount, ref dirtyEntityCount, in newEntityBudget, in enteredEntityBudget);
}
}
}