using System.Linq; using Content.Client.Projectiles; using Content.Shared._RMC14.Weapons.Ranged.Prediction; using Content.Shared.Projectiles; using Content.Shared.Weapons.Ranged.Events; using Content.Shared.Weapons.Ranged.Systems; using Robust.Client.GameObjects; using Robust.Client.Physics; using Robust.Client.Player; using Robust.Shared.Map; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Events; using Robust.Shared.Physics.Systems; using Robust.Shared.Timing; namespace Content.Client._RMC14.Weapons.Ranged.Prediction; public sealed class GunPredictionSystem : SharedGunPredictionSystem { [Dependency] private readonly SharedGunSystem _gun = default!; [Dependency] private readonly PhysicsSystem _physics = default!; [Dependency] private readonly IPlayerManager _player = default!; [Dependency] private readonly ProjectileSystem _projectile = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; private EntityQuery _ignorePredictionHideQuery; private EntityQuery _spriteQuery; public override void Initialize() { base.Initialize(); _ignorePredictionHideQuery = GetEntityQuery(); _spriteQuery = GetEntityQuery(); SubscribeLocalEvent(OnBeforeSolve); SubscribeLocalEvent(OnAfterSolve); SubscribeLocalEvent(OnShootRequest); SubscribeLocalEvent(OnClientProjectileUpdateIsPredicted); SubscribeLocalEvent(OnClientProjectileStartCollide); SubscribeLocalEvent(OnServerProjectileStartup); UpdatesBefore.Add(typeof(TransformSystem)); } private void OnBeforeSolve(ref PhysicsUpdateBeforeSolveEvent ev) { var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var predicted)) { predicted.Coordinates = Transform(uid).Coordinates; } } private void OnAfterSolve(ref PhysicsUpdateAfterSolveEvent ev) { var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var predicted)) { if (_timing.IsFirstTimePredicted) continue; if (predicted.Coordinates is { } coordinates) _transform.SetCoordinates(uid, coordinates); predicted.Coordinates = null; } } private void OnShootRequest(RequestShootEvent ev, EntitySessionEventArgs args) { if (_timing.IsFirstTimePredicted) return; _gun.ShootRequested(ev.Gun, ev.Coordinates, ev.Target, null, args.SenderSession); } private void OnClientProjectileUpdateIsPredicted(Entity ent, ref UpdateIsPredictedEvent args) { args.IsPredicted = true; } private void OnClientProjectileStartCollide(Entity ent, ref StartCollideEvent args) { if (ent.Comp.Hit) return; if (!TryComp(ent, out ProjectileComponent? projectile) || !TryComp(ent, out PhysicsComponent? physics)) { return; } var netEnt = GetNetEntity(args.OtherEntity); var pos = _transform.GetMapCoordinates(args.OtherEntity); var hit = new HashSet<(NetEntity, MapCoordinates)> { (netEnt, pos) }; var ev = new PredictedProjectileHitEvent(ent.Owner.Id, hit); RaiseNetworkEvent(ev); _projectile.ProjectileCollide((ent, projectile, physics), args.OtherEntity); } private void OnServerProjectileStartup(Entity ent, ref ComponentStartup args) { if (!GunPrediction) return; if (ent.Comp.ClientEnt != _player.LocalEntity) return; if (_ignorePredictionHideQuery.HasComp(ent)) return; if (_spriteQuery.TryComp(ent, out var sprite)) sprite.Visible = false; } public override void Update(float frameTime) { base.Update(frameTime); if (!_timing.IsFirstTimePredicted) return; // TODO gun prediction remove this once the client reliably detects collisions var projectiles = EntityQueryEnumerator(); while (projectiles.MoveNext(out var uid, out var predicted, out var projectile, out var physics)) { if (predicted.Hit) continue; var contacts = _physics.GetContactingEntities(uid, physics, true); if (contacts.Count == 0) continue; var hit = new HashSet<(NetEntity, MapCoordinates)>(); foreach (var contact in contacts) { var netEnt = GetNetEntity(contact); var pos = _transform.GetMapCoordinates(contact); hit.Add((netEnt, pos)); } var ev = new PredictedProjectileHitEvent(uid.Id, hit); RaiseNetworkEvent(ev); _projectile.ProjectileCollide((uid, projectile, physics), contacts.First()); } var predictedQuery = EntityQueryEnumerator(); while (predictedQuery.MoveNext(out var hit, out var sprite, out var xform)) { var origin = hit.Origin; var coordinates = xform.Coordinates; if (!origin.TryDistance(EntityManager, _transform, coordinates, out var distance) || distance >= hit.Distance) { sprite.Visible = false; } } } public override void FrameUpdate(float frameTime) { base.FrameUpdate(frameTime); // TODO bullet prediction remove this when lerping doesnt make the client's entity slightly slower var projectiles = EntityQueryEnumerator(); while (projectiles.MoveNext(out _, out var xform)) { xform.ActivelyLerping = false; } } }