Predict anomaly synchronizer (#39321)

* predict anomaly synchronizer

* pvs

* lambda

* Update Resources/Locale/en-US/anomaly/anomaly.ftl

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
This commit is contained in:
slarticodefast
2025-08-03 01:07:32 +02:00
committed by GitHub
parent 6c9368dc60
commit c538d7fb2b
3 changed files with 101 additions and 76 deletions

View File

@@ -1,33 +1,30 @@
using System.Linq; 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.Anomaly.Components;
using Content.Shared.DeviceLinking;
using Content.Shared.Examine; using Content.Shared.Examine;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Power; using Content.Shared.Power;
using Robust.Shared.Audio.Systems; using Content.Shared.Power.EntitySystems;
using Content.Shared.Verbs; using Content.Shared.Verbs;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Timing; using Robust.Shared.Timing;
namespace Content.Server.Anomaly; namespace Content.Shared.Anomaly;
/// <summary> /// <summary>
/// a device that allows you to translate anomaly activity into multitool signals. /// A device that allows you to translate anomaly activity into multitool signals.
/// </summary> /// </summary>
public sealed partial class AnomalySynchronizerSystem : EntitySystem 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 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 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() public override void Initialize()
{ {
@@ -47,27 +44,41 @@ public sealed partial class AnomalySynchronizerSystem : EntitySystem
{ {
base.Update(frameTime); base.Update(frameTime);
var curTime = _timing.CurTime;
var query = EntityQueryEnumerator<AnomalySynchronizerComponent, TransformComponent>(); var query = EntityQueryEnumerator<AnomalySynchronizerComponent, TransformComponent>();
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; continue;
if (_timing.CurTime < sync.NextCheckTime) if (curTime < sync.NextCheckTime)
continue; continue;
sync.NextCheckTime += sync.CheckFrequency; 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; 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; continue;
if (distance > sync.AttachRange) if (distance > sync.AttachRange)
DisconnectFromAnomaly((uid, sync), sync.ConnectedAnomaly.Value); DisconnectFromAnomaly((uid, sync));
} }
} }
@@ -76,11 +87,9 @@ public sealed partial class AnomalySynchronizerSystem : EntitySystem
/// </summary> /// </summary>
public bool TryAttachNearbyAnomaly(Entity<AnomalySynchronizerComponent> ent, EntityUid? user = null) public bool TryAttachNearbyAnomaly(Entity<AnomalySynchronizerComponent> ent, EntityUid? user = null)
{ {
if (!_power.IsPowered(ent)) if (!_power.IsPowered(ent.Owner))
{ {
if (user is not null) _popup.PopupClient(Loc.GetString("base-computer-ui-component-not-powered", ("machine", ent)), ent, user);
_popup.PopupEntity(Loc.GetString("base-computer-ui-component-not-powered", ("machine", ent)), ent, user.Value);
return false; return false;
} }
@@ -89,13 +98,11 @@ public sealed partial class AnomalySynchronizerSystem : EntitySystem
if (anomaly.Owner is { Valid: false }) // no anomaly in range if (anomaly.Owner is { Valid: false }) // no anomaly in range
{ {
if (user is not null) _popup.PopupClient(Loc.GetString("anomaly-sync-no-anomaly"), ent, user);
_popup.PopupEntity(Loc.GetString("anomaly-sync-no-anomaly"), ent, user.Value);
return false; return false;
} }
ConnectToAnomaly(ent, anomaly); ConnectToAnomaly(ent, anomaly, user);
return true; return true;
} }
@@ -104,10 +111,10 @@ public sealed partial class AnomalySynchronizerSystem : EntitySystem
if (args.Powered) if (args.Powered)
return; return;
if (ent.Comp.ConnectedAnomaly is null) if (ent.Comp.ConnectedAnomaly == null)
return; return;
DisconnectFromAnomaly(ent, ent.Comp.ConnectedAnomaly.Value); DisconnectFromAnomaly(ent);
} }
private void OnExamined(Entity<AnomalySynchronizerComponent> ent, ref ExaminedEvent args) private void OnExamined(Entity<AnomalySynchronizerComponent> ent, ref ExaminedEvent args)
@@ -117,19 +124,29 @@ public sealed partial class AnomalySynchronizerSystem : EntitySystem
private void OnGetInteractionVerbs(Entity<AnomalySynchronizerComponent> ent, ref GetVerbsEvent<InteractionVerb> args) private void OnGetInteractionVerbs(Entity<AnomalySynchronizerComponent> ent, ref GetVerbsEvent<InteractionVerb> args)
{ {
if (!args.CanAccess || !args.CanInteract || args.Hands is null || ent.Comp.ConnectedAnomaly.HasValue) if (!args.CanAccess || !args.CanInteract || args.Hands == null)
return; return;
var user = args.User; var user = args.User;
args.Verbs.Add(new()
if (ent.Comp.ConnectedAnomaly == null)
{ {
Act = () => args.Verbs.Add(new()
{ {
TryAttachNearbyAnomaly(ent, user); Act = () => TryAttachNearbyAnomaly(ent, user),
}, Message = Loc.GetString("anomaly-sync-connect-verb-message", ("machine", ent)),
Message = Loc.GetString("anomaly-sync-connect-verb-message", ("machine", ent)), Text = Loc.GetString("anomaly-sync-connect-verb-text"),
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<AnomalySynchronizerComponent> ent, ref InteractHandEvent args) private void OnInteractHand(Entity<AnomalySynchronizerComponent> ent, ref InteractHandEvent args)
@@ -137,12 +154,13 @@ public sealed partial class AnomalySynchronizerSystem : EntitySystem
TryAttachNearbyAnomaly(ent, args.User); TryAttachNearbyAnomaly(ent, args.User);
} }
private void ConnectToAnomaly(Entity<AnomalySynchronizerComponent> ent, Entity<AnomalyComponent> anomaly) private void ConnectToAnomaly(Entity<AnomalySynchronizerComponent> ent, Entity<AnomalyComponent> anomaly, EntityUid? user = null)
{ {
if (ent.Comp.ConnectedAnomaly == anomaly) if (ent.Comp.ConnectedAnomaly == anomaly)
return; return;
ent.Comp.ConnectedAnomaly = anomaly; ent.Comp.ConnectedAnomaly = anomaly;
Dirty(ent);
//move the anomaly to the center of the synchronizer, for aesthetics. //move the anomaly to the center of the synchronizer, for aesthetics.
var targetXform = _transform.GetWorldPosition(ent); var targetXform = _transform.GetWorldPosition(ent);
_transform.SetWorldPosition(anomaly, targetXform); _transform.SetWorldPosition(anomaly, targetXform);
@@ -150,27 +168,27 @@ public sealed partial class AnomalySynchronizerSystem : EntitySystem
if (ent.Comp.PulseOnConnect) if (ent.Comp.PulseOnConnect)
_anomaly.DoAnomalyPulse(anomaly, anomaly); _anomaly.DoAnomalyPulse(anomaly, anomaly);
_popup.PopupEntity(Loc.GetString("anomaly-sync-connected"), ent, PopupType.Medium); _popup.PopupPredicted(Loc.GetString("anomaly-sync-connected"), ent, user, PopupType.Medium);
_audio.PlayPvs(ent.Comp.ConnectedSound, ent); _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. //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. //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<AnomalySynchronizerComponent> ent, EntityUid other) private void DisconnectFromAnomaly(Entity<AnomalySynchronizerComponent> ent, EntityUid? user = null)
{ {
if (ent.Comp.ConnectedAnomaly == null) if (ent.Comp.ConnectedAnomaly == null)
return; return;
if (TryComp<AnomalyComponent>(other, out var anomaly)) if (ent.Comp.PulseOnDisconnect && TryComp<AnomalyComponent>(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); _popup.PopupPredicted(Loc.GetString("anomaly-sync-disconnected"), ent, user, PopupType.Large);
_audio.PlayPvs(ent.Comp.ConnectedSound, ent); _audio.PlayPredicted(ent.Comp.DisconnectedSound, ent, user);
ent.Comp.ConnectedAnomaly = null; ent.Comp.ConnectedAnomaly = null;
Dirty(ent);
} }
private void OnAnomalyPulse(ref AnomalyPulseEvent args) private void OnAnomalyPulse(ref AnomalyPulseEvent args)
@@ -184,19 +202,19 @@ public sealed partial class AnomalySynchronizerSystem : EntitySystem
if (!_power.IsPowered(uid)) if (!_power.IsPowered(uid))
continue; continue;
_signal.InvokePort(uid, component.PulsePort); _deviceLink.InvokePort(uid, component.PulsePort);
} }
} }
private void OnAnomalySeverityChanged(ref AnomalySeverityChangedEvent args) private void OnAnomalySeverityChanged(ref AnomalySeverityChangedEvent args)
{ {
var query = EntityQueryEnumerator<AnomalySynchronizerComponent>(); var query = EntityQueryEnumerator<AnomalySynchronizerComponent>();
while (query.MoveNext(out var ent, out var component)) while (query.MoveNext(out var uid, out var component))
{ {
if (args.Anomaly != component.ConnectedAnomaly) if (args.Anomaly != component.ConnectedAnomaly)
continue; continue;
if (!_power.IsPowered(ent)) if (!_power.IsPowered(uid))
continue; continue;
//The superscritical port is invoked not at the AnomalySupercriticalEvent, //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, //ATTENTION! the console command supercriticalanomaly does not work here,
//as it forcefully causes growth to start without increasing severity. //as it forcefully causes growth to start without increasing severity.
if (args.Severity >= 1) if (args.Severity >= 1)
_signal.InvokePort(ent, component.SupercritPort); _deviceLink.InvokePort(uid, component.SupercritPort);
} }
} }
private void OnAnomalyStabilityChanged(ref AnomalyStabilityChangedEvent args) private void OnAnomalyStabilityChanged(ref AnomalyStabilityChangedEvent args)
{ {
Entity<AnomalyComponent> anomaly = (args.Anomaly, Comp<AnomalyComponent>(args.Anomaly)); var anomaly = Comp<AnomalyComponent>(args.Anomaly);
var query = EntityQueryEnumerator<AnomalySynchronizerComponent>(); var query = EntityQueryEnumerator<AnomalySynchronizerComponent>();
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; continue;
if (!_power.IsPowered(ent)) if (!_power.IsPowered(uid))
continue; 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 else
{ {
_signal.InvokePort(ent, component.StabilizePort); _deviceLink.InvokePort(uid, sync.StabilizePort);
} }
} }
} }

View File

@@ -1,46 +1,51 @@
using Content.Shared.DeviceLinking; using Content.Shared.DeviceLinking;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Anomaly.Components; namespace Content.Shared.Anomaly.Components;
/// <summary> /// <summary>
/// a device that allows you to translate anomaly activity into multitool signals. /// A device that allows you to translate anomaly activity into multitool signals.
/// </summary> /// </summary>
[RegisterComponent, AutoGenerateComponentPause, Access(typeof(AnomalySynchronizerSystem))] [RegisterComponent, NetworkedComponent]
[AutoGenerateComponentState, AutoGenerateComponentPause]
[Access(typeof(AnomalySynchronizerSystem))]
public sealed partial class AnomalySynchronizerComponent : Component public sealed partial class AnomalySynchronizerComponent : Component
{ {
/// <summary> /// <summary>
/// The uid of the anomaly to which the synchronizer is connected. /// The uid of the anomaly to which the synchronizer is connected.
/// </summary> /// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)] [DataField, AutoNetworkedField]
public EntityUid? ConnectedAnomaly; public EntityUid? ConnectedAnomaly;
/// <summary> /// <summary>
/// Should the anomaly pulse when connected to the synchronizer? /// Should the anomaly pulse when connected to the synchronizer?
/// </summary> /// </summary>
[DataField] [DataField, AutoNetworkedField]
public bool PulseOnConnect = true; public bool PulseOnConnect = true;
/// <summary> /// <summary>
/// Should the anomaly pulse when disconnected from synchronizer? /// Should the anomaly pulse when disconnected from synchronizer?
/// </summary> /// </summary>
[DataField] [DataField, AutoNetworkedField]
public bool PulseOnDisconnect = false; public bool PulseOnDisconnect = false;
/// <summary> /// <summary>
/// minimum distance from the synchronizer to the anomaly to be attached /// Minimum distance from the synchronizer to the anomaly to be attached.
/// </summary> /// </summary>
[DataField] [DataField, AutoNetworkedField]
public float AttachRange = 0.4f; public float AttachRange = 0.4f;
/// <summary> /// <summary>
/// Periodicheski checks to see if the anomaly has moved to disconnect it. /// Periodically checks to see if the anomaly has moved to disconnect it.
/// </summary> /// </summary>
[DataField] [DataField, AutoNetworkedField]
public TimeSpan CheckFrequency = TimeSpan.FromSeconds(1f); public TimeSpan CheckFrequency = TimeSpan.FromSeconds(1f);
[DataField, AutoPausedField] [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoNetworkedField, AutoPausedField]
public TimeSpan NextCheckTime = TimeSpan.Zero; public TimeSpan NextCheckTime = TimeSpan.Zero;
[DataField] [DataField]
@@ -58,9 +63,9 @@ public sealed partial class AnomalySynchronizerComponent : Component
[DataField] [DataField]
public ProtoId<SourcePortPrototype> SupercritPort = "Supercritical"; public ProtoId<SourcePortPrototype> SupercritPort = "Supercritical";
[DataField, ViewVariables(VVAccess.ReadWrite)] [DataField]
public SoundSpecifier ConnectedSound = new SoundPathSpecifier("/Audio/Machines/anomaly_sync_connect.ogg"); 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"); public SoundSpecifier DisconnectedSound = new SoundPathSpecifier("/Audio/Machines/anomaly_sync_connect.ogg");
} }

View File

@@ -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-examine-not-connected = It is [color=darkred]not attached[/color] to an anomaly.
anomaly-sync-connect-verb-text = Attach anomaly anomaly-sync-connect-verb-text = Attach anomaly
anomaly-sync-connect-verb-message = Attach a nearby anomaly to {THE($machine)}. 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-ui-title = Anomaly Generator
anomaly-generator-fuel-display = Fuel: 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-unknown = [color=red]ERROR. Cannot be read.[/color]
anomaly-behavior-title = behavior deviation analysis: 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-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] 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-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-fast = [color=crimson]The pulsation frequency is strongly increased.[/color]
anomaly-behavior-strenght = [color=crimson]The pulsation power is significantly increased.[/color] anomaly-behavior-strenght = [color=crimson]The pulsation power is significantly increased.[/color]
anomaly-behavior-moving = [color=crimson]Coordinate instability was detected.[/color] anomaly-behavior-moving = [color=crimson]Coordinate instability was detected.[/color]