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); } } }