Files
wwdpublic/Content.Shared/Projectiles/SharedProjectileSystem.cs
VMSolidus edf9b969f3 Pretty Big Atmos Fixes PR (#2486)
# Description

This PR ports some fixes to the order of operations for air pressure
processing, which will help fix issues with temperature not correctly
diffusing, as well as errors in the order of operations processing that
made it so that Space Wind was receiving wildly incorrect pressure
values.

Additionally, this fixes a math error that made it so that the diagonal
airflows were contributing 41% more to airflows, making diagonal motion
unusually harsh. There's still two more bugs I need to fix though.
2025-07-20 13:21:40 +10:00

331 lines
12 KiB
C#

using System.Linq;
using System.Numerics;
using Content.Shared.Damage;
using Content.Shared.DoAfter;
using Content.Shared.Examine;
using Content.Shared.IdentityManagement;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Physics;
using Content.Shared.Popups;
using Content.Shared._Shitmed.Targeting;
using Content.Shared._White.Penetrated;
using Content.Shared._White.Projectile;
using Content.Shared.Item.ItemToggle;
using Content.Shared.Throwing;
using Content.Shared.UserInterface;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Events;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
using Content.Shared.Standing;
using Robust.Shared.Network;
namespace Content.Shared.Projectiles;
public abstract partial class SharedProjectileSystem : EntitySystem
{
public const string ProjectileFixture = "projectile";
[Dependency] private readonly INetManager _netManager = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly StandingStateSystem _standing = default!;
[Dependency] private readonly PenetratedSystem _penetrated = default!; // WD EDIT
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ProjectileComponent, PreventCollideEvent>(PreventCollision);
SubscribeLocalEvent<EmbeddableProjectileComponent, ProjectileHitEvent>(OnEmbedProjectileHit);
SubscribeLocalEvent<EmbeddableProjectileComponent, ThrowDoHitEvent>(OnEmbedThrowDoHit);
SubscribeLocalEvent<EmbeddableProjectileComponent, ActivateInWorldEvent>(OnEmbedActivate, before: new[] { typeof(ActivatableUISystem), typeof(ItemToggleSystem), });
SubscribeLocalEvent<EmbeddableProjectileComponent, RemoveEmbeddedProjectileEvent>(OnEmbedRemove);
SubscribeLocalEvent<EmbeddableProjectileComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<EmbeddableProjectileComponent, PreventCollideEvent>(OnPreventCollision); // WD EDIT
}
// TODO: rename Embedded to Target in every context
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<ActiveEmbeddableProjectileComponent>();
var curTime = _timing.CurTime;
while (query.MoveNext(out var uid, out var _))
{
if (!TryComp(uid, out EmbeddableProjectileComponent? comp))
{
RemCompDeferred<ActiveEmbeddableProjectileComponent>(uid);
continue;
}
if (comp.AutoRemoveTime == null || comp.AutoRemoveTime > curTime)
continue;
if (comp.Target is { } targetUid)
_popup.PopupClient(Loc.GetString("throwing-embed-falloff", ("item", uid)), targetUid, targetUid);
RemoveEmbed(uid, comp);
}
}
private void OnEmbedActivate(EntityUid uid, EmbeddableProjectileComponent component, ActivateInWorldEvent args)
{
// Nuh uh
if (component.RemovalTime == null)
return;
if (args.Handled || !args.Complex || !TryComp<PhysicsComponent>(uid, out var physics) || physics.BodyType != BodyType.Static)
return;
args.Handled = true;
if (component.Target is {} targetUid)
_popup.PopupClient(Loc.GetString("throwing-embed-remove-alert-owner", ("item", uid), ("other", args.User)),
args.User, targetUid);
_doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, component.RemovalTime.Value,
new RemoveEmbeddedProjectileEvent(), eventTarget: uid, target: uid)
{
DistanceThreshold = SharedInteractionSystem.InteractionRange,
BreakOnMove = true,
NeedHand = true,
});
}
private void OnEmbedRemove(EntityUid uid, EmbeddableProjectileComponent component, RemoveEmbeddedProjectileEvent args)
{
if (args.Cancelled)
return;
RemoveEmbed(uid, component, args.User);
}
public void RemoveEmbed(EntityUid uid, EmbeddableProjectileComponent component, EntityUid? remover = null)
{
component.AutoRemoveTime = null;
component.Target = null;
component.TargetBodyPart = null;
RemCompDeferred<ActiveEmbeddableProjectileComponent>(uid);
var ev = new RemoveEmbedEvent(remover);
RaiseLocalEvent(uid, ref ev);
if (component.DeleteOnRemove)
{
QueueDel(uid);
FreePenetrated(uid); // WD EDIT
return;
}
if (!TryComp(uid, out PhysicsComponent? physics))
return;
var xform = Transform(uid);
_physics.SetBodyType(uid, BodyType.Dynamic, body: physics, xform: xform);
_transform.AttachToGridOrMap(uid, xform);
// Reset whether the projectile has damaged anything if it successfully was removed
if (TryComp<ProjectileComponent>(uid, out var projectile))
{
projectile.Shooter = null;
projectile.Weapon = null;
projectile.DamagedEntity = false;
}
FreePenetrated(uid); // WD EDIT
// Land it just coz uhhh yeah
var landEv = new LandEvent(remover, true);
RaiseLocalEvent(uid, ref landEv);
_physics.WakeBody(uid, body: physics);
// try place it in the user's hand
if (remover is { } removerUid)
_hands.TryPickupAnyHand(removerUid, uid);
Dirty(uid, component);
}
private void OnEmbedThrowDoHit(EntityUid uid, EmbeddableProjectileComponent component, ThrowDoHitEvent args)
{
if (!component.EmbedOnThrow
|| HasComp<ThrownItemImmuneComponent>(args.Target))
// || _standing.IsDown(args.Target)) // WWDP edit; hit prone targets
return;
TryEmbed(uid, args.Target, null, component, args.TargetPart);
}
private void OnEmbedProjectileHit(EntityUid uid, EmbeddableProjectileComponent component, ref ProjectileHitEvent args)
{
if (!(args.Target is { }) // || _standing.IsDown(args.Target) // WWDP
|| !TryComp(uid, out ProjectileComponent? projectile)
|| projectile.Weapon is null
|| !TryEmbed(uid, args.Target, args.Shooter, component))
return;
// Raise a specific event for projectiles.
var ev = new ProjectileEmbedEvent(projectile.Shooter, projectile.Weapon!.Value, args.Target);
RaiseLocalEvent(uid, ref ev);
}
public bool TryEmbed(EntityUid uid, EntityUid target, EntityUid? user, EmbeddableProjectileComponent component, TargetBodyPart? targetPart = null, bool raiseEvent = true)
{
// WD EDIT START
if (!TryComp<PhysicsComponent>(uid, out var physics)
|| TerminatingOrDeleted(target)
|| physics.LinearVelocity.Length() < component.MinimumSpeed
|| _netManager.IsClient)
return false;
var attemptEmbedEvent = new AttemptEmbedEvent(user, target);
RaiseLocalEvent(uid, ref attemptEmbedEvent);
var xform = Transform(uid);
if (!TryComp<PenetratedProjectileComponent>(uid, out var penetratedProjectile)
|| !penetratedProjectile.PenetratedUid.HasValue
|| (penetratedProjectile.PenetratedUid != target
&& !HasComp<PenetratedComponent>(target)))
{
EnsureComp<ActiveEmbeddableProjectileComponent>(uid);
_physics.SetLinearVelocity(uid, Vector2.Zero, body: physics);
_physics.SetBodyType(uid, BodyType.Static, body: physics);
_transform.SetParent(uid, xform, target);
}
// WD EDIT END
if (component.Offset != Vector2.Zero)
{
var rotation = xform.LocalRotation;
if (TryComp<ThrowingAngleComponent>(uid, out var throwingAngleComp))
rotation += throwingAngleComp.Angle;
_transform.SetLocalPosition(uid, xform.LocalPosition + rotation.RotateVec(component.Offset),
xform);
}
// WD EDIT START
if (!raiseEvent)
return true;
// WD EDIT END
_audio.PlayPredicted(component.Sound, uid, null);
component.TargetBodyPart = targetPart;
var ev = new EmbedEvent(user, target, targetPart);
RaiseLocalEvent(uid, ref ev);
if (component.AutoRemoveDuration != 0)
component.AutoRemoveTime = _timing.CurTime + TimeSpan.FromSeconds(component.AutoRemoveDuration);
component.Target = target;
Dirty(uid, component);
return true;
}
private void PreventCollision(EntityUid uid, ProjectileComponent component, ref PreventCollideEvent args)
{
if (component.IgnoreShooter && (args.OtherEntity == component.Shooter || args.OtherEntity == component.Weapon))
args.Cancelled = true;
}
public void SetShooter(EntityUid id, ProjectileComponent component, EntityUid shooterId)
{
if (component.Shooter == shooterId)
return;
component.Shooter = shooterId;
Dirty(id, component);
}
private void OnExamined(EntityUid uid, EmbeddableProjectileComponent component, ExaminedEvent args)
{
if (!(component.Target is { } target))
return;
var targetIdentity = Identity.Entity(target, EntityManager);
var loc = component.TargetBodyPart == null
? Loc.GetString("throwing-examine-embedded",
("embedded", uid),
("target", targetIdentity))
: Loc.GetString("throwing-examine-embedded-part",
("embedded", uid),
("target", targetIdentity),
("targetName", Name(targetIdentity)), // WWDP
("targetPart", Loc.GetString($"body-part-{component.TargetBodyPart.ToString()}")));
args.PushMarkup(loc);
}
// WD EDIT START
private void OnPreventCollision(EntityUid uid, EmbeddableProjectileComponent component, ref PreventCollideEvent args)
{
// Opaque collision mask doesn't work for EmbeddableProjectileComponent
if (component.PreventCollide && TryComp(args.OtherEntity, out FixturesComponent? fixtures) &&
fixtures.Fixtures.All(fix => (fix.Value.CollisionLayer & (int) CollisionGroup.Opaque) == 0))
args.Cancelled = true;
}
private void FreePenetrated(EntityUid uid, PenetratedProjectileComponent? penetratedProjectile = null)
{
if (!Resolve(uid, ref penetratedProjectile, false)
|| !penetratedProjectile.PenetratedUid.HasValue)
return;
_penetrated.FreePenetrated(penetratedProjectile.PenetratedUid.Value);
}
// WD EDIT END
[Serializable, NetSerializable]
private sealed partial class RemoveEmbeddedProjectileEvent : DoAfterEvent
{
public override DoAfterEvent Clone() => this;
}
}
[Serializable, NetSerializable]
public sealed class ImpactEffectEvent : EntityEventArgs
{
public string Prototype;
public NetCoordinates Coordinates;
public ImpactEffectEvent(string prototype, NetCoordinates coordinates)
{
Prototype = prototype;
Coordinates = coordinates;
}
}
/// <summary>
/// Raised when an entity is just about to be hit with a projectile but can reflect it
/// </summary>
[ByRefEvent]
public record struct ProjectileReflectAttemptEvent(EntityUid ProjUid, ProjectileComponent Component, bool Cancelled);
/// <summary>
/// Raised when a projectile hits an entity
/// </summary>
[ByRefEvent]
public record struct ProjectileHitEvent(DamageSpecifier Damage, EntityUid Target, EntityUid? Shooter = null);
/// <summary>
/// Raised after a projectile has dealt it's damage.
/// </summary>
[ByRefEvent]
public record struct AfterProjectileHitEvent(DamageSpecifier Damage, EntityUid Target);