using Robust.Client.Input;
using Robust.Shared.Map;
-namespace Content.Client.DragDrop;
+namespace Content.Client.Interaction;
/// <summary>
/// Helper for implementing drag and drop interactions.
+using System.Numerics;
using Content.Client.CombatMode;
using Content.Client.Gameplay;
using Content.Client.Outline;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Popups;
-using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
-using System.Numerics;
using DrawDepth = Content.Shared.DrawDepth.DrawDepth;
-namespace Content.Client.DragDrop;
+namespace Content.Client.Interaction;
/// <summary>
/// Handles clientside drag and drop logic
/// </summary>
-[UsedImplicitly]
public sealed class DragDropSystem : SharedDragDropSystem
{
[Dependency] private readonly IStateManager _stateManager = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
- private ISawmill _sawmill = default!;
-
// how often to recheck possible targets (prevents calling expensive
// check logic each update)
private const float TargetRecheckInterval = 0.25f;
public override void Initialize()
{
base.Initialize();
- _sawmill = Logger.GetSawmill("drag_drop");
UpdatesOutsidePrediction = true;
UpdatesAfter.Add(typeof(SharedEyeSystem));
return;
}
- _sawmill.Warning($"Unable to display drag shadow for {ToPrettyString(_draggedEntity.Value)} because it has no sprite component.");
+ Log.Warning($"Unable to display drag shadow for {ToPrettyString(_draggedEntity.Value)} because it has no sprite component.");
}
private bool UpdateDrag(float frameTime)
}
// tell the server about the drop attempt
- RaiseNetworkEvent(new DragDropRequestEvent(GetNetEntity(_draggedEntity.Value), GetNetEntity(entity)));
+ RaisePredictiveEvent(new DragDropRequestEvent(GetNetEntity(_draggedEntity.Value), GetNetEntity(entity)));
EndDrag();
return true;
}
+++ /dev/null
-using Content.Client.Interactable;
-using Content.Shared.Climbing;
-using Content.Shared.DragDrop;
-
-namespace Content.Client.Movement.Systems;
-
-public sealed class ClimbSystem : SharedClimbSystem
-{
- [Dependency] private readonly InteractionSystem _interactionSystem = default!;
-
- public override void Initialize()
- {
- base.Initialize();
- SubscribeLocalEvent<ClimbableComponent, CanDropTargetEvent>(OnCanDragDropOn);
- }
-
- protected override void OnCanDragDropOn(EntityUid uid, ClimbableComponent component, ref CanDropTargetEvent args)
- {
- base.OnCanDragDropOn(uid, component, ref args);
-
- if (!args.CanDrop)
- return;
-
- var user = args.User;
- var target = uid;
- var dragged = args.Dragged;
- bool Ignored(EntityUid entity) => entity == target || entity == user || entity == dragged;
-
- args.CanDrop = _interactionSystem.InRangeUnobstructed(user, target, component.Range, predicate: Ignored)
- && _interactionSystem.InRangeUnobstructed(user, dragged, component.Range, predicate: Ignored);
- args.Handled = true;
- }
-}
using System.Runtime.InteropServices;
using Content.Client.Actions;
using Content.Client.Construction;
-using Content.Client.DragDrop;
using Content.Client.Gameplay;
using Content.Client.Hands;
+using Content.Client.Interaction;
using Content.Client.Outline;
using Content.Client.UserInterface.Controls;
using Content.Client.UserInterface.Systems.Actions.Controls;
#nullable enable
using Content.IntegrationTests.Tests.Interaction;
-using Content.Server.Climbing;
-using Content.Shared.Climbing;
using Robust.Shared.Maths;
+using ClimbingComponent = Content.Shared.Climbing.Components.ClimbingComponent;
+using ClimbSystem = Content.Shared.Climbing.Systems.ClimbSystem;
namespace Content.IntegrationTests.Tests.Climbing;
+++ /dev/null
-using System.Numerics;
-using Content.Server.Body.Systems;
-using Content.Server.Climbing.Components;
-using Content.Server.Interaction;
-using Content.Server.Popups;
-using Content.Server.Stunnable;
-using Content.Shared.ActionBlocker;
-using Content.Shared.Body.Components;
-using Content.Shared.Body.Part;
-using Content.Shared.Buckle.Components;
-using Content.Shared.Climbing;
-using Content.Shared.Climbing.Events;
-using Content.Shared.Damage;
-using Content.Shared.DoAfter;
-using Content.Shared.DragDrop;
-using Content.Shared.GameTicking;
-using Content.Shared.Hands.Components;
-using Content.Shared.IdentityManagement;
-using Content.Shared.Physics;
-using Content.Shared.Popups;
-using Content.Shared.Verbs;
-using JetBrains.Annotations;
-using Robust.Server.GameObjects;
-using Robust.Shared.Physics;
-using Robust.Shared.Physics.Collision.Shapes;
-using Robust.Shared.Physics.Components;
-using Robust.Shared.Physics.Dynamics;
-using Robust.Shared.Physics.Events;
-using Robust.Shared.Physics.Systems;
-using Robust.Shared.Player;
-
-namespace Content.Server.Climbing;
-
-[UsedImplicitly]
-public sealed class ClimbSystem : SharedClimbSystem
-{
- [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
- [Dependency] private readonly AudioSystem _audio = default!;
- [Dependency] private readonly BodySystem _bodySystem = default!;
- [Dependency] private readonly DamageableSystem _damageableSystem = default!;
- [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
- [Dependency] private readonly FixtureSystem _fixtureSystem = default!;
- [Dependency] private readonly PopupSystem _popupSystem = default!;
- [Dependency] private readonly InteractionSystem _interactionSystem = default!;
- [Dependency] private readonly StunSystem _stunSystem = default!;
- [Dependency] private readonly SharedPhysicsSystem _physics = default!;
-
- private const string ClimbingFixtureName = "climb";
- private const int ClimbingCollisionGroup = (int) (CollisionGroup.TableLayer | CollisionGroup.LowImpassable);
-
- private readonly Dictionary<EntityUid, Dictionary<string, Fixture>> _fixtureRemoveQueue = new();
-
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
- SubscribeLocalEvent<ClimbableComponent, GetVerbsEvent<AlternativeVerb>>(AddClimbableVerb);
- SubscribeLocalEvent<ClimbableComponent, DragDropTargetEvent>(OnClimbableDragDrop);
-
- SubscribeLocalEvent<ClimbingComponent, ClimbDoAfterEvent>(OnDoAfter);
- SubscribeLocalEvent<ClimbingComponent, EndCollideEvent>(OnClimbEndCollide);
- SubscribeLocalEvent<ClimbingComponent, BuckleChangeEvent>(OnBuckleChange);
-
- SubscribeLocalEvent<GlassTableComponent, ClimbedOnEvent>(OnGlassClimbed);
- }
-
- protected override void OnCanDragDropOn(EntityUid uid, ClimbableComponent component, ref CanDropTargetEvent args)
- {
- base.OnCanDragDropOn(uid, component, ref args);
-
- if (!args.CanDrop)
- return;
-
- string reason;
- var canVault = args.User == args.Dragged
- ? CanVault(component, args.User, uid, out reason)
- : CanVault(component, args.User, args.Dragged, uid, out reason);
-
- if (!canVault)
- _popupSystem.PopupEntity(reason, args.User, args.User);
-
- args.CanDrop = canVault;
- args.Handled = true;
- }
-
- private void AddClimbableVerb(EntityUid uid, ClimbableComponent component, GetVerbsEvent<AlternativeVerb> args)
- {
- if (!args.CanAccess || !args.CanInteract || !_actionBlockerSystem.CanMove(args.User))
- return;
-
- if (!TryComp(args.User, out ClimbingComponent? climbingComponent) || climbingComponent.IsClimbing)
- return;
-
- // TODO VERBS ICON add a climbing icon?
- args.Verbs.Add(new AlternativeVerb
- {
- Act = () => TryClimb(args.User, args.User, args.Target, out _, component),
- Text = Loc.GetString("comp-climbable-verb-climb")
- });
- }
-
- private void OnClimbableDragDrop(EntityUid uid, ClimbableComponent component, ref DragDropTargetEvent args)
- {
- // definitely a better way to check if two entities are equal
- // but don't have computer access and i have to do this without syntax
- if (args.Handled || args.User != args.Dragged && !HasComp<HandsComponent>(args.User))
- return;
- TryClimb(args.User, args.Dragged, uid, out _, component);
- }
-
- public bool TryClimb(EntityUid user,
- EntityUid entityToMove,
- EntityUid climbable,
- out DoAfterId? id,
- ClimbableComponent? comp = null,
- ClimbingComponent? climbing = null)
- {
- id = null;
-
- if (!Resolve(climbable, ref comp) || !Resolve(entityToMove, ref climbing))
- return false;
-
- // Note, IsClimbing does not mean a DoAfter is active, it means the target has already finished a DoAfter and
- // is currently on top of something..
- if (climbing.IsClimbing)
- return true;
-
- var args = new DoAfterArgs(EntityManager, user, comp.ClimbDelay, new ClimbDoAfterEvent(), entityToMove, target: climbable, used: entityToMove)
- {
- BreakOnTargetMove = true,
- BreakOnUserMove = true,
- BreakOnDamage = true
- };
-
- _audio.PlayPvs(comp.StartClimbSound, climbable);
- _doAfterSystem.TryStartDoAfter(args, out id);
- return true;
- }
-
- private void OnDoAfter(EntityUid uid, ClimbingComponent component, ClimbDoAfterEvent args)
- {
- if (args.Handled || args.Cancelled || args.Args.Target == null || args.Args.Used == null)
- return;
-
- Climb(uid, args.Args.User, args.Args.Used.Value, args.Args.Target.Value, climbing: component);
-
- args.Handled = true;
- }
-
- private void Climb(EntityUid uid, EntityUid user, EntityUid instigator, EntityUid climbable, bool silent = false, ClimbingComponent? climbing = null,
- PhysicsComponent? physics = null, FixturesComponent? fixtures = null, ClimbableComponent? comp = null)
- {
- if (!Resolve(uid, ref climbing, ref physics, ref fixtures, false))
- return;
-
- if (!Resolve(climbable, ref comp))
- return;
-
- if (!ReplaceFixtures(climbing, fixtures))
- return;
-
- climbing.IsClimbing = true;
- Dirty(climbing);
-
- _audio.PlayPvs(comp.FinishClimbSound, climbable);
- MoveEntityToward(uid, climbable, physics, climbing);
- // we may potentially need additional logic since we're forcing a player onto a climbable
- // there's also the cases where the user might collide with the person they are forcing onto the climbable that i haven't accounted for
-
- RaiseLocalEvent(uid, new StartClimbEvent(climbable), false);
- RaiseLocalEvent(climbable, new ClimbedOnEvent(uid, user), false);
-
- if (silent)
- return;
- if (user == uid)
- {
- var othersMessage = Loc.GetString("comp-climbable-user-climbs-other", ("user", Identity.Entity(uid, EntityManager)),
- ("climbable", climbable));
- uid.PopupMessageOtherClients(othersMessage);
-
- var selfMessage = Loc.GetString("comp-climbable-user-climbs", ("climbable", climbable));
- uid.PopupMessage(selfMessage);
- }
- else
- {
- var othersMessage = Loc.GetString("comp-climbable-user-climbs-force-other", ("user", Identity.Entity(user, EntityManager)),
- ("moved-user", Identity.Entity(uid, EntityManager)), ("climbable", climbable));
- user.PopupMessageOtherClients(othersMessage);
-
- var selfMessage = Loc.GetString("comp-climbable-user-climbs-force", ("moved-user", Identity.Entity(uid, EntityManager)),
- ("climbable", climbable));
- user.PopupMessage(selfMessage);
- }
- }
-
- /// <summary>
- /// Replaces the current fixtures with non-climbing collidable versions so that climb end can be detected
- /// </summary>
- /// <returns>Returns whether adding the new fixtures was successful</returns>
- private bool ReplaceFixtures(ClimbingComponent climbingComp, FixturesComponent fixturesComp)
- {
- var uid = climbingComp.Owner;
-
- // Swap fixtures
- foreach (var (name, fixture) in fixturesComp.Fixtures)
- {
- if (climbingComp.DisabledFixtureMasks.ContainsKey(name)
- || fixture.Hard == false
- || (fixture.CollisionMask & ClimbingCollisionGroup) == 0)
- continue;
-
- climbingComp.DisabledFixtureMasks.Add(name, fixture.CollisionMask & ClimbingCollisionGroup);
- _physics.SetCollisionMask(uid, name, fixture, fixture.CollisionMask & ~ClimbingCollisionGroup, fixturesComp);
- }
-
- if (!_fixtureSystem.TryCreateFixture(
- uid,
- new PhysShapeCircle(0.35f),
- ClimbingFixtureName,
- collisionLayer: (int) CollisionGroup.None,
- collisionMask: ClimbingCollisionGroup,
- hard: false,
- manager: fixturesComp))
- {
- return false;
- }
-
- return true;
- }
-
- private void OnClimbEndCollide(EntityUid uid, ClimbingComponent component, ref EndCollideEvent args)
- {
- if (args.OurFixtureId != ClimbingFixtureName
- || !component.IsClimbing
- || component.OwnerIsTransitioning)
- return;
-
- foreach (var fixture in args.OurFixture.Contacts.Keys)
- {
- if (fixture == args.OtherFixture)
- continue;
- // If still colliding with a climbable, do not stop climbing
- if (HasComp<ClimbableComponent>(args.OtherEntity))
- return;
- }
-
- StopClimb(uid, component);
- }
-
- private void StopClimb(EntityUid uid, ClimbingComponent? climbing = null, FixturesComponent? fixtures = null)
- {
- if (!Resolve(uid, ref climbing, ref fixtures, false))
- return;
-
- foreach (var (name, fixtureMask) in climbing.DisabledFixtureMasks)
- {
- if (!fixtures.Fixtures.TryGetValue(name, out var fixture))
- {
- continue;
- }
-
- _physics.SetCollisionMask(uid, name, fixture, fixture.CollisionMask | fixtureMask, fixtures);
- }
- climbing.DisabledFixtureMasks.Clear();
-
- if (!_fixtureRemoveQueue.TryGetValue(uid, out var removeQueue))
- {
- removeQueue = new Dictionary<string, Fixture>();
- _fixtureRemoveQueue.Add(uid, removeQueue);
- }
-
- if (fixtures.Fixtures.TryGetValue(ClimbingFixtureName, out var climbingFixture))
- removeQueue.Add(ClimbingFixtureName, climbingFixture);
-
- climbing.IsClimbing = false;
- climbing.OwnerIsTransitioning = false;
- var ev = new EndClimbEvent();
- RaiseLocalEvent(uid, ref ev);
- Dirty(climbing);
- }
-
- /// <summary>
- /// Checks if the user can vault the target
- /// </summary>
- /// <param name="component">The component of the entity that is being vaulted</param>
- /// <param name="user">The entity that wants to vault</param>
- /// <param name="target">The object that is being vaulted</param>
- /// <param name="reason">The reason why it cant be dropped</param>
- /// <returns></returns>
- public bool CanVault(ClimbableComponent component, EntityUid user, EntityUid target, out string reason)
- {
- if (!_actionBlockerSystem.CanInteract(user, target))
- {
- reason = Loc.GetString("comp-climbable-cant-interact");
- return false;
- }
-
- if (!HasComp<ClimbingComponent>(user)
- || !TryComp(user, out BodyComponent? body)
- || !_bodySystem.BodyHasPartType(user, BodyPartType.Leg, body)
- || !_bodySystem.BodyHasPartType(user, BodyPartType.Foot, body))
- {
- reason = Loc.GetString("comp-climbable-cant-climb");
- return false;
- }
-
- if (!_interactionSystem.InRangeUnobstructed(user, target, component.Range))
- {
- reason = Loc.GetString("comp-climbable-cant-reach");
- return false;
- }
-
- reason = string.Empty;
- return true;
- }
-
- /// <summary>
- /// Checks if the user can vault the dragged entity onto the the target
- /// </summary>
- /// <param name="component">The climbable component of the object being vaulted onto</param>
- /// <param name="user">The user that wants to vault the entity</param>
- /// <param name="dragged">The entity that is being vaulted</param>
- /// <param name="target">The object that is being vaulted onto</param>
- /// <param name="reason">The reason why it cant be dropped</param>
- /// <returns></returns>
- public bool CanVault(ClimbableComponent component, EntityUid user, EntityUid dragged, EntityUid target,
- out string reason)
- {
- if (!_actionBlockerSystem.CanInteract(user, dragged) || !_actionBlockerSystem.CanInteract(user, target))
- {
- reason = Loc.GetString("comp-climbable-cant-interact");
- return false;
- }
-
- if (!HasComp<ClimbingComponent>(dragged))
- {
- reason = Loc.GetString("comp-climbable-cant-climb");
- return false;
- }
-
- bool Ignored(EntityUid entity) => entity == target || entity == user || entity == dragged;
-
- if (!_interactionSystem.InRangeUnobstructed(user, target, component.Range, predicate: Ignored)
- || !_interactionSystem.InRangeUnobstructed(user, dragged, component.Range, predicate: Ignored))
- {
- reason = Loc.GetString("comp-climbable-cant-reach");
- return false;
- }
-
- reason = string.Empty;
- return true;
- }
-
- public void ForciblySetClimbing(EntityUid uid, EntityUid climbable, ClimbingComponent? component = null)
- {
- Climb(uid, uid, uid, climbable, true, component);
- }
-
- private void OnBuckleChange(EntityUid uid, ClimbingComponent component, ref BuckleChangeEvent args)
- {
- if (!args.Buckling)
- return;
- StopClimb(uid, component);
- }
-
- private void OnGlassClimbed(EntityUid uid, GlassTableComponent component, ClimbedOnEvent args)
- {
- if (TryComp<PhysicsComponent>(args.Climber, out var physics) && physics.Mass <= component.MassLimit)
- return;
-
- _damageableSystem.TryChangeDamage(args.Climber, component.ClimberDamage, origin: args.Climber);
- _damageableSystem.TryChangeDamage(uid, component.TableDamage, origin: args.Climber);
- _stunSystem.TryParalyze(args.Climber, TimeSpan.FromSeconds(component.StunTime), true);
-
- // Not shown to the user, since they already get a 'you climb on the glass table' popup
- _popupSystem.PopupEntity(
- Loc.GetString("glass-table-shattered-others", ("table", uid), ("climber", Identity.Entity(args.Climber, EntityManager))), args.Climber,
- Filter.PvsExcept(args.Climber), true);
- }
-
- /// <summary>
- /// Moves the entity toward the target climbed entity
- /// </summary>
- public void MoveEntityToward(EntityUid uid, EntityUid target, PhysicsComponent? physics = null, ClimbingComponent? climbing = null)
- {
- if (!Resolve(uid, ref physics, ref climbing, false))
- return;
-
- var from = Transform(uid).WorldPosition;
- var to = Transform(target).WorldPosition;
- var (x, y) = (to - from).Normalized();
-
- if (MathF.Abs(x) < 0.6f) // user climbed mostly vertically so lets make it a clean straight line
- to = new Vector2(from.X, to.Y);
- else if (MathF.Abs(y) < 0.6f) // user climbed mostly horizontally so lets make it a clean straight line
- to = new Vector2(to.X, from.Y);
-
- var velocity = (to - from).Length();
-
- if (velocity <= 0.0f)
- return;
-
- // Since there are bodies with different masses:
- // mass * 10 seems enough to move entity
- // instead of launching cats like rockets against the walls with constant impulse value.
- _physics.ApplyLinearImpulse(uid, (to - from).Normalized() * velocity * physics.Mass * 10, body: physics);
- _physics.SetBodyType(uid, BodyType.Dynamic, body: physics);
- climbing.OwnerIsTransitioning = true;
- _actionBlockerSystem.UpdateCanMove(uid);
-
- // Transition back to KinematicController after BufferTime
- climbing.Owner.SpawnTimer((int) (ClimbingComponent.BufferTime * 1000), () =>
- {
- if (climbing.Deleted)
- return;
-
- _physics.SetBodyType(uid, BodyType.KinematicController);
- climbing.OwnerIsTransitioning = false;
- _actionBlockerSystem.UpdateCanMove(uid);
- });
- }
-
- public override void Update(float frameTime)
- {
- foreach (var (uid, fixtures) in _fixtureRemoveQueue)
- {
- if (!TryComp<PhysicsComponent>(uid, out var physicsComp)
- || !TryComp<FixturesComponent>(uid, out var fixturesComp))
- {
- continue;
- }
-
- foreach (var fixture in fixtures)
- {
- _fixtureSystem.DestroyFixture(uid, fixture.Key, fixture.Value, body: physicsComp, manager: fixturesComp);
- }
- }
-
- _fixtureRemoveQueue.Clear();
- }
-
- private void Reset(RoundRestartCleanupEvent ev)
- {
- _fixtureRemoveQueue.Clear();
- }
-
-}
-
-/// <summary>
-/// Raised on an entity when it is climbed on.
-/// </summary>
-public sealed class ClimbedOnEvent : EntityEventArgs
-{
- public EntityUid Climber;
- public EntityUid Instigator;
-
- public ClimbedOnEvent(EntityUid climber, EntityUid instigator)
- {
- Climber = climber;
- Instigator = instigator;
- }
-}
-
-/// <summary>
-/// Raised on an entity when it successfully climbs on something.
-/// </summary>
-public sealed class StartClimbEvent : EntityEventArgs
-{
- public EntityUid Climbable;
-
- public StartClimbEvent(EntityUid climbable)
- {
- Climbable = climbable;
- }
-}
--- /dev/null
+using Content.Shared.DragDrop;
+
+namespace Content.Server.Interaction;
+
+public sealed class DragDropSystem : SharedDragDropSystem
+{
+
+}
{
base.Initialize();
- SubscribeNetworkEvent<DragDropRequestEvent>(HandleDragDropRequestEvent);
-
SubscribeLocalEvent<BoundUserInterfaceCheckRangeEvent>(HandleUserInterfaceRangeCheck);
}
return _uiSystem.SessionHasOpenUi(container.Owner, StorageComponent.StorageUiKey.Key, actor.PlayerSession);
}
- #region Drag drop
-
- private void HandleDragDropRequestEvent(DragDropRequestEvent msg, EntitySessionEventArgs args)
- {
- var dragged = GetEntity(msg.Dragged);
- var target = GetEntity(msg.Target);
-
- if (Deleted(dragged) || Deleted(target))
- return;
-
- var user = args.SenderSession.AttachedEntity;
-
- if (user == null || !_actionBlockerSystem.CanInteract(user.Value, target))
- return;
-
- // must be in range of both the target and the object they are drag / dropping
- // Client also does this check but ya know we gotta validate it.
- if (!InRangeUnobstructed(user.Value, dragged, popup: true)
- || !InRangeUnobstructed(user.Value, target, popup: true))
- {
- return;
- }
-
- var dragArgs = new DragDropDraggedEvent(user.Value, target);
-
- // trigger dragdrops on the dropped entity
- RaiseLocalEvent(dragged, ref dragArgs);
-
- if (dragArgs.Handled)
- return;
-
- var dropArgs = new DragDropTargetEvent(user.Value, dragged);
-
- // trigger dragdrops on the target entity (what you are dropping onto)
- RaiseLocalEvent(GetEntity(msg.Target), ref dropArgs);
- }
-
- #endregion
-
private void HandleUserInterfaceRangeCheck(ref BoundUserInterfaceCheckRangeEvent ev)
{
if (ev.Player.AttachedEntity is not { } user)
using System.Numerics;
using Content.Server.Body.Components;
-using Content.Server.Climbing;
using Content.Server.Construction;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Materials;
using Content.Shared.Audio;
using Content.Shared.CCVar;
using Content.Shared.Chemistry.Components;
+using Content.Shared.Climbing.Events;
using Content.Shared.Construction.Components;
using Content.Shared.Database;
using Content.Shared.DoAfter;
});
}
- private void OnClimbedOn(EntityUid uid, BiomassReclaimerComponent component, ClimbedOnEvent args)
+ private void OnClimbedOn(EntityUid uid, BiomassReclaimerComponent component, ref ClimbedOnEvent args)
{
if (!CanGib(uid, args.Climber, component))
{
using Content.Server.Body.Systems;
using Content.Server.Chemistry.Components.SolutionManager;
using Content.Server.Chemistry.EntitySystems;
-using Content.Server.Climbing;
using Content.Server.Medical.Components;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
using Robust.Server.GameObjects;
using Robust.Shared.Timing;
using Content.Server.Temperature.Components;
+using Content.Shared.Climbing.Systems;
namespace Content.Server.Medical;
-using Content.Server.Climbing;
using Content.Server.Cloning;
using Content.Server.Medical.Components;
using Content.Shared.Destructible;
using Content.Shared.DeviceLinking.Events;
using Content.Server.Power.EntitySystems;
using Content.Shared.Body.Components;
+using Content.Shared.Climbing.Systems;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Robust.Server.Containers;
using Robust.Shared.Physics.Events;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
+using ClimbableComponent = Content.Shared.Climbing.Components.ClimbableComponent;
namespace Content.Server.NPC.Pathfinding;
using Content.Shared.Physics;
using Robust.Shared.Map;
using Robust.Shared.Physics.Components;
+using ClimbingComponent = Content.Shared.Climbing.Components.ClimbingComponent;
namespace Content.Server.NPC.Systems;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Utility;
+using ClimbableComponent = Content.Shared.Climbing.Components.ClimbableComponent;
+using ClimbingComponent = Content.Shared.Climbing.Components.ClimbingComponent;
namespace Content.Server.NPC.Systems;
{
return SteeringObstacleStatus.Completed;
}
- else if (climbing.OwnerIsTransitioning)
+ else if (climbing.NextTransition != null)
{
return SteeringObstacleStatus.Continuing;
}
-using System.Linq;
using System.Numerics;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Administration.Managers;
-using Content.Server.Climbing;
using Content.Server.DoAfter;
using Content.Server.Doors.Systems;
using Content.Server.NPC.Components;
using Content.Server.NPC.Events;
using Content.Server.NPC.Pathfinding;
using Content.Shared.CCVar;
+using Content.Shared.Climbing.Systems;
using Content.Shared.CombatMode;
using Content.Shared.Interaction;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using Content.Shared.NPC;
-using Content.Shared.NPC;
using Content.Shared.NPC.Events;
using Content.Shared.Physics;
using Content.Shared.Weapons.Melee;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Random;
-using Robust.Shared.Threading;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Content.Shared.Prying.Systems;
RaiseLocalEvent(uid, ev);
if (component.CanMove == ev.Cancelled)
- Dirty(component);
+ Dirty(uid, component);
component.CanMove = !ev.Cancelled;
return !ev.Cancelled;
+++ /dev/null
-using Robust.Shared.GameStates;
-
-namespace Content.Shared.Climbing;
-
-[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
-public sealed partial class ClimbingComponent : Component
-{
- /// <summary>
- /// Whether the owner is climbing on a climbable entity.
- /// </summary>
- [ViewVariables, AutoNetworkedField]
- public bool IsClimbing { get; set; }
-
- /// <summary>
- /// Whether the owner is being moved onto the climbed entity.
- /// </summary>
- [ViewVariables, AutoNetworkedField]
- public bool OwnerIsTransitioning { get; set; }
-
- /// <summary>
- /// We'll launch the mob onto the table and give them at least this amount of time to be on it.
- /// </summary>
- public const float BufferTime = 0.3f;
-
- [ViewVariables]
- public Dictionary<string, int> DisabledFixtureMasks { get; } = new();
-}
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
-namespace Content.Shared.Climbing;
+namespace Content.Shared.Climbing.Components;
/// <summary>
/// Makes entity do damage and stun entities with ClumsyComponent
/// upon DragDrop or Climb interactions.
/// </summary>
-[RegisterComponent, NetworkedComponent, Access(typeof(BonkSystem))]
+[RegisterComponent, NetworkedComponent, Access(typeof(Systems.BonkSystem))]
public sealed partial class BonkableComponent : Component
{
/// <summary>
-using Content.Shared.CCVar;
-using Content.Shared.Damage;
using Content.Shared.Interaction;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
-namespace Content.Shared.Climbing
+namespace Content.Shared.Climbing.Components
{
+ /// <summary>
+ /// Indicates this entity can be vaulted on top of.
+ /// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class ClimbableComponent : Component
{
/// The time it takes to climb onto the entity.
/// </summary>
[DataField("delay")]
- public float ClimbDelay = 0.8f;
+ public float ClimbDelay = 1.5f;
/// <summary>
/// Sound to be played when a climb is started.
--- /dev/null
+using System.Numerics;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Shared.Climbing.Components;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class ClimbingComponent : Component
+{
+ /// <summary>
+ /// Whether the owner is climbing on a climbable entity.
+ /// </summary>
+ [AutoNetworkedField, DataField]
+ public bool IsClimbing;
+
+ /// <summary>
+ /// Whether the owner is being moved onto the climbed entity.
+ /// </summary>
+ [AutoNetworkedField, DataField(customTypeSerializer:typeof(TimeOffsetSerializer))]
+ public TimeSpan? NextTransition;
+
+ /// <summary>
+ /// Direction to move when transition.
+ /// </summary>
+ [AutoNetworkedField, DataField]
+ public Vector2 Direction;
+
+ /// <summary>
+ /// How fast the entity is moved when climbing.
+ /// </summary>
+ [DataField]
+ public float TransitionRate = 5f;
+
+ [AutoNetworkedField, DataField]
+ public Dictionary<string, int> DisabledFixtureMasks = new();
+}
using Content.Shared.Damage;
-namespace Content.Server.Climbing.Components;
+namespace Content.Shared.Climbing.Components;
/// <summary>
/// Glass tables shatter and stun you when climbed on.
/// This is a really entity-specific behavior, so opted to make it
/// not very generalized with regards to naming.
/// </summary>
-[RegisterComponent, Access(typeof(ClimbSystem))]
+[RegisterComponent, Access(typeof(Systems.ClimbSystem))]
public sealed partial class GlassTableComponent : Component
{
/// <summary>
--- /dev/null
+namespace Content.Shared.Climbing.Events;
+
+/// <summary>
+/// Raised on an entity when it is climbed on.
+/// </summary>
+[ByRefEvent]
+public readonly record struct ClimbedOnEvent(EntityUid Climber, EntityUid Instigator);
/// Raised on an entity when it ends climbing.
/// </summary>
[ByRefEvent]
-public readonly record struct EndClimbEvent
-{
-
-}
+public readonly record struct EndClimbEvent;
--- /dev/null
+namespace Content.Shared.Climbing.Events;
+
+/// <summary>
+/// Raised on an entity when it successfully climbs on something.
+/// </summary>
+[ByRefEvent]
+public readonly record struct StartClimbEvent(EntityUid Climbable);
+++ /dev/null
-using Content.Shared.DoAfter;
-using Content.Shared.DragDrop;
-using Content.Shared.Movement.Events;
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Climbing;
-
-public abstract partial class SharedClimbSystem : EntitySystem
-{
- public override void Initialize()
- {
- base.Initialize();
- SubscribeLocalEvent<ClimbingComponent, UpdateCanMoveEvent>(HandleMoveAttempt);
- }
-
- private static void HandleMoveAttempt(EntityUid uid, ClimbingComponent component, UpdateCanMoveEvent args)
- {
- if (component.LifeStage > ComponentLifeStage.Running)
- return;
-
- if (component.OwnerIsTransitioning)
- args.Cancel();
- }
-
- protected virtual void OnCanDragDropOn(EntityUid uid, ClimbableComponent component, ref CanDropTargetEvent args)
- {
- args.CanDrop = HasComp<ClimbingComponent>(args.Dragged);
- }
-
- [Serializable, NetSerializable]
- protected sealed partial class ClimbDoAfterEvent : SimpleDoAfterEvent
- {
- }
-}
-using Content.Shared.Interaction;
-using Content.Shared.Stunnable;
using Content.Shared.CCVar;
+using Content.Shared.Climbing.Components;
using Content.Shared.Damage;
using Content.Shared.DoAfter;
using Content.Shared.DragDrop;
-using Robust.Shared.Configuration;
-using Content.Shared.Popups;
using Content.Shared.IdentityManagement;
+using Content.Shared.Interaction;
using Content.Shared.Interaction.Components;
+using Content.Shared.Popups;
+using Content.Shared.Stunnable;
+using Robust.Shared.Configuration;
using Robust.Shared.Player;
using Robust.Shared.Serialization;
-namespace Content.Shared.Climbing;
+namespace Content.Shared.Climbing.Systems;
public sealed partial class BonkSystem : EntitySystem
{
SubscribeLocalEvent<BonkableComponent, BonkDoAfterEvent>(OnBonkDoAfter);
}
- private void OnBonkDoAfter(EntityUid uid, BonkableComponent component, BonkDoAfterEvent args)
+ private void OnBonkDoAfter(EntityUid uid, Components.BonkableComponent component, BonkDoAfterEvent args)
{
if (args.Handled || args.Cancelled || args.Args.Target == null)
return;
}
- public bool TryBonk(EntityUid user, EntityUid bonkableUid, BonkableComponent? bonkableComponent = null)
+ public bool TryBonk(EntityUid user, EntityUid bonkableUid, Components.BonkableComponent? bonkableComponent = null)
{
if (!Resolve(bonkableUid, ref bonkableComponent, false))
return false;
}
- private void OnDragDrop(EntityUid uid, BonkableComponent component, ref DragDropTargetEvent args)
+ private void OnDragDrop(EntityUid uid, Components.BonkableComponent component, ref DragDropTargetEvent args)
{
if (args.Handled || !HasComp<ClumsyComponent>(args.Dragged))
return;
--- /dev/null
+using System.Numerics;
+using Content.Shared.ActionBlocker;
+using Content.Shared.Body.Components;
+using Content.Shared.Body.Part;
+using Content.Shared.Body.Systems;
+using Content.Shared.Buckle.Components;
+using Content.Shared.Climbing.Components;
+using Content.Shared.Climbing.Events;
+using Content.Shared.Damage;
+using Content.Shared.DoAfter;
+using Content.Shared.DragDrop;
+using Content.Shared.Hands.Components;
+using Content.Shared.IdentityManagement;
+using Content.Shared.Interaction;
+using Content.Shared.Movement.Events;
+using Content.Shared.Movement.Systems;
+using Content.Shared.Physics;
+using Content.Shared.Popups;
+using Content.Shared.Stunnable;
+using Content.Shared.Verbs;
+using Robust.Shared.Physics;
+using Robust.Shared.Physics.Collision.Shapes;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Physics.Controllers;
+using Robust.Shared.Physics.Events;
+using Robust.Shared.Physics.Systems;
+using Robust.Shared.Player;
+using Robust.Shared.Serialization;
+using Robust.Shared.Timing;
+
+namespace Content.Shared.Climbing.Systems;
+
+public sealed partial class ClimbSystem : VirtualController
+{
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
+ [Dependency] private readonly DamageableSystem _damageableSystem = default!;
+ [Dependency] private readonly FixtureSystem _fixtureSystem = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly SharedBodySystem _bodySystem = default!;
+ [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
+ [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
+ [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
+ [Dependency] private readonly SharedPhysicsSystem _physics = default!;
+ [Dependency] private readonly SharedStunSystem _stunSystem = default!;
+ [Dependency] private readonly SharedTransformSystem _xformSystem = default!;
+
+ private const string ClimbingFixtureName = "climb";
+ private const int ClimbingCollisionGroup = (int) (CollisionGroup.TableLayer | CollisionGroup.LowImpassable);
+
+ private EntityQuery<FixturesComponent> _fixturesQuery;
+ private EntityQuery<TransformComponent> _xformQuery;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _fixturesQuery = GetEntityQuery<FixturesComponent>();
+ _xformQuery = GetEntityQuery<TransformComponent>();
+
+ SubscribeLocalEvent<ClimbingComponent, UpdateCanMoveEvent>(OnMoveAttempt);
+ SubscribeLocalEvent<ClimbingComponent, EntParentChangedMessage>(OnParentChange);
+ SubscribeLocalEvent<ClimbingComponent, ClimbDoAfterEvent>(OnDoAfter);
+ SubscribeLocalEvent<ClimbingComponent, EndCollideEvent>(OnClimbEndCollide);
+ SubscribeLocalEvent<ClimbingComponent, BuckleChangeEvent>(OnBuckleChange);
+ SubscribeLocalEvent<ClimbingComponent, EntityUnpausedEvent>(OnClimbableUnpaused);
+
+ SubscribeLocalEvent<ClimbableComponent, CanDropTargetEvent>(OnCanDragDropOn);
+ SubscribeLocalEvent<ClimbableComponent, GetVerbsEvent<AlternativeVerb>>(AddClimbableVerb);
+ SubscribeLocalEvent<ClimbableComponent, DragDropTargetEvent>(OnClimbableDragDrop);
+
+ SubscribeLocalEvent<GlassTableComponent, ClimbedOnEvent>(OnGlassClimbed);
+ }
+
+ private void OnClimbableUnpaused(EntityUid uid, ClimbingComponent component, ref EntityUnpausedEvent args)
+ {
+ if (component.NextTransition == null)
+ return;
+
+ component.NextTransition = component.NextTransition.Value + args.PausedTime;
+ Dirty(uid, component);
+ }
+
+ public override void UpdateBeforeSolve(bool prediction, float frameTime)
+ {
+ base.UpdateBeforeSolve(prediction, frameTime);
+
+ var query = EntityQueryEnumerator<ClimbingComponent>();
+ var curTime = _timing.CurTime;
+
+ // Move anything still climb in the specified direction.
+ while (query.MoveNext(out var uid, out var comp))
+ {
+ if (comp.NextTransition == null)
+ continue;
+
+ if (comp.NextTransition < curTime)
+ {
+ FinishTransition(uid, comp);
+ continue;
+ }
+
+ var xform = _xformQuery.GetComponent(uid);
+ _xformSystem.SetLocalPositionNoLerp(uid, xform.LocalPosition + comp.Direction * frameTime, xform);
+ }
+ }
+
+ private void FinishTransition(EntityUid uid, ClimbingComponent comp)
+ {
+ // TODO: Validate climb here
+ comp.NextTransition = null;
+ _actionBlockerSystem.UpdateCanMove(uid);
+ Dirty(uid, comp);
+
+ // Stop if necessary.
+ if (!_fixturesQuery.TryGetComponent(uid, out var fixtures) ||
+ !IsClimbing(uid, fixtures))
+ {
+ StopClimb(uid, comp);
+ return;
+ }
+ }
+
+ /// <summary>
+ /// Returns true if entity currently has a valid vault.
+ /// </summary>
+ private bool IsClimbing(EntityUid uid, FixturesComponent? fixturesComp = null)
+ {
+ if (!_fixturesQuery.Resolve(uid, ref fixturesComp) || !fixturesComp.Fixtures.TryGetValue(ClimbingFixtureName, out var climbFixture))
+ return false;
+
+ foreach (var contact in climbFixture.Contacts.Values)
+ {
+ var other = uid == contact.EntityA ? contact.EntityB : contact.EntityA;
+
+ if (HasComp<ClimbableComponent>(other))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private void OnMoveAttempt(EntityUid uid, ClimbingComponent component, UpdateCanMoveEvent args)
+ {
+ // Can't move when transition.
+ if (component.NextTransition != null)
+ args.Cancel();
+ }
+
+ private void OnParentChange(EntityUid uid, ClimbingComponent component, ref EntParentChangedMessage args)
+ {
+ if (component.NextTransition != null)
+ {
+ StopClimb(uid, component);
+ }
+ }
+
+ private void OnCanDragDropOn(EntityUid uid, ClimbableComponent component, ref CanDropTargetEvent args)
+ {
+ if (args.Handled)
+ return;
+
+ var canVault = args.User == args.Dragged
+ ? CanVault(component, args.User, uid, out _)
+ : CanVault(component, args.User, args.Dragged, uid, out _);
+
+ args.CanDrop = canVault;
+ args.Handled = true;
+ }
+
+ private void AddClimbableVerb(EntityUid uid, ClimbableComponent component, GetVerbsEvent<AlternativeVerb> args)
+ {
+ if (!args.CanAccess || !args.CanInteract || !_actionBlockerSystem.CanMove(args.User))
+ return;
+
+ if (!TryComp(args.User, out ClimbingComponent? climbingComponent) || climbingComponent.IsClimbing)
+ return;
+
+ // TODO VERBS ICON add a climbing icon?
+ args.Verbs.Add(new AlternativeVerb
+ {
+ Act = () => TryClimb(args.User, args.User, args.Target, out _, component),
+ Text = Loc.GetString("comp-climbable-verb-climb")
+ });
+ }
+
+ private void OnClimbableDragDrop(EntityUid uid, ClimbableComponent component, ref DragDropTargetEvent args)
+ {
+ // definitely a better way to check if two entities are equal
+ // but don't have computer access and i have to do this without syntax
+ if (args.Handled || args.User != args.Dragged && !HasComp<HandsComponent>(args.User))
+ return;
+
+ TryClimb(args.User, args.Dragged, uid, out _, component);
+ }
+
+ public bool TryClimb(
+ EntityUid user,
+ EntityUid entityToMove,
+ EntityUid climbable,
+ out DoAfterId? id,
+ ClimbableComponent? comp = null,
+ ClimbingComponent? climbing = null)
+ {
+ id = null;
+
+ if (!Resolve(climbable, ref comp) || !Resolve(entityToMove, ref climbing))
+ return false;
+
+ // Note, IsClimbing does not mean a DoAfter is active, it means the target has already finished a DoAfter and
+ // is currently on top of something..
+ if (climbing.IsClimbing)
+ return true;
+
+ var args = new DoAfterArgs(EntityManager, user, comp.ClimbDelay, new ClimbDoAfterEvent(),
+ entityToMove,
+ target: climbable,
+ used: entityToMove)
+ {
+ BreakOnTargetMove = true,
+ BreakOnUserMove = true,
+ BreakOnDamage = true
+ };
+
+ _audio.PlayPredicted(comp.StartClimbSound, climbable, user);
+ return _doAfterSystem.TryStartDoAfter(args, out id);
+ }
+
+ private void OnDoAfter(EntityUid uid, ClimbingComponent component, ClimbDoAfterEvent args)
+ {
+ if (args.Handled || args.Cancelled || args.Args.Target == null || args.Args.Used == null)
+ return;
+
+ Climb(uid, args.Args.User, args.Args.Target.Value, climbing: component);
+ args.Handled = true;
+ }
+
+ private void Climb(EntityUid uid, EntityUid user, EntityUid climbable, bool silent = false, ClimbingComponent? climbing = null,
+ PhysicsComponent? physics = null, FixturesComponent? fixtures = null, ClimbableComponent? comp = null)
+ {
+ if (!Resolve(uid, ref climbing, ref physics, ref fixtures, false))
+ return;
+
+ if (!Resolve(climbable, ref comp))
+ return;
+
+ if (!ReplaceFixtures(uid, climbing, fixtures))
+ return;
+
+ var xform = _xformQuery.GetComponent(uid);
+ var (worldPos, worldRot) = _xformSystem.GetWorldPositionRotation(xform);
+ var worldDirection = _xformSystem.GetWorldPosition(climbable) - worldPos;
+ var distance = worldDirection.Length();
+ var parentRot = (worldRot - xform.LocalRotation);
+ // Need direction relative to climber's parent.
+ var localDirection = (-parentRot).RotateVec(worldDirection);
+
+ climbing.IsClimbing = true;
+ var climbDuration = TimeSpan.FromSeconds(distance / climbing.TransitionRate);
+ climbing.NextTransition = _timing.CurTime + climbDuration;
+
+ climbing.Direction = localDirection.Normalized() * climbing.TransitionRate;
+ Dirty(uid, climbing);
+
+ _audio.PlayPredicted(comp.FinishClimbSound, climbable, user);
+ _actionBlockerSystem.UpdateCanMove(uid);
+
+ var startEv = new StartClimbEvent(climbable);
+ var climbedEv = new ClimbedOnEvent(uid, user);
+ RaiseLocalEvent(uid, ref startEv);
+ RaiseLocalEvent(climbable, ref climbedEv);
+
+ if (silent)
+ return;
+
+ string selfMessage;
+ string othersMessage;
+
+ if (user == uid)
+ {
+ othersMessage = Loc.GetString("comp-climbable-user-climbs-other",
+ ("user", Identity.Entity(uid, EntityManager)),
+ ("climbable", climbable));
+
+ selfMessage = Loc.GetString("comp-climbable-user-climbs", ("climbable", climbable));
+ }
+ else
+ {
+ othersMessage = Loc.GetString("comp-climbable-user-climbs-force-other",
+ ("user", Identity.Entity(user, EntityManager)),
+ ("moved-user", Identity.Entity(uid, EntityManager)), ("climbable", climbable));
+
+ selfMessage = Loc.GetString("comp-climbable-user-climbs-force", ("moved-user", Identity.Entity(uid, EntityManager)),
+ ("climbable", climbable));
+ }
+
+ _popupSystem.PopupEntity(othersMessage, uid, Filter.PvsExcept(user, entityManager: EntityManager), true);
+ _popupSystem.PopupClient(selfMessage, uid, user);
+ }
+
+ /// <summary>
+ /// Replaces the current fixtures with non-climbing collidable versions so that climb end can be detected
+ /// </summary>
+ /// <returns>Returns whether adding the new fixtures was successful</returns>
+ private bool ReplaceFixtures(EntityUid uid, ClimbingComponent climbingComp, FixturesComponent fixturesComp)
+ {
+ // Swap fixtures
+ foreach (var (name, fixture) in fixturesComp.Fixtures)
+ {
+ if (climbingComp.DisabledFixtureMasks.ContainsKey(name)
+ || fixture.Hard == false
+ || (fixture.CollisionMask & ClimbingCollisionGroup) == 0)
+ {
+ continue;
+ }
+
+ climbingComp.DisabledFixtureMasks.Add(name, fixture.CollisionMask & ClimbingCollisionGroup);
+ _physics.SetCollisionMask(uid, name, fixture, fixture.CollisionMask & ~ClimbingCollisionGroup, fixturesComp);
+ }
+
+ if (!_fixtureSystem.TryCreateFixture(
+ uid,
+ new PhysShapeCircle(0.35f),
+ ClimbingFixtureName,
+ collisionLayer: (int) CollisionGroup.None,
+ collisionMask: ClimbingCollisionGroup,
+ hard: false,
+ manager: fixturesComp))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ private void OnClimbEndCollide(EntityUid uid, ClimbingComponent component, ref EndCollideEvent args)
+ {
+ if (args.OurFixtureId != ClimbingFixtureName
+ || !component.IsClimbing
+ || component.NextTransition != null)
+ {
+ return;
+ }
+
+ foreach (var fixture in args.OurFixture.Contacts.Keys)
+ {
+ if (fixture == args.OtherFixture)
+ continue;
+
+ // If still colliding with a climbable, do not stop climbing
+ if (HasComp<ClimbableComponent>(args.OtherEntity))
+ return;
+ }
+
+ StopClimb(uid, component);
+ }
+
+ private void StopClimb(EntityUid uid, ClimbingComponent? climbing = null, FixturesComponent? fixtures = null)
+ {
+ if (!Resolve(uid, ref climbing, ref fixtures, false))
+ return;
+
+ foreach (var (name, fixtureMask) in climbing.DisabledFixtureMasks)
+ {
+ if (!fixtures.Fixtures.TryGetValue(name, out var fixture))
+ {
+ continue;
+ }
+
+ _physics.SetCollisionMask(uid, name, fixture, fixture.CollisionMask | fixtureMask, fixtures);
+ }
+
+ climbing.DisabledFixtureMasks.Clear();
+ _fixtureSystem.DestroyFixture(uid, ClimbingFixtureName, manager: fixtures);
+ climbing.IsClimbing = false;
+ climbing.NextTransition = null;
+ var ev = new EndClimbEvent();
+ RaiseLocalEvent(uid, ref ev);
+ Dirty(uid, climbing);
+ }
+
+ /// <summary>
+ /// Checks if the user can vault the target
+ /// </summary>
+ /// <param name="component">The component of the entity that is being vaulted</param>
+ /// <param name="user">The entity that wants to vault</param>
+ /// <param name="target">The object that is being vaulted</param>
+ /// <param name="reason">The reason why it cant be dropped</param>
+ public bool CanVault(ClimbableComponent component, EntityUid user, EntityUid target, out string reason)
+ {
+ if (!_actionBlockerSystem.CanInteract(user, target))
+ {
+ reason = Loc.GetString("comp-climbable-cant-interact");
+ return false;
+ }
+
+ if (!HasComp<ClimbingComponent>(user)
+ || !TryComp(user, out BodyComponent? body)
+ || !_bodySystem.BodyHasPartType(user, BodyPartType.Leg, body)
+ || !_bodySystem.BodyHasPartType(user, BodyPartType.Foot, body))
+ {
+ reason = Loc.GetString("comp-climbable-cant-climb");
+ return false;
+ }
+
+ if (!_interactionSystem.InRangeUnobstructed(user, target, component.Range))
+ {
+ reason = Loc.GetString("comp-climbable-cant-reach");
+ return false;
+ }
+
+ reason = string.Empty;
+ return true;
+ }
+
+ /// <summary>
+ /// Checks if the user can vault the dragged entity onto the the target
+ /// </summary>
+ /// <param name="component">The climbable component of the object being vaulted onto</param>
+ /// <param name="user">The user that wants to vault the entity</param>
+ /// <param name="dragged">The entity that is being vaulted</param>
+ /// <param name="target">The object that is being vaulted onto</param>
+ /// <param name="reason">The reason why it cant be dropped</param>
+ /// <returns></returns>
+ public bool CanVault(ClimbableComponent component, EntityUid user, EntityUid dragged, EntityUid target,
+ out string reason)
+ {
+ if (!_actionBlockerSystem.CanInteract(user, dragged) || !_actionBlockerSystem.CanInteract(user, target))
+ {
+ reason = Loc.GetString("comp-climbable-cant-interact");
+ return false;
+ }
+
+ if (!HasComp<ClimbingComponent>(dragged))
+ {
+ reason = Loc.GetString("comp-climbable-cant-climb");
+ return false;
+ }
+
+ bool Ignored(EntityUid entity) => entity == target || entity == user || entity == dragged;
+
+ if (!_interactionSystem.InRangeUnobstructed(user, target, component.Range, predicate: Ignored)
+ || !_interactionSystem.InRangeUnobstructed(user, dragged, component.Range, predicate: Ignored))
+ {
+ reason = Loc.GetString("comp-climbable-cant-reach");
+ return false;
+ }
+
+ reason = string.Empty;
+ return true;
+ }
+
+ public void ForciblySetClimbing(EntityUid uid, EntityUid climbable, ClimbingComponent? component = null)
+ {
+ Climb(uid, uid, climbable, true, component);
+ }
+
+ private void OnBuckleChange(EntityUid uid, ClimbingComponent component, ref BuckleChangeEvent args)
+ {
+ if (!args.Buckling)
+ return;
+ StopClimb(uid, component);
+ }
+
+ private void OnGlassClimbed(EntityUid uid, GlassTableComponent component, ref ClimbedOnEvent args)
+ {
+ if (TryComp<PhysicsComponent>(args.Climber, out var physics) && physics.Mass <= component.MassLimit)
+ return;
+
+ _damageableSystem.TryChangeDamage(args.Climber, component.ClimberDamage, origin: args.Climber);
+ _damageableSystem.TryChangeDamage(uid, component.TableDamage, origin: args.Climber);
+ _stunSystem.TryParalyze(args.Climber, TimeSpan.FromSeconds(component.StunTime), true);
+
+ // Not shown to the user, since they already get a 'you climb on the glass table' popup
+ _popupSystem.PopupEntity(
+ Loc.GetString("glass-table-shattered-others", ("table", uid), ("climber", Identity.Entity(args.Climber, EntityManager))), args.Climber,
+ Filter.PvsExcept(args.Climber), true);
+ }
+
+ [Serializable, NetSerializable]
+ private sealed partial class ClimbDoAfterEvent : SimpleDoAfterEvent
+ {
+ }
+}
doAfter.CancelledTime = doAfter.CancelledTime.Value + args.PausedTime;
}
- Dirty(component);
+ Dirty(uid, component);
}
private void OnStateChanged(EntityUid uid, DoAfterComponent component, MobStateChangedEvent args)
{
InternalCancel(doAfter, component);
}
- Dirty(component);
+ Dirty(uid, component);
}
/// <summary>
/// </summary>
private void OnDamage(EntityUid uid, DoAfterComponent component, DamageChangedEvent args)
{
- if (!args.InterruptsDoAfters || !args.DamageIncreased || args.DamageDelta == null)
+ // If we're applying state then let the server state handle the do_after prediction.
+ // This is to avoid scenarios where a do_after is erroneously cancelled on the final tick.
+ if (!args.InterruptsDoAfters || !args.DamageIncreased || args.DamageDelta == null || GameTiming.ApplyingState)
return;
- var delta = args.DamageDelta?.Total;
+ var delta = args.DamageDelta.GetTotal();
var dirty = false;
foreach (var doAfter in component.DoAfters.Values)
}
if (dirty)
- Dirty(component);
+ Dirty(uid, component);
}
private void RaiseDoAfterEvents(DoAfter doAfter, DoAfterComponent component)
comp.DoAfters.Add(doAfter.Index, doAfter);
EnsureComp<ActiveDoAfterComponent>(args.User);
- Dirty(comp);
+ Dirty(args.User, comp);
args.Event.DoAfter = doAfter;
return true;
}
-namespace Content.Shared.DragDrop;
+using Content.Shared.ActionBlocker;
+using Content.Shared.Interaction;
+
+namespace Content.Shared.DragDrop;
public abstract class SharedDragDropSystem : EntitySystem
{
+ [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
+ [Dependency] private readonly SharedInteractionSystem _interaction = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeAllEvent<DragDropRequestEvent>(OnDragDropRequestEvent);
+ }
+
+ private void OnDragDropRequestEvent(DragDropRequestEvent msg, EntitySessionEventArgs args)
+ {
+ var dragged = GetEntity(msg.Dragged);
+ var target = GetEntity(msg.Target);
+
+ if (Deleted(dragged) || Deleted(target))
+ return;
+
+ var user = args.SenderSession.AttachedEntity;
+
+ if (user == null || !_actionBlockerSystem.CanInteract(user.Value, target))
+ return;
+
+ // must be in range of both the target and the object they are drag / dropping
+ // Client also does this check but ya know we gotta validate it.
+ if (!_interaction.InRangeUnobstructed(user.Value, dragged, popup: true)
+ || !_interaction.InRangeUnobstructed(user.Value, target, popup: true))
+ {
+ return;
+ }
+
+ var dragArgs = new DragDropDraggedEvent(user.Value, target);
+
+ // trigger dragdrops on the dropped entity
+ RaiseLocalEvent(dragged, ref dragArgs);
+
+ if (dragArgs.Handled)
+ return;
+
+ var dropArgs = new DragDropTargetEvent(user.Value, dragged);
+ // trigger dragdrops on the target entity (what you are dropping onto)
+ RaiseLocalEvent(GetEntity(msg.Target), ref dropArgs);
+ }
}
using Content.Shared.Administration.Managers;
using Content.Shared.CombatMode;
using Content.Shared.Database;
+using Content.Shared.DragDrop;
using Content.Shared.Hands;
using Content.Shared.Hands.Components;
using Content.Shared.Input;