Prevent invalid prototypes from being spawned (#4279)

This commit is contained in:
Leon Friedrich
2023-08-21 14:29:02 +12:00
committed by GitHub
parent 9df4606492
commit e37c131fb4
8 changed files with 80 additions and 59 deletions

View File

@@ -81,30 +81,35 @@ namespace Robust.Server.GameObjects
private protected override EntityUid CreateEntity(string? prototypeName, EntityUid uid = default, IEntityLoadContext? context = null)
{
var entity = base.CreateEntity(prototypeName, uid, context);
if (prototypeName == null)
return base.CreateEntity(prototypeName, uid, context);
if (!string.IsNullOrWhiteSpace(prototypeName))
{
var prototype = PrototypeManager.Index<EntityPrototype>(prototypeName);
if (!PrototypeManager.TryIndex<EntityPrototype>(prototypeName, out var prototype))
throw new EntityCreationException($"Attempted to spawn an entity with an invalid prototype: {prototypeName}");
// At this point in time, all data configure on the entity *should* be purely from the prototype.
// As such, we can reset the modified ticks to Zero,
// which indicates "not different from client's own deserialization".
// So the initial data for the component or even the creation doesn't have to be sent over the wire.
foreach (var (netId, component) in GetNetComponents(entity))
{
// Make sure to ONLY get components that are defined in the prototype.
// Others could be instantiated directly by AddComponent (e.g. ContainerManager).
// And those aren't guaranteed to exist on the client, so don't clear them.
var compName = ComponentFactory.GetComponentName(component.GetType());
if (prototype.Components.ContainsKey(compName))
component.ClearTicks();
}
}
var entity = base.CreateEntity(prototype, uid, context);
// At this point in time, all data configure on the entity *should* be purely from the prototype.
// As such, we can reset the modified ticks to Zero,
// which indicates "not different from client's own deserialization".
// So the initial data for the component or even the creation doesn't have to be sent over the wire.
ClearTicks(entity, prototype);
return entity;
}
private void ClearTicks(EntityUid entity, EntityPrototype prototype)
{
foreach (var (netId, component) in GetNetComponents(entity))
{
// Make sure to ONLY get components that are defined in the prototype.
// Others could be instantiated directly by AddComponent (e.g. ContainerManager).
// And those aren't guaranteed to exist on the client, so don't clear them.
var compName = ComponentFactory.GetComponentName(netId);
if (prototype.Components.ContainsKey(compName))
component.ClearTicks();
}
}
public override EntityStringRepresentation ToPrettyString(EntityUid uid)
{
TryGetComponent(uid, out ActorComponent? actor);

View File

@@ -1034,26 +1034,20 @@ internal sealed partial class PvsSystem : EntitySystem
while (query.MoveNext(out var uid, out var md))
{
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized);
DebugTools.Assert(md.EntityLifeStage < EntityLifeStage.Terminating);
if (md.EntityLastModifiedTick <= fromTick)
continue;
var state = GetEntityState(player, uid, fromTick, md);
// Temporary debugging code.
// TODO REMOVE TEMPORARY CODE
if (state.Empty)
{
var msg = $"{nameof(GetEntityState)} returned an empty state while enumerating all. Entity {ToPrettyString(uid)}. Net component Data:";
foreach (var (_, cmp) in EntityManager.GetNetComponents(uid))
{
msg += $"\nName: {_factory.GetComponentName(cmp.GetType())}" +
$"Enabled: {cmp.NetSyncEnabled}, " +
$"Lifestage: {cmp.LifeStage}, " +
$"OwnerOnly: {cmp.SendOnlyToOwner}, " +
$"SessionSpecific: {cmp.SessionSpecific}, " +
$"LastModified: {cmp.LastModifiedTick}";
}
Log.Error(msg);
Log.Error($@"{nameof(GetEntityState)} returned an empty state while enumerating entities.
Tick: {fromTick}--{_gameTiming.CurTick}
Entity: {ToPrettyString(uid)}
Last modified: {md.EntityLastModifiedTick}
Metadata last modified: {md.LastModifiedTick}
Transform last modified: {Transform(uid).LastModifiedTick}");
}
stateEntities.Add(state);
@@ -1077,26 +1071,21 @@ internal sealed partial class PvsSystem : EntitySystem
continue;
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized);
DebugTools.Assert(md.EntityLifeStage < EntityLifeStage.Terminating);
DebugTools.Assert(md.EntityLastModifiedTick >= md.CreationTick || md.EntityLastModifiedTick == GameTick.Zero);
DebugTools.Assert(md.EntityLastModifiedTick > fromTick || md.EntityLastModifiedTick == GameTick.Zero);
var state = GetEntityState(player, uid, fromTick, md);
// Temporary debugging code.
// TODO REMOVE TEMPORARY CODE
if (state.Empty)
{
var msg = $"{nameof(GetEntityState)} returned an empty state for new entity {ToPrettyString(uid)}. Net component Data:";
foreach (var (_, cmp) in EntityManager.GetNetComponents(uid))
{
msg += $"\nName: {_factory.GetComponentName(cmp.GetType())}" +
$"Enabled: {cmp.NetSyncEnabled}, " +
$"Lifestage: {cmp.LifeStage}, " +
$"OwnerOnly: {cmp.SendOnlyToOwner}, " +
$"SessionSpecific: {cmp.SessionSpecific}, " +
$"LastModified: {cmp.LastModifiedTick}";
}
Log.Error(msg);
Log.Error($@"{nameof(GetEntityState)} returned an empty state for a new entity.
Tick: {fromTick}--{_gameTiming.CurTick}
Entity: {ToPrettyString(uid)}
Last modified: {md.EntityLastModifiedTick}
Metadata last modified: {md.LastModifiedTick}
Transform last modified: {Transform(uid).LastModifiedTick}");
continue;
}
stateEntities.Add(state);
@@ -1109,6 +1098,7 @@ internal sealed partial class PvsSystem : EntitySystem
continue;
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized);
DebugTools.Assert(md.EntityLifeStage < EntityLifeStage.Terminating);
DebugTools.Assert(md.EntityLastModifiedTick >= md.CreationTick || md.EntityLastModifiedTick == GameTick.Zero);
DebugTools.Assert(md.EntityLastModifiedTick > fromTick || md.EntityLastModifiedTick == GameTick.Zero);

View File

@@ -328,6 +328,12 @@ namespace Robust.Shared.GameObjects
return GetRegistration(componentType).Name;
}
[Pure]
public string GetComponentName(ushort netID)
{
return GetRegistration(netID).Name;
}
public ComponentRegistration GetRegistration(ushort netID)
{
if (_networkedComponents is null)

View File

@@ -751,8 +751,17 @@ namespace Robust.Shared.GameObjects
if (prototypeName == null)
return AllocEntity(out _, uid);
PrototypeManager.TryIndex<EntityPrototype>(prototypeName, out var prototype);
if (!PrototypeManager.TryIndex<EntityPrototype>(prototypeName, out var prototype))
throw new EntityCreationException($"Attempted to spawn an entity with an invalid prototype: {prototypeName}");
return CreateEntity(prototype, uid, context);
}
/// <summary>
/// Allocates an entity and loads components but does not do initialization.
/// </summary>
private protected EntityUid CreateEntity(EntityPrototype prototype, EntityUid uid = default, IEntityLoadContext? context = null)
{
var entity = AllocEntity(prototype, out var metadata, uid);
try
{
@@ -764,7 +773,7 @@ namespace Robust.Shared.GameObjects
// Exception during entity loading.
// Need to delete the entity to avoid corrupt state causing crashes later.
DeleteEntity(entity);
throw new EntityCreationException($"Exception inside CreateEntity with prototype {prototypeName}", e);
throw new EntityCreationException($"Exception inside CreateEntity with prototype {prototype.ID}", e);
}
}

View File

@@ -156,6 +156,17 @@ namespace Robust.Shared.GameObjects
/// </exception>
[Pure]
string GetComponentName(Type componentType);
/// <summary>
/// Gets the name of a component, throwing an exception if it does not exist.
/// </summary>
/// <param name="netID">The network ID corresponding to the component.</param>
/// <returns>The registered name of the component</returns>
/// <exception cref="UnknownComponentException">
/// Thrown if no component with id <see cref="netID"/> exists.
/// </exception>
[Pure]
string GetComponentName(ushort netID);
/// <summary>
/// Gets the registration belonging to a component, throwing an exception if it does not exist.

View File

@@ -65,12 +65,12 @@ public sealed class PvsSystemTests : RobustIntegrationTest
var mapCoords = new EntityCoordinates(map, new Vector2(2, 2));
await server.WaitPost(() =>
{
player = sEntMan.SpawnEntity("", gridCoords);
other = sEntMan.SpawnEntity("", gridCoords);
player = sEntMan.SpawnEntity(null, gridCoords);
other = sEntMan.SpawnEntity(null, gridCoords);
otherXform = sEntMan.GetComponent<TransformComponent>(other);
// Ensure map PVS chunk is not empty
sEntMan.SpawnEntity("", mapCoords);
sEntMan.SpawnEntity(null, mapCoords);
// Attach player.
var session = (IPlayerSession) sPlayerMan.Sessions.First();

View File

@@ -84,7 +84,7 @@ public sealed class DeletionNetworkingTests : RobustIntegrationTest
await server.WaitPost(() =>
{
var coords = new EntityCoordinates(grid1, new Vector2(0.5f, 0.5f));
player = sEntMan.SpawnEntity("", coords);
player = sEntMan.SpawnEntity(null, coords);
var session = (IPlayerSession) sPlayerMan.Sessions.First();
session.AttachToEntity(player);
session.JoinGame();
@@ -107,10 +107,10 @@ public sealed class DeletionNetworkingTests : RobustIntegrationTest
var coords = new EntityCoordinates(grid2, new Vector2(0.5f, 0.5f));
await server.WaitPost(() =>
{
entA = sEntMan.SpawnEntity("", coords);
entB = sEntMan.SpawnEntity("", coords);
childA = sEntMan.SpawnEntity("", new EntityCoordinates(entA, default));
childB = sEntMan.SpawnEntity("", new EntityCoordinates(entB, default));
entA = sEntMan.SpawnEntity(null, coords);
entB = sEntMan.SpawnEntity(null, coords);
childA = sEntMan.SpawnEntity(null, new EntityCoordinates(entA, default));
childB = sEntMan.SpawnEntity(null, new EntityCoordinates(entB, default));
});
await RunTicks();
@@ -122,10 +122,10 @@ public sealed class DeletionNetworkingTests : RobustIntegrationTest
EntityUid clientChildB = default;
await client.WaitPost(() =>
{
entC = cEntMan.SpawnEntity("", coords);
childC = cEntMan.SpawnEntity("", new EntityCoordinates(entC, default));
clientChildA = cEntMan.SpawnEntity("", new EntityCoordinates(entA, default));
clientChildB = cEntMan.SpawnEntity("", new EntityCoordinates(entB, default));
entC = cEntMan.SpawnEntity(null, coords);
childC = cEntMan.SpawnEntity(null, new EntityCoordinates(entC, default));
clientChildA = cEntMan.SpawnEntity(null, new EntityCoordinates(entA, default));
clientChildB = cEntMan.SpawnEntity(null, new EntityCoordinates(entB, default));
});
await RunTicks();

View File

@@ -74,7 +74,7 @@ public sealed class BroadphaseNetworkingTest : RobustIntegrationTest
await server.WaitPost(() =>
{
var coords = new EntityCoordinates(grid1, new Vector2(0.5f, 0.5f));
player = sEntMan.SpawnEntity("", coords);
player = sEntMan.SpawnEntity(null, coords);
// Enable physics
var physics = sEntMan.AddComponent<PhysicsComponent>(player);