using System.Numerics; using Content.Shared.CombatMode; using Content.Shared.Hands; using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction; using Content.Shared.Movement.Events; using Content.Shared.Physics; using Content.Shared.Projectiles; using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Systems; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.Network; using Robust.Shared.Physics; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Controllers; using Robust.Shared.Physics.Dynamics.Joints; using Robust.Shared.Physics.Systems; using Robust.Shared.Serialization; using Robust.Shared.Timing; namespace Content.Shared.Weapons.Misc; public abstract class SharedGrapplingGunSystem : VirtualController { [Dependency] protected readonly IGameTiming Timing = default!; [Dependency] private readonly IEntityManager _entities = default!; [Dependency] private readonly INetManager _netManager = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedHandsSystem _hands = default!; [Dependency] private readonly SharedJointSystem _joints = default!; [Dependency] private readonly SharedGunSystem _gun = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly SharedContainerSystem _container = default!; public const string GrapplingJoint = "grappling"; public override void Initialize() { SubscribeLocalEvent(OnGrappleCollide); SubscribeLocalEvent(OnGrappleJointRemoved); SubscribeLocalEvent(OnWeightlessMove); SubscribeAllEvent(OnGrapplingReel); // TODO: After step trigger refactor, dropping a grappling gun should manually try and activate step triggers it's suppressing. SubscribeLocalEvent(OnGrapplingShot); SubscribeLocalEvent(OnGunActivate); SubscribeLocalEvent(OnGrapplingDeselected); UpdatesBefore.Add(typeof(SharedJointSystem)); // We want to run before joints are solved base.Initialize(); } private void OnGrappleJointRemoved(EntityUid uid, GrapplingProjectileComponent component, JointRemovedEvent args) { if (_netManager.IsServer) QueueDel(uid); } private void OnGrapplingShot(EntityUid uid, GrapplingGunComponent component, ref GunShotEvent args) { foreach (var (shotUid, _) in args.Ammo) { if (!HasComp(shotUid)) continue; //todo: this doesn't actually support multigrapple // At least show the visuals. component.Projectile = shotUid.Value; DirtyField(uid, component, nameof(GrapplingGunComponent.Projectile)); var visuals = EnsureComp(shotUid.Value); visuals.Sprite = component.RopeSprite; visuals.Target = uid; Dirty(shotUid.Value, visuals); } TryComp(uid, out var appearance); _appearance.SetData(uid, SharedTetherGunSystem.TetherVisualsStatus.Key, false, appearance); } private void OnGrapplingDeselected(EntityUid uid, GrapplingGunComponent component, HandDeselectedEvent args) { SetReeling(uid, component, false, args.User); } private void OnGrapplingReel(RequestGrapplingReelMessage msg, EntitySessionEventArgs args) { if (args.SenderSession.AttachedEntity is not { } player) return; if (!_hands.TryGetActiveItem(player, out var activeItem) || !TryComp(activeItem, out var grappling)) { return; } if (msg.Reeling && (!TryComp(player, out var combatMode) || !combatMode.IsInCombatMode)) { return; } SetReeling(activeItem.Value, grappling, msg.Reeling, player); } private void OnWeightlessMove(ref CanWeightlessMoveEvent ev) { if (ev.CanMove || !TryComp(ev.Uid, out var relayComp)) return; foreach (var relay in relayComp.Relayed) { if (TryComp(relay, out var jointRelay) && jointRelay.GetJoints.ContainsKey(GrapplingJoint)) { ev.CanMove = true; return; } } } /// /// Ungrapples the grappling hook, destroying the hook and severing the joint /// /// Entity for the grappling gun /// Whether to play the sound for the rope breaking /// The user responsible for the ungrapple. Optional public void Ungrapple(Entity grapple, bool isBreak, EntityUid? user = null) { if (!Timing.IsFirstTimePredicted || grapple.Comp.Projectile is not { } projectile) return; if(isBreak) _audio.PlayPredicted(grapple.Comp.BreakSound, grapple.Owner, user); _appearance.SetData(grapple.Owner, SharedTetherGunSystem.TetherVisualsStatus.Key, true); if (_netManager.IsServer) QueueDel(projectile); SetReeling(grapple.Owner, grapple.Comp, false, user); grapple.Comp.Projectile = null; DirtyField(grapple.Owner, grapple.Comp, nameof(GrapplingGunComponent.Projectile)); _gun.ChangeBasicEntityAmmoCount(grapple.Owner, 1); } private void OnGunActivate(EntityUid uid, GrapplingGunComponent component, ActivateInWorldEvent args) { if (!Timing.IsFirstTimePredicted || args.Handled || !args.Complex) return; _audio.PlayPredicted(component.CycleSound, uid, args.User); Ungrapple((uid, component), false, args.User); args.Handled = true; } private void SetReeling(EntityUid uid, GrapplingGunComponent component, bool value, EntityUid? user) { if (TryComp(uid, out var jointComp) && jointComp.GetJoints.TryGetValue(GrapplingJoint, out var joint) && joint is DistanceJoint distance) { if (distance.MaxLength <= distance.MinLength + component.RopeFullyReeledMargin) value = false; } if (component.Reeling == value) return; if (value) { // We null-coalesce here because playing the sound again will cause it to become eternally stuck playing component.Stream ??= _audio.PlayPredicted(component.ReelSound, uid, user)?.Entity; } else if (!value && component.Stream.HasValue && Timing.IsFirstTimePredicted) { // The IsFirstTimePredicted check is important here because otherwise component.Stream will be set to null from an early cancellation if this isn't FirstTimePredicted component.Stream = _audio.Stop(component.Stream); } component.Reeling = value; DirtyField(uid, component, nameof(GrapplingGunComponent.Reeling)); } public override void UpdateBeforeSolve(bool prediction, float frameTime) { base.UpdateBeforeSolve(prediction, frameTime); var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var grappling, out var jointComp)) { if (!jointComp.GetJoints.TryGetValue(GrapplingJoint, out var joint) || joint is not DistanceJoint distance || !_entities.TryGetComponent(joint.BodyAUid, out var hookJointComp)) { if (_netManager.IsServer) // Client might not receive the joint due to PVS culling, so lets not spam them with 23895739 mispredicted ungrapples Ungrapple((uid, grappling), true); continue; } // If the joint breaks, it gets disabled if (distance.Enabled == false) { Ungrapple((uid, grappling), true); continue; } var physicalGrapple = jointComp.Relay.HasValue ? jointComp.Relay.Value : joint.BodyBUid; var physicalHook = hookJointComp.Relay.HasValue ? hookJointComp.Relay.Value : joint.BodyAUid; // HACK: preventing both ends of the grappling hook from sleeping if neither are on the same grid, so that grid movement works as expected if (_transform.GetGrid(physicalHook) != _transform.GetGrid(physicalGrapple)) { _physics.WakeBody(physicalHook); _physics.WakeBody(physicalGrapple); } // END OF HACK var bodyAWorldPos = _transform.GetWorldPosition(physicalHook); var bodyBWorldPos = _transform.GetWorldPosition(physicalGrapple); // The solver does not handle setting the rope's length, but we still need to work with a copy of it to prevent jank. var ropeLength = (bodyAWorldPos - bodyBWorldPos).Length(); // Rope should just break, instantly, if the user is teleported past its max length if (ropeLength >= distance.MaxLength + grappling.RopeMargin) { Ungrapple((uid, grappling), true); continue; } if (!grappling.Reeling) { // Just in case. if (grappling.Stream.HasValue && Timing.IsFirstTimePredicted) grappling.Stream = _audio.Stop(grappling.Stream); continue; } // TODO: Contracting DistanceJoints should be in engine if (distance.MaxLength >= ropeLength + grappling.RopeMargin) { distance.MaxLength = MathF.Max(distance.MinLength + grappling.RopeMargin, distance.MaxLength - grappling.ReelRate * frameTime); distance.MaxLength = MathF.Max(ropeLength + grappling.RopeMargin, distance.MaxLength); ropeLength = MathF.Min(distance.MaxLength, ropeLength); distance.Length = ropeLength; } if (ropeLength <= distance.MinLength + grappling.RopeFullyReeledMargin) { SetReeling(uid, grappling, false, null); } else if (ropeLength >= distance.MaxLength - grappling.RopeMargin) { var targetDirection = (bodyAWorldPos - bodyBWorldPos).Normalized(); var grapplerUidA = _container.TryGetOuterContainer(physicalHook, Transform(physicalHook), out var containerA) ? containerA.Owner : physicalHook; var grapplerBodyA = Comp(grapplerUidA); var massFactorA = MathF.Min(grapplerBodyA.InvMass * grappling.ReelMassCoefficient, 1f); _physics.ApplyLinearImpulse(grapplerUidA, targetDirection * grappling.ReelForce * massFactorA * frameTime * -1, body: grapplerBodyA); var grapplerUidB = _container.TryGetOuterContainer(physicalGrapple, Transform(physicalGrapple), out var containerB) ? containerB.Owner : physicalGrapple; var grapplerBodyB = Comp(grapplerUidB); var massFactorB = MathF.Min(grapplerBodyB.InvMass * grappling.ReelMassCoefficient, 1f); _physics.ApplyLinearImpulse(grapplerUidB, targetDirection * grappling.ReelForce * massFactorB * frameTime, body: grapplerBodyB); } Dirty(uid, jointComp); } } /// /// Checks whether the entity is hooked to something via grappling gun. /// /// Entity to check. /// True if hooked, false otherwise. public bool IsEntityHooked(Entity entity) { if (!Resolve(entity, ref entity.Comp, false)) return false; foreach (var uid in entity.Comp.Relayed) { if (HasComp(uid)) return true; } return false; } private void OnGrappleCollide(EntityUid uid, GrapplingProjectileComponent component, ref ProjectileEmbedEvent args) { if (!Timing.IsFirstTimePredicted || !args.Weapon.HasValue || !_entities.TryGetComponent(args.Weapon, out var grapple)) return; var grapplePos = _transform.GetWorldPosition(args.Weapon.Value); var hookPos = _transform.GetWorldPosition(uid); if ((grapplePos - hookPos).Length() >= grapple.RopeMaxLength) { Ungrapple((args.Weapon.Value, grapple), true); return; } var joint = _joints.CreateDistanceJoint(uid, args.Weapon.Value, id: GrapplingJoint); joint.MaxLength = joint.Length + grapple.RopeMargin; joint.Stiffness = grapple.RopeStiffness; joint.MinLength = grapple.RopeMinLength; // Length of a tile to prevent pulling yourself into / through walls joint.Breakpoint = grapple.RopeBreakPoint; var jointCompHook = _entities.GetComponent(uid); // we use get here because if the component doesn't exist then something has fucked up bigtime var jointCompGrapple = _entities.GetComponent(args.Weapon.Value); _joints.SetRelay(uid, args.Embedded, jointCompHook); _joints.RefreshRelay(args.Weapon.Value, jointCompGrapple); } [Serializable, NetSerializable] protected sealed class RequestGrapplingReelMessage : EntityEventArgs { public bool Reeling; public RequestGrapplingReelMessage(bool reeling) { Reeling = reeling; } } }