From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Tue, 10 Oct 2023 23:41:11 +0000 (+1100) Subject: Climbing refactor (#20516) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=edbfef22d6913361e09cc45b44995940ea3cae63;p=space-station-14.git Climbing refactor (#20516) --- diff --git a/Content.Client/DragDrop/DragDropHelper.cs b/Content.Client/Interaction/DragDropHelper.cs similarity index 99% rename from Content.Client/DragDrop/DragDropHelper.cs rename to Content.Client/Interaction/DragDropHelper.cs index d8286ee705..ce5e08207c 100644 --- a/Content.Client/DragDrop/DragDropHelper.cs +++ b/Content.Client/Interaction/DragDropHelper.cs @@ -1,7 +1,7 @@ using Robust.Client.Input; using Robust.Shared.Map; -namespace Content.Client.DragDrop; +namespace Content.Client.Interaction; /// /// Helper for implementing drag and drop interactions. diff --git a/Content.Client/DragDrop/DragDropSystem.cs b/Content.Client/Interaction/DragDropSystem.cs similarity index 97% rename from Content.Client/DragDrop/DragDropSystem.cs rename to Content.Client/Interaction/DragDropSystem.cs index a8c1a06686..66571a9d27 100644 --- a/Content.Client/DragDrop/DragDropSystem.cs +++ b/Content.Client/Interaction/DragDropSystem.cs @@ -1,3 +1,4 @@ +using System.Numerics; using Content.Client.CombatMode; using Content.Client.Gameplay; using Content.Client.Outline; @@ -7,7 +8,6 @@ using Content.Shared.DragDrop; 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; @@ -20,15 +20,13 @@ using Robust.Shared.Map; 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; /// /// Handles clientside drag and drop logic /// -[UsedImplicitly] public sealed class DragDropSystem : SharedDragDropSystem { [Dependency] private readonly IStateManager _stateManager = default!; @@ -45,8 +43,6 @@ public sealed class DragDropSystem : SharedDragDropSystem [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; @@ -110,7 +106,6 @@ public sealed class DragDropSystem : SharedDragDropSystem public override void Initialize() { base.Initialize(); - _sawmill = Logger.GetSawmill("drag_drop"); UpdatesOutsidePrediction = true; UpdatesAfter.Add(typeof(SharedEyeSystem)); @@ -263,7 +258,7 @@ public sealed class DragDropSystem : SharedDragDropSystem 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) @@ -392,7 +387,7 @@ public sealed class DragDropSystem : SharedDragDropSystem } // 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; } diff --git a/Content.Client/Movement/Systems/ClimbSystem.cs b/Content.Client/Movement/Systems/ClimbSystem.cs deleted file mode 100644 index 003b478b30..0000000000 --- a/Content.Client/Movement/Systems/ClimbSystem.cs +++ /dev/null @@ -1,33 +0,0 @@ -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(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; - } -} diff --git a/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs b/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs index bb83e370fe..b2ff36d05c 100644 --- a/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs +++ b/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs @@ -3,9 +3,9 @@ using System.Numerics; 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; diff --git a/Content.IntegrationTests/Tests/Climbing/ClimbingTest.cs b/Content.IntegrationTests/Tests/Climbing/ClimbingTest.cs index f6bcc6e129..d8d3086520 100644 --- a/Content.IntegrationTests/Tests/Climbing/ClimbingTest.cs +++ b/Content.IntegrationTests/Tests/Climbing/ClimbingTest.cs @@ -1,8 +1,8 @@ #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; diff --git a/Content.Server/Climbing/ClimbSystem.cs b/Content.Server/Climbing/ClimbSystem.cs deleted file mode 100644 index e9d25f361f..0000000000 --- a/Content.Server/Climbing/ClimbSystem.cs +++ /dev/null @@ -1,476 +0,0 @@ -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> _fixtureRemoveQueue = new(); - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(Reset); - SubscribeLocalEvent>(AddClimbableVerb); - SubscribeLocalEvent(OnClimbableDragDrop); - - SubscribeLocalEvent(OnDoAfter); - SubscribeLocalEvent(OnClimbEndCollide); - SubscribeLocalEvent(OnBuckleChange); - - SubscribeLocalEvent(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 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(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); - } - } - - /// - /// Replaces the current fixtures with non-climbing collidable versions so that climb end can be detected - /// - /// Returns whether adding the new fixtures was successful - 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(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(); - _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); - } - - /// - /// Checks if the user can vault the target - /// - /// The component of the entity that is being vaulted - /// The entity that wants to vault - /// The object that is being vaulted - /// The reason why it cant be dropped - /// - 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(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; - } - - /// - /// Checks if the user can vault the dragged entity onto the the target - /// - /// The climbable component of the object being vaulted onto - /// The user that wants to vault the entity - /// The entity that is being vaulted - /// The object that is being vaulted onto - /// The reason why it cant be dropped - /// - 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(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(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); - } - - /// - /// Moves the entity toward the target climbed entity - /// - 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(uid, out var physicsComp) - || !TryComp(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(); - } - -} - -/// -/// Raised on an entity when it is climbed on. -/// -public sealed class ClimbedOnEvent : EntityEventArgs -{ - public EntityUid Climber; - public EntityUid Instigator; - - public ClimbedOnEvent(EntityUid climber, EntityUid instigator) - { - Climber = climber; - Instigator = instigator; - } -} - -/// -/// Raised on an entity when it successfully climbs on something. -/// -public sealed class StartClimbEvent : EntityEventArgs -{ - public EntityUid Climbable; - - public StartClimbEvent(EntityUid climbable) - { - Climbable = climbable; - } -} diff --git a/Content.Server/Interaction/DragDropSystem.cs b/Content.Server/Interaction/DragDropSystem.cs new file mode 100644 index 0000000000..9a4c26e3f9 --- /dev/null +++ b/Content.Server/Interaction/DragDropSystem.cs @@ -0,0 +1,8 @@ +using Content.Shared.DragDrop; + +namespace Content.Server.Interaction; + +public sealed class DragDropSystem : SharedDragDropSystem +{ + +} diff --git a/Content.Server/Interaction/InteractionSystem.cs b/Content.Server/Interaction/InteractionSystem.cs index c39c086960..a612b73840 100644 --- a/Content.Server/Interaction/InteractionSystem.cs +++ b/Content.Server/Interaction/InteractionSystem.cs @@ -32,8 +32,6 @@ namespace Content.Server.Interaction { base.Initialize(); - SubscribeNetworkEvent(HandleDragDropRequestEvent); - SubscribeLocalEvent(HandleUserInterfaceRangeCheck); } @@ -58,45 +56,6 @@ namespace Content.Server.Interaction 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) diff --git a/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs b/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs index 40637c5362..45f8d2ed98 100644 --- a/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs +++ b/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs @@ -1,6 +1,5 @@ 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; @@ -9,6 +8,7 @@ using Content.Shared.Administration.Logs; 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; @@ -160,7 +160,7 @@ namespace Content.Server.Medical.BiomassReclaimer }); } - 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)) { diff --git a/Content.Server/Medical/CryoPodSystem.cs b/Content.Server/Medical/CryoPodSystem.cs index ddd29d26a2..98f8e305b6 100644 --- a/Content.Server/Medical/CryoPodSystem.cs +++ b/Content.Server/Medical/CryoPodSystem.cs @@ -7,7 +7,6 @@ using Content.Server.Body.Components; 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; @@ -32,6 +31,7 @@ using Content.Shared.Verbs; using Robust.Server.GameObjects; using Robust.Shared.Timing; using Content.Server.Temperature.Components; +using Content.Shared.Climbing.Systems; namespace Content.Server.Medical; diff --git a/Content.Server/Medical/MedicalScannerSystem.cs b/Content.Server/Medical/MedicalScannerSystem.cs index 57ca815cb7..d4694e8fb8 100644 --- a/Content.Server/Medical/MedicalScannerSystem.cs +++ b/Content.Server/Medical/MedicalScannerSystem.cs @@ -1,4 +1,3 @@ -using Content.Server.Climbing; using Content.Server.Cloning; using Content.Server.Medical.Components; using Content.Shared.Destructible; @@ -13,6 +12,7 @@ using Content.Server.DeviceLinking.Systems; 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; diff --git a/Content.Server/NPC/Pathfinding/PathfindingSystem.Grid.cs b/Content.Server/NPC/Pathfinding/PathfindingSystem.Grid.cs index 95d267f7d7..72d6606c91 100644 --- a/Content.Server/NPC/Pathfinding/PathfindingSystem.Grid.cs +++ b/Content.Server/NPC/Pathfinding/PathfindingSystem.Grid.cs @@ -18,6 +18,7 @@ using Robust.Shared.Physics.Components; 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; diff --git a/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs b/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs index 920db537df..6507f24edf 100644 --- a/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs +++ b/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs @@ -10,6 +10,7 @@ using Content.Shared.NPC; 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; diff --git a/Content.Server/NPC/Systems/NPCSteeringSystem.Obstacles.cs b/Content.Server/NPC/Systems/NPCSteeringSystem.Obstacles.cs index 87deec9ea9..70d1e89bc4 100644 --- a/Content.Server/NPC/Systems/NPCSteeringSystem.Obstacles.cs +++ b/Content.Server/NPC/Systems/NPCSteeringSystem.Obstacles.cs @@ -9,6 +9,8 @@ using Content.Shared.NPC; 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; @@ -132,7 +134,7 @@ public sealed partial class NPCSteeringSystem { return SteeringObstacleStatus.Completed; } - else if (climbing.OwnerIsTransitioning) + else if (climbing.NextTransition != null) { return SteeringObstacleStatus.Continuing; } diff --git a/Content.Server/NPC/Systems/NPCSteeringSystem.cs b/Content.Server/NPC/Systems/NPCSteeringSystem.cs index 0fa28f6af7..61b43df6f0 100644 --- a/Content.Server/NPC/Systems/NPCSteeringSystem.cs +++ b/Content.Server/NPC/Systems/NPCSteeringSystem.cs @@ -1,21 +1,19 @@ -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; @@ -28,7 +26,6 @@ using Robust.Shared.Physics.Systems; 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; diff --git a/Content.Shared/ActionBlocker/ActionBlockerSystem.cs b/Content.Shared/ActionBlocker/ActionBlockerSystem.cs index 93aa5dd909..d2b12a4b29 100644 --- a/Content.Shared/ActionBlocker/ActionBlockerSystem.cs +++ b/Content.Shared/ActionBlocker/ActionBlockerSystem.cs @@ -47,7 +47,7 @@ namespace Content.Shared.ActionBlocker RaiseLocalEvent(uid, ev); if (component.CanMove == ev.Cancelled) - Dirty(component); + Dirty(uid, component); component.CanMove = !ev.Cancelled; return !ev.Cancelled; diff --git a/Content.Shared/Climbing/ClimbingComponent.cs b/Content.Shared/Climbing/ClimbingComponent.cs deleted file mode 100644 index cd443af6aa..0000000000 --- a/Content.Shared/Climbing/ClimbingComponent.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Robust.Shared.GameStates; - -namespace Content.Shared.Climbing; - -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] -public sealed partial class ClimbingComponent : Component -{ - /// - /// Whether the owner is climbing on a climbable entity. - /// - [ViewVariables, AutoNetworkedField] - public bool IsClimbing { get; set; } - - /// - /// Whether the owner is being moved onto the climbed entity. - /// - [ViewVariables, AutoNetworkedField] - public bool OwnerIsTransitioning { get; set; } - - /// - /// We'll launch the mob onto the table and give them at least this amount of time to be on it. - /// - public const float BufferTime = 0.3f; - - [ViewVariables] - public Dictionary DisabledFixtureMasks { get; } = new(); -} diff --git a/Content.Shared/Climbing/BonkableComponent.cs b/Content.Shared/Climbing/Components/BonkableComponent.cs similarity index 90% rename from Content.Shared/Climbing/BonkableComponent.cs rename to Content.Shared/Climbing/Components/BonkableComponent.cs index afffe1ff99..cc85e1c562 100644 --- a/Content.Shared/Climbing/BonkableComponent.cs +++ b/Content.Shared/Climbing/Components/BonkableComponent.cs @@ -2,13 +2,13 @@ using Content.Shared.Damage; using Robust.Shared.Audio; using Robust.Shared.GameStates; -namespace Content.Shared.Climbing; +namespace Content.Shared.Climbing.Components; /// /// Makes entity do damage and stun entities with ClumsyComponent /// upon DragDrop or Climb interactions. /// -[RegisterComponent, NetworkedComponent, Access(typeof(BonkSystem))] +[RegisterComponent, NetworkedComponent, Access(typeof(Systems.BonkSystem))] public sealed partial class BonkableComponent : Component { /// diff --git a/Content.Shared/Climbing/ClimbableComponent.cs b/Content.Shared/Climbing/Components/ClimbableComponent.cs similarity index 84% rename from Content.Shared/Climbing/ClimbableComponent.cs rename to Content.Shared/Climbing/Components/ClimbableComponent.cs index 7ad289348a..1a924e5c30 100644 --- a/Content.Shared/Climbing/ClimbableComponent.cs +++ b/Content.Shared/Climbing/Components/ClimbableComponent.cs @@ -1,11 +1,12 @@ -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 { + /// + /// Indicates this entity can be vaulted on top of. + /// [RegisterComponent, NetworkedComponent] public sealed partial class ClimbableComponent : Component { @@ -18,7 +19,7 @@ namespace Content.Shared.Climbing /// The time it takes to climb onto the entity. /// [DataField("delay")] - public float ClimbDelay = 0.8f; + public float ClimbDelay = 1.5f; /// /// Sound to be played when a climb is started. diff --git a/Content.Shared/Climbing/Components/ClimbingComponent.cs b/Content.Shared/Climbing/Components/ClimbingComponent.cs new file mode 100644 index 0000000000..9738c0cee9 --- /dev/null +++ b/Content.Shared/Climbing/Components/ClimbingComponent.cs @@ -0,0 +1,36 @@ +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 +{ + /// + /// Whether the owner is climbing on a climbable entity. + /// + [AutoNetworkedField, DataField] + public bool IsClimbing; + + /// + /// Whether the owner is being moved onto the climbed entity. + /// + [AutoNetworkedField, DataField(customTypeSerializer:typeof(TimeOffsetSerializer))] + public TimeSpan? NextTransition; + + /// + /// Direction to move when transition. + /// + [AutoNetworkedField, DataField] + public Vector2 Direction; + + /// + /// How fast the entity is moved when climbing. + /// + [DataField] + public float TransitionRate = 5f; + + [AutoNetworkedField, DataField] + public Dictionary DisabledFixtureMasks = new(); +} diff --git a/Content.Server/Climbing/Components/GlassTableComponent.cs b/Content.Shared/Climbing/Components/GlassTableComponent.cs similarity index 90% rename from Content.Server/Climbing/Components/GlassTableComponent.cs rename to Content.Shared/Climbing/Components/GlassTableComponent.cs index 009fba91f4..d191793adf 100644 --- a/Content.Server/Climbing/Components/GlassTableComponent.cs +++ b/Content.Shared/Climbing/Components/GlassTableComponent.cs @@ -1,13 +1,13 @@ using Content.Shared.Damage; -namespace Content.Server.Climbing.Components; +namespace Content.Shared.Climbing.Components; /// /// 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. /// -[RegisterComponent, Access(typeof(ClimbSystem))] +[RegisterComponent, Access(typeof(Systems.ClimbSystem))] public sealed partial class GlassTableComponent : Component { /// diff --git a/Content.Shared/Climbing/Events/ClimbedOnEvent.cs b/Content.Shared/Climbing/Events/ClimbedOnEvent.cs new file mode 100644 index 0000000000..8b0484d5d6 --- /dev/null +++ b/Content.Shared/Climbing/Events/ClimbedOnEvent.cs @@ -0,0 +1,7 @@ +namespace Content.Shared.Climbing.Events; + +/// +/// Raised on an entity when it is climbed on. +/// +[ByRefEvent] +public readonly record struct ClimbedOnEvent(EntityUid Climber, EntityUid Instigator); diff --git a/Content.Shared/Climbing/Events/EndClimbEvent.cs b/Content.Shared/Climbing/Events/EndClimbEvent.cs index 12eaac236d..6963cabf30 100644 --- a/Content.Shared/Climbing/Events/EndClimbEvent.cs +++ b/Content.Shared/Climbing/Events/EndClimbEvent.cs @@ -4,7 +4,4 @@ namespace Content.Shared.Climbing.Events; /// Raised on an entity when it ends climbing. /// [ByRefEvent] -public readonly record struct EndClimbEvent -{ - -} +public readonly record struct EndClimbEvent; diff --git a/Content.Shared/Climbing/Events/StartClimbEvent.cs b/Content.Shared/Climbing/Events/StartClimbEvent.cs new file mode 100644 index 0000000000..3563a39bb8 --- /dev/null +++ b/Content.Shared/Climbing/Events/StartClimbEvent.cs @@ -0,0 +1,7 @@ +namespace Content.Shared.Climbing.Events; + +/// +/// Raised on an entity when it successfully climbs on something. +/// +[ByRefEvent] +public readonly record struct StartClimbEvent(EntityUid Climbable); diff --git a/Content.Shared/Climbing/SharedClimbSystem.cs b/Content.Shared/Climbing/SharedClimbSystem.cs deleted file mode 100644 index 12b84bbb7e..0000000000 --- a/Content.Shared/Climbing/SharedClimbSystem.cs +++ /dev/null @@ -1,34 +0,0 @@ -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(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(args.Dragged); - } - - [Serializable, NetSerializable] - protected sealed partial class ClimbDoAfterEvent : SimpleDoAfterEvent - { - } -} diff --git a/Content.Shared/Climbing/BonkSystem.cs b/Content.Shared/Climbing/Systems/BonkSystem.cs similarity index 88% rename from Content.Shared/Climbing/BonkSystem.cs rename to Content.Shared/Climbing/Systems/BonkSystem.cs index eda392fa31..6ded524b19 100644 --- a/Content.Shared/Climbing/BonkSystem.cs +++ b/Content.Shared/Climbing/Systems/BonkSystem.cs @@ -1,17 +1,18 @@ -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 { @@ -30,7 +31,7 @@ public sealed partial class BonkSystem : EntitySystem SubscribeLocalEvent(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; @@ -41,7 +42,7 @@ public sealed partial class BonkSystem : EntitySystem } - 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; @@ -71,7 +72,7 @@ public sealed partial class BonkSystem : EntitySystem } - 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(args.Dragged)) return; diff --git a/Content.Shared/Climbing/Systems/ClimbSystem.cs b/Content.Shared/Climbing/Systems/ClimbSystem.cs new file mode 100644 index 0000000000..4e25fa4ac0 --- /dev/null +++ b/Content.Shared/Climbing/Systems/ClimbSystem.cs @@ -0,0 +1,486 @@ +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 _fixturesQuery; + private EntityQuery _xformQuery; + + public override void Initialize() + { + base.Initialize(); + + _fixturesQuery = GetEntityQuery(); + _xformQuery = GetEntityQuery(); + + SubscribeLocalEvent(OnMoveAttempt); + SubscribeLocalEvent(OnParentChange); + SubscribeLocalEvent(OnDoAfter); + SubscribeLocalEvent(OnClimbEndCollide); + SubscribeLocalEvent(OnBuckleChange); + SubscribeLocalEvent(OnClimbableUnpaused); + + SubscribeLocalEvent(OnCanDragDropOn); + SubscribeLocalEvent>(AddClimbableVerb); + SubscribeLocalEvent(OnClimbableDragDrop); + + SubscribeLocalEvent(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(); + 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; + } + } + + /// + /// Returns true if entity currently has a valid vault. + /// + 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(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 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(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); + } + + /// + /// Replaces the current fixtures with non-climbing collidable versions so that climb end can be detected + /// + /// Returns whether adding the new fixtures was successful + 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(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); + } + + /// + /// Checks if the user can vault the target + /// + /// The component of the entity that is being vaulted + /// The entity that wants to vault + /// The object that is being vaulted + /// The reason why it cant be dropped + 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(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; + } + + /// + /// Checks if the user can vault the dragged entity onto the the target + /// + /// The climbable component of the object being vaulted onto + /// The user that wants to vault the entity + /// The entity that is being vaulted + /// The object that is being vaulted onto + /// The reason why it cant be dropped + /// + 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(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(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 + { + } +} diff --git a/Content.Shared/DoAfter/SharedDoAfterSystem.cs b/Content.Shared/DoAfter/SharedDoAfterSystem.cs index 691d9a4758..382ecb5a9a 100644 --- a/Content.Shared/DoAfter/SharedDoAfterSystem.cs +++ b/Content.Shared/DoAfter/SharedDoAfterSystem.cs @@ -43,7 +43,7 @@ public abstract partial class SharedDoAfterSystem : EntitySystem doAfter.CancelledTime = doAfter.CancelledTime.Value + args.PausedTime; } - Dirty(component); + Dirty(uid, component); } private void OnStateChanged(EntityUid uid, DoAfterComponent component, MobStateChangedEvent args) @@ -55,7 +55,7 @@ public abstract partial class SharedDoAfterSystem : EntitySystem { InternalCancel(doAfter, component); } - Dirty(component); + Dirty(uid, component); } /// @@ -63,10 +63,12 @@ public abstract partial class SharedDoAfterSystem : EntitySystem /// 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) @@ -79,7 +81,7 @@ public abstract partial class SharedDoAfterSystem : EntitySystem } if (dirty) - Dirty(component); + Dirty(uid, component); } private void RaiseDoAfterEvents(DoAfter doAfter, DoAfterComponent component) @@ -254,7 +256,7 @@ public abstract partial class SharedDoAfterSystem : EntitySystem comp.DoAfters.Add(doAfter.Index, doAfter); EnsureComp(args.User); - Dirty(comp); + Dirty(args.User, comp); args.Event.DoAfter = doAfter; return true; } diff --git a/Content.Shared/DragDrop/SharedDragDropSystem.cs b/Content.Shared/DragDrop/SharedDragDropSystem.cs index 7f1f6c23f7..24c79015d8 100644 --- a/Content.Shared/DragDrop/SharedDragDropSystem.cs +++ b/Content.Shared/DragDrop/SharedDragDropSystem.cs @@ -1,6 +1,51 @@ -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(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); + } } diff --git a/Content.Shared/Interaction/SharedInteractionSystem.cs b/Content.Shared/Interaction/SharedInteractionSystem.cs index d79a892c71..4a9a43ca2c 100644 --- a/Content.Shared/Interaction/SharedInteractionSystem.cs +++ b/Content.Shared/Interaction/SharedInteractionSystem.cs @@ -6,6 +6,7 @@ using Content.Shared.Administration.Logs; 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;