From c538d7fb2b0dbdbddaed3df7d079e8a438c5c56e Mon Sep 17 00:00:00 2001 From: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Date: Sun, 3 Aug 2025 01:07:32 +0200 Subject: [PATCH] Predict anomaly synchronizer (#39321) * predict anomaly synchronizer * pvs * lambda * Update Resources/Locale/en-US/anomaly/anomaly.ftl --------- Co-authored-by: Pieter-Jan Briers --- .../Anomaly/AnomalySynchronizerSystem.cs | 140 ++++++++++-------- .../AnomalySynchronizerComponent.cs | 31 ++-- Resources/Locale/en-US/anomaly/anomaly.ftl | 6 +- 3 files changed, 101 insertions(+), 76 deletions(-) rename {Content.Server => Content.Shared}/Anomaly/AnomalySynchronizerSystem.cs (59%) rename {Content.Server => Content.Shared}/Anomaly/Components/AnomalySynchronizerComponent.cs (66%) diff --git a/Content.Server/Anomaly/AnomalySynchronizerSystem.cs b/Content.Shared/Anomaly/AnomalySynchronizerSystem.cs similarity index 59% rename from Content.Server/Anomaly/AnomalySynchronizerSystem.cs rename to Content.Shared/Anomaly/AnomalySynchronizerSystem.cs index b1814c2741..32883cd775 100644 --- a/Content.Server/Anomaly/AnomalySynchronizerSystem.cs +++ b/Content.Shared/Anomaly/AnomalySynchronizerSystem.cs @@ -1,33 +1,30 @@ using System.Linq; -using System.Numerics; -using Content.Server.Anomaly.Components; -using Content.Server.DeviceLinking.Systems; -using Content.Server.Power.Components; -using Content.Server.Power.EntitySystems; using Content.Shared.Anomaly.Components; +using Content.Shared.DeviceLinking; using Content.Shared.Examine; using Content.Shared.Interaction; using Content.Shared.Popups; using Content.Shared.Power; -using Robust.Shared.Audio.Systems; +using Content.Shared.Power.EntitySystems; using Content.Shared.Verbs; +using Robust.Shared.Audio.Systems; using Robust.Shared.Timing; -namespace Content.Server.Anomaly; +namespace Content.Shared.Anomaly; /// -/// a device that allows you to translate anomaly activity into multitool signals. +/// A device that allows you to translate anomaly activity into multitool signals. /// public sealed partial class AnomalySynchronizerSystem : EntitySystem { - [Dependency] private readonly AnomalySystem _anomaly = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly EntityLookupSystem _entityLookup = default!; - [Dependency] private readonly DeviceLinkSystem _signal = default!; - [Dependency] private readonly SharedTransformSystem _transform = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly PowerReceiverSystem _power = default!; [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedAnomalySystem _anomaly = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedDeviceLinkSystem _deviceLink = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedPowerReceiverSystem _power = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; public override void Initialize() { @@ -47,27 +44,41 @@ public sealed partial class AnomalySynchronizerSystem : EntitySystem { base.Update(frameTime); + var curTime = _timing.CurTime; var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var sync, out var xform)) + while (query.MoveNext(out var uid, out var sync, out var synchronizerTransform)) { - if (sync.ConnectedAnomaly is null) + if (sync.ConnectedAnomaly == null) continue; - if (_timing.CurTime < sync.NextCheckTime) + if (curTime < sync.NextCheckTime) continue; + sync.NextCheckTime += sync.CheckFrequency; + Dirty(uid, sync); - if (Transform(sync.ConnectedAnomaly.Value).MapUid != Transform(uid).MapUid) + if (TerminatingOrDeleted(sync.ConnectedAnomaly)) { - DisconnectFromAnomaly((uid, sync), sync.ConnectedAnomaly.Value); + DisconnectFromAnomaly((uid, sync)); continue; } - if (!xform.Coordinates.TryDistance(EntityManager, Transform(sync.ConnectedAnomaly.Value).Coordinates, out var distance)) + // Use TryComp instead of Transform(uid) to take care of cases where the anomaly is out of + // PVS range on the client, but the synchronizer isn't. + if (!TryComp(sync.ConnectedAnomaly.Value, out TransformComponent? anomalyTransform)) + continue; + + if (anomalyTransform.MapUid != synchronizerTransform.MapUid) + { + DisconnectFromAnomaly((uid, sync)); + continue; + } + + if (!synchronizerTransform.Coordinates.TryDistance(EntityManager, anomalyTransform.Coordinates, out var distance)) continue; if (distance > sync.AttachRange) - DisconnectFromAnomaly((uid, sync), sync.ConnectedAnomaly.Value); + DisconnectFromAnomaly((uid, sync)); } } @@ -76,11 +87,9 @@ public sealed partial class AnomalySynchronizerSystem : EntitySystem /// public bool TryAttachNearbyAnomaly(Entity ent, EntityUid? user = null) { - if (!_power.IsPowered(ent)) + if (!_power.IsPowered(ent.Owner)) { - if (user is not null) - _popup.PopupEntity(Loc.GetString("base-computer-ui-component-not-powered", ("machine", ent)), ent, user.Value); - + _popup.PopupClient(Loc.GetString("base-computer-ui-component-not-powered", ("machine", ent)), ent, user); return false; } @@ -89,13 +98,11 @@ public sealed partial class AnomalySynchronizerSystem : EntitySystem if (anomaly.Owner is { Valid: false }) // no anomaly in range { - if (user is not null) - _popup.PopupEntity(Loc.GetString("anomaly-sync-no-anomaly"), ent, user.Value); - + _popup.PopupClient(Loc.GetString("anomaly-sync-no-anomaly"), ent, user); return false; } - ConnectToAnomaly(ent, anomaly); + ConnectToAnomaly(ent, anomaly, user); return true; } @@ -104,10 +111,10 @@ public sealed partial class AnomalySynchronizerSystem : EntitySystem if (args.Powered) return; - if (ent.Comp.ConnectedAnomaly is null) + if (ent.Comp.ConnectedAnomaly == null) return; - DisconnectFromAnomaly(ent, ent.Comp.ConnectedAnomaly.Value); + DisconnectFromAnomaly(ent); } private void OnExamined(Entity ent, ref ExaminedEvent args) @@ -117,19 +124,29 @@ public sealed partial class AnomalySynchronizerSystem : EntitySystem private void OnGetInteractionVerbs(Entity ent, ref GetVerbsEvent args) { - if (!args.CanAccess || !args.CanInteract || args.Hands is null || ent.Comp.ConnectedAnomaly.HasValue) + if (!args.CanAccess || !args.CanInteract || args.Hands == null) return; var user = args.User; - args.Verbs.Add(new() + + if (ent.Comp.ConnectedAnomaly == null) { - Act = () => + args.Verbs.Add(new() { - TryAttachNearbyAnomaly(ent, user); - }, - Message = Loc.GetString("anomaly-sync-connect-verb-message", ("machine", ent)), - Text = Loc.GetString("anomaly-sync-connect-verb-text"), - }); + Act = () => TryAttachNearbyAnomaly(ent, user), + Message = Loc.GetString("anomaly-sync-connect-verb-message", ("machine", ent)), + Text = Loc.GetString("anomaly-sync-connect-verb-text"), + }); + } + else + { + args.Verbs.Add(new() + { + Act = () => DisconnectFromAnomaly(ent, user), + Message = Loc.GetString("anomaly-sync-disconnect-verb-message", ("machine", ent)), + Text = Loc.GetString("anomaly-sync-disconnect-verb-text"), + }); + } } private void OnInteractHand(Entity ent, ref InteractHandEvent args) @@ -137,12 +154,13 @@ public sealed partial class AnomalySynchronizerSystem : EntitySystem TryAttachNearbyAnomaly(ent, args.User); } - private void ConnectToAnomaly(Entity ent, Entity anomaly) + private void ConnectToAnomaly(Entity ent, Entity anomaly, EntityUid? user = null) { if (ent.Comp.ConnectedAnomaly == anomaly) return; ent.Comp.ConnectedAnomaly = anomaly; + Dirty(ent); //move the anomaly to the center of the synchronizer, for aesthetics. var targetXform = _transform.GetWorldPosition(ent); _transform.SetWorldPosition(anomaly, targetXform); @@ -150,27 +168,27 @@ public sealed partial class AnomalySynchronizerSystem : EntitySystem if (ent.Comp.PulseOnConnect) _anomaly.DoAnomalyPulse(anomaly, anomaly); - _popup.PopupEntity(Loc.GetString("anomaly-sync-connected"), ent, PopupType.Medium); - _audio.PlayPvs(ent.Comp.ConnectedSound, ent); + _popup.PopupPredicted(Loc.GetString("anomaly-sync-connected"), ent, user, PopupType.Medium); + _audio.PlayPredicted(ent.Comp.ConnectedSound, ent, user); } //TODO: disconnection from the anomaly should also be triggered if the anomaly is far away from the synchronizer. //Currently only bluespace anomaly can do this, but for some reason it is the only one that cannot be connected to the synchronizer. - private void DisconnectFromAnomaly(Entity ent, EntityUid other) + private void DisconnectFromAnomaly(Entity ent, EntityUid? user = null) { if (ent.Comp.ConnectedAnomaly == null) return; - if (TryComp(other, out var anomaly)) + if (ent.Comp.PulseOnDisconnect && TryComp(ent.Comp.ConnectedAnomaly, out var anomaly)) { - if (ent.Comp.PulseOnDisconnect) - _anomaly.DoAnomalyPulse(ent.Comp.ConnectedAnomaly.Value, anomaly); + _anomaly.DoAnomalyPulse(ent.Comp.ConnectedAnomaly.Value, anomaly); } - _popup.PopupEntity(Loc.GetString("anomaly-sync-disconnected"), ent, PopupType.Large); - _audio.PlayPvs(ent.Comp.ConnectedSound, ent); + _popup.PopupPredicted(Loc.GetString("anomaly-sync-disconnected"), ent, user, PopupType.Large); + _audio.PlayPredicted(ent.Comp.DisconnectedSound, ent, user); ent.Comp.ConnectedAnomaly = null; + Dirty(ent); } private void OnAnomalyPulse(ref AnomalyPulseEvent args) @@ -184,19 +202,19 @@ public sealed partial class AnomalySynchronizerSystem : EntitySystem if (!_power.IsPowered(uid)) continue; - _signal.InvokePort(uid, component.PulsePort); + _deviceLink.InvokePort(uid, component.PulsePort); } } private void OnAnomalySeverityChanged(ref AnomalySeverityChangedEvent args) { var query = EntityQueryEnumerator(); - while (query.MoveNext(out var ent, out var component)) + while (query.MoveNext(out var uid, out var component)) { if (args.Anomaly != component.ConnectedAnomaly) continue; - if (!_power.IsPowered(ent)) + if (!_power.IsPowered(uid)) continue; //The superscritical port is invoked not at the AnomalySupercriticalEvent, @@ -204,34 +222,34 @@ public sealed partial class AnomalySynchronizerSystem : EntitySystem //ATTENTION! the console command supercriticalanomaly does not work here, //as it forcefully causes growth to start without increasing severity. if (args.Severity >= 1) - _signal.InvokePort(ent, component.SupercritPort); + _deviceLink.InvokePort(uid, component.SupercritPort); } } private void OnAnomalyStabilityChanged(ref AnomalyStabilityChangedEvent args) { - Entity anomaly = (args.Anomaly, Comp(args.Anomaly)); + var anomaly = Comp(args.Anomaly); var query = EntityQueryEnumerator(); - while (query.MoveNext(out var ent, out var component)) + while (query.MoveNext(out var uid, out var sync)) { - if (component.ConnectedAnomaly != anomaly) + if (sync.ConnectedAnomaly != args.Anomaly) continue; - if (!_power.IsPowered(ent)) + if (!_power.IsPowered(uid)) continue; - if (args.Stability < anomaly.Comp.DecayThreshold) + if (args.Stability < anomaly.DecayThreshold) { - _signal.InvokePort(ent, component.DecayingPort); + _deviceLink.InvokePort(uid, sync.DecayingPort); } - else if (args.Stability > anomaly.Comp.GrowthThreshold) + else if (args.Stability > anomaly.GrowthThreshold) { - _signal.InvokePort(ent, component.GrowingPort); + _deviceLink.InvokePort(uid, sync.GrowingPort); } else { - _signal.InvokePort(ent, component.StabilizePort); + _deviceLink.InvokePort(uid, sync.StabilizePort); } } } diff --git a/Content.Server/Anomaly/Components/AnomalySynchronizerComponent.cs b/Content.Shared/Anomaly/Components/AnomalySynchronizerComponent.cs similarity index 66% rename from Content.Server/Anomaly/Components/AnomalySynchronizerComponent.cs rename to Content.Shared/Anomaly/Components/AnomalySynchronizerComponent.cs index 3127f091e5..7e605ba150 100644 --- a/Content.Server/Anomaly/Components/AnomalySynchronizerComponent.cs +++ b/Content.Shared/Anomaly/Components/AnomalySynchronizerComponent.cs @@ -1,46 +1,51 @@ using Content.Shared.DeviceLinking; using Robust.Shared.Audio; +using Robust.Shared.GameStates; using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; -namespace Content.Server.Anomaly.Components; +namespace Content.Shared.Anomaly.Components; /// -/// a device that allows you to translate anomaly activity into multitool signals. +/// A device that allows you to translate anomaly activity into multitool signals. /// -[RegisterComponent, AutoGenerateComponentPause, Access(typeof(AnomalySynchronizerSystem))] +[RegisterComponent, NetworkedComponent] +[AutoGenerateComponentState, AutoGenerateComponentPause] +[Access(typeof(AnomalySynchronizerSystem))] public sealed partial class AnomalySynchronizerComponent : Component { /// /// The uid of the anomaly to which the synchronizer is connected. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField, AutoNetworkedField] public EntityUid? ConnectedAnomaly; /// /// Should the anomaly pulse when connected to the synchronizer? /// - [DataField] + [DataField, AutoNetworkedField] public bool PulseOnConnect = true; /// /// Should the anomaly pulse when disconnected from synchronizer? /// - [DataField] + [DataField, AutoNetworkedField] public bool PulseOnDisconnect = false; /// - /// minimum distance from the synchronizer to the anomaly to be attached + /// Minimum distance from the synchronizer to the anomaly to be attached. /// - [DataField] + [DataField, AutoNetworkedField] public float AttachRange = 0.4f; /// - /// Periodicheski checks to see if the anomaly has moved to disconnect it. + /// Periodically checks to see if the anomaly has moved to disconnect it. /// - [DataField] + [DataField, AutoNetworkedField] public TimeSpan CheckFrequency = TimeSpan.FromSeconds(1f); - [DataField, AutoPausedField] + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + [AutoNetworkedField, AutoPausedField] public TimeSpan NextCheckTime = TimeSpan.Zero; [DataField] @@ -58,9 +63,9 @@ public sealed partial class AnomalySynchronizerComponent : Component [DataField] public ProtoId SupercritPort = "Supercritical"; - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public SoundSpecifier ConnectedSound = new SoundPathSpecifier("/Audio/Machines/anomaly_sync_connect.ogg"); - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public SoundSpecifier DisconnectedSound = new SoundPathSpecifier("/Audio/Machines/anomaly_sync_connect.ogg"); } diff --git a/Resources/Locale/en-US/anomaly/anomaly.ftl b/Resources/Locale/en-US/anomaly/anomaly.ftl index c8d099777d..77608da435 100644 --- a/Resources/Locale/en-US/anomaly/anomaly.ftl +++ b/Resources/Locale/en-US/anomaly/anomaly.ftl @@ -54,6 +54,8 @@ anomaly-sync-examine-connected = It is [color=darkgreen]attached[/color] to an a anomaly-sync-examine-not-connected = It is [color=darkred]not attached[/color] to an anomaly. anomaly-sync-connect-verb-text = Attach anomaly anomaly-sync-connect-verb-message = Attach a nearby anomaly to {THE($machine)}. +anomaly-sync-disconnect-verb-text = Detach anomaly +anomaly-sync-disconnect-verb-message = Detach the connected anomaly from {THE($machine)}. anomaly-generator-ui-title = Anomaly Generator anomaly-generator-fuel-display = Fuel: @@ -78,7 +80,7 @@ anomaly-generator-flavor-right = v1.1 anomaly-behavior-unknown = [color=red]ERROR. Cannot be read.[/color] anomaly-behavior-title = behavior deviation analysis: -anomaly-behavior-point =[color=gold]Anomaly produces {$mod}% of the points[/color] +anomaly-behavior-point = [color=gold]Anomaly produces {$mod}% of the points[/color] anomaly-behavior-safe = [color=forestgreen]The anomaly is extremely stable. Extremely rare pulsations.[/color] anomaly-behavior-slow = [color=forestgreen]The frequency of pulsations is much less frequent.[/color] @@ -94,4 +96,4 @@ anomaly-behavior-secret = Interference detected. Some data cannot be read anomaly-behavior-inconstancy = [color=crimson]Impermanence has been detected. Particle types can change over time.[/color] anomaly-behavior-fast = [color=crimson]The pulsation frequency is strongly increased.[/color] anomaly-behavior-strenght = [color=crimson]The pulsation power is significantly increased.[/color] -anomaly-behavior-moving = [color=crimson]Coordinate instability was detected.[/color] \ No newline at end of file +anomaly-behavior-moving = [color=crimson]Coordinate instability was detected.[/color]