using System.Linq;
using Content.Server.Power.Components;
using Content.Server.Research.Systems;
using Content.Shared.UserInterface;
using Content.Server.Xenoarchaeology.Equipment.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Content.Shared.Audio;
using Content.Shared.DeviceLinking;
using Content.Shared.DeviceLinking.Events;
using Content.Shared.Paper;
using Content.Shared.Placeable;
using Content.Shared.Popups;
using Content.Shared.Power;
using Content.Shared.Power.EntitySystems;
using Content.Shared.Research.Components;
using Content.Shared.Xenoarchaeology.Equipment;
using Content.Shared.Xenoarchaeology.XenoArtifacts;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.Xenoarchaeology.Equipment.Systems;
///
/// This system is used for managing the artifact analyzer as well as the analysis console.
/// It also hanadles scanning and ui updates for both systems.
///
public sealed class ArtifactAnalyzerSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly ArtifactSystem _artifact = default!;
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
[Dependency] private readonly PaperSystem _paper = default!;
[Dependency] private readonly ResearchSystem _research = default!;
[Dependency] private readonly SharedAmbientSoundSystem _ambientSound = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedPowerReceiverSystem _receiver = default!;
[Dependency] private readonly TraversalDistorterSystem _traversalDistorter = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
///
public override void Initialize()
{
SubscribeLocalEvent(OnArtifactActivated);
SubscribeLocalEvent(OnAnalyzeStart);
SubscribeLocalEvent(OnAnalyzeEnd);
SubscribeLocalEvent(OnPowerChanged);
SubscribeLocalEvent(OnItemPlaced);
SubscribeLocalEvent(OnItemRemoved);
SubscribeLocalEvent(OnMapInit);
SubscribeLocalEvent(OnNewLink);
SubscribeLocalEvent(OnPortDisconnected);
SubscribeLocalEvent(OnServerSelectionMessage);
SubscribeLocalEvent(OnScanButton);
SubscribeLocalEvent(OnPrintButton);
SubscribeLocalEvent(OnExtractButton);
SubscribeLocalEvent(OnBiasButton);
SubscribeLocalEvent((e, c, _) => UpdateUserInterface(e, c),
after: new[] { typeof(ResearchSystem) });
SubscribeLocalEvent((e, c, _) => UpdateUserInterface(e, c),
after: new[] { typeof(ResearchSystem) });
SubscribeLocalEvent((e, c, _) => UpdateUserInterface(e, c));
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator();
while (query.MoveNext(out var uid, out var active, out var scan))
{
if (active.AnalysisPaused)
continue;
if (_timing.CurTime - active.StartTime < scan.AnalysisDuration - active.AccumulatedRunTime)
continue;
FinishScan(uid, scan, active);
}
}
///
/// Resets the current scan on the artifact analyzer
///
/// The analyzer being reset
///
[PublicAPI]
public void ResetAnalyzer(EntityUid uid, ArtifactAnalyzerComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
component.LastAnalyzedArtifact = null;
component.ReadyToPrint = false;
UpdateAnalyzerInformation(uid, component);
}
///
/// Goes through the current entities on
/// the analyzer and returns a valid artifact
///
///
///
///
private EntityUid? GetArtifactForAnalysis(EntityUid? uid, ItemPlacerComponent? placer = null)
{
if (uid == null || !Resolve(uid.Value, ref placer))
return null;
return placer.PlacedEntities.FirstOrNull();
}
///
/// Updates the current scan information based on
/// the last artifact that was scanned.
///
///
///
private void UpdateAnalyzerInformation(EntityUid uid, ArtifactAnalyzerComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
if (component.LastAnalyzedArtifact == null)
{
component.LastAnalyzerPointValue = null;
component.LastAnalyzedNode = null;
}
else if (TryComp(component.LastAnalyzedArtifact, out var artifact))
{
var lastNode = artifact.CurrentNodeId == null
? null
: (ArtifactNode?) _artifact.GetNodeFromId(artifact.CurrentNodeId.Value, artifact).Clone();
component.LastAnalyzedNode = lastNode;
component.LastAnalyzerPointValue = _artifact.GetResearchPointValue(component.LastAnalyzedArtifact.Value, artifact);
}
}
private void OnMapInit(EntityUid uid, ArtifactAnalyzerComponent component, MapInitEvent args)
{
if (!TryComp(uid, out var sink))
return;
foreach (var source in sink.LinkedSources)
{
if (!TryComp(source, out var analysis))
continue;
component.Console = source;
analysis.AnalyzerEntity = uid;
return;
}
}
private void OnNewLink(EntityUid uid, AnalysisConsoleComponent component, NewLinkEvent args)
{
if (!TryComp(args.Sink, out var analyzer))
return;
component.AnalyzerEntity = args.Sink;
analyzer.Console = uid;
UpdateUserInterface(uid, component);
}
private void OnPortDisconnected(EntityUid uid, AnalysisConsoleComponent component, PortDisconnectedEvent args)
{
if (args.Port == component.LinkingPort && component.AnalyzerEntity != null)
{
if (TryComp(component.AnalyzerEntity, out var analyzezr))
analyzezr.Console = null;
component.AnalyzerEntity = null;
}
UpdateUserInterface(uid, component);
}
private void UpdateUserInterface(EntityUid uid, AnalysisConsoleComponent? component = null)
{
if (!Resolve(uid, ref component, false))
return;
EntityUid? artifact = null;
FormattedMessage? msg = null;
TimeSpan? totalTime = null;
var canScan = false;
var canPrint = false;
var points = 0;
if (TryComp(component.AnalyzerEntity, out var analyzer))
{
artifact = analyzer.LastAnalyzedArtifact;
msg = GetArtifactScanMessage(analyzer);
totalTime = analyzer.AnalysisDuration;
if (TryComp(component.AnalyzerEntity, out var placer))
canScan = placer.PlacedEntities.Any();
canPrint = analyzer.ReadyToPrint;
// the artifact that's actually on the scanner right now.
if (GetArtifactForAnalysis(component.AnalyzerEntity, placer) is { } current)
points = _artifact.GetResearchPointValue(current);
}
var analyzerConnected = component.AnalyzerEntity != null;
var serverConnected = TryComp(uid, out var client) && client.ConnectedToServer;
var scanning = TryComp(component.AnalyzerEntity, out var active);
var paused = active != null ? active.AnalysisPaused : false;
var biasDirection = BiasDirection.Up;
if (TryComp(component.AnalyzerEntity, out var trav))
biasDirection = trav.BiasDirection;
var state = new AnalysisConsoleUpdateState(GetNetEntity(artifact), analyzerConnected, serverConnected,
canScan, canPrint, msg, scanning, paused, active?.StartTime, active?.AccumulatedRunTime, totalTime, points, biasDirection == BiasDirection.Down);
_ui.SetUiState(uid, ArtifactAnalzyerUiKey.Key, state);
}
///
/// opens the server selection menu.
///
///
///
///
private void OnServerSelectionMessage(EntityUid uid, AnalysisConsoleComponent component, AnalysisConsoleServerSelectionMessage args)
{
_ui.OpenUi(uid, ResearchClientUiKey.Key, args.Actor);
}
///
/// Starts scanning the artifact.
///
///
///
///
private void OnScanButton(EntityUid uid, AnalysisConsoleComponent component, AnalysisConsoleScanButtonPressedMessage args)
{
if (component.AnalyzerEntity == null)
return;
if (HasComp(component.AnalyzerEntity))
return;
var ent = GetArtifactForAnalysis(component.AnalyzerEntity);
if (ent == null)
return;
var activeComp = EnsureComp(component.AnalyzerEntity.Value);
activeComp.StartTime = _timing.CurTime;
activeComp.AccumulatedRunTime = TimeSpan.Zero;
activeComp.Artifact = ent.Value;
if (TryComp(component.AnalyzerEntity.Value, out var powa))
activeComp.AnalysisPaused = !powa.Powered;
var activeArtifact = EnsureComp(ent.Value);
activeArtifact.Scanner = component.AnalyzerEntity.Value;
UpdateUserInterface(uid, component);
}
private void OnPrintButton(EntityUid uid, AnalysisConsoleComponent component, AnalysisConsolePrintButtonPressedMessage args)
{
if (component.AnalyzerEntity == null)
return;
if (!TryComp(component.AnalyzerEntity, out var analyzer) ||
analyzer.LastAnalyzedNode == null ||
analyzer.LastAnalyzerPointValue == null ||
!analyzer.ReadyToPrint)
{
return;
}
analyzer.ReadyToPrint = false;
var report = Spawn(component.ReportEntityId, Transform(uid).Coordinates);
_metaSystem.SetEntityName(report, Loc.GetString("analysis-report-title", ("id", analyzer.LastAnalyzedNode.Id)));
var msg = GetArtifactScanMessage(analyzer);
if (msg == null)
return;
_popup.PopupEntity(Loc.GetString("analysis-console-print-popup"), uid);
if (TryComp(report, out var paperComp))
_paper.SetContent((report, paperComp), msg.ToMarkup());
UpdateUserInterface(uid, component);
}
private FormattedMessage? GetArtifactScanMessage(ArtifactAnalyzerComponent component)
{
var msg = new FormattedMessage();
if (component.LastAnalyzedNode == null)
return null;
var n = component.LastAnalyzedNode;
msg.AddMarkupOrThrow(Loc.GetString("analysis-console-info-id", ("id", n.Id)));
msg.PushNewline();
msg.AddMarkupOrThrow(Loc.GetString("analysis-console-info-depth", ("depth", n.Depth)));
msg.PushNewline();
var activated = n.Triggered
? "analysis-console-info-triggered-true"
: "analysis-console-info-triggered-false";
msg.AddMarkupOrThrow(Loc.GetString(activated));
msg.PushNewline();
msg.PushNewline();
var needSecondNewline = false;
var triggerProto = _prototype.Index(n.Trigger);
if (triggerProto.TriggerHint != null)
{
msg.AddMarkupOrThrow(Loc.GetString("analysis-console-info-trigger",
("trigger", Loc.GetString(triggerProto.TriggerHint))) + "\n");
needSecondNewline = true;
}
var effectproto = _prototype.Index(n.Effect);
if (effectproto.EffectHint != null)
{
msg.AddMarkupOrThrow(Loc.GetString("analysis-console-info-effect",
("effect", Loc.GetString(effectproto.EffectHint))) + "\n");
needSecondNewline = true;
}
if (needSecondNewline)
msg.PushNewline();
msg.AddMarkupOrThrow(Loc.GetString("analysis-console-info-edges", ("edges", n.Edges.Count)));
msg.PushNewline();
if (component.LastAnalyzerPointValue != null)
msg.AddMarkupOrThrow(Loc.GetString("analysis-console-info-value", ("value", component.LastAnalyzerPointValue)));
return msg;
}
///
/// Extracts points from the artifact and updates the server points
///
///
///
///
private void OnExtractButton(EntityUid uid, AnalysisConsoleComponent component, AnalysisConsoleExtractButtonPressedMessage args)
{
if (component.AnalyzerEntity == null)
return;
if (!_research.TryGetClientServer(uid, out var server, out var serverComponent))
return;
var artifact = GetArtifactForAnalysis(component.AnalyzerEntity);
if (artifact == null)
return;
var pointValue = _artifact.GetResearchPointValue(artifact.Value);
// no new nodes triggered so nothing to add
if (pointValue == 0)
return;
_research.ModifyServerPoints(server.Value, pointValue, serverComponent);
_artifact.AdjustConsumedPoints(artifact.Value, pointValue);
_audio.PlayPvs(component.ExtractSound, component.AnalyzerEntity.Value, AudioParams.Default.WithVolume(2f));
_popup.PopupEntity(Loc.GetString("analyzer-artifact-extract-popup"),
component.AnalyzerEntity.Value, PopupType.Large);
UpdateUserInterface(uid, component);
}
private void OnBiasButton(EntityUid uid, AnalysisConsoleComponent component, AnalysisConsoleBiasButtonPressedMessage args)
{
if (component.AnalyzerEntity == null)
return;
if (!TryComp(component.AnalyzerEntity, out var trav))
return;
if (!_traversalDistorter.SetState(component.AnalyzerEntity.Value, trav, args.IsDown))
return;
UpdateUserInterface(uid, component);
}
///
/// Cancels scans if the artifact changes nodes (is activated) during the scan.
///
private void OnArtifactActivated(EntityUid uid, ActiveScannedArtifactComponent component, ArtifactActivatedEvent args)
{
CancelScan(uid);
}
///
/// Stops the current scan
///
[PublicAPI]
public void CancelScan(EntityUid artifact, ActiveScannedArtifactComponent? component = null, ArtifactAnalyzerComponent? analyzer = null)
{
if (!Resolve(artifact, ref component, false))
return;
if (!Resolve(component.Scanner, ref analyzer))
return;
_audio.PlayPvs(component.ScanFailureSound, component.Scanner, AudioParams.Default.WithVolume(3f));
RemComp(component.Scanner);
if (analyzer.Console != null)
UpdateUserInterface(analyzer.Console.Value);
RemCompDeferred(artifact, component);
}
///
/// Finishes the current scan.
///
[PublicAPI]
public void FinishScan(EntityUid uid, ArtifactAnalyzerComponent? component = null, ActiveArtifactAnalyzerComponent? active = null)
{
if (!Resolve(uid, ref component, ref active))
return;
component.ReadyToPrint = true;
_audio.PlayPvs(component.ScanFinishedSound, uid);
component.LastAnalyzedArtifact = active.Artifact;
UpdateAnalyzerInformation(uid, component);
RemComp(active.Artifact);
RemComp(uid, active);
if (component.Console != null)
UpdateUserInterface(component.Console.Value);
}
[PublicAPI]
public void PauseScan(EntityUid uid, ArtifactAnalyzerComponent? component = null, ActiveArtifactAnalyzerComponent? active = null)
{
if (!Resolve(uid, ref component, ref active) || active.AnalysisPaused)
return;
active.AnalysisPaused = true;
// As we pause, we store what was already completed.
active.AccumulatedRunTime = (_timing.CurTime - active.StartTime) + active.AccumulatedRunTime;
if (Exists(component.Console))
UpdateUserInterface(component.Console.Value);
}
[PublicAPI]
public void ResumeScan(EntityUid uid, ArtifactAnalyzerComponent? component = null, ActiveArtifactAnalyzerComponent? active = null)
{
if (!Resolve(uid, ref component, ref active) || !active.AnalysisPaused)
return;
active.StartTime = _timing.CurTime;
active.AnalysisPaused = false;
if (Exists(component.Console))
UpdateUserInterface(component.Console.Value);
}
private void OnItemPlaced(EntityUid uid, ArtifactAnalyzerComponent component, ref ItemPlacedEvent args)
{
if (component.Console != null && Exists(component.Console))
UpdateUserInterface(component.Console.Value);
}
private void OnItemRemoved(EntityUid uid, ArtifactAnalyzerComponent component, ref ItemRemovedEvent args)
{
// Scanners shouldn't give permanent remove vision to an artifact, and the scanned artifact doesn't have any
// component to track analyzers that have scanned it for removal if the artifact gets deleted.
// So we always clear this on removal.
component.LastAnalyzedArtifact = null;
// cancel the scan if the artifact moves off the analyzer
CancelScan(args.OtherEntity);
if (Exists(component.Console))
UpdateUserInterface(component.Console.Value);
}
private void OnAnalyzeStart(EntityUid uid, ActiveArtifactAnalyzerComponent component, ComponentStartup args)
{
_receiver.SetNeedsPower(uid, true);
_ambientSound.SetAmbience(uid, true);
}
private void OnAnalyzeEnd(EntityUid uid, ActiveArtifactAnalyzerComponent component, ComponentShutdown args)
{
_receiver.SetNeedsPower(uid, false);
_ambientSound.SetAmbience(uid, false);
}
private void OnPowerChanged(EntityUid uid, ActiveArtifactAnalyzerComponent active, ref PowerChangedEvent args)
{
if (!args.Powered)
{
PauseScan(uid, null, active);
}
else
{
ResumeScan(uid, null, active);
}
}
}