Entity spawn prediction v1 (#5841)

* Entity spawn prediction v1

Client can't properly interact with this but that requires additional work on top so we can cleanly split this.

* This

* delete fix

* cats
This commit is contained in:
metalgearsloth
2025-04-19 16:50:41 +10:00
committed by GitHub
parent 02b451db2a
commit 65d2f2dd2f
8 changed files with 381 additions and 3 deletions

View File

@@ -0,0 +1,126 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects;
public sealed partial class ClientEntityManager
{
public override EntityUid PredictedSpawnAttachedTo(string? protoName, EntityCoordinates coordinates, ComponentRegistry? overrides = null, Angle rotation = default)
{
var ent = SpawnAttachedTo(protoName, coordinates, overrides, rotation);
FlagPredicted(ent);
return ent;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override EntityUid PredictedSpawn(string? protoName = null, ComponentRegistry? overrides = null, bool doMapInit = true)
{
var ent = Spawn(protoName, overrides, doMapInit);
FlagPredicted(ent);
return ent;
}
public override EntityUid PredictedSpawn(string? protoName, MapCoordinates coordinates, ComponentRegistry? overrides = null, Angle rotation = default!)
{
var ent = Spawn(protoName, coordinates, overrides, rotation);
FlagPredicted(ent);
return ent;
}
public override EntityUid PredictedSpawnAtPosition(string? protoName, EntityCoordinates coordinates, ComponentRegistry? overrides = null)
{
var ent = SpawnAtPosition(protoName, coordinates, overrides);
FlagPredicted(ent);
return ent;
}
public override bool PredictedTrySpawnNextTo(
string? protoName,
EntityUid target,
[NotNullWhen(true)] out EntityUid? uid,
TransformComponent? xform = null,
ComponentRegistry? overrides = null)
{
if (!TrySpawnNextTo(protoName, target, out uid, xform, overrides))
return false;
FlagPredicted(uid.Value);
return true;
}
public override bool PredictedTrySpawnInContainer(
string? protoName,
EntityUid containerUid,
string containerId,
[NotNullWhen(true)] out EntityUid? uid,
ContainerManagerComponent? containerComp = null,
ComponentRegistry? overrides = null)
{
if (!TrySpawnInContainer(protoName, containerUid, containerId, out uid, containerComp, overrides))
return false;
FlagPredicted(uid.Value);
return true;
}
public override EntityUid PredictedSpawnNextToOrDrop(string? protoName, EntityUid target, TransformComponent? xform = null, ComponentRegistry? overrides = null)
{
var ent = SpawnNextToOrDrop(protoName, target, xform, overrides);
FlagPredicted(ent);
return ent;
}
public override EntityUid PredictedSpawnInContainerOrDrop(
string? protoName,
EntityUid containerUid,
string containerId,
TransformComponent? xform = null,
ContainerManagerComponent? containerComp = null,
ComponentRegistry? overrides = null)
{
var ent = SpawnInContainerOrDrop(protoName, containerUid, containerId, xform, containerComp, overrides);
FlagPredicted(ent);
return ent;
}
public override EntityUid PredictedSpawnInContainerOrDrop(
string? protoName,
EntityUid containerUid,
string containerId,
out bool inserted,
TransformComponent? xform = null,
ContainerManagerComponent? containerComp = null,
ComponentRegistry? overrides = null)
{
var ent = SpawnInContainerOrDrop(protoName,
containerUid,
containerId,
out inserted,
xform,
containerComp,
overrides);
FlagPredicted(ent);
return ent;
}
public override void FlagPredicted(Entity<MetaDataComponent?> ent)
{
if (!MetaQuery.Resolve(ent.Owner, ref ent.Comp))
return;
DebugTools.Assert(IsClientSide(ent.Owner, ent.Comp));
EnsureComponent<PredictedSpawnComponent>(ent.Owner);
// Server has no knowledge of the entity we are so we generate a clientside nentity and send it to server.
DebugTools.Assert(ent.Comp.NetEntity.IsClientSide());
// TODO: Need to map call site or something, needs to be consistent between client and server.
}
}

View File

@@ -291,5 +291,42 @@ namespace Robust.Client.GameObjects
}
}
#endregion
/// <inheritdoc />
public override void PredictedDeleteEntity(Entity<MetaDataComponent?, TransformComponent?> ent)
{
if (Deleted(ent.Owner) || !TransformQuery.Resolve(ent.Owner, ref ent.Comp2))
return;
// So there's 3 scenarios:
// 1. Networked entity we just move to nullspace and rely on state handling.
// 2. Clientside predicted entity we delete and rely on state handling.
// 3. Clientside only entity that actually needs deleting here.
if (HasComponent<PredictedSpawnComponent>(ent.Owner))
{
DeleteEntity(ent);
}
else
{
_xforms.DetachEntity(ent, ent.Comp2);
}
}
/// <inheritdoc />
public override void PredictedQueueDeleteEntity(Entity<MetaDataComponent?, TransformComponent?> ent)
{
if (IsQueuedForDeletion(ent.Owner) || !TransformQuery.Resolve(ent.Owner, ref ent.Comp2))
return;
if (HasComponent<PredictedSpawnComponent>(ent.Owner))
{
QueueDeleteEntity(ent);
}
else
{
_xforms.DetachEntity(ent.Owner, ent.Comp2);
}
}
}
}

View File

@@ -13,6 +13,7 @@ using Robust.Client.Physics;
using Robust.Client.Player;
using Robust.Client.Timing;
using Robust.Shared;
using Robust.Shared.Collections;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Containers;
@@ -563,6 +564,21 @@ namespace Robust.Client.GameStates
var metaQuery = _entities.GetEntityQuery<MetaDataComponent>();
RemQueue<IComponent> toRemove = new();
// Handle predicted entity spawns.
var predicted = new ValueList<EntityUid>();
var predictedQuery = _entities.AllEntityQueryEnumerator<PredictedSpawnComponent>();
while (predictedQuery.MoveNext(out var uid, out var _))
{
predicted.Add(uid);
}
// Entity will get re-created as part of the tick.
foreach (var ent in predicted)
{
_entities.DeleteEntity(ent);
}
foreach (var entity in system.DirtyEntities)
{
DebugTools.Assert(toRemove.Count == 0);

View File

@@ -0,0 +1,7 @@
namespace Robust.Shared.GameObjects;
/// <summary>
/// Indicates the attached entity was spawn predicted and should be reconciled when the server states comes in.
/// </summary>
[RegisterComponent]
public sealed partial class PredictedSpawnComponent : Component;

View File

@@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Robust.Shared.Containers;
using Robust.Shared.Maths;
using Robust.Shared.Serialization;
namespace Robust.Shared.GameObjects;
@@ -206,4 +207,91 @@ public partial class EntityManager
return uid;
}
#region Prediction
public virtual EntityUid PredictedSpawnAttachedTo(string? protoName, EntityCoordinates coordinates, ComponentRegistry? overrides = null, Angle rotation = default)
{
return SpawnAttachedTo(protoName, coordinates, overrides, rotation);
}
public virtual EntityUid PredictedSpawn(string? protoName = null, ComponentRegistry? overrides = null, bool doMapInit = true)
{
return Spawn(protoName, overrides, doMapInit);
}
public virtual EntityUid PredictedSpawn(string? protoName, MapCoordinates coordinates, ComponentRegistry? overrides = null, Angle rotation = default!)
{
return Spawn(protoName, coordinates, overrides, rotation);
}
public virtual EntityUid PredictedSpawnAtPosition(string? protoName, EntityCoordinates coordinates, ComponentRegistry? overrides = null)
{
return SpawnAtPosition(protoName, coordinates, overrides);
}
public virtual bool PredictedTrySpawnNextTo(
string? protoName,
EntityUid target,
[NotNullWhen(true)] out EntityUid? uid,
TransformComponent? xform = null,
ComponentRegistry? overrides = null)
{
return TrySpawnNextTo(protoName, target, out uid, xform, overrides);
}
public virtual bool PredictedTrySpawnInContainer(
string? protoName,
EntityUid containerUid,
string containerId,
[NotNullWhen(true)] out EntityUid? uid,
ContainerManagerComponent? containerComp = null,
ComponentRegistry? overrides = null)
{
return TrySpawnInContainer(protoName, containerUid, containerId, out uid, containerComp, overrides);
}
public virtual EntityUid PredictedSpawnNextToOrDrop(string? protoName, EntityUid target, TransformComponent? xform = null, ComponentRegistry? overrides = null)
{
return SpawnNextToOrDrop(protoName, target, xform, overrides);
}
public virtual EntityUid PredictedSpawnInContainerOrDrop(
string? protoName,
EntityUid containerUid,
string containerId,
TransformComponent? xform = null,
ContainerManagerComponent? containerComp = null,
ComponentRegistry? overrides = null)
{
return SpawnInContainerOrDrop(protoName, containerUid, containerId, xform, containerComp, overrides);
}
public virtual EntityUid PredictedSpawnInContainerOrDrop(
string? protoName,
EntityUid containerUid,
string containerId,
out bool inserted,
TransformComponent? xform = null,
ContainerManagerComponent? containerComp = null,
ComponentRegistry? overrides = null)
{
return SpawnInContainerOrDrop(protoName,
containerUid,
containerId,
out inserted,
xform,
containerComp,
overrides);
}
/// <summary>
/// Flags an entity as being a predicted spawn and should be deleted when its corresponding tick comes in.
/// </summary>
public virtual void FlagPredicted(Entity<MetaDataComponent?> ent)
{
}
#endregion
}

View File

@@ -48,7 +48,7 @@ namespace Robust.Shared.GameObjects
// I feel like PJB might shed me for putting a system dependency here, but its required for setting entity
// positions on spawn....
private SharedTransformSystem _xforms = default!;
protected SharedTransformSystem _xforms = default!;
private SharedContainerSystem _containers = default!;
public EntityQuery<MetaDataComponent> MetaQuery;
@@ -693,6 +693,18 @@ namespace Robust.Shared.GameObjects
public bool IsQueuedForDeletion(EntityUid uid) => QueuedDeletionsSet.Contains(uid);
/// <inheritdoc />
public virtual void PredictedDeleteEntity(Entity<MetaDataComponent?, TransformComponent?> ent)
{
DeleteEntity(ent.Owner);
}
/// <inheritdoc />
public virtual void PredictedQueueDeleteEntity(Entity<MetaDataComponent?, TransformComponent?> ent)
{
QueueDeleteEntity(ent.Owner);
}
public bool EntityExists(EntityUid uid)
{
return MetaQuery.HasComponentInternal(uid);

View File

@@ -782,6 +782,20 @@ public partial class EntitySystem
EntityManager.QueueDeleteEntity(uid);
}
/// <inheritdoc cref="IEntityManager.DeleteEntity(EntityUid)" />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected void PredictedDel(Entity<MetaDataComponent?, TransformComponent?> ent)
{
EntityManager.PredictedDeleteEntity(ent);
}
/// <inheritdoc cref="IEntityManager.QueueDeleteEntity(EntityUid)" />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected void PredictedQueueDel(Entity<MetaDataComponent?, TransformComponent?> ent)
{
EntityManager.PredictedQueueDeleteEntity(ent);
}
/// <inheritdoc cref="IEntityManager.TryQueueDeleteEntity(EntityUid?)" />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected bool TryQueueDel(EntityUid? uid)
@@ -812,8 +826,8 @@ public partial class EntitySystem
/// <inheritdoc cref="IEntityManager.SpawnAttachedTo" />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected EntityUid SpawnAttachedTo(string? prototype, EntityCoordinates coordinates, ComponentRegistry? overrides = null)
=> EntityManager.SpawnAttachedTo(prototype, coordinates, overrides);
protected EntityUid SpawnAttachedTo(string? prototype, EntityCoordinates coordinates, ComponentRegistry? overrides = null, Angle rotation = default)
=> EntityManager.SpawnAttachedTo(prototype, coordinates, overrides, rotation);
/// <inheritdoc cref="IEntityManager.SpawnAtPosition" />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -871,6 +885,74 @@ public partial class EntitySystem
#endregion
#region PredictedSpawning
protected void FlagPredicted(Entity<MetaDataComponent?> ent)
{
EntityManager.FlagPredicted(ent);
}
/// <inheritdoc cref="IEntityManager.SpawnAttachedTo" />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected EntityUid PredictedSpawnAttachedTo(string? prototype, EntityCoordinates coordinates, ComponentRegistry? overrides = null, Angle rotation = default)
=> EntityManager.PredictedSpawnAttachedTo(prototype, coordinates, overrides, rotation);
/// <inheritdoc cref="IEntityManager.SpawnAtPosition" />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected EntityUid PredictedSpawnAtPosition(string? prototype, EntityCoordinates coordinates, ComponentRegistry? overrides = null)
=> EntityManager.PredictedSpawnAtPosition(prototype, coordinates, overrides);
/// <inheritdoc cref="IEntityManager.TrySpawnInContainer" />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected bool PredictedTrySpawnInContainer(
string? protoName,
EntityUid containerUid,
string containerId,
[NotNullWhen(true)] out EntityUid? uid,
ContainerManagerComponent? containerComp = null,
ComponentRegistry? overrides = null)
{
return EntityManager.PredictedTrySpawnInContainer(protoName, containerUid, containerId, out uid, containerComp, overrides);
}
/// <inheritdoc cref="IEntityManager.TrySpawnNextTo" />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected bool PredictedTrySpawnNextTo(
string? protoName,
EntityUid target,
[NotNullWhen(true)] out EntityUid? uid,
TransformComponent? xform = null,
ComponentRegistry? overrides = null)
{
return EntityManager.PredictedTrySpawnNextTo(protoName, target, out uid, xform, overrides);
}
/// <inheritdoc cref="IEntityManager.SpawnNextToOrDrop" />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected EntityUid PredictedSpawnNextToOrDrop(
string? protoName,
EntityUid target,
TransformComponent? xform = null,
ComponentRegistry? overrides = null)
{
return EntityManager.PredictedSpawnNextToOrDrop(protoName, target, xform, overrides);
}
/// <inheritdoc cref="IEntityManager.SpawnInContainerOrDrop" />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected EntityUid PredictedSpawnInContainerOrDrop(
string? protoName,
EntityUid containerUid,
string containerId,
TransformComponent? xform = null,
ContainerManagerComponent? container = null,
ComponentRegistry? overrides = null)
{
return EntityManager.PredictedSpawnInContainerOrDrop(protoName, containerUid, containerId, xform, container, overrides);
}
#endregion
#region Utils
/// <summary>

View File

@@ -162,6 +162,16 @@ namespace Robust.Shared.GameObjects
public bool IsQueuedForDeletion(EntityUid uid);
/// <summary>
/// Tries to predict entity deletion. On the server it runs the normal code path and on the client the entity is detached.
/// </summary>
void PredictedDeleteEntity(Entity<MetaDataComponent?, TransformComponent?> ent);
/// <summary>
/// Tries to predict entity deletion. On the server it runs the normal code path and on the client the entity is detached.
/// </summary>
void PredictedQueueDeleteEntity(Entity<MetaDataComponent?, TransformComponent?> ent);
/// <summary>
/// Shuts-down and removes the entity with the given <see cref="Robust.Shared.GameObjects.EntityUid"/>. This is also broadcast to all clients.
/// </summary>