if (_actionsSystem == null)
return;
- for (var i = 0; i < assignments.Count; i++)
+ _actions.Clear();
+ foreach (var assign in assignments)
{
- _actions[i] = assignments[i].ActionId;
+ _actions.Add(assign.ActionId);
}
_container?.SetActionData(_actionsSystem, _actions.ToArray());
public RobustIntegrationTest.ServerIntegrationInstance Server { get; private set; } = default!;
public RobustIntegrationTest.ClientIntegrationInstance Client { get; private set; } = default!;
+ public void Deconstruct(
+ out RobustIntegrationTest.ServerIntegrationInstance server,
+ out RobustIntegrationTest.ClientIntegrationInstance client)
+ {
+ server = Server;
+ client = Client;
+ }
+
public ICommonSession? Player => Server.PlayerMan.Sessions.FirstOrDefault();
public ContentPlayerData? PlayerData => Player?.Data.ContentData();
public void Kill()
{
State = PairState.Dead;
+ ServerLogHandler.ShuttingDown = true;
+ ClientLogHandler.ShuttingDown = true;
Server.Dispose();
Client.Dispose();
}
_prefix = prefix != null ? $"{prefix}: " : "";
}
+ public bool ShuttingDown;
+
public void Log(string sawmillName, LogEvent message)
{
+ var level = message.Level.ToRobust();
+
+ if (ShuttingDown && (FailureLevel == null || level < FailureLevel))
+ return;
+
if (ActiveContext is not { } testContext)
{
// If this gets hit it means something is logging to this instance while it's "between" tests.
throw new InvalidOperationException("Log to pool test log handler without active test context");
}
- var level = message.Level.ToRobust();
var name = LogMessage.LogLevelToName(level);
var seconds = _stopwatch.Elapsed.TotalSeconds;
var rendered = message.RenderMessage();
AssertAnchored(false);
// Repeat for screwdriver interaction.
- AssertDeleted(false);
+ AssertExists();
await Interact(Screw, awaitDoAfters: false);
await CancelDoAfters();
- AssertDeleted(false);
+ AssertExists();
await Interact(Screw);
AssertDeleted();
}
/// <remarks>
/// Automatically enables welders.
/// </remarks>
- protected async Task<EntityUid?> PlaceInHands(string? id, int quantity = 1, bool enableWelder = true)
+ protected async Task<NetEntity> PlaceInHands(string id, int quantity = 1, bool enableWelder = true)
{
- return await PlaceInHands(id == null ? null : (id, quantity), enableWelder);
+ return await PlaceInHands((id, quantity), enableWelder);
}
/// <summary>
/// <remarks>
/// Automatically enables welders.
/// </remarks>
- protected async Task<EntityUid?> PlaceInHands(EntitySpecifier? entity, bool enableWelder = true)
+ protected async Task<NetEntity> PlaceInHands(EntitySpecifier entity, bool enableWelder = true)
{
if (Hands.ActiveHand == null)
{
return default;
}
+ Assert.That(!string.IsNullOrWhiteSpace(entity.Prototype));
await DeleteHeldEntity();
- if (entity == null || string.IsNullOrWhiteSpace(entity.Prototype))
- {
- await RunTicks(1);
- Assert.That(Hands.ActiveHandEntity, Is.Null);
- return null;
- }
-
// spawn and pick up the new item
var item = await SpawnEntity(entity, SEntMan.GetCoordinates(PlayerCoords));
ItemToggleComponent? itemToggle = null;
if (enableWelder && itemToggle != null)
Assert.That(itemToggle.Activated);
- return item;
+ return SEntMan.GetNetEntity(item);
}
/// <summary>
}
}
+ /// <summary>
+ /// Throw the currently held entity. Defaults to targeting the current <see cref="TargetCoords"/>
+ /// </summary>
+ protected async Task<bool> ThrowItem(NetCoordinates? target = null, float minDistance = 4)
+ {
+ var actualTarget = SEntMan.GetCoordinates(target ?? TargetCoords);
+ var result = false;
+ await Server.WaitPost(() => result = HandSys.ThrowHeldItem(SEntMan.GetEntity(Player), actualTarget, minDistance));
+ return result;
+ }
+
#endregion
/// <summary>
});
}
- protected void AssertDeleted(bool deleted = true, NetEntity? target = null)
+ protected void AssertDeleted(NetEntity? target = null)
{
target ??= Target;
if (target == null)
Assert.Multiple(() =>
{
- Assert.That(SEntMan.Deleted(SEntMan.GetEntity(target)), Is.EqualTo(deleted));
- Assert.That(CEntMan.Deleted(CEntMan.GetEntity(target)), Is.EqualTo(deleted));
+ Assert.That(SEntMan.Deleted(SEntMan.GetEntity(target)));
+ Assert.That(CEntMan.Deleted(CEntMan.GetEntity(target)));
+ });
+ }
+
+ protected void AssertExists(NetEntity? target = null)
+ {
+ target ??= Target;
+ if (target == null)
+ {
+ Assert.Fail("No target specified");
+ return;
+ }
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(SEntMan.EntityExists(SEntMan.GetEntity(target)));
+ Assert.That(CEntMan.EntityExists(CEntMan.GetEntity(target)));
});
}
await RunTicks(5);
}
+ protected Task Delete(NetEntity nuid)
+ {
+ return Delete(SEntMan.GetEntity(nuid));
+ }
+
#region Time/Tick managment
protected async Task RunTicks(int ticks)
}
#endregion
+
+ #region Networking
+
+ protected EntityUid ToServer(NetEntity nent) => SEntMan.GetEntity(nent);
+ protected EntityUid ToClient(NetEntity nent) => CEntMan.GetEntity(nent);
+ protected EntityUid? ToServer(NetEntity? nent) => SEntMan.GetEntity(nent);
+ protected EntityUid? ToClient(NetEntity? nent) => CEntMan.GetEntity(nent);
+ protected EntityUid ToServer(EntityUid cuid) => SEntMan.GetEntity(CEntMan.GetNetEntity(cuid));
+ protected EntityUid ToClient(EntityUid cuid) => CEntMan.GetEntity(SEntMan.GetNetEntity(cuid));
+ protected EntityUid? ToServer(EntityUid? cuid) => SEntMan.GetEntity(CEntMan.GetNetEntity(cuid));
+ protected EntityUid? ToClient(EntityUid? cuid) => CEntMan.GetEntity(SEntMan.GetNetEntity(cuid));
+
+ protected EntityCoordinates ToServer(NetCoordinates coords) => SEntMan.GetCoordinates(coords);
+ protected EntityCoordinates ToClient(NetCoordinates coords) => CEntMan.GetCoordinates(coords);
+ protected EntityCoordinates? ToServer(NetCoordinates? coords) => SEntMan.GetCoordinates(coords);
+ protected EntityCoordinates? ToClient(NetCoordinates? coords) => CEntMan.GetCoordinates(coords);
+
+ #endregion
+
+ #region Metadata & Transforms
+
+ protected MetaDataComponent Meta(NetEntity uid) => Meta(ToServer(uid));
+ protected MetaDataComponent Meta(EntityUid uid) => SEntMan.GetComponent<MetaDataComponent>(uid);
+
+ protected TransformComponent Xform(NetEntity uid) => Xform(ToServer(uid));
+ protected TransformComponent Xform(EntityUid uid) => SEntMan.GetComponent<TransformComponent>(uid);
+
+ protected EntityCoordinates Position(NetEntity uid) => Position(ToServer(uid));
+ protected EntityCoordinates Position(EntityUid uid) => Xform(uid).Coordinates;
+
+ #endregion
}
using Content.Client.Examine;
using Content.IntegrationTests.Pair;
using Content.Server.Body.Systems;
+using Content.Server.Hands.Systems;
using Content.Server.Stack;
using Content.Server.Tools;
using Content.Shared.Body.Part;
using Content.Shared.DoAfter;
using Content.Shared.Hands.Components;
-using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Server.Item;
using Content.Shared.Mind;
/// </summary>
protected NetEntity Player;
+ protected EntityUid SPlayer => ToServer(Player);
+ protected EntityUid CPlayer => ToClient(Player);
+
protected ICommonSession ClientSession = default!;
protected ICommonSession ServerSession = default!;
/// </remarks>
protected NetEntity? Target;
+ protected EntityUid? STarget => ToServer(Target);
+ protected EntityUid? CTarget => ToClient(Target);
+
/// <summary>
/// When attempting to start construction, this is the client-side ID of the construction ghost.
/// </summary>
protected IPrototypeManager ProtoMan = default!;
protected IGameTiming STiming = default!;
protected IComponentFactory Factory = default!;
- protected SharedHandsSystem HandSys = default!;
+ protected HandsSystem HandSys = default!;
protected StackSystem Stack = default!;
protected SharedInteractionSystem InteractSys = default!;
protected Content.Server.Construction.ConstructionSystem SConstruction = default!;
ProtoMan = Server.ResolveDependency<IPrototypeManager>();
Factory = Server.ResolveDependency<IComponentFactory>();
STiming = Server.ResolveDependency<IGameTiming>();
- HandSys = SEntMan.System<SharedHandsSystem>();
+ HandSys = SEntMan.System<HandsSystem>();
InteractSys = SEntMan.System<SharedInteractionSystem>();
ToolSys = SEntMan.System<ToolSystem>();
ItemToggleSys = SEntMan.System<SharedItemToggleSystem>();
--- /dev/null
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Prototypes;
+
+namespace Content.IntegrationTests.Tests.Networking;
+
+[TestFixture]
+public sealed class PvsCommandTest
+{
+ public static EntProtoId TestEnt = "MobHuman";
+
+ [Test]
+ public async Task TestPvsCommands()
+ {
+ await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true, DummyTicker = false});
+ var (server, client) = pair;
+ await pair.RunTicksSync(5);
+
+ // Spawn a complex entity.
+ EntityUid entity = default;
+ await server.WaitPost(() => entity = server.EntMan.Spawn(TestEnt));
+ await pair.RunTicksSync(5);
+
+ // Check that the client has a variety pf entities.
+ Assert.That(client.EntMan.EntityCount, Is.GreaterThan(0));
+ Assert.That(client.EntMan.Count<MapComponent>, Is.GreaterThan(0));
+ Assert.That(client.EntMan.Count<MapGridComponent>, Is.GreaterThan(0));
+
+ var meta = client.MetaData(pair.ToClientUid(entity));
+ var lastApplied = meta.LastStateApplied;
+
+ // Dirty all entities
+ await server.ExecuteCommand("dirty");
+ await pair.RunTicksSync(5);
+ Assert.That(meta.LastStateApplied, Is.GreaterThan(lastApplied));
+ await pair.RunTicksSync(5);
+
+ // Do a client-side full state reset
+ await client.ExecuteCommand("resetallents");
+ await pair.RunTicksSync(5);
+
+ // Request a full server state.
+ lastApplied = meta.LastStateApplied;
+ await client.ExecuteCommand("fullstatereset");
+ await pair.RunTicksSync(10);
+ Assert.That(meta.LastStateApplied, Is.GreaterThan(lastApplied));
+
+ await server.WaitPost(() => server.EntMan.DeleteEntity(entity));
+ await pair.CleanReturnAsync();
+ }
+}
--- /dev/null
+using Content.IntegrationTests.Tests.Interaction;
+using Content.Shared.Damage.Components;
+using Content.Shared.Throwing;
+using Robust.Server.GameObjects;
+using Robust.Shared.Physics.Components;
+
+namespace Content.IntegrationTests.Tests.Physics;
+
+public sealed class ItemThrowingTest : InteractionTest
+{
+ /// <summary>
+ /// Check that an egg breaks when thrown at a wall.
+ /// </summary>
+ [Test]
+ [TestOf(typeof(ThrownItemComponent))]
+ [TestOf(typeof(DamageOnHighSpeedImpactComponent))]
+ public async Task TestThrownEggBreaks()
+ {
+ // Setup entities
+ var egg = await PlaceInHands("FoodEgg");
+ await SpawnTarget("WallSolid");
+ await RunTicks(5);
+ AssertExists(egg);
+
+ // Currently not a "thrown" item.
+ AssertComp<ThrownItemComponent>(hasComp: false, egg);
+ Assert.That(Comp<PhysicsComponent>(egg).BodyStatus, Is.Not.EqualTo(BodyStatus.InAir));
+
+ // Throw it.
+ await ThrowItem();
+ await RunTicks(1);
+ AssertExists(egg);
+ AssertComp<ThrownItemComponent>(hasComp: true, egg);
+ Assert.That(Comp<PhysicsComponent>(egg).BodyStatus, Is.EqualTo(BodyStatus.InAir));
+
+ // Splat
+ await RunTicks(30);
+ AssertDeleted(egg);
+ }
+
+ /// <summary>
+ /// Check that an egg thrown into space continues to be an egg.
+ /// I.e., verify that the deletions that happen in the other two tests aren't coincidental.
+ /// </summary>
+ [Test]
+ //[TestOf(typeof(Egg))]
+ public async Task TestEggIsEgg()
+ {
+ // Setup entities
+ var egg = await PlaceInHands("FoodEgg");
+ await RunTicks(5);
+ AssertExists(egg);
+
+ // Currently not a "thrown" item.
+ AssertComp<ThrownItemComponent>(hasComp: false, egg);
+ Assert.That(Comp<PhysicsComponent>(egg).BodyStatus, Is.Not.EqualTo(BodyStatus.InAir));
+
+ // Throw it
+ await ThrowItem();
+ await RunTicks(5);
+ AssertExists(egg);
+ AssertComp<ThrownItemComponent>(hasComp: true, egg);
+ Assert.That(Comp<PhysicsComponent>(egg).BodyStatus, Is.EqualTo(BodyStatus.InAir));
+
+ // Wait a while
+ await RunTicks(60);
+
+ // Egg is egg
+ AssertExists(egg);
+ AssertPrototype("FoodEgg", egg);
+ AssertComp<ThrownItemComponent>(hasComp: false, egg);
+ Assert.That(Comp<PhysicsComponent>(egg).BodyStatus, Is.Not.EqualTo(BodyStatus.InAir));
+ }
+
+ /// <summary>
+ /// Check that a physics can handle deleting a thrown entity. As to why this exists, see
+ /// https://github.com/space-wizards/RobustToolbox/pull/4746
+ /// </summary>
+ [Test]
+ [TestOf(typeof(ThrownItemComponent))]
+ [TestOf(typeof(PhysicsComponent))]
+ public async Task TestDeleteThrownItem()
+ {
+ // Setup entities
+ var pen = await PlaceInHands("Pen");
+ var physics = Comp<PhysicsComponent>(pen);
+ await RunTicks(5);
+ AssertExists(pen);
+
+ // Currently not a "thrown" item.
+ AssertComp<ThrownItemComponent>(hasComp: false, pen);
+ Assert.That(physics.BodyStatus, Is.Not.EqualTo(BodyStatus.InAir));
+
+ // Throw it
+ await ThrowItem();
+ await RunTicks(5);
+ AssertExists(pen);
+ AssertComp<ThrownItemComponent>(hasComp: true, pen);
+ Assert.That(physics.BodyStatus, Is.EqualTo(BodyStatus.InAir));
+ Assert.That(physics.CanCollide);
+
+ // Attempt to make it sleep mid-air. This happens automatically due to the sleep timer, but we just do it manually.
+ await Server.WaitPost(() => Server.System<PhysicsSystem>().SetAwake((ToServer(pen), physics), false));
+
+ // Then try and delete it
+ await Delete(pen);
+ await RunTicks(5);
+ AssertDeleted(pen);
+ }
+}
+
private static void DirtyAll(IEntityManager manager, EntityUid entityUid)
{
- foreach (var component in manager.GetComponents(entityUid))
+ foreach (var component in manager.GetNetComponents(entityUid))
{
- manager.Dirty((Component)component);
+ manager.Dirty(entityUid, component.component);
}
}
}
{
base.Initialize();
- SubscribeLocalEvent<HandsComponent, DisarmedEvent>(OnDisarmed, before: new[] { typeof(StunSystem) });
+ SubscribeLocalEvent<HandsComponent, DisarmedEvent>(OnDisarmed, before: new[] {typeof(StunSystem)});
SubscribeLocalEvent<HandsComponent, PullStartedMessage>(HandlePullStarted);
SubscribeLocalEvent<HandsComponent, PullStoppedMessage>(HandlePullStopped);
{
foreach (var hand in ent.Comp.Hands.Values)
{
- if (hand.HeldEntity is {} uid)
+ if (hand.HeldEntity is { } uid)
args.Contents.Add(uid);
}
}
return;
// Break any pulls
- if (TryComp(uid, out SharedPullerComponent? puller) && puller.Pulling is EntityUid pulled && TryComp(pulled, out SharedPullableComponent? pullable))
+ if (TryComp(uid, out SharedPullerComponent? puller) && puller.Pulling is EntityUid pulled &&
+ TryComp(pulled, out SharedPullableComponent? pullable))
_pullingSystem.TryStopPull(pullable);
if (!_handsSystem.TryDrop(uid, component.ActiveHand!, null, checkActionBlocker: false))
}
#region pulling
+
private void HandlePullStarted(EntityUid uid, HandsComponent component, PullStartedMessage args)
{
if (args.Puller.Owner != uid)
break;
}
}
+
#endregion
#region interactions
+
private bool HandleThrowItem(ICommonSession? playerSession, EntityCoordinates coordinates, EntityUid entity)
{
- if (playerSession == null)
+ if (playerSession?.AttachedEntity is not {Valid: true} player || !Exists(player))
return false;
- if (playerSession.AttachedEntity is not {Valid: true} player ||
- !Exists(player) ||
- ContainerSystem.IsEntityInContainer(player) ||
+ return ThrowHeldItem(player, coordinates);
+ }
+
+ /// <summary>
+ /// Throw the player's currently held item.
+ /// </summary>
+ public bool ThrowHeldItem(EntityUid player, EntityCoordinates coordinates, float minDistance = 0.1f)
+ {
+ if (ContainerSystem.IsEntityInContainer(player) ||
!TryComp(player, out HandsComponent? hands) ||
hands.ActiveHandEntity is not { } throwEnt ||
!_actionBlockerSystem.CanThrow(player, throwEnt))
if (direction == Vector2.Zero)
return true;
- direction = direction.Normalized() * Math.Min(direction.Length(), hands.ThrowRange);
+ var length = direction.Length();
+ var distance = Math.Clamp(length, minDistance, hands.ThrowRange);
+ direction *= distance/length;
var throwStrength = hands.ThrowForceMultiplier;
if (position == null)
{
var transform = Transform(uid);
- if (!_mapManager.TryGetGrid(transform.GridUid, out grid))
+ if (!_mapManager.TryGetGrid(transform.GridUid, out grid) || TerminatingOrDeleted(transform.GridUid.Value))
return neighbors;
tile = grid.TileIndicesFor(transform.Coordinates);
}
while (directionEnumerator.MoveNext(out var ent))
{
DebugTools.Assert(Transform(ent.Value).Anchored);
- if (spreaderQuery.HasComponent(ent))
+ if (spreaderQuery.HasComponent(ent) && !TerminatingOrDeleted(ent.Value))
neighbors.Add(ent.Value);
}
}
public float DamageCooldown = 2f;
[DataField("lastHit", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
- public TimeSpan LastHit = TimeSpan.Zero;
+ public TimeSpan? LastHit;
[DataField("damage", required: true), ViewVariables(VVAccess.ReadWrite)]
public DamageSpecifier Damage = default!;
if (speed < component.MinimumSpeed)
return;
- if ((_gameTiming.CurTime - component.LastHit).TotalSeconds < component.DamageCooldown)
+ if (component.LastHit != null
+ && (_gameTiming.CurTime - component.LastHit.Value).TotalSeconds < component.DamageCooldown)
return;
component.LastHit = _gameTiming.CurTime;
var impulseVector = direction.Normalized() * strength * physics.Mass;
_physics.ApplyLinearImpulse(uid, impulseVector, body: physics);
- if (comp.LandTime <= TimeSpan.Zero)
+ if (comp.LandTime == null || comp.LandTime <= TimeSpan.Zero)
{
_thrownSystem.LandComponent(uid, comp, physics, playSound);
}