diff --git a/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs b/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs index aae06965ef9..0a73943923e 100644 --- a/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs +++ b/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs @@ -773,9 +773,10 @@ private void AssignSlots(List assignments) 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()); diff --git a/Content.IntegrationTests/Pair/TestPair.cs b/Content.IntegrationTests/Pair/TestPair.cs index 2672b4db56e..ba2faacb7e0 100644 --- a/Content.IntegrationTests/Pair/TestPair.cs +++ b/Content.IntegrationTests/Pair/TestPair.cs @@ -28,6 +28,14 @@ public sealed partial class TestPair 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(); @@ -78,6 +86,8 @@ await Client.WaitPost(() => public void Kill() { State = PairState.Dead; + ServerLogHandler.ShuttingDown = true; + ClientLogHandler.ShuttingDown = true; Server.Dispose(); Client.Dispose(); } diff --git a/Content.IntegrationTests/PoolTestLogHandler.cs b/Content.IntegrationTests/PoolTestLogHandler.cs index efa185e148c..909bee9785a 100644 --- a/Content.IntegrationTests/PoolTestLogHandler.cs +++ b/Content.IntegrationTests/PoolTestLogHandler.cs @@ -36,8 +36,15 @@ public PoolTestLogHandler(string? prefix) _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. @@ -45,7 +52,6 @@ public void Log(string sawmillName, LogEvent message) 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(); diff --git a/Content.IntegrationTests/Tests/DoAfter/DoAfterCancellationTests.cs b/Content.IntegrationTests/Tests/DoAfter/DoAfterCancellationTests.cs index 5522ce8c54b..d47eb13273f 100644 --- a/Content.IntegrationTests/Tests/DoAfter/DoAfterCancellationTests.cs +++ b/Content.IntegrationTests/Tests/DoAfter/DoAfterCancellationTests.cs @@ -38,10 +38,10 @@ public async Task CancelWallDeconstruct() AssertAnchored(false); // Repeat for screwdriver interaction. - AssertDeleted(false); + AssertExists(); await Interact(Screw, awaitDoAfters: false); await CancelDoAfters(); - AssertDeleted(false); + AssertExists(); await Interact(Screw); AssertDeleted(); } diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs index 695de4461db..9aa0157c8af 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs @@ -134,9 +134,9 @@ await Server.WaitPost(() => /// /// Automatically enables welders. /// - protected async Task PlaceInHands(string? id, int quantity = 1, bool enableWelder = true) + protected async Task PlaceInHands(string id, int quantity = 1, bool enableWelder = true) { - return await PlaceInHands(id == null ? null : (id, quantity), enableWelder); + return await PlaceInHands((id, quantity), enableWelder); } /// @@ -145,7 +145,7 @@ await Server.WaitPost(() => /// /// Automatically enables welders. /// - protected async Task PlaceInHands(EntitySpecifier? entity, bool enableWelder = true) + protected async Task PlaceInHands(EntitySpecifier entity, bool enableWelder = true) { if (Hands.ActiveHand == null) { @@ -153,15 +153,9 @@ await Server.WaitPost(() => 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; @@ -184,7 +178,7 @@ await Server.WaitPost(() => if (enableWelder && itemToggle != null) Assert.That(itemToggle.Activated); - return item; + return SEntMan.GetNetEntity(item); } /// @@ -330,6 +324,17 @@ protected async Task Interact(params EntitySpecifier[] specifiers) } } + /// + /// Throw the currently held entity. Defaults to targeting the current + /// + protected async Task 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 /// @@ -483,7 +488,7 @@ protected void AssertAnchored(bool anchored = true, NetEntity? target = null) }); } - protected void AssertDeleted(bool deleted = true, NetEntity? target = null) + protected void AssertDeleted(NetEntity? target = null) { target ??= Target; if (target == null) @@ -494,8 +499,24 @@ protected void AssertDeleted(bool deleted = true, NetEntity? 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))); }); } @@ -733,6 +754,11 @@ protected async Task Delete(EntityUid uid) await RunTicks(5); } + protected Task Delete(NetEntity nuid) + { + return Delete(SEntMan.GetEntity(nuid)); + } + #region Time/Tick managment protected async Task RunTicks(int ticks) @@ -1064,4 +1090,35 @@ protected async Task Move(DirectionFlag dir, float seconds) } #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(uid); + + protected TransformComponent Xform(NetEntity uid) => Xform(ToServer(uid)); + protected TransformComponent Xform(EntityUid uid) => SEntMan.GetComponent(uid); + + protected EntityCoordinates Position(NetEntity uid) => Position(ToServer(uid)); + protected EntityCoordinates Position(EntityUid uid) => Xform(uid).Coordinates; + + #endregion } diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs index f54d772881b..45a880fe867 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs @@ -5,12 +5,12 @@ 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; @@ -66,6 +66,9 @@ public abstract partial class InteractionTest /// protected NetEntity Player; + protected EntityUid SPlayer => ToServer(Player); + protected EntityUid CPlayer => ToClient(Player); + protected ICommonSession ClientSession = default!; protected ICommonSession ServerSession = default!; @@ -81,6 +84,9 @@ public abstract partial class InteractionTest /// protected NetEntity? Target; + protected EntityUid? STarget => ToServer(Target); + protected EntityUid? CTarget => ToClient(Target); + /// /// When attempting to start construction, this is the client-side ID of the construction ghost. /// @@ -93,7 +99,7 @@ public abstract partial class InteractionTest 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!; @@ -152,7 +158,7 @@ public virtual async Task Setup() ProtoMan = Server.ResolveDependency(); Factory = Server.ResolveDependency(); STiming = Server.ResolveDependency(); - HandSys = SEntMan.System(); + HandSys = SEntMan.System(); InteractSys = SEntMan.System(); ToolSys = SEntMan.System(); ItemToggleSys = SEntMan.System(); diff --git a/Content.IntegrationTests/Tests/Networking/PvsCommandTest.cs b/Content.IntegrationTests/Tests/Networking/PvsCommandTest.cs new file mode 100644 index 00000000000..4783d21a053 --- /dev/null +++ b/Content.IntegrationTests/Tests/Networking/PvsCommandTest.cs @@ -0,0 +1,51 @@ +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, Is.GreaterThan(0)); + Assert.That(client.EntMan.Count, 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(); + } +} diff --git a/Content.IntegrationTests/Tests/Physics/ItemThrowingTest.cs b/Content.IntegrationTests/Tests/Physics/ItemThrowingTest.cs new file mode 100644 index 00000000000..4b762d18b2a --- /dev/null +++ b/Content.IntegrationTests/Tests/Physics/ItemThrowingTest.cs @@ -0,0 +1,111 @@ +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 +{ + /// + /// Check that an egg breaks when thrown at a wall. + /// + [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(hasComp: false, egg); + Assert.That(Comp(egg).BodyStatus, Is.Not.EqualTo(BodyStatus.InAir)); + + // Throw it. + await ThrowItem(); + await RunTicks(1); + AssertExists(egg); + AssertComp(hasComp: true, egg); + Assert.That(Comp(egg).BodyStatus, Is.EqualTo(BodyStatus.InAir)); + + // Splat + await RunTicks(30); + AssertDeleted(egg); + } + + /// + /// 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. + /// + [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(hasComp: false, egg); + Assert.That(Comp(egg).BodyStatus, Is.Not.EqualTo(BodyStatus.InAir)); + + // Throw it + await ThrowItem(); + await RunTicks(5); + AssertExists(egg); + AssertComp(hasComp: true, egg); + Assert.That(Comp(egg).BodyStatus, Is.EqualTo(BodyStatus.InAir)); + + // Wait a while + await RunTicks(60); + + // Egg is egg + AssertExists(egg); + AssertPrototype("FoodEgg", egg); + AssertComp(hasComp: false, egg); + Assert.That(Comp(egg).BodyStatus, Is.Not.EqualTo(BodyStatus.InAir)); + } + + /// + /// Check that a physics can handle deleting a thrown entity. As to why this exists, see + /// https://github.com/space-wizards/RobustToolbox/pull/4746 + /// + [Test] + [TestOf(typeof(ThrownItemComponent))] + [TestOf(typeof(PhysicsComponent))] + public async Task TestDeleteThrownItem() + { + // Setup entities + var pen = await PlaceInHands("Pen"); + var physics = Comp(pen); + await RunTicks(5); + AssertExists(pen); + + // Currently not a "thrown" item. + AssertComp(hasComp: false, pen); + Assert.That(physics.BodyStatus, Is.Not.EqualTo(BodyStatus.InAir)); + + // Throw it + await ThrowItem(); + await RunTicks(5); + AssertExists(pen); + AssertComp(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().SetAwake((ToServer(pen), physics), false)); + + // Then try and delete it + await Delete(pen); + await RunTicks(5); + AssertDeleted(pen); + } +} + diff --git a/Content.Server/Administration/Commands/DirtyCommand.cs b/Content.Server/Administration/Commands/DirtyCommand.cs index 0ed8689d7fc..cc0b0fcec67 100644 --- a/Content.Server/Administration/Commands/DirtyCommand.cs +++ b/Content.Server/Administration/Commands/DirtyCommand.cs @@ -38,9 +38,9 @@ public async void Execute(IConsoleShell shell, string argStr, string[] args) 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); } } } diff --git a/Content.Server/Hands/Systems/HandsSystem.cs b/Content.Server/Hands/Systems/HandsSystem.cs index f41298e98de..ae5a99bf28c 100644 --- a/Content.Server/Hands/Systems/HandsSystem.cs +++ b/Content.Server/Hands/Systems/HandsSystem.cs @@ -34,7 +34,7 @@ public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnDisarmed, before: new[] { typeof(StunSystem) }); + SubscribeLocalEvent(OnDisarmed, before: new[] {typeof(StunSystem)}); SubscribeLocalEvent(HandlePullStarted); SubscribeLocalEvent(HandlePullStopped); @@ -67,7 +67,7 @@ private void OnExploded(Entity ent, ref BeforeExplodeEvent args) { foreach (var hand in ent.Comp.Hands.Values) { - if (hand.HeldEntity is {} uid) + if (hand.HeldEntity is { } uid) args.Contents.Add(uid); } } @@ -78,7 +78,8 @@ private void OnDisarmed(EntityUid uid, HandsComponent component, DisarmedEvent a 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)) @@ -114,6 +115,7 @@ private void HandleBodyPartRemoved(EntityUid uid, HandsComponent component, ref } #region pulling + private void HandlePullStarted(EntityUid uid, HandsComponent component, PullStartedMessage args) { if (args.Puller.Owner != uid) @@ -146,17 +148,25 @@ private void HandlePullStopped(EntityUid uid, HandsComponent component, PullStop 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); + } + + /// + /// Throw the player's currently held item. + /// + 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)) @@ -176,7 +186,9 @@ hands.ActiveHandEntity is not { } 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; diff --git a/Content.Server/Spreader/SpreaderSystem.cs b/Content.Server/Spreader/SpreaderSystem.cs index 8afc7e6bd54..545d2cf3f12 100644 --- a/Content.Server/Spreader/SpreaderSystem.cs +++ b/Content.Server/Spreader/SpreaderSystem.cs @@ -312,7 +312,7 @@ public List GetSpreadableNeighbors(EntityUid uid, AirtightComponent? 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); } @@ -337,7 +337,7 @@ public List GetSpreadableNeighbors(EntityUid uid, AirtightComponent? 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); } } diff --git a/Content.Shared/Damage/Components/DamageOnHighSpeedImpactComponent.cs b/Content.Shared/Damage/Components/DamageOnHighSpeedImpactComponent.cs index 3eef1d70a4b..c56ed6537ca 100644 --- a/Content.Shared/Damage/Components/DamageOnHighSpeedImpactComponent.cs +++ b/Content.Shared/Damage/Components/DamageOnHighSpeedImpactComponent.cs @@ -32,7 +32,7 @@ public sealed partial class DamageOnHighSpeedImpactComponent : Component 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!; diff --git a/Content.Shared/Damage/Systems/DamageOnHighSpeedImpactSystem.cs b/Content.Shared/Damage/Systems/DamageOnHighSpeedImpactSystem.cs index 12bca522747..6768048defe 100644 --- a/Content.Shared/Damage/Systems/DamageOnHighSpeedImpactSystem.cs +++ b/Content.Shared/Damage/Systems/DamageOnHighSpeedImpactSystem.cs @@ -38,7 +38,8 @@ private void HandleCollide(EntityUid uid, DamageOnHighSpeedImpactComponent compo 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; diff --git a/Content.Shared/Throwing/ThrowingSystem.cs b/Content.Shared/Throwing/ThrowingSystem.cs index 229d8a72b24..a4c95ec5e36 100644 --- a/Content.Shared/Throwing/ThrowingSystem.cs +++ b/Content.Shared/Throwing/ThrowingSystem.cs @@ -145,7 +145,7 @@ public void TryThrow(EntityUid uid, 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); }