--- /dev/null
+using Content.Shared.Silicons.StationAi;
+using Robust.Client.UserInterface;
+
+namespace Content.Client.Silicons.StationAi;
+
+public sealed class StationAiFixerConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
+{
+ private StationAiFixerConsoleWindow? _window;
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _window = this.CreateWindow<StationAiFixerConsoleWindow>();
+ _window.SetOwner(Owner);
+
+ _window.SendStationAiFixerConsoleMessageAction += SendStationAiFixerConsoleMessage;
+ _window.OpenConfirmationDialogAction += OpenConfirmationDialog;
+ }
+
+ public override void Update()
+ {
+ base.Update();
+ _window?.UpdateState();
+ }
+
+ private void OpenConfirmationDialog()
+ {
+ if (_window == null)
+ return;
+
+ _window.ConfirmationDialog?.Close();
+ _window.ConfirmationDialog = new StationAiFixerConsoleConfirmationDialog();
+ _window.ConfirmationDialog.OpenCentered();
+ _window.ConfirmationDialog.SendStationAiFixerConsoleMessageAction += SendStationAiFixerConsoleMessage;
+ }
+
+ private void SendStationAiFixerConsoleMessage(StationAiFixerConsoleAction action)
+ {
+ SendPredictedMessage(new StationAiFixerConsoleMessage(action));
+ }
+}
--- /dev/null
+<controls:FancyWindow xmlns="https://spacestation14.io"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+ Title="{Loc 'station-ai-fixer-console-window-purge-warning-title'}"
+ Resizable="False">
+ <BoxContainer Orientation="Vertical" VerticalExpand="True" SetWidth="400">
+ <RichTextLabel Name="PurgeWarningLabel1" Margin="20 10 20 0"/>
+ <RichTextLabel Name="PurgeWarningLabel2" Margin="20 10 20 0"/>
+ <RichTextLabel Name="PurgeWarningLabel3" Margin="20 10 20 10"/>
+ <BoxContainer HorizontalExpand="True">
+ <Button Name="CancelPurge"
+ Text="{Loc 'station-ai-fixer-console-window-cancel-action'}"
+ SetWidth="150"
+ Margin="20 10 0 10"/>
+ <Control HorizontalExpand="True"/>
+ <Button Name="ContinuePurge"
+ Text="{Loc 'station-ai-fixer-console-window-continue-action'}"
+ SetWidth="150"
+ Margin="0 10 20 10"/>
+ </BoxContainer>
+ </BoxContainer>
+</controls:FancyWindow>
--- /dev/null
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Silicons.StationAi;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Silicons.StationAi;
+
+[GenerateTypedNameReferences]
+public sealed partial class StationAiFixerConsoleConfirmationDialog : FancyWindow
+{
+ public event Action<StationAiFixerConsoleAction>? SendStationAiFixerConsoleMessageAction;
+
+ public StationAiFixerConsoleConfirmationDialog()
+ {
+ RobustXamlLoader.Load(this);
+
+ PurgeWarningLabel1.SetMessage(Loc.GetString($"station-ai-fixer-console-window-purge-warning-1"));
+ PurgeWarningLabel2.SetMessage(Loc.GetString($"station-ai-fixer-console-window-purge-warning-2"));
+ PurgeWarningLabel3.SetMessage(Loc.GetString($"station-ai-fixer-console-window-purge-warning-3"));
+
+ CancelPurge.OnButtonDown += _ => Close();
+ ContinuePurge.OnButtonDown += _ => OnSendStationAiFixerConsoleMessage(StationAiFixerConsoleAction.Purge);
+ }
+
+ public void OnSendStationAiFixerConsoleMessage(StationAiFixerConsoleAction action)
+ {
+ SendStationAiFixerConsoleMessageAction?.Invoke(action);
+ Close();
+ }
+}
--- /dev/null
+using Content.Shared.Silicons.StationAi;
+using Robust.Client.GameObjects;
+
+namespace Content.Client.Silicons.StationAi;
+
+public sealed partial class StationAiFixerConsoleSystem : SharedStationAiFixerConsoleSystem
+{
+ [Dependency] private readonly SharedUserInterfaceSystem _userInterface = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<StationAiFixerConsoleComponent, AppearanceChangeEvent>(OnAppearanceChange);
+ }
+
+ private void OnAppearanceChange(Entity<StationAiFixerConsoleComponent> ent, ref AppearanceChangeEvent args)
+ {
+ if (_userInterface.TryGetOpenUi(ent.Owner, StationAiFixerConsoleUiKey.Key, out var bui))
+ {
+ bui?.Update<StationAiFixerConsoleBoundUserInterfaceState>();
+ }
+ }
+}
--- /dev/null
+<controls:FancyWindow xmlns="https://spacestation14.io"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+ xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
+ Title="{Loc 'station-ai-fixer-console-window'}"
+ Resizable="False">
+ <BoxContainer Orientation="Vertical" VerticalExpand="True">
+ <BoxContainer VerticalExpand="True" HorizontalExpand="True" Orientation="Horizontal">
+
+ <!-- Left side - AI display -->
+ <BoxContainer VerticalExpand="True" HorizontalExpand="True" Orientation="Vertical" MinWidth="225" Margin="20 15 20 20">
+
+ <!-- AI panel -->
+ <PanelContainer>
+ <PanelContainer.PanelOverride>
+ <gfx:StyleBoxFlat BackgroundColor="#1B1B1E" />
+ </PanelContainer.PanelOverride>
+
+ <BoxContainer Orientation="Vertical">
+ <!-- AI name -->
+ <Label Name="StationAiNameLabel"
+ HorizontalAlignment="Center"
+ Margin="0 5 0 0"
+ Text="{Loc 'station-ai-fixer-console-window-no-station-ai'}"/>
+
+ <!-- AI portrait -->
+ <AnimatedTextureRect Name="StationAiPortraitTexture" VerticalAlignment="Center" SetSize="128 128" />
+ </BoxContainer>
+ </PanelContainer>
+
+ <!-- AI status panel-->
+ <PanelContainer Name="StationAiStatus">
+ <PanelContainer.PanelOverride>
+ <gfx:StyleBoxFlat BackgroundColor="#757575" />
+ </PanelContainer.PanelOverride>
+
+ <!-- AI name -->
+ <Label Name="StationAiStatusLabel"
+ HorizontalAlignment="Center"
+ Text="{Loc 'station-ai-fixer-console-window-no-station-ai-status'}"/>
+ </PanelContainer>
+ </BoxContainer>
+
+ <!-- Central divider -->
+ <PanelContainer StyleClasses="LowDivider" VerticalExpand="True" Margin="0 0 0 0" SetWidth="2"/>
+
+ <!-- Right side - control panel -->
+ <BoxContainer VerticalExpand="True" HorizontalExpand="True" Orientation="Vertical" MinWidth="225" Margin="10 10 10 10">
+
+ <!-- Locked controls -->
+ <BoxContainer Name="LockScreen"
+ VerticalExpand="True"
+ HorizontalExpand="True"
+ Orientation="Vertical"
+ ReservesSpace="False">
+
+ <controls:StripeBack VerticalExpand="True" HorizontalExpand="True" Margin="0 0 0 5">
+ <PanelContainer VerticalExpand="True" HorizontalExpand="True">
+ <BoxContainer VerticalExpand="True" HorizontalExpand="True" Orientation="Vertical">
+ <Control VerticalExpand="True"/>
+ <TextureRect VerticalAlignment="Center"
+ HorizontalAlignment="Center"
+ SetSize="64 64"
+ Stretch="KeepAspectCentered"
+ TexturePath="/Textures/Interface/VerbIcons/lock.svg.192dpi.png">
+ </TextureRect>
+ <Label Text="{Loc 'station-ai-fixer-console-window-controls-locked'}"
+ VerticalAlignment="Center"
+ HorizontalAlignment="Center"
+ Margin="0 5 0 0"/>
+ <Control VerticalExpand="True"/>
+ </BoxContainer>
+ </PanelContainer>
+ </controls:StripeBack>
+ </BoxContainer>
+
+ <!-- Action progress screen -->
+ <BoxContainer Name="ActionProgressScreen"
+ VerticalExpand="True"
+ HorizontalExpand="True"
+ Orientation="Vertical"
+ ReservesSpace="False"
+ Visible="False">
+
+ <Control VerticalExpand="True" Margin="0 0 0 0"/>
+ <Label Name="ActionInProgressLabel" Text="???" HorizontalAlignment="Center"/>
+ <ProgressBar Name="ActionProgressBar"
+ MinValue="0"
+ MaxValue="1"
+ SetHeight="20"
+ Margin="5 10 5 10">
+ </ProgressBar>
+ <Label Name="ActionProgressEtaLabel" Text="???" HorizontalAlignment="Center"/>
+
+ <!-- Cancel button -->
+ <Button Name="CancelButton" HorizontalExpand="True" Margin="0 20 0 10" SetHeight="40"
+ Text="{Loc 'station-ai-fixer-console-window-cancel-action'}">
+ <TextureRect HorizontalAlignment="Left"
+ VerticalAlignment="Center"
+ SetSize="24 24"
+ Stretch="KeepAspectCentered"
+ TexturePath="/Textures/Interface/Nano/cross.svg.png">
+ </TextureRect>
+ </Button>
+ </BoxContainer>
+
+ <!-- Visible controls -->
+ <BoxContainer Name="MainControls"
+ VerticalExpand="True"
+ HorizontalExpand="True"
+ Orientation="Vertical"
+ ReservesSpace="False"
+ Visible="False">
+
+ <controls:StripeBack>
+ <PanelContainer>
+ <Label Text="{Loc 'Controls'}"
+ HorizontalExpand="True"
+ HorizontalAlignment="Center"/>
+ </PanelContainer>
+ </controls:StripeBack>
+
+ <!-- Eject button -->
+ <Button Name="EjectButton" HorizontalExpand="True" Margin="0 10 0 0" SetHeight="40"
+ Text="{Loc 'station-ai-fixer-console-window-station-ai-eject'}">
+ <TextureRect HorizontalAlignment="Left"
+ VerticalAlignment="Center"
+ SetSize="32 32"
+ Stretch="KeepAspectCentered"
+ TexturePath="/Textures/Interface/VerbIcons/eject.svg.192dpi.png">
+ </TextureRect>
+ </Button>
+
+ <!-- Repair button -->
+ <Button Name="RepairButton" HorizontalExpand="True" Margin="0 10 0 0" SetHeight="40"
+ Text="{Loc 'station-ai-fixer-console-window-station-ai-repair'}">
+ <TextureRect HorizontalAlignment="Left"
+ VerticalAlignment="Center"
+ SetSize="32 32"
+ Stretch="KeepAspectCentered"
+ TexturePath="/Textures/Interface/hammer_scaled.svg.192dpi.png">
+ </TextureRect>
+ </Button>
+
+ <!-- Purge button -->
+ <Button Name="PurgeButton" HorizontalExpand="True" Margin="0 10 0 0" SetHeight="40"
+ Text="{Loc 'station-ai-fixer-console-window-station-ai-purge'}">
+ <TextureRect HorizontalAlignment="Left"
+ VerticalAlignment="Center"
+ SetSize="32 32"
+ Stretch="KeepAspectCentered"
+ TexturePath="/Textures/Interface/VerbIcons/delete_transparent.svg.192dpi.png">
+ </TextureRect>
+ </Button>
+
+ </BoxContainer>
+ </BoxContainer>
+ </BoxContainer>
+
+ <!-- Footer -->
+ <BoxContainer Orientation="Vertical">
+ <PanelContainer StyleClasses="LowDivider" />
+ <BoxContainer Orientation="Horizontal" Margin="10 2 5 0" VerticalAlignment="Bottom">
+ <Label Text="{Loc 'station-ai-fixer-console-window-flavor-left'}" StyleClasses="WindowFooterText" />
+ <Label Text="{Loc 'station-ai-fixer-console-window-flavor-right'}" StyleClasses="WindowFooterText"
+ HorizontalAlignment="Right" HorizontalExpand="True" Margin="0 0 5 0" />
+ <TextureRect StyleClasses="NTLogoDark" Stretch="KeepAspectCentered"
+ VerticalAlignment="Center" HorizontalAlignment="Right" SetSize="19 19"/>
+ </BoxContainer>
+ </BoxContainer>
+ </BoxContainer>
+</controls:FancyWindow>
--- /dev/null
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Lock;
+using Content.Shared.Silicons.StationAi;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+using System.Numerics;
+
+namespace Content.Client.Silicons.StationAi;
+
+[GenerateTypedNameReferences]
+public sealed partial class StationAiFixerConsoleWindow : FancyWindow
+{
+ [Dependency] private readonly IEntityManager _entManager = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+
+ private readonly StationAiFixerConsoleSystem _stationAiFixerConsole;
+ private readonly SharedStationAiSystem _stationAi;
+
+ private EntityUid? _owner;
+
+ private readonly SpriteSpecifier.Rsi _emptyPortrait = new(new("Mobs/Silicon/station_ai.rsi"), "ai_empty");
+ private readonly SpriteSpecifier.Rsi _rebootingPortrait = new(new("Mobs/Silicon/station_ai.rsi"), "ai_fuzz");
+ private SpriteSpecifier? _currentPortrait;
+
+ public event Action<StationAiFixerConsoleAction>? SendStationAiFixerConsoleMessageAction;
+ public event Action? OpenConfirmationDialogAction;
+
+ public StationAiFixerConsoleConfirmationDialog? ConfirmationDialog;
+
+ private readonly Dictionary<StationAiState, Color> _statusColors = new()
+ {
+ [StationAiState.Empty] = Color.FromHex("#464966"),
+ [StationAiState.Occupied] = Color.FromHex("#3E6C45"),
+ [StationAiState.Rebooting] = Color.FromHex("#A5762F"),
+ [StationAiState.Dead] = Color.FromHex("#BB3232"),
+ };
+
+ public StationAiFixerConsoleWindow()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ _stationAiFixerConsole = _entManager.System<StationAiFixerConsoleSystem>();
+ _stationAi = _entManager.System<StationAiSystem>();
+
+ StationAiPortraitTexture.DisplayRect.TextureScale = new Vector2(4f, 4f);
+
+ CancelButton.OnButtonDown += _ => OnSendStationAiFixerConsoleMessage(StationAiFixerConsoleAction.Cancel);
+ EjectButton.OnButtonDown += _ => OnSendStationAiFixerConsoleMessage(StationAiFixerConsoleAction.Eject);
+ RepairButton.OnButtonDown += _ => OnSendStationAiFixerConsoleMessage(StationAiFixerConsoleAction.Repair);
+ PurgeButton.OnButtonDown += _ => OnOpenConfirmationDialog();
+
+ CancelButton.Label.HorizontalAlignment = HAlignment.Left;
+ EjectButton.Label.HorizontalAlignment = HAlignment.Left;
+ RepairButton.Label.HorizontalAlignment = HAlignment.Left;
+ PurgeButton.Label.HorizontalAlignment = HAlignment.Left;
+
+ CancelButton.Label.Margin = new Thickness(40, 0, 0, 0);
+ EjectButton.Label.Margin = new Thickness(40, 0, 0, 0);
+ RepairButton.Label.Margin = new Thickness(40, 0, 0, 0);
+ PurgeButton.Label.Margin = new Thickness(40, 0, 0, 0);
+ }
+
+ public void OnSendStationAiFixerConsoleMessage(StationAiFixerConsoleAction action)
+ {
+ SendStationAiFixerConsoleMessageAction?.Invoke(action);
+ }
+
+ public void OnOpenConfirmationDialog()
+ {
+ OpenConfirmationDialogAction?.Invoke();
+ }
+
+ public override void Close()
+ {
+ base.Close();
+ ConfirmationDialog?.Close();
+ }
+
+ public void SetOwner(EntityUid owner)
+ {
+ _owner = owner;
+ UpdateState();
+ }
+
+ public void UpdateState()
+ {
+ if (!_entManager.TryGetComponent<StationAiFixerConsoleComponent>(_owner, out var stationAiFixerConsole))
+ return;
+
+ var ent = (_owner.Value, stationAiFixerConsole);
+ var isLocked = _entManager.TryGetComponent<LockComponent>(_owner, out var lockable) && lockable.Locked;
+
+ var stationAiHolderInserted = _stationAiFixerConsole.IsStationAiHolderInserted((_owner.Value, stationAiFixerConsole));
+ var stationAi = stationAiFixerConsole.ActionTarget;
+ var stationAiState = StationAiState.Empty;
+
+ if (_entManager.TryGetComponent<StationAiCustomizationComponent>(stationAi, out var stationAiCustomization))
+ {
+ stationAiState = stationAiCustomization.State;
+ }
+
+ // Set subscreen visibility
+ LockScreen.Visible = isLocked;
+ MainControls.Visible = !isLocked && !_stationAiFixerConsole.IsActionInProgress(ent);
+ ActionProgressScreen.Visible = !isLocked && _stationAiFixerConsole.IsActionInProgress(ent);
+
+ // Update station AI name
+ StationAiNameLabel.Text = GetStationAiName(stationAi);
+ StationAiStatusLabel.Text = Loc.GetString("station-ai-fixer-console-window-no-station-ai-status");
+
+ // Update station AI portrait
+ var portrait = _emptyPortrait;
+ var statusColor = _statusColors[StationAiState.Empty];
+
+ if (stationAiState == StationAiState.Rebooting)
+ {
+ portrait = _rebootingPortrait;
+ StationAiStatusLabel.Text = Loc.GetString("station-ai-fixer-console-window-station-ai-rebooting");
+ _statusColors.TryGetValue(StationAiState.Rebooting, out statusColor);
+ }
+ else if (stationAi != null &&
+ stationAiCustomization != null &&
+ _stationAi.TryGetCustomizedAppearanceData((stationAi.Value, stationAiCustomization), out var layerData))
+ {
+ StationAiStatusLabel.Text = stationAiState == StationAiState.Occupied ?
+ Loc.GetString("station-ai-fixer-console-window-station-ai-online") :
+ Loc.GetString("station-ai-fixer-console-window-station-ai-offline");
+
+ if (layerData.TryGetValue(stationAiState.ToString(), out var stateData) && stateData is { RsiPath: not null, State: not null })
+ {
+ portrait = new SpriteSpecifier.Rsi(new ResPath(stateData.RsiPath), stateData.State);
+ }
+
+ _statusColors.TryGetValue(stationAiState, out statusColor);
+ }
+
+ if (_currentPortrait == null || !_currentPortrait.Equals(portrait))
+ {
+ StationAiPortraitTexture.SetFromSpriteSpecifier(portrait);
+ _currentPortrait = portrait;
+ }
+
+ StationAiStatus.PanelOverride = new StyleBoxFlat
+ {
+ BackgroundColor = statusColor,
+ };
+
+ // Update buttons
+ EjectButton.Disabled = !stationAiHolderInserted;
+ RepairButton.Disabled = !stationAiHolderInserted || stationAiState != StationAiState.Dead;
+ PurgeButton.Disabled = !stationAiHolderInserted || stationAiState == StationAiState.Empty;
+
+ // Update progress bar
+ if (ActionProgressScreen.Visible)
+ UpdateProgressBar(ent);
+ }
+
+ public void UpdateProgressBar(Entity<StationAiFixerConsoleComponent> ent)
+ {
+ ActionInProgressLabel.Text = ent.Comp.ActionType == StationAiFixerConsoleAction.Repair ?
+ Loc.GetString("station-ai-fixer-console-window-action-progress-repair") :
+ Loc.GetString("station-ai-fixer-console-window-action-progress-purge");
+
+ var fullTimeSpan = ent.Comp.ActionEndTime - ent.Comp.ActionStartTime;
+ var remainingTimeSpan = ent.Comp.ActionEndTime - _timing.CurTime;
+
+ var time = remainingTimeSpan.TotalSeconds > 60 ? remainingTimeSpan.TotalMinutes : remainingTimeSpan.TotalSeconds;
+ var units = remainingTimeSpan.TotalSeconds > 60 ? Loc.GetString("generic-minutes") : Loc.GetString("generic-seconds");
+ ActionProgressEtaLabel.Text = Loc.GetString("station-ai-fixer-console-window-action-progress-eta", ("time", (int)time), ("units", units));
+
+ ActionProgressBar.Value = 1f - (float)remainingTimeSpan.Divide(fullTimeSpan);
+ }
+
+ private string GetStationAiName(EntityUid? uid)
+ {
+ if (_entManager.TryGetComponent<MetaDataComponent>(uid, out var metadata))
+ {
+ return metadata.EntityName;
+ }
+
+ return Loc.GetString("station-ai-fixer-console-window-no-station-ai");
+ }
+
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ if (!ActionProgressScreen.Visible)
+ return;
+
+ if (!_entManager.TryGetComponent<StationAiFixerConsoleComponent>(_owner, out var stationAiFixerConsole))
+ return;
+
+ UpdateProgressBar((_owner.Value, stationAiFixerConsole));
+ }
+}
if (args.Sprite == null)
return;
- if (_appearance.TryGetData<PrototypeLayerData>(entity.Owner, StationAiVisualState.Key, out var layerData, args.Component))
- _sprite.LayerSetData((entity.Owner, args.Sprite), StationAiVisualState.Key, layerData);
+ if (_appearance.TryGetData<PrototypeLayerData>(entity.Owner, StationAiVisualLayers.Icon, out var layerData, args.Component))
+ _sprite.LayerSetData((entity.Owner, args.Sprite), StationAiVisualLayers.Icon, layerData);
- _sprite.LayerSetVisible((entity.Owner, args.Sprite), StationAiVisualState.Key, layerData != null);
+ _sprite.LayerSetVisible((entity.Owner, args.Sprite), StationAiVisualLayers.Icon, layerData != null);
}
public override void Shutdown()
using Content.Shared.Holopad;
using Content.Shared.IdentityManagement;
using Content.Shared.Labels.Components;
+using Content.Shared.Mobs;
+using Content.Shared.Mobs.Systems;
using Content.Shared.Power;
using Content.Shared.Silicons.StationAi;
using Content.Shared.Speech;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly PvsOverrideSystem _pvs = default!;
+ [Dependency] private readonly MobStateSystem _mobState = default!;
private float _updateTimer = 1.0f;
private const float UpdateTime = 1.0f;
SubscribeLocalEvent<HolopadComponent, EntRemovedFromContainerMessage>(OnAiRemove);
SubscribeLocalEvent<HolopadComponent, EntParentChangedMessage>(OnParentChanged);
SubscribeLocalEvent<HolopadComponent, PowerChangedEvent>(OnPowerChanged);
+ SubscribeLocalEvent<HolopadUserComponent, MobStateChangedEvent>(OnMobStateChanged);
+
}
#region: Holopad UI bound user interface messages
if (!_stationAiSystem.TryGetHeld((receiver, receiverStationAiCore), out var insertedAi))
continue;
- if (_userInterfaceSystem.TryOpenUi(receiverUid, HolopadUiKey.AiRequestWindow, insertedAi))
+ if (_userInterfaceSystem.TryOpenUi(receiverUid, HolopadUiKey.AiRequestWindow, insertedAi.Value))
LinkHolopadToUser(entity, args.Actor);
}
UpdateHolopadControlLockoutStartTime(entity);
}
+ private void OnMobStateChanged(Entity<HolopadUserComponent> ent, ref MobStateChangedEvent args)
+ {
+ if (!HasComp<StationAiHeldComponent>(ent))
+ return;
+
+ foreach (var holopad in ent.Comp.LinkedHolopads)
+ {
+ ShutDownHolopad(holopad);
+ }
+ }
+
#endregion
public override void Update(float frameTime)
if (entity.Comp.Hologram != null)
DeleteHologram(entity.Comp.Hologram.Value, entity);
- if (entity.Comp.User != null)
+ // Check if the associated holopad user is an AI
+ if (HasComp<StationAiHeldComponent>(entity.Comp.User) &&
+ _stationAiSystem.TryGetCore(entity.Comp.User.Value, out var stationAiCore))
{
- // Check if the associated holopad user is an AI
- if (TryComp<StationAiHeldComponent>(entity.Comp.User, out var stationAiHeld) &&
- _stationAiSystem.TryGetCore(entity.Comp.User.Value, out var stationAiCore))
- {
- // Return the AI eye to free roaming
- _stationAiSystem.SwitchRemoteEntityMode(stationAiCore, true);
+ // Return the AI eye to free roaming
+ _stationAiSystem.SwitchRemoteEntityMode(stationAiCore, true);
- // If the AI core is still broadcasting, end its calls
- if (entity.Owner != stationAiCore.Owner &&
- TryComp<TelephoneComponent>(stationAiCore, out var stationAiCoreTelephone) &&
- _telephoneSystem.IsTelephoneEngaged((stationAiCore.Owner, stationAiCoreTelephone)))
- {
- _telephoneSystem.EndTelephoneCalls((stationAiCore.Owner, stationAiCoreTelephone));
- }
+ // If the AI core is still broadcasting, end its calls
+ if (TryComp<TelephoneComponent>(stationAiCore, out var stationAiCoreTelephone) &&
+ _telephoneSystem.IsTelephoneEngaged((stationAiCore.Owner, stationAiCoreTelephone)))
+ {
+ _telephoneSystem.EndTelephoneCalls((stationAiCore.Owner, stationAiCoreTelephone));
}
-
- UnlinkHolopadFromUser(entity, entity.Comp.User.Value);
+ }
+ else
+ {
+ UnlinkHolopadFromUser(entity, entity.Comp.User);
}
Dirty(entity);
--- /dev/null
+using Content.Shared.Silicons.StationAi;
+using Content.Server.EUI;
+using Content.Server.Ghost;
+using Content.Server.Mind;
+using Robust.Shared.Audio.Systems;
+using Robust.Server.Player;
+using Content.Shared.Popups;
+
+namespace Content.Server.Silicons.StationAi;
+
+public sealed partial class StationAiFixerConsoleSystem : SharedStationAiFixerConsoleSystem
+{
+ [Dependency] private readonly EuiManager _eui = default!;
+ [Dependency] private readonly IPlayerManager _player = default!;
+ [Dependency] private readonly MindSystem _mind = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+
+ protected override void FinalizeAction(Entity<StationAiFixerConsoleComponent> ent)
+ {
+ if (IsActionInProgress(ent) && ent.Comp.ActionTarget != null)
+ {
+ switch (ent.Comp.ActionType)
+ {
+ case StationAiFixerConsoleAction.Repair:
+
+ // Send message to disembodied player that they are being revived
+ if (_mind.TryGetMind(ent.Comp.ActionTarget.Value, out _, out var mind) &&
+ mind.IsVisitingEntity &&
+ _player.TryGetSessionById(mind.UserId, out var session))
+ {
+ _eui.OpenEui(new ReturnToBodyEui(mind, _mind, _player), session);
+ _popup.PopupEntity(Loc.GetString("station-ai-fixer-console-repair-finished"), ent);
+ }
+ else
+ {
+ _popup.PopupEntity(Loc.GetString("station-ai-fixer-console-repair-successful"), ent);
+ }
+
+ // TODO: make predicted once a user is not required
+ if (ent.Comp.RepairFinishedSound != null)
+ {
+ _audio.PlayPvs(ent.Comp.RepairFinishedSound, ent);
+ }
+
+ break;
+
+ case StationAiFixerConsoleAction.Purge:
+
+ _popup.PopupEntity(Loc.GetString("station-ai-fixer-console-purge-successful"), ent);
+
+ // TODO: make predicted once a user is not required
+ if (ent.Comp.PurgeFinishedSound != null)
+ {
+ _audio.PlayPvs(ent.Comp.PurgeFinishedSound, ent);
+ }
+
+ break;
+ }
+ }
+
+ base.FinalizeAction(ent);
+ }
+}
using Content.Server.Chat.Systems;
+using Content.Server.Construction;
+using Content.Server.Destructible;
+using Content.Server.Ghost;
+using Content.Server.Mind;
+using Content.Server.Power.Components;
+using Content.Server.Power.EntitySystems;
+using Content.Server.Roles;
+using Content.Server.Spawners.Components;
+using Content.Server.Spawners.EntitySystems;
+using Content.Server.Station.Systems;
+using Content.Shared.Alert;
using Content.Shared.Chat.Prototypes;
+using Content.Shared.Containers.ItemSlots;
+using Content.Shared.Damage;
+using Content.Shared.Destructible;
using Content.Shared.DeviceNetwork.Components;
+using Content.Shared.DoAfter;
+using Content.Shared.Mobs;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.Popups;
+using Content.Shared.Power.Components;
+using Content.Shared.Rejuvenate;
+using Content.Shared.Roles;
using Content.Shared.Silicons.StationAi;
+using Content.Shared.Speech.Components;
using Content.Shared.StationAi;
using Content.Shared.Turrets;
using Content.Shared.Weapons.Ranged.Events;
+using Robust.Server.Containers;
+using Robust.Shared.Containers;
using Robust.Shared.Map.Components;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
{
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly SharedTransformSystem _xforms = default!;
+ [Dependency] private readonly ContainerSystem _container = default!;
+ [Dependency] private readonly MindSystem _mind = default!;
+ [Dependency] private readonly RoleSystem _roles = default!;
+ [Dependency] private readonly ItemSlotsSystem _slots = default!;
+ [Dependency] private readonly GhostSystem _ghost = default!;
+ [Dependency] private readonly AlertsSystem _alerts = default!;
+ [Dependency] private readonly DestructibleSystem _destructible = default!;
+ [Dependency] private readonly BatterySystem _battery = default!;
+ [Dependency] private readonly DamageableSystem _damageable = default!;
+ [Dependency] private readonly SharedPopupSystem _popups = default!;
+ [Dependency] private readonly StationSystem _station = default!;
+ [Dependency] private readonly StationJobsSystem _stationJobs = default!;
+ [Dependency] private readonly IPrototypeManager _proto = default!;
+ [Dependency] private readonly MobStateSystem _mobState = default!;
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
private readonly HashSet<Entity<StationAiCoreComponent>> _stationAiCores = new();
+
private readonly ProtoId<ChatNotificationPrototype> _turretIsAttackingChatNotificationPrototype = "TurretIsAttacking";
private readonly ProtoId<ChatNotificationPrototype> _aiWireSnippedChatNotificationPrototype = "AiWireSnipped";
+ private readonly ProtoId<ChatNotificationPrototype> _aiLosingPowerChatNotificationPrototype = "AiLosingPower";
+ private readonly ProtoId<ChatNotificationPrototype> _aiCriticalPowerChatNotificationPrototype = "AiCriticalPower";
+
+ private readonly ProtoId<JobPrototype> _stationAiJob = "StationAi";
+ private readonly EntProtoId _stationAiBrain = "StationAiBrain";
+
+ private readonly ProtoId<AlertPrototype> _batteryAlert = "BorgBattery";
+ private readonly ProtoId<AlertPrototype> _damageAlert = "BorgHealth";
public override void Initialize()
{
base.Initialize();
+ SubscribeLocalEvent<StationAiCoreComponent, AfterConstructionChangeEntityEvent>(AfterConstructionChangeEntity);
+ SubscribeLocalEvent<StationAiCoreComponent, ContainerSpawnEvent>(OnContainerSpawn);
+ SubscribeLocalEvent<StationAiCoreComponent, ApcPowerReceiverBatteryChangedEvent>(OnApcBatteryChanged);
+ SubscribeLocalEvent<StationAiCoreComponent, ChargeChangedEvent>(OnChargeChanged);
+ SubscribeLocalEvent<StationAiCoreComponent, DamageChangedEvent>(OnDamageChanged);
+ SubscribeLocalEvent<StationAiCoreComponent, DestructionEventArgs>(OnDestruction);
+ SubscribeLocalEvent<StationAiCoreComponent, DoAfterAttemptEvent<IntellicardDoAfterEvent>>(OnDoAfterAttempt);
+ SubscribeLocalEvent<StationAiCoreComponent, RejuvenateEvent>(OnRejuvenate);
+
SubscribeLocalEvent<ExpandICChatRecipientsEvent>(OnExpandICChatRecipients);
SubscribeLocalEvent<StationAiTurretComponent, AmmoShotEvent>(OnAmmoShot);
}
+ private void AfterConstructionChangeEntity(Entity<StationAiCoreComponent> ent, ref AfterConstructionChangeEntityEvent args)
+ {
+ if (!_container.TryGetContainer(ent, StationAiCoreComponent.BrainContainer, out var container) ||
+ container.Count == 0)
+ {
+ return;
+ }
+
+ var brain = container.ContainedEntities[0];
+
+ if (_mind.TryGetMind(brain, out var mindId, out var mind))
+ {
+ // Found an existing mind to transfer into the AI core
+ var aiBrain = Spawn(_stationAiBrain, Transform(ent.Owner).Coordinates);
+ _roles.MindAddJobRole(mindId, mind, false, _stationAiJob);
+ _mind.TransferTo(mindId, aiBrain);
+
+ if (!TryComp<StationAiHolderComponent>(ent, out var targetHolder) ||
+ !_slots.TryInsert(ent, targetHolder.Slot, aiBrain, null))
+ {
+ QueueDel(aiBrain);
+ }
+ }
+
+ // TODO: We should consider keeping the borg brain inside the AI core.
+ // When the core is destroyed, the station AI can be transferred into the brain,
+ // then dropped on the ground. The deceased AI can then be revived later,
+ // instead of being lost forever.
+ QueueDel(brain);
+ }
+
+ private void OnContainerSpawn(Entity<StationAiCoreComponent> ent, ref ContainerSpawnEvent args)
+ {
+ // Ensure that players that recently joined the round will spawn
+ // into an AI core that has a full battery and full integrity.
+ if (TryComp<BatteryComponent>(ent, out var battery))
+ {
+ _battery.SetCharge(ent, battery.MaxCharge);
+ }
+
+ if (TryComp<DamageableComponent>(ent, out var damageable))
+ {
+ _damageable.SetAllDamage(ent, damageable, 0);
+ }
+ }
+
+ protected override void OnAiInsert(Entity<StationAiCoreComponent> ent, ref EntInsertedIntoContainerMessage args)
+ {
+ base.OnAiInsert(ent, ref args);
+
+ UpdateBatteryAlert(ent);
+ UpdateCoreIntegrityAlert(ent);
+ UpdateDamagedAccent(ent);
+ }
+
+ protected override void OnAiRemove(Entity<StationAiCoreComponent> ent, ref EntRemovedFromContainerMessage args)
+ {
+ base.OnAiRemove(ent, ref args);
+
+ _alerts.ClearAlert(args.Entity, _batteryAlert);
+ _alerts.ClearAlert(args.Entity, _damageAlert);
+
+ if (TryComp<DamagedSiliconAccentComponent>(args.Entity, out var accent))
+ {
+ accent.OverrideChargeLevel = null;
+ accent.OverrideTotalDamage = null;
+ accent.DamageAtMaxCorruption = null;
+ }
+ }
+
+ protected override void OnMobStateChanged(Entity<StationAiCustomizationComponent> ent, ref MobStateChangedEvent args)
+ {
+ if (args.NewMobState != MobState.Alive)
+ {
+ SetStationAiState(ent, StationAiState.Dead);
+ return;
+ }
+
+ var state = StationAiState.Rebooting;
+
+ if (_mind.TryGetMind(ent, out var _, out var mind) && !mind.IsVisitingEntity)
+ {
+ state = StationAiState.Occupied;
+ }
+
+ if (TryGetCore(ent, out var aiCore) && aiCore.Comp != null)
+ {
+ var aiCoreEnt = (aiCore.Owner, aiCore.Comp);
+
+ if (SetupEye(aiCoreEnt))
+ AttachEye(aiCoreEnt);
+ }
+
+ SetStationAiState(ent, state);
+ }
+
+ private void OnDestruction(Entity<StationAiCoreComponent> ent, ref DestructionEventArgs args)
+ {
+ var station = _station.GetOwningStation(ent);
+
+ if (station == null)
+ return;
+
+ if (!HasComp<ContainerSpawnPointComponent>(ent))
+ return;
+
+ // If the destroyed core could act as a player spawn point,
+ // reduce the number of available AI jobs by one
+ _stationJobs.TryAdjustJobSlot(station.Value, _stationAiJob, -1, false, true);
+ }
+
+ private void OnApcBatteryChanged(Entity<StationAiCoreComponent> ent, ref ApcPowerReceiverBatteryChangedEvent args)
+ {
+ if (!args.Enabled)
+ return;
+
+ if (!TryGetHeld((ent.Owner, ent.Comp), out var held))
+ return;
+
+ var ev = new ChatNotificationEvent(_aiLosingPowerChatNotificationPrototype, ent);
+ RaiseLocalEvent(held.Value, ref ev);
+ }
+
+ private void OnChargeChanged(Entity<StationAiCoreComponent> entity, ref ChargeChangedEvent args)
+ {
+ UpdateBatteryAlert(entity);
+ UpdateDamagedAccent(entity);
+ }
+
+ private void OnDamageChanged(Entity<StationAiCoreComponent> entity, ref DamageChangedEvent args)
+ {
+ UpdateCoreIntegrityAlert(entity);
+ UpdateDamagedAccent(entity);
+ }
+
+ private void UpdateDamagedAccent(Entity<StationAiCoreComponent> ent)
+ {
+ if (!TryGetHeld((ent.Owner, ent.Comp), out var held))
+ return;
+
+ if (!TryComp<DamagedSiliconAccentComponent>(held, out var accent))
+ return;
+
+ if (TryComp<BatteryComponent>(ent, out var battery))
+ accent.OverrideChargeLevel = battery.CurrentCharge / battery.MaxCharge;
+
+ if (TryComp<DamageableComponent>(ent, out var damageable))
+ accent.OverrideTotalDamage = damageable.TotalDamage;
+
+ if (TryComp<DestructibleComponent>(ent, out var destructible))
+ accent.DamageAtMaxCorruption = _destructible.DestroyedAt(ent, destructible);
+
+ Dirty(held.Value, accent);
+ }
+
+ private void UpdateBatteryAlert(Entity<StationAiCoreComponent> ent)
+ {
+ if (!TryComp<BatteryComponent>(ent, out var battery))
+ return;
+
+ if (!TryGetHeld((ent.Owner, ent.Comp), out var held))
+ return;
+
+ if (!_proto.TryIndex(_batteryAlert, out var proto))
+ return;
+
+ var chargePercent = battery.CurrentCharge / battery.MaxCharge;
+ var chargeLevel = Math.Round(chargePercent * proto.MaxSeverity);
+
+ _alerts.ShowAlert(held.Value, _batteryAlert, (short)Math.Clamp(chargeLevel, 0, proto.MaxSeverity));
+
+ if (TryComp<ApcPowerReceiverBatteryComponent>(ent, out var apcBattery) &&
+ apcBattery.Enabled &&
+ chargePercent < 0.2)
+ {
+ var ev = new ChatNotificationEvent(_aiCriticalPowerChatNotificationPrototype, ent);
+ RaiseLocalEvent(held.Value, ref ev);
+ }
+ }
+
+ private void UpdateCoreIntegrityAlert(Entity<StationAiCoreComponent> ent)
+ {
+ if (!TryComp<DamageableComponent>(ent, out var damageable))
+ return;
+
+ if (!TryComp<DestructibleComponent>(ent, out var destructible))
+ return;
+
+ if (!TryGetHeld((ent.Owner, ent.Comp), out var held))
+ return;
+
+ if (!_proto.TryIndex(_damageAlert, out var proto))
+ return;
+
+ var damagePercent = damageable.TotalDamage / _destructible.DestroyedAt(ent, destructible);
+ var damageLevel = Math.Round(damagePercent.Float() * proto.MaxSeverity);
+
+ _alerts.ShowAlert(held.Value, _damageAlert, (short)Math.Clamp(damageLevel, 0, proto.MaxSeverity));
+ }
+
+ private void OnDoAfterAttempt(Entity<StationAiCoreComponent> ent, ref DoAfterAttemptEvent<IntellicardDoAfterEvent> args)
+ {
+ if (TryGetHeld((ent.Owner, ent.Comp), out _))
+ return;
+
+ // Prevent AIs from being uploaded into an unpowered or broken AI core.
+
+ if (TryComp<ApcPowerReceiverComponent>(ent, out var apcPower) && !apcPower.Powered)
+ {
+ _popups.PopupEntity(Loc.GetString("station-ai-has-no-power-for-upload"), ent, args.Event.User);
+ args.Cancel();
+ }
+ else if (TryComp<DestructibleComponent>(ent, out var destructible) && destructible.IsBroken)
+ {
+ _popups.PopupEntity(Loc.GetString("station-ai-is-too-damaged-for-upload"), ent, args.Event.User);
+ args.Cancel();
+ }
+ }
+
+ public override void KillHeldAi(Entity<StationAiCoreComponent> ent)
+ {
+ base.KillHeldAi(ent);
+
+ if (TryGetHeld((ent.Owner, ent.Comp), out var held) &&
+ _mind.TryGetMind(held.Value, out var mindId, out var mind))
+ {
+ _ghost.OnGhostAttempt(mindId, canReturnGlobal: true, mind: mind);
+ RemComp<StationAiOverlayComponent>(held.Value);
+ }
+
+ ClearEye(ent);
+ }
+
+ private void OnRejuvenate(Entity<StationAiCoreComponent> ent, ref RejuvenateEvent args)
+ {
+ if (TryGetHeld((ent.Owner, ent.Comp), out var held))
+ {
+ _mobState.ChangeMobState(held.Value, MobState.Alive);
+ EnsureComp<StationAiOverlayComponent>(held.Value);
+ }
+
+ if (TryComp<StationAiHolderComponent>(ent, out var holder))
+ {
+ _appearance.SetData(ent, StationAiVisuals.Broken, false);
+ UpdateAppearance((ent, holder));
+ }
+ }
+
private void OnExpandICChatRecipients(ExpandICChatRecipientsEvent ev)
{
var xformQuery = GetEntityQuery<TransformComponent>();
if (!TryGetHeld((stationAiCore, stationAiCore.Comp), out var insertedAi))
continue;
- hashSet.Add(insertedAi);
+ hashSet.Add(insertedAi.Value);
}
return hashSet;
-using Content.Server.GameTicking;
+using Content.Server.GameTicking;
using Content.Server.Spawners.Components;
using Content.Server.Station.Systems;
using Content.Shared.Preferences;
-using Content.Shared.Roles;
using Robust.Server.Containers;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
if (!_container.Insert(args.SpawnResult.Value, container, containerXform: xform))
continue;
+ var ev = new ContainerSpawnEvent(args.SpawnResult.Value);
+ RaiseLocalEvent(uid, ref ev);
+
return;
}
args.SpawnResult = null;
}
}
+
+/// <summary>
+/// Raised on a container when a player is spawned into it.
+/// </summary>
+[ByRefEvent]
+public record struct ContainerSpawnEvent(EntityUid Player);
--- /dev/null
+using Content.Shared.Administration.Logs;
+using Content.Shared.Containers.ItemSlots;
+using Content.Shared.Database;
+using Content.Shared.Examine;
+using Content.Shared.Lock;
+using Content.Shared.Mobs;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.Power;
+using Robust.Shared.Containers;
+using Robust.Shared.Timing;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Content.Shared.Silicons.StationAi;
+
+/// <summary>
+/// This system is used to handle the actions of AI Restoration Consoles.
+/// These consoles can be used to revive dead station AIs, or destroy them.
+/// </summary>
+public abstract partial class SharedStationAiFixerConsoleSystem : EntitySystem
+{
+ [Dependency] private readonly SharedUserInterfaceSystem _userInterface = default!;
+ [Dependency] private readonly ItemSlotsSystem _itemSlots = default!;
+ [Dependency] private readonly SharedContainerSystem _container = default!;
+ [Dependency] private readonly MobStateSystem _mobState = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+ [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<StationAiFixerConsoleComponent, EntInsertedIntoContainerMessage>(OnInserted);
+ SubscribeLocalEvent<StationAiFixerConsoleComponent, EntRemovedFromContainerMessage>(OnRemoved);
+ SubscribeLocalEvent<StationAiFixerConsoleComponent, LockToggledEvent>(OnLockToggle);
+ SubscribeLocalEvent<StationAiFixerConsoleComponent, StationAiFixerConsoleMessage>(OnMessage);
+ SubscribeLocalEvent<StationAiFixerConsoleComponent, PowerChangedEvent>(OnPowerChanged);
+ SubscribeLocalEvent<StationAiFixerConsoleComponent, ExaminedEvent>(OnExamined);
+
+ SubscribeLocalEvent<StationAiCustomizationComponent, StationAiCustomizationStateChanged>(OnStationAiCustomizationStateChanged);
+ }
+
+ private void OnInserted(Entity<StationAiFixerConsoleComponent> ent, ref EntInsertedIntoContainerMessage args)
+ {
+ if (args.Container.ID != ent.Comp.StationAiHolderSlot)
+ return;
+
+ if (TryGetTarget(ent, out var target))
+ {
+ ent.Comp.ActionTarget = target;
+ Dirty(ent);
+ }
+
+ UpdateAppearance(ent);
+ }
+
+ private void OnRemoved(Entity<StationAiFixerConsoleComponent> ent, ref EntRemovedFromContainerMessage args)
+ {
+ if (args.Container.ID != ent.Comp.StationAiHolderSlot)
+ return;
+
+ ent.Comp.ActionTarget = null;
+
+ StopAction(ent);
+ }
+
+ private void OnLockToggle(Entity<StationAiFixerConsoleComponent> ent, ref LockToggledEvent args)
+ {
+ if (_userInterface.TryGetOpenUi(ent.Owner, StationAiFixerConsoleUiKey.Key, out var bui))
+ bui.Update<StationAiFixerConsoleBoundUserInterfaceState>();
+ }
+
+ private void OnMessage(Entity<StationAiFixerConsoleComponent> ent, ref StationAiFixerConsoleMessage args)
+ {
+ if (TryComp<LockComponent>(ent, out var lockable) && lockable.Locked)
+ return;
+
+ switch (args.Action)
+ {
+ case StationAiFixerConsoleAction.Eject:
+ EjectStationAiHolder(ent, args.Actor);
+ break;
+ case StationAiFixerConsoleAction.Repair:
+ RepairStationAi(ent, args.Actor);
+ break;
+ case StationAiFixerConsoleAction.Purge:
+ PurgeStationAi(ent, args.Actor);
+ break;
+ case StationAiFixerConsoleAction.Cancel:
+ CancelAction(ent, args.Actor);
+ break;
+ }
+ }
+
+ private void OnPowerChanged(Entity<StationAiFixerConsoleComponent> ent, ref PowerChangedEvent args)
+ {
+ if (args.Powered)
+ return;
+
+ StopAction(ent);
+ }
+
+ private void OnExamined(Entity<StationAiFixerConsoleComponent> ent, ref ExaminedEvent args)
+ {
+ var message = TryGetStationAiHolder(ent, out var holder) ?
+ Loc.GetString("station-ai-fixer-console-examination-station-ai-holder-present", ("holder", Name(holder.Value))) :
+ Loc.GetString("station-ai-fixer-console-examination-station-ai-holder-absent");
+
+ args.PushMarkup(message);
+ }
+
+ private void OnStationAiCustomizationStateChanged(Entity<StationAiCustomizationComponent> ent, ref StationAiCustomizationStateChanged args)
+ {
+ if (_container.TryGetOuterContainer(ent, Transform(ent), out var outerContainer) &&
+ TryComp<StationAiFixerConsoleComponent>(outerContainer.Owner, out var stationAiFixerConsole))
+ {
+ UpdateAppearance((outerContainer.Owner, stationAiFixerConsole));
+ }
+ }
+
+ private void EjectStationAiHolder(Entity<StationAiFixerConsoleComponent> ent, EntityUid user)
+ {
+ if (!TryComp<ItemSlotsComponent>(ent, out var slots))
+ return;
+
+ if (!_itemSlots.TryGetSlot(ent, ent.Comp.StationAiHolderSlot, out var holderSlot, slots))
+ return;
+
+ if (_itemSlots.TryEjectToHands(ent, holderSlot, user, true))
+ _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(user):user} ejected a station AI holder from AI restoration console ({ToPrettyString(ent.Owner)})");
+ }
+
+ private void RepairStationAi(Entity<StationAiFixerConsoleComponent> ent, EntityUid user)
+ {
+ if (ent.Comp.ActionTarget == null)
+ return;
+
+ _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(user):user} started a repair of {ToPrettyString(ent.Comp.ActionTarget)} using an AI restoration console ({ToPrettyString(ent.Owner)})");
+ StartAction(ent, StationAiFixerConsoleAction.Repair);
+ }
+
+ private void PurgeStationAi(Entity<StationAiFixerConsoleComponent> ent, EntityUid user)
+ {
+ if (ent.Comp.ActionTarget == null)
+ return;
+
+ _adminLogger.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(user):user} started a purge of {ToPrettyString(ent.Comp.ActionTarget)} using {ToPrettyString(ent.Owner)}");
+ StartAction(ent, StationAiFixerConsoleAction.Purge);
+ }
+
+ private void CancelAction(Entity<StationAiFixerConsoleComponent> ent, EntityUid user)
+ {
+ if (!IsActionInProgress(ent))
+ return;
+
+ _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(user):user} canceled operation involving {ToPrettyString(ent.Comp.ActionTarget)} and {ToPrettyString(ent.Owner)} ({ent.Comp.ActionType} action)");
+ StopAction(ent);
+ }
+
+ /// <summary>
+ /// Initiates an action upon a target entity by the specified console.
+ /// </summary>
+ /// <param name="ent">The console.</param>
+ /// <param name="actionType">The action to be enacted on the target.</param>
+ private void StartAction(Entity<StationAiFixerConsoleComponent> ent, StationAiFixerConsoleAction actionType)
+ {
+ if (IsActionInProgress(ent))
+ {
+ StopAction(ent);
+ }
+
+ if (IsTargetValid(ent, actionType))
+ {
+ var duration = actionType == StationAiFixerConsoleAction.Repair ?
+ ent.Comp.RepairDuration :
+ ent.Comp.PurgeDuration;
+
+ ent.Comp.ActionType = actionType;
+ ent.Comp.ActionStartTime = _timing.CurTime;
+ ent.Comp.ActionEndTime = _timing.CurTime + duration;
+ ent.Comp.CurrentActionStage = 0;
+ Dirty(ent);
+ }
+
+ UpdateAppearance(ent);
+ }
+
+ /// <summary>
+ /// Updates the current action being conducted by the specified console.
+ /// </summary>
+ /// <param name="ent">The console.</param>
+ private void UpdateAction(Entity<StationAiFixerConsoleComponent> ent)
+ {
+ if (IsActionInProgress(ent))
+ {
+ if (ent.Comp.ActionTarget == null)
+ {
+ StopAction(ent);
+ return;
+ }
+
+ if (_timing.CurTime >= ent.Comp.ActionEndTime)
+ {
+ FinalizeAction(ent);
+ return;
+ }
+
+ var currentStage = CalculateActionStage(ent);
+
+ if (currentStage != ent.Comp.CurrentActionStage)
+ {
+ ent.Comp.CurrentActionStage = currentStage;
+ Dirty(ent);
+ }
+ }
+
+ UpdateAppearance(ent);
+ }
+
+ /// <summary>
+ /// Terminates any action being conducted by the specified console.
+ /// </summary>
+ /// <param name="ent">The console.</param>
+ private void StopAction(Entity<StationAiFixerConsoleComponent> ent)
+ {
+ ent.Comp.ActionType = StationAiFixerConsoleAction.None;
+ Dirty(ent);
+
+ UpdateAppearance(ent);
+ }
+
+ /// <summary>
+ /// Finalizes the action being conducted by the specified console
+ /// (i.e., repairing or purging a target).
+ /// </summary>
+ /// <param name="ent">The console.</param>
+ protected virtual void FinalizeAction(Entity<StationAiFixerConsoleComponent> ent)
+ {
+ if (IsActionInProgress(ent) && ent.Comp.ActionTarget != null)
+ {
+ if (ent.Comp.ActionType == StationAiFixerConsoleAction.Repair)
+ {
+ _mobState.ChangeMobState(ent.Comp.ActionTarget.Value, MobState.Alive);
+ }
+ else if (ent.Comp.ActionType == StationAiFixerConsoleAction.Purge &&
+ TryGetStationAiHolder(ent, out var holder))
+ {
+ _container.RemoveEntity(holder.Value, ent.Comp.ActionTarget.Value, force: true);
+ PredictedQueueDel(ent.Comp.ActionTarget);
+
+ ent.Comp.ActionTarget = null;
+ Dirty(ent);
+ }
+ }
+
+ StopAction(ent);
+ }
+
+ /// <summary>
+ /// Updates the appearance of the specified console based on its current state.
+ /// </summary>
+ /// <param name="ent">The console.</param>
+ private void UpdateAppearance(Entity<StationAiFixerConsoleComponent> ent)
+ {
+ if (!TryComp<AppearanceComponent>(ent, out var appearance))
+ return;
+
+ if (IsActionInProgress(ent))
+ {
+ var currentStage = ent.Comp.ActionType + ent.Comp.CurrentActionStage.ToString();
+
+ if (!_appearance.TryGetData(ent, StationAiFixerConsoleVisuals.Key, out string oldStage, appearance) ||
+ oldStage != currentStage)
+ {
+ _appearance.SetData(ent, StationAiFixerConsoleVisuals.Key, currentStage, appearance);
+ }
+
+ return;
+ }
+
+ var target = ent.Comp.ActionTarget;
+ var state = StationAiState.Empty;
+
+ if (TryComp<StationAiCustomizationComponent>(target, out var customization) && !EntityManager.IsQueuedForDeletion(target.Value))
+ {
+ state = customization.State;
+ }
+
+ _appearance.SetData(ent, StationAiFixerConsoleVisuals.Key, state.ToString(), appearance);
+ }
+
+ /// <summary>
+ /// Calculates the current stage of any in-progress actions.
+ /// </summary>
+ /// <param name="ent">The console.</param>
+ /// <returns>The current stage.</returns>
+ private int CalculateActionStage(Entity<StationAiFixerConsoleComponent> ent)
+ {
+ var completionPercentage = (_timing.CurTime - ent.Comp.ActionStartTime) / (ent.Comp.ActionEndTime - ent.Comp.ActionStartTime);
+
+ return (int)(completionPercentage * ent.Comp.ActionStageCount);
+ }
+
+ /// <summary>
+ /// Try to find a valid target being stored inside the specified console.
+ /// </summary>
+ /// <param name="ent">The console.</param>
+ /// <param name="target">The found target.</param>
+ /// <returns>True if a valid target was found.</returns>
+ public bool TryGetTarget(Entity<StationAiFixerConsoleComponent> ent, [NotNullWhen(true)] out EntityUid? target)
+ {
+ target = null;
+
+ if (!TryGetStationAiHolder(ent, out var holder))
+ return false;
+
+ if (!_container.TryGetContainer(holder.Value, ent.Comp.StationAiMindSlot, out var stationAiMindSlot) || stationAiMindSlot.Count == 0)
+ return false;
+
+ var stationAi = stationAiMindSlot.ContainedEntities[0];
+
+ if (!HasComp<MobStateComponent>(stationAi))
+ return false;
+
+ target = stationAi;
+
+ return !EntityManager.IsQueuedForDeletion(target.Value);
+ }
+
+ /// <summary>
+ /// Try to find a station AI holder being stored inside the specified console.
+ /// </summary>
+ /// <param name="ent">The console.</param>
+ /// <param name="holder">The found holder.</param>
+ /// <returns>True if a valid holder was found.</returns>
+ public bool TryGetStationAiHolder(Entity<StationAiFixerConsoleComponent> ent, [NotNullWhen(true)] out EntityUid? holder)
+ {
+ holder = null;
+
+ if (!_container.TryGetContainer(ent, ent.Comp.StationAiHolderSlot, out var holderContainer) ||
+ holderContainer.Count == 0)
+ {
+ return false;
+ }
+
+ holder = holderContainer.ContainedEntities[0];
+
+ return true;
+ }
+
+ /// <summary>
+ /// Determines if the specified console can act upon its action target.
+ /// </summary>
+ /// <param name="ent">The console.</param>
+ /// <param name="actionType">The action to be enacted on the target.</param>
+ /// <returns>True, if the target is valid for the specified console action.</returns>
+ public bool IsTargetValid(Entity<StationAiFixerConsoleComponent> ent, StationAiFixerConsoleAction actionType)
+ {
+ if (ent.Comp.ActionTarget == null)
+ return false;
+
+ if (actionType == StationAiFixerConsoleAction.Purge)
+ return true;
+
+ if (actionType == StationAiFixerConsoleAction.Repair &&
+ _mobState.IsDead(ent.Comp.ActionTarget.Value))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Returns whether an station AI holder is inserted into the specified console.
+ /// </summary>
+ /// <param name="ent">The console.</param>
+ /// <returns>True if a station AI holder is inserted.</returns>
+ public bool IsStationAiHolderInserted(Entity<StationAiFixerConsoleComponent> ent)
+ {
+ return TryGetStationAiHolder(ent, out var _);
+ }
+
+ /// <summary>
+ /// Returns whether the specified console has an action in progress.
+ /// </summary>
+ /// <param name="ent">The console.</param>
+ /// <returns>Ture, if an action is in progress.</returns>
+ public bool IsActionInProgress(Entity<StationAiFixerConsoleComponent> ent)
+ {
+ return ent.Comp.ActionType != StationAiFixerConsoleAction.None;
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var query = AllEntityQuery<StationAiFixerConsoleComponent>();
+
+ while (query.MoveNext(out var uid, out var stationAiFixerConsole))
+ {
+ var ent = (uid, stationAiFixerConsole);
+
+ if (!IsActionInProgress(ent))
+ continue;
+
+ UpdateAction(ent);
+ }
+ }
+}
using Content.Shared.Holopad;
+using Content.Shared.Mobs;
+using Robust.Shared.Player;
using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+using System.Diagnostics.CodeAnalysis;
namespace Content.Shared.Silicons.StationAi;
private ProtoId<StationAiCustomizationGroupPrototype> _stationAiCoreCustomGroupProtoId = "StationAiCoreIconography";
private ProtoId<StationAiCustomizationGroupPrototype> _stationAiHologramCustomGroupProtoId = "StationAiHolograms";
+ private readonly SpriteSpecifier.Rsi _stationAiRebooting = new(new ResPath("Mobs/Silicon/station_ai.rsi"), "ai_fuzz");
+
private void InitializeCustomization()
{
SubscribeLocalEvent<StationAiCoreComponent, StationAiCustomizationMessage>(OnStationAiCustomization);
+
+ SubscribeLocalEvent<StationAiCustomizationComponent, PlayerAttachedEvent>(OnPlayerAttached);
+ SubscribeLocalEvent<StationAiCustomizationComponent, PlayerDetachedEvent>(OnPlayerDetached);
+ SubscribeLocalEvent<StationAiCustomizationComponent, MobStateChangedEvent>(OnMobStateChanged);
}
private void OnStationAiCustomization(Entity<StationAiCoreComponent> entity, ref StationAiCustomizationMessage args)
stationAiCustomization.ProtoIds[args.GroupProtoId] = args.CustomizationProtoId;
- Dirty(held, stationAiCustomization);
+ Dirty(held.Value, stationAiCustomization);
// Update hologram
if (groupPrototype.Category == StationAiCustomizationType.Hologram)
- UpdateHolographicAvatar((held, stationAiCustomization));
+ UpdateHolographicAvatar((held.Value, stationAiCustomization));
// Update core iconography
if (groupPrototype.Category == StationAiCustomizationType.CoreIconography && TryComp<StationAiHolderComponent>(entity, out var stationAiHolder))
UpdateAppearance((entity, stationAiHolder));
}
+ private void OnPlayerAttached(Entity<StationAiCustomizationComponent> ent, ref PlayerAttachedEvent args)
+ {
+ var state = _mobState.IsDead(ent) ? StationAiState.Dead : StationAiState.Occupied;
+ SetStationAiState(ent, state);
+ }
+
+ private void OnPlayerDetached(Entity<StationAiCustomizationComponent> ent, ref PlayerDetachedEvent args)
+ {
+ var state = _mobState.IsDead(ent) ? StationAiState.Dead : StationAiState.Rebooting;
+ SetStationAiState(ent, state);
+ }
+
+ protected virtual void OnMobStateChanged(Entity<StationAiCustomizationComponent> ent, ref MobStateChangedEvent args)
+ {
+ var state = (args.NewMobState == MobState.Dead) ? StationAiState.Dead : StationAiState.Rebooting;
+ SetStationAiState(ent, state);
+ }
+
+ protected void SetStationAiState(Entity<StationAiCustomizationComponent> ent, StationAiState state)
+ {
+ if (ent.Comp.State != state)
+ {
+ ent.Comp.State = state;
+ Dirty(ent);
+
+ var ev = new StationAiCustomizationStateChanged(state);
+ RaiseLocalEvent(ent, ref ev);
+ }
+
+ if (_containers.TryGetContainingContainer(ent.Owner, out var container) &&
+ TryComp<StationAiHolderComponent>(container.Owner, out var holder))
+ {
+ UpdateAppearance((container.Owner, holder));
+ }
+ }
+
private void UpdateHolographicAvatar(Entity<StationAiCustomizationComponent> entity)
{
if (!TryComp<HolographicAvatarComponent>(entity, out var avatar))
{
var stationAi = GetInsertedAI(entity);
- if (stationAi == null)
+ if (!TryComp<StationAiCustomizationComponent>(stationAi, out var stationAiCustomization) ||
+ !TryGetCustomizedAppearanceData((stationAi.Value, stationAiCustomization), out var layerData) ||
+ !layerData.TryGetValue(state.ToString(), out var stateData))
{
- _appearance.RemoveData(entity.Owner, StationAiVisualState.Key);
return;
}
- if (!TryComp<StationAiCustomizationComponent>(stationAi, out var stationAiCustomization) ||
- !stationAiCustomization.ProtoIds.TryGetValue(_stationAiCoreCustomGroupProtoId, out var protoId) ||
- !_protoManager.Resolve(protoId, out var prototype) ||
- !prototype.LayerData.TryGetValue(state.ToString(), out var layerData))
+ // This data is handled manually in the client StationAiSystem
+ _appearance.SetData(entity.Owner, StationAiVisualLayers.Icon, stateData);
+ }
+
+ /// <summary>
+ /// Returns a dictionary containing the station AI's appearance for different states.
+ /// </summary>
+ /// <param name="entity">The station AI.</param>
+ /// <param name="layerData">The apperance data, indexed by possible AI states.</param>
+ /// <returns>True if the apperance data was found.</returns>
+ public bool TryGetCustomizedAppearanceData(Entity<StationAiCustomizationComponent> entity, [NotNullWhen(true)] out Dictionary<string, PrototypeLayerData>? layerData)
+ {
+ layerData = null;
+
+ if (!entity.Comp.ProtoIds.TryGetValue(_stationAiCoreCustomGroupProtoId, out var protoId) ||
+ !_protoManager.Resolve(protoId, out var prototype) ||
+ prototype.LayerData.Count == 0)
{
- return;
+ return false;
}
- // This data is handled manually in the client StationAiSystem
- _appearance.SetData(entity.Owner, StationAiVisualState.Key, layerData);
+ layerData = prototype.LayerData;
+
+ return true;
}
}
using Content.Shared.Verbs;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
+using System.Diagnostics.CodeAnalysis;
namespace Content.Shared.Silicons.StationAi;
SubscribeLocalEvent<StationAiHeldComponent, InteractionAttemptEvent>(OnHeldInteraction);
SubscribeLocalEvent<StationAiHeldComponent, AttemptRelayActionComponentChangeEvent>(OnHeldRelay);
SubscribeLocalEvent<StationAiHeldComponent, JumpToCoreEvent>(OnCoreJump);
+
SubscribeLocalEvent<TryGetIdentityShortInfoEvent>(OnTryGetIdentityShortInfo);
}
if (!TryGetCore(ent.Owner, out var core) || core.Comp?.RemoteEntity == null)
return;
- _xforms.DropNextTo(core.Comp.RemoteEntity.Value, core.Owner) ;
+ _xforms.DropNextTo(core.Comp.RemoteEntity.Value, core.Owner);
}
/// <summary>
- /// Tries to get the entity held in the AI core using StationAiCore.
+ /// Tries to find an AI being held in by an entity using <see cref="StationAiHolderComponent"/>.
/// </summary>
- public bool TryGetHeld(Entity<StationAiCoreComponent?> entity, out EntityUid held)
+ /// <param name="entity">The station AI holder.</param>
+ /// <param name="held">The found AI.</param>
+ /// <returns>True if an AI is found.</returns>
+ public bool TryGetHeld(Entity<StationAiHolderComponent?> entity, [NotNullWhen(true)] out EntityUid? held)
{
held = EntityUid.Invalid;
if (!Resolve(entity.Owner, ref entity.Comp))
return false;
- if (!_containers.TryGetContainer(entity.Owner, StationAiCoreComponent.Container, out var container) ||
+ if (!_containers.TryGetContainer(entity.Owner, StationAiHolderComponent.Container, out var container) ||
container.ContainedEntities.Count == 0)
return false;
return true;
}
+
/// <summary>
- /// Tries to get the entity held in the AI using StationAiHolder.
+ /// Tries to find an AI being held in by an entity using <see cref="StationAiCoreComponent"/>.
/// </summary>
- public bool TryGetHeld(Entity<StationAiHolderComponent?> entity, out EntityUid held)
+ /// <param name="entity">The station AI core.</param>
+ /// <param name="held">The found AI.</param>
+ /// <returns>True if an AI is found.</returns>
+ public bool TryGetHeld(Entity<StationAiCoreComponent?> entity, [NotNullWhen(true)] out EntityUid? held)
{
- TryComp<StationAiCoreComponent>(entity.Owner, out var stationAiCore);
+ held = null;
- return TryGetHeld((entity.Owner, stationAiCore), out held);
+ return TryComp<StationAiHolderComponent>(entity.Owner, out var holder) &&
+ TryGetHeld((entity, holder), out held);
}
+ /// <summary>
+ /// Tries to find the station AI core holding an AI.
+ /// </summary>
+ /// <param name="entity">The AI.</param>
+ /// <param name="core">The found AI core.</param>
+ /// <returns>True if an AI core is found.</returns>
public bool TryGetCore(EntityUid entity, out Entity<StationAiCoreComponent?> core)
{
- var xform = Transform(entity);
- var meta = MetaData(entity);
- var ent = new Entity<TransformComponent?, MetaDataComponent?>(entity, xform, meta);
-
- if (!_containers.TryGetContainingContainer(ent, out var container) ||
+ if (!_containers.TryGetContainingContainer(entity, out var container) ||
container.ID != StationAiCoreComponent.Container ||
- !TryComp(container.Owner, out StationAiCoreComponent? coreComp) ||
- coreComp.RemoteEntity == null)
+ !TryComp(container.Owner, out StationAiCoreComponent? coreComp))
{
core = (EntityUid.Invalid, null);
return false;
using Content.Shared.Chat.Prototypes;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Database;
+using Content.Shared.Destructible;
using Content.Shared.Doors.Systems;
using Content.Shared.DoAfter;
using Content.Shared.Electrocution;
using Content.Shared.Interaction;
using Content.Shared.Item.ItemToggle;
using Content.Shared.Mind;
+using Content.Shared.Mobs;
+using Content.Shared.Mobs.Systems;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using Content.Shared.Popups;
using Content.Shared.Power;
using Content.Shared.Power.EntitySystems;
+using Content.Shared.Repairable;
using Content.Shared.StationAi;
using Content.Shared.Verbs;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
-using System.Diagnostics.CodeAnalysis;
namespace Content.Shared.Silicons.StationAi;
public abstract partial class SharedStationAiSystem : EntitySystem
{
- [Dependency] private readonly ISharedAdminManager _admin = default!;
- [Dependency] private readonly IGameTiming _timing = default!;
- [Dependency] private readonly INetManager _net = default!;
- [Dependency] private readonly ItemSlotsSystem _slots = default!;
- [Dependency] private readonly ItemToggleSystem _toggles = default!;
- [Dependency] private readonly ActionBlockerSystem _blocker = default!;
- [Dependency] private readonly MetaDataSystem _metadata = default!;
- [Dependency] private readonly SharedAirlockSystem _airlocks = default!;
- [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
- [Dependency] private readonly SharedAudioSystem _audio = default!;
- [Dependency] private readonly SharedContainerSystem _containers = default!;
- [Dependency] private readonly SharedDoorSystem _doors = default!;
- [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
- [Dependency] private readonly SharedElectrocutionSystem _electrify = default!;
- [Dependency] private readonly SharedEyeSystem _eye = default!;
+ [Dependency] private readonly ISharedAdminManager _admin = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly INetManager _net = default!;
+ [Dependency] private readonly ItemSlotsSystem _slots = default!;
+ [Dependency] private readonly ItemToggleSystem _toggles = default!;
+ [Dependency] private readonly ActionBlockerSystem _blocker = default!;
+ [Dependency] private readonly MetaDataSystem _metadata = default!;
+ [Dependency] private readonly SharedAirlockSystem _airlocks = default!;
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly SharedContainerSystem _containers = default!;
+ [Dependency] private readonly SharedDoorSystem _doors = default!;
+ [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
+ [Dependency] private readonly SharedElectrocutionSystem _electrify = default!;
+ [Dependency] private readonly SharedEyeSystem _eye = default!;
[Dependency] protected readonly SharedMapSystem Maps = default!;
- [Dependency] private readonly SharedMindSystem _mind = default!;
- [Dependency] private readonly SharedMoverController _mover = default!;
- [Dependency] private readonly SharedPopupSystem _popup = default!;
- [Dependency] private readonly SharedPowerReceiverSystem PowerReceiver = default!;
- [Dependency] private readonly SharedTransformSystem _xforms = default!;
- [Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
- [Dependency] private readonly StationAiVisionSystem _vision = default!;
- [Dependency] private readonly IPrototypeManager _protoManager = default!;
+ [Dependency] private readonly SharedMindSystem _mind = default!;
+ [Dependency] private readonly SharedMoverController _mover = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly SharedPowerReceiverSystem PowerReceiver = default!;
+ [Dependency] private readonly SharedTransformSystem _xforms = default!;
+ [Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
+ [Dependency] private readonly StationAiVisionSystem _vision = default!;
+ [Dependency] private readonly IPrototypeManager _protoManager = default!;
+ [Dependency] private readonly MobStateSystem _mobState = default!;
// StationAiHeld is added to anything inside of an AI core.
// StationAiHolder indicates it can hold an AI positronic brain (e.g. holocard / core).
private static readonly EntProtoId DefaultAi = "StationAiBrain";
private readonly ProtoId<ChatNotificationPrototype> _downloadChatNotificationPrototype = "IntellicardDownload";
- private const float MaxVisionMultiplier = 5f;
-
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StationAiCoreComponent, EntInsertedIntoContainerMessage>(OnAiInsert);
SubscribeLocalEvent<StationAiCoreComponent, EntRemovedFromContainerMessage>(OnAiRemove);
- SubscribeLocalEvent<StationAiCoreComponent, MapInitEvent>(OnAiMapInit);
SubscribeLocalEvent<StationAiCoreComponent, ComponentShutdown>(OnAiShutdown);
SubscribeLocalEvent<StationAiCoreComponent, PowerChangedEvent>(OnCorePower);
SubscribeLocalEvent<StationAiCoreComponent, GetVerbsEvent<Verb>>(OnCoreVerbs);
+
+ SubscribeLocalEvent<StationAiCoreComponent, BreakageEventArgs>(OnBroken);
+ SubscribeLocalEvent<StationAiCoreComponent, RepairedEvent>(OnRepaired);
}
private void OnCoreVerbs(Entity<StationAiCoreComponent> ent, ref GetVerbsEvent<Verb> args)
args.Verbs.Add(new Verb()
{
Text = Loc.GetString("station-ai-customization-menu"),
- Act = () => _uiSystem.TryOpenUi(ent.Owner, StationAiCustomizationUiKey.Key, insertedAi),
+ Act = () => _uiSystem.TryOpenUi(ent.Owner, StationAiCustomizationUiKey.Key, insertedAi.Value),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/emotes.svg.192dpi.png")),
});
}
if (!TryComp(args.Used, out IntellicardComponent? intelliComp))
return;
- var cardHasAi = _slots.CanEject(ent.Owner, args.User, ent.Comp.Slot);
- var coreHasAi = _slots.CanEject(args.Target.Value, args.User, targetHolder.Slot);
+ var cardHasAi = ent.Comp.Slot.Item != null;
+ var coreHasAi = targetHolder.Slot.Item != null;
if (cardHasAi && coreHasAi)
{
if (TryGetHeld((args.Target.Value, targetHolder), out var held))
{
var ev = new ChatNotificationEvent(_downloadChatNotificationPrototype, args.Used, args.User);
- RaiseLocalEvent(held, ref ev);
+ RaiseLocalEvent(held.Value, ref ev);
}
var doAfterArgs = new DoAfterArgs(EntityManager, args.User, cardHasAi ? intelliComp.UploadTime : intelliComp.DownloadTime, new IntellicardDoAfterEvent(), args.Target, ent.Owner)
BreakOnDamage = true,
BreakOnMove = true,
NeedHand = true,
- BreakOnDropItem = true
+ BreakOnDropItem = true,
+ AttemptFrequency = AttemptFrequency.EveryTick,
};
_doAfter.TryStartDoAfter(doAfterArgs);
private void OnHolderMapInit(Entity<StationAiHolderComponent> ent, ref MapInitEvent args)
{
- UpdateAppearance(ent.Owner);
+ UpdateAppearance((ent.Owner, ent.Comp));
}
private void OnAiShutdown(Entity<StationAiCoreComponent> ent, ref ComponentShutdown args)
private void OnCorePower(Entity<StationAiCoreComponent> ent, ref PowerChangedEvent args)
{
- // TODO: I think in 13 they just straightup die so maybe implement that
- if (args.Powered)
- {
- if (!SetupEye(ent))
- return;
-
- AttachEye(ent);
- }
- else
+ if (!args.Powered)
{
- ClearEye(ent);
+ KillHeldAi(ent);
}
}
- private void OnAiMapInit(Entity<StationAiCoreComponent> ent, ref MapInitEvent args)
+ private void OnBroken(Entity<StationAiCoreComponent> ent, ref BreakageEventArgs args)
{
- SetupEye(ent);
- AttachEye(ent);
+ KillHeldAi(ent);
+
+ if (TryComp<AppearanceComponent>(ent, out var appearance))
+ _appearance.SetData(ent, StationAiVisuals.Broken, true, appearance);
+ }
+
+ private void OnRepaired(Entity<StationAiCoreComponent> ent, ref RepairedEvent args)
+ {
+ if (TryComp<AppearanceComponent>(ent, out var appearance))
+ _appearance.SetData(ent, StationAiVisuals.Broken, false, appearance);
+ }
+
+ public virtual void KillHeldAi(Entity<StationAiCoreComponent> ent)
+ {
+ if (TryGetHeld((ent.Owner, ent.Comp), out var held))
+ {
+ _mobState.ChangeMobState(held.Value, MobState.Dead);
+ }
}
public void SwitchRemoteEntityMode(Entity<StationAiCoreComponent?> entity, bool isRemote)
_eye.SetDrawFov(user.Value, !isRemote);
}
- private bool SetupEye(Entity<StationAiCoreComponent> ent, EntityCoordinates? coords = null)
+ protected bool SetupEye(Entity<StationAiCoreComponent> ent, EntityCoordinates? coords = null)
{
if (_net.IsClient)
return false;
return true;
}
- private void ClearEye(Entity<StationAiCoreComponent> ent)
+ protected void ClearEye(Entity<StationAiCoreComponent> ent)
{
if (_net.IsClient)
return;
QueueDel(ent.Comp.RemoteEntity);
ent.Comp.RemoteEntity = null;
Dirty(ent);
+
+ if (TryGetHeld((ent, ent.Comp), out var held) &&
+ TryComp(held, out EyeComponent? eyeComp))
+ {
+ _eye.SetDrawFov(held.Value, true, eyeComp);
+ _eye.SetTarget(held.Value, null, eyeComp);
+ }
}
- private void AttachEye(Entity<StationAiCoreComponent> ent)
+ protected void AttachEye(Entity<StationAiCoreComponent> ent)
{
if (ent.Comp.RemoteEntity == null)
return;
return container.ContainedEntities[0];
}
- private void OnAiInsert(Entity<StationAiCoreComponent> ent, ref EntInsertedIntoContainerMessage args)
+ protected virtual void OnAiInsert(Entity<StationAiCoreComponent> ent, ref EntInsertedIntoContainerMessage args)
{
if (args.Container.ID != StationAiCoreComponent.Container)
return;
if (_timing.ApplyingState)
return;
+ ClearEye(ent);
ent.Comp.Remote = true;
- SetupEye(ent);
// Just so text and the likes works properly
_metadata.SetEntityName(ent.Owner, MetaData(args.Entity).EntityName);
- AttachEye(ent);
+ if (SetupEye(ent))
+ AttachEye(ent);
}
- private void OnAiRemove(Entity<StationAiCoreComponent> ent, ref EntRemovedFromContainerMessage args)
+ protected virtual void OnAiRemove(Entity<StationAiCoreComponent> ent, ref EntRemovedFromContainerMessage args)
{
+ if (args.Container.ID != StationAiCoreComponent.Container)
+ return;
+
if (_timing.ApplyingState)
return;
ClearEye(ent);
}
- private void UpdateAppearance(Entity<StationAiHolderComponent?> entity)
+ protected void UpdateAppearance(Entity<StationAiHolderComponent?> entity)
{
if (!Resolve(entity.Owner, ref entity.Comp, false))
return;
- // Todo: when AIs can die, add a check to see if the AI is in the 'dead' state
var state = StationAiState.Empty;
- if (_containers.TryGetContainer(entity.Owner, StationAiHolderComponent.Container, out var container) && container.Count > 0)
- state = StationAiState.Occupied;
+ // Get what visual state the held AI holder is in
+ if (TryGetHeld(entity, out var stationAi) &&
+ TryComp<StationAiCustomizationComponent>(stationAi, out var customization))
+ {
+ state = customization.State;
+ }
+
+ // If the entity is not an AI core, let generic visualizers handle the appearance update
+ if (!TryComp<StationAiCoreComponent>(entity, out var stationAiCore))
+ {
+ _appearance.SetData(entity.Owner, StationAiVisualLayers.Icon, state);
+ return;
+ }
- // If the entity is a station AI core, attempt to customize its appearance
- if (TryComp<StationAiCoreComponent>(entity, out var stationAiCore))
+ // The AI core is empty
+ if (state == StationAiState.Empty)
{
- CustomizeAppearance((entity, stationAiCore), state);
+ _appearance.RemoveData(entity.Owner, StationAiVisualLayers.Icon);
+ return;
+ }
+
+ // The AI core is rebooting
+ if (state == StationAiState.Rebooting)
+ {
+ var rebootingData = new PrototypeLayerData()
+ {
+ RsiPath = _stationAiRebooting.RsiPath.ToString(),
+ State = _stationAiRebooting.RsiState,
+ };
+
+ _appearance.SetData(entity.Owner, StationAiVisualLayers.Icon, rebootingData);
return;
}
- // Otherwise let generic visualizers handle the appearance update
- _appearance.SetData(entity.Owner, StationAiVisualState.Key, state);
+ // Otherwise attempt to set the AI core's appearance
+ CustomizeAppearance((entity, stationAiCore), state);
}
public virtual bool SetVisionEnabled(Entity<StationAiVisionComponent> entity, bool enabled, bool announce = false)
public sealed partial class IntellicardDoAfterEvent : SimpleDoAfterEvent;
[Serializable, NetSerializable]
-public enum StationAiVisualState : byte
+public enum StationAiVisualLayers : byte
{
- Key,
+ Base,
+ Icon,
}
[Serializable, NetSerializable]
-public enum StationAiSpriteState : byte
+public enum StationAiVisuals : byte
{
- Key,
+ Broken,
}
[Serializable, NetSerializable]
Empty,
Occupied,
Dead,
+ Rebooting,
Hologram,
}
[DataField(readOnly: true)]
public EntProtoId? PhysicalEntityProto = "StationAiHoloLocal";
+ /// <summary>
+ /// Name of the container slot that holds the inhabiting AI's mind
+ /// </summary>
public const string Container = "station_ai_mind_slot";
+
+ /// <summary>
+ /// Name of the container slot that holds the 'brain' used to construct the AI core
+ /// </summary>
+ public const string BrainContainer = "station_ai_brain_slot";
}
/// <summary>
-/// This event is raised on a station AI 'eye' that is being replaced with a new one
+/// This event is raised on a station AI 'eye' that is being replaced with a new one
/// </summary>
/// <param name="NewRemoteEntity">The entity UID of the replacement entity</param>
[ByRefEvent]
/// </summary>
[DataField, AutoNetworkedField]
public Dictionary<ProtoId<StationAiCustomizationGroupPrototype>, ProtoId<StationAiCustomizationPrototype>> ProtoIds = new();
+
+ /// <summary>
+ /// The current visual state of the associated entity.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public StationAiState State = StationAiState.Occupied;
}
/// <summary>
}
}
+/// <summary>
+/// Event raised when the station AI customization visual state changes
+/// </summary>
+[ByRefEvent]
+public record StationAiCustomizationStateChanged(StationAiState NewState);
+
/// <summary>
/// Key for opening the station AI customization UI
/// </summary>
--- /dev/null
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Silicons.StationAi;
+
+/// <summary>
+/// This component holds data needed for AI Restoration Consoles to function.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
+[Access(typeof(SharedStationAiFixerConsoleSystem))]
+public sealed partial class StationAiFixerConsoleComponent : Component
+{
+ /// <summary>
+ /// Determines how long a repair takes to complete (in seconds).
+ /// </summary>
+ [DataField]
+ public TimeSpan RepairDuration = TimeSpan.FromSeconds(30);
+
+ /// <summary>
+ /// Determines how long a purge takes to complete (in seconds).
+ /// </summary>
+ [DataField]
+ public TimeSpan PurgeDuration = TimeSpan.FromSeconds(30);
+
+ /// <summary>
+ /// The number of stages that a console action (repair or purge)
+ /// progresses through before it concludes. Each stage has an equal
+ /// duration. The appearance data of the entity is updated with
+ /// each new stage reached.
+ /// </summary>
+ [DataField]
+ public int ActionStageCount = 4;
+
+ /// <summary>
+ /// The time at which the current action commenced.
+ /// </summary>
+ [DataField, AutoNetworkedField, AutoPausedField]
+ public TimeSpan ActionStartTime = TimeSpan.FromSeconds(0);
+
+ /// <summary>
+ /// The time at which the current action will end.
+ /// </summary>
+ [DataField, AutoNetworkedField, AutoPausedField]
+ public TimeSpan ActionEndTime = TimeSpan.FromSeconds(0);
+
+ /// <summary>
+ /// The type of action that is currently in progress.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public StationAiFixerConsoleAction ActionType = StationAiFixerConsoleAction.None;
+
+ /// <summary>
+ /// The target of the current action.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public EntityUid? ActionTarget;
+
+ /// <summary>
+ /// The current stage of the action in progress.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public int CurrentActionStage;
+
+ /// <summary>
+ /// Sound clip that is played when a repair is completed.
+ /// </summary>
+ [DataField]
+ public SoundSpecifier? RepairFinishedSound = new SoundPathSpecifier("/Audio/Items/beep.ogg");
+
+ /// <summary>
+ /// Sound clip that is played when a repair is completed.
+ /// </summary>
+ [DataField]
+ public SoundSpecifier? PurgeFinishedSound = new SoundPathSpecifier("/Audio/Machines/beep.ogg");
+
+ /// <summary>
+ /// The name of the console slot which is used to contain station AI holders.
+ /// </summary>
+ [DataField]
+ public string StationAiHolderSlot = "station_ai_holder";
+
+ /// <summary>
+ /// The name of the station AI holder slot which actually contains the station AI.
+ /// </summary>
+ [DataField]
+ public string StationAiMindSlot = "station_ai_mind_slot";
+}
+
+/// <summary>
+/// Message sent from the server to the client to update the UI of AI Restoration Consoles.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed class StationAiFixerConsoleBoundUserInterfaceState : BoundUserInterfaceState;
+
+/// <summary>
+/// Message sent from the client to the server to handle player UI inputs from AI Restoration Consoles.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed class StationAiFixerConsoleMessage : BoundUserInterfaceMessage
+{
+ public StationAiFixerConsoleAction Action;
+
+ public StationAiFixerConsoleMessage(StationAiFixerConsoleAction action)
+ {
+ Action = action;
+ }
+}
+
+/// <summary>
+/// Potential actions that AI Restoration Consoles can perform.
+/// </summary>
+[Serializable, NetSerializable]
+public enum StationAiFixerConsoleAction
+{
+ None,
+ Eject,
+ Repair,
+ Purge,
+ Cancel,
+}
+
+/// <summary>
+/// Appearance keys for AI Restoration Consoles.
+/// </summary>
+[Serializable, NetSerializable]
+public enum StationAiFixerConsoleVisuals : byte
+{
+ Key,
+ ActionProgress,
+ MobState,
+ RepairProgress,
+ PurgeProgress,
+}
+
+/// <summary>
+/// Interactable UI key for AI Restoration Consoles.
+/// </summary>
+[Serializable, NetSerializable]
+public enum StationAiFixerConsoleUiKey
+{
+ Key,
+}
+
generic-hours = hours
generic-minutes = minutes
+generic-seconds = seconds
generic-playtime-title = Playtime
construction-graph-component-power-cell = power cell
construction-graph-component-apc-electronics = APC electronics
construction-graph-component-payload-trigger = trigger
+construction-graph-component-borg-brain = MMI or positronic brain
construction-graph-tag-door-electronics-circuit-board = door electronics circuit board
construction-graph-tag-firelock-electronics-circuit-board = firelock electronics circuit board
construction-graph-tag-conveyor-belt-assembly = conveyor belt assembly
+construction-graph-tag-station-ai-core-electronics = station AI core electronics
# tools
construction-graph-tag-multitool = a multitool
--- /dev/null
+# System
+station-ai-fixer-console-is-locked = The console is locked.
+station-ai-fixer-console-station-ai-holder-required = Only AI storage units can be inserted into the console.
+station-ai-fixer-console-examination-station-ai-holder-present = There is {INDEFINITE($holder)} [color=cyan]{$holder}[/color] inserted in the console.
+station-ai-fixer-console-examination-station-ai-holder-absent = There is an unoccupied slot for an [color=cyan]AI storage unit[/color].
+station-ai-fixer-console-repair-finished = Repair complete. Attempting to reboot AI...
+station-ai-fixer-console-repair-successful = Repair complete. AI successfully rebooted.
+station-ai-fixer-console-purge-successful = Purge complete. AI successfully deleted.
+
+# UI
+station-ai-fixer-console-window = AI restoration console
+station-ai-fixer-console-window-no-station-ai = No AI detected
+station-ai-fixer-console-window-no-station-ai-status = Waiting
+station-ai-fixer-console-window-station-ai-online = Online
+station-ai-fixer-console-window-station-ai-offline = Offline
+station-ai-fixer-console-window-station-ai-rebooting = Rebooting...
+
+station-ai-fixer-console-window-controls-locked = Controls locked
+
+station-ai-fixer-console-window-station-ai-eject = Eject storage unit
+station-ai-fixer-console-window-station-ai-repair = Run repair tool
+station-ai-fixer-console-window-station-ai-purge = Initiate AI purge
+
+station-ai-fixer-console-window-action-progress-repair = Repair in progress...
+station-ai-fixer-console-window-action-progress-purge = Purge in progress...
+station-ai-fixer-console-window-action-progress-eta = Time remaining: {$time} {$units}
+
+station-ai-fixer-console-window-flavor-left = Lock this console when it is not in use
+station-ai-fixer-console-window-flavor-right = v4.0.4
+
+station-ai-fixer-console-window-continue-action = Continue
+station-ai-fixer-console-window-cancel-action = Cancel
+
+station-ai-fixer-console-window-purge-warning-title = Initiating AI purge
+station-ai-fixer-console-window-purge-warning-1 = You are about to permanently delete an artifical intelligence.
+station-ai-fixer-console-window-purge-warning-2 = Once this operation is complete, the intelligence will be gone and cannot be revived.
+station-ai-fixer-console-window-purge-warning-3 = Do you wish to proceed?
\ No newline at end of file
wire-name-ai-act-light = AIA
station-ai-takeover = AI takeover
station-ai-eye-name = AI eye - {$name}
+station-ai-has-no-power-for-upload = Upload failed - the AI core is unpowered.
+station-ai-is-too-damaged-for-upload = Upload failed - the AI core must be repaired.
+station-ai-core-losing-power = Your AI core is now running on reserve battery power.
+station-ai-core-critical-power = Your AI core is critically low on power. External power must be re-established or severe data corruption may occur!
# Radial actions
ai-open = Open actions
cost: 2000
category: cargoproduct-category-name-science
group: market
+
+- type: cargoProduct
+ id: StationAiCore
+ icon:
+ sprite: Mobs/Silicon/station_ai.rsi
+ state: frame_4
+ product: CrateStationAiCore
+ cost: 10000
+ category: cargoproduct-category-name-science
+ group: market
\ No newline at end of file
- id: CrewMonitoringServerFlatpack
- id: CrewMonitoringComputerFlatpack
amount: 3
+
+- type: entity
+ id: CrateStationAiCore
+ parent: CrateScienceSecure
+ name: station AI core crate
+ description: Contains the components for constructing a station AI core. Positronic brain not included. Requires Science access to open.
+ components:
+ - type: StorageFill
+ contents:
+ - id: StationAiCoreElectronics
+ - id: SheetPlasteel1
+ amount: 4
+ - id: CableApcStack1
+ amount: 1
+ - id: SheetRGlass1
+ amount: 2
\ No newline at end of file
- id: ProtolatheMachineCircuitboard
- id: ResearchComputerCircuitboard
- id: CargoRequestScienceComputerCircuitboard
+ - id: StationAiFixerCircuitboard
- id: RubberStampRd
# Hardsuit table, used for suit storage as well
color: Pink
nextDelay: 12
notifyBySource: true
+
+- type: chatNotification
+ id: AiLosingPower
+ message: station-ai-core-losing-power
+ sound: /Audio/Misc/notice2.ogg
+ color: Orange
+ nextDelay: 30
+
+- type: chatNotification
+ id: AiCriticalPower
+ message: station-ai-core-critical-power
+ sound: /Audio/Effects/alert.ogg
+ color: Red
+ nextDelay: 120
\ No newline at end of file
- type: Appearance
- type: GenericVisualizer
visuals:
- enum.StationAiVisualState.Key:
+ enum.StationAiVisualLayers.Icon:
unshaded:
Empty: { state: empty }
Occupied: { state: full }
+ Rebooting: { state: dead }
+ Dead: { state: dead }
- type: Intellicard
- type: entity
- state: ai
shader: unshaded
+# Empty AI core
- type: entity
id: PlayerStationAiEmpty
name: AI Core
blacklist:
tags:
- GhostOnlyWarp
+ - type: Fixtures
+ fixtures:
+ fix1:
+ shape:
+ !type:PhysShapeAabb
+ bounds: "-0.5,-0.5,0.5,0.5"
+ mask:
+ - MachineMask
+ layer:
+ - MachineLayer
+ density: 200
- type: ContainerComp
proto: AiHeld
container: station_ai_mind_slot
+ - type: Damageable
+ damageModifierSet: StrongMetallic
+ - type: Repairable
+ doAfterDelay: 10
+ allowSelfRepair: false
- type: Destructible
thresholds:
- trigger:
!type:DamageTrigger
- damage: 100
+ damage: 400
behaviors:
- !type:PlaySoundBehavior
sound:
collection: MetalBreak
- !type:DoActsBehavior
- acts: [ "Destruction" ]
+ acts: [ "Breakage" ]
+ - trigger:
+ !type:DamageTrigger
+ damage: 800
+ behaviors:
+ - !type:PlaySoundBehavior
+ sound:
+ collection: MetalBreak
+ - !type:SpawnEntitiesBehavior
+ spawn:
+ ShardGlassReinforced:
+ min: 1
+ max: 2
+ SheetPlasteel:
+ min: 2
+ max: 2
+ - !type:DoActsBehavior
+ acts: ["Destruction"]
+ - type: DamageVisuals
+ thresholds: [25, 50, 75, 100, 125, 150, 175]
+ damageDivisor: 4
+ trackAllDamage: true
+ damageOverlay:
+ sprite: Mobs/Silicon/station_ai_cracks.rsi
- type: ApcPowerReceiver
- powerLoad: 1000
- needsPower: false
+ powerLoad: 500
+ - type: ExtensionCableReceiver
+ - type: Battery
+ maxCharge: 300000
+ startingCharge: 300000
+ - type: ApcPowerReceiverBattery
+ idleLoad: 500
+ batteryRechargeRate: 1000
+ batteryRechargeEfficiency: 0 # Setting to zero until the light flickering issue associated with dynamic power loads is fixed
- type: StationAiCore
- type: StationAiVision
- type: InteractionOutline
layers:
- state: base
- state: ai_empty
+ map: ["enum.StationAiVisualLayers.Base"]
shader: unshaded
- state: ai
- map: ["enum.StationAiVisualState.Key"]
+ map: ["enum.StationAiVisualLayers.Icon"]
shader: unshaded
visible: false
+ - state: ai_unpowered
+ map: ["enum.PowerDeviceVisualLayers.Powered"]
+ visible: false
- type: Appearance
+ - type: GenericVisualizer
+ visuals:
+ enum.PowerDeviceVisuals.Powered:
+ enum.PowerDeviceVisualLayers.Powered:
+ False: { visible: true }
+ True: { visible: false }
+ enum.StationAiVisuals.Broken:
+ enum.StationAiVisualLayers.Base:
+ False: { state: ai_empty }
+ True: { state: ai_error }
- type: InteractionPopup
interactSuccessString: petting-success-station-ai
interactFailureString: petting-failure-station-ai
type: HolopadBoundUserInterface
enum.StationAiCustomizationUiKey.Key:
type: StationAiCustomizationBoundUserInterface
-
+ - type: Construction
+ graph: StationAiCore
+ node: stationAiCore
+ - type: ContainerContainer
+ containers:
+ board: !type:Container
+ station_ai_brain_slot: !type:Container
+ station_ai_mind_slot: !type:ContainerSlot
+ showEnts: true
+ - type: ContainerFill
+ containers:
+ board:
+ - StationAiCoreElectronics
+ - type: StaticPrice
+ price: 5000
+
# The job-ready version of an AI spawn.
- type: entity
id: PlayerStationAi
containerId: station_ai_mind_slot
job: StationAi
+# The station AI core assembly
+- type: entity
+ parent: BaseStructure
+ id: PlayerStationAiAssembly
+ name: AI Core Assembly
+ description: An unfinished computer core for housing an artifical intelligence.
+ components:
+ - type: Anchorable
+ flags:
+ - Anchorable
+ - type: Rotatable
+ - type: Sprite
+ snapCardinals: true
+ sprite: Mobs/Silicon/station_ai.rsi
+ layers:
+ - state: frame_0
+ map: [ "enum.ConstructionVisuals.Layer" ]
+ - type: Appearance
+ - type: GenericVisualizer
+ visuals:
+ enum.ConstructionVisuals.Key:
+ enum.ConstructionVisuals.Layer:
+ frame: { state: frame_0 }
+ frameWithElectronics: { state: frame_1 }
+ frameWithSecuredElectronics: { state: frame_2 }
+ frameWithWires: { state: frame_3 }
+ frameWithBrain: { state: frame_3b }
+ frameWithBrainFinished: { state: frame_4 }
+ frameWithoutBrainFinished: { state: frame_4 }
+ - type: InteractionOutline
+ - type: Fixtures
+ fixtures:
+ fix1:
+ shape:
+ !type:PhysShapeAabb
+ bounds: "-0.5,-0.5,0.5,0.5"
+ mask:
+ - MachineMask
+ layer:
+ - MachineLayer
+ density: 200
+ - type: Damageable
+ damageModifierSet: StrongMetallic
+ - type: Destructible
+ thresholds:
+ - trigger:
+ !type:DamageTrigger
+ damage: 400
+ behaviors:
+ - !type:PlaySoundBehavior
+ sound:
+ collection: MetalBreak
+ - !type:SpawnEntitiesBehavior
+ spawn:
+ SheetPlasteel:
+ min: 2
+ max: 4
+ - !type:EmptyContainersBehaviour
+ containers:
+ - station_ai_brain_slot
+ - board
+ - !type:DoActsBehavior
+ acts: ["Destruction"]
+ - type: Construction
+ graph: StationAiCore
+ node: frame
+ - type: ContainerContainer
+ containers:
+ board: !type:Container
+ station_ai_brain_slot: !type:Container
+
# The actual brain inside the core
- type: entity
id: StationAiBrain
- type: Sprite
# Once it's in a core it's pretty much an abstract entity at that point.
visible: false
- - type: BlockMovement
- blockInteraction: false
- type: SiliconLawProvider
laws: Crewsimov
- type: SiliconLawBound
drawFov: false
- type: Examiner
- type: InputMover
+ - type: BlockMovement
+ blockInteraction: false
+ - type: GhostOnMove
+ mustBeDead: true
- type: Speech
speechVerb: Robotic
speechSounds: Borg
+ - type: DamagedSiliconAccent
+ startPowerCorruptionAtCharIdx: 4
+ maxPowerCorruptionAtCharIdx: 20
- type: Tag
tags:
- HideContextMenu
parent: BaseComputerCircuitboard
id: StationAiUploadCircuitboard
name: AI upload console board
- description: A computer printed circuit board for a AI upload console.
+ description: A computer printed circuit board for an AI upload console.
components:
- type: Sprite
state: cpu_science
- type: ComputerBoard
prototype: StationAiUploadComputer
+
+- type: entity
+ parent: BaseComputerCircuitboard
+ id: StationAiFixerCircuitboard
+ name: AI restoration console
+ description: A computer printed circuit board for an AI restoration console console.
+ components:
+ - type: Sprite
+ state: cpu_science
+ - type: ComputerBoard
+ prototype: StationAiFixerComputer
\ No newline at end of file
--- /dev/null
+- type: entity
+ id: StationAiCoreElectronics
+ parent: BaseElectronics
+ name: station AI core electronics
+ description: An electronics board used in station AI cores.
+ components:
+ - type: Sprite
+ sprite: Objects/Misc/module.rsi
+ state: mainboard
+ - type: Tag
+ tags:
+ - StationAiCoreElectronics
+ - type: StaticPrice
+ price: 404
proto: robot
- type: Speech
speechSounds: Pai
+ - type: Alerts
- type: MobState
allowedStates:
- Alive
+ - Dead
- type: Appearance
- type: Tag
tags:
fireCost: 100
- type: Battery
maxCharge: 2000
- startingCharge: 0
+ startingCharge: 2000
- type: ApcPowerReceiverBattery
idleLoad: 5
batteryRechargeRate: 200
- type: HTN
rootTask:
task: EnergyTurretCompound
+ - type: StaticPrice
+ price: 200
\ No newline at end of file
containers:
circuit_holder: !type:ContainerSlot
board: !type:Container
+
+- type: entity
+ id: StationAiFixerComputer
+ parent: BaseComputer
+ name: AI restoration console
+ description: Used to repair damaged artifical intelligences.
+ components:
+ - type: Sprite
+ layers:
+ - map: [ "computerLayerBody" ]
+ state: computer
+ - map: [ "computerLayerKeyboard" ]
+ state: generic_keyboard
+ - map: [ "computerLayerScreen" ]
+ state: ai-fixer-empty
+ - map: [ "computerLayerKeys" ]
+ state: rd_key
+ - map: [ "enum.WiresVisualLayers.MaintenancePanel" ]
+ state: generic_panel_open
+ - type: Appearance
+ - type: GenericVisualizer
+ visuals:
+ enum.ComputerVisuals.Powered:
+ computerLayerScreen:
+ True: { visible: true, shader: unshaded }
+ False: { visible: false }
+ computerLayerKeys:
+ True: { visible: true, shader: unshaded }
+ False: { visible: true, shader: shaded }
+ enum.StationAiFixerConsoleVisuals.Key:
+ computerLayerScreen:
+ Repair0: { state: ai-fixer-progress-0 }
+ Repair1: { state: ai-fixer-progress-1 }
+ Repair2: { state: ai-fixer-progress-2 }
+ Repair3: { state: ai-fixer-progress-3 }
+ Purge0: { state: ai-fixer-purge-0 }
+ Purge1: { state: ai-fixer-purge-1 }
+ Purge2: { state: ai-fixer-purge-2 }
+ Purge3: { state: ai-fixer-purge-3 }
+ Empty: { state: ai-fixer-empty }
+ Occupied: { state: ai-fixer-full }
+ Rebooting: { state: ai-fixer-404 }
+ Dead: { state: ai-fixer-404 }
+ enum.WiresVisuals.MaintenancePanelState:
+ enum.WiresVisualLayers.MaintenancePanel:
+ True: { visible: false }
+ False: { visible: true }
+ - type: ApcPowerReceiver
+ powerLoad: 1000
+ - type: Computer
+ board: StationAiFixerCircuitboard
+ - type: AccessReader
+ access: [ [ "ResearchDirector" ] ]
+ - type: Lock
+ unlockOnClick: false
+ - type: StationAiFixerConsole
+ - type: ItemSlotsLock
+ slots:
+ - station_ai_holder
+ - type: ItemSlotRequiresPower
+ - type: ItemSlots
+ slots:
+ station_ai_holder:
+ ejectOnBreak: true
+ lockedFailPopup: station-ai-fixer-console-is-locked
+ whitelistFailPopup: station-ai-fixer-console-station-ai-holder-required
+ whitelist:
+ requireAll: true
+ components:
+ - StationAiHolder
+ - Item
+ - type: ContainerContainer
+ containers:
+ station_ai_holder: !type:ContainerSlot
+ board: !type:Container
+ - type: ActivatableUI
+ key: enum.StationAiFixerConsoleUiKey.Key
+ - type: UserInterface
+ interfaces:
+ enum.StationAiFixerConsoleUiKey.Key:
+ type: StationAiFixerConsoleBoundUserInterface
+ enum.WiresUiKey.Key:
+ type: WiresBoundUserInterface
\ No newline at end of file
--- /dev/null
+- type: constructionGraph
+ id: StationAiCore
+ start: start
+ graph:
+ - node: start
+ edges:
+ - to: frame
+ steps:
+ - material: Plasteel
+ amount: 4
+ doAfter: 4
+
+ - node: frame
+ entity: PlayerStationAiAssembly
+ actions:
+ - !type:AppearanceChange
+ edges:
+ - to: frameWithElectronics
+ steps:
+ - tag: StationAiCoreElectronics
+ name: construction-graph-tag-station-ai-core-electronics
+ store: board
+ icon:
+ sprite: "Objects/Misc/module.rsi"
+ state: "mainboard"
+ - to: start
+ completed:
+ - !type:SpawnPrototype
+ prototype: SheetPlasteel1
+ amount: 4
+ - !type:DeleteEntity {}
+ steps:
+ - tool: Welding
+ doAfter: 8
+
+ - node: frameWithElectronics
+ actions:
+ - !type:AppearanceChange
+ edges:
+ - to: frameWithSecuredElectronics
+ steps:
+ - tool: Screwing
+ doAfter: 2
+ - to: frame
+ completed:
+ - !type:EmptyContainer
+ container: board
+ steps:
+ - tool: Prying
+ doAfter: 2
+
+ - node: frameWithSecuredElectronics
+ actions:
+ - !type:AppearanceChange
+ edges:
+ - to: frameWithWires
+ steps:
+ - material: Cable
+ amount: 1
+ doAfter: 1
+ - to: frameWithElectronics
+ steps:
+ - tool: Screwing
+ doAfter: 2
+
+ - node: frameWithWires
+ actions:
+ - !type:AppearanceChange
+ edges:
+ - to: frameWithBrain
+ steps:
+ - component: BorgBrain
+ name: construction-graph-component-borg-brain
+ store: station_ai_brain_slot
+ icon:
+ sprite: "Objects/Specific/Robotics/mmi.rsi"
+ state: "mmi_icon"
+ - to: frameWithoutBrainFinished
+ steps:
+ - material: ReinforcedGlass
+ amount: 2
+ doAfter: 2
+ - to: frameWithSecuredElectronics
+ completed:
+ - !type:SpawnPrototype
+ prototype: CableApcStack1
+ amount: 1
+ steps:
+ - tool: Cutting
+ doAfter: 2
+
+ - node: frameWithBrain
+ actions:
+ - !type:AppearanceChange
+ edges:
+ - to: frameWithBrainFinished
+ steps:
+ - material: ReinforcedGlass
+ amount: 2
+ doAfter: 2
+ - to: frameWithWires
+ completed:
+ - !type:EmptyContainer
+ container: station_ai_brain_slot
+ steps:
+ - tool: Prying
+ doAfter: 4
+
+ - node: frameWithBrainFinished
+ actions:
+ - !type:AppearanceChange
+ edges:
+ - to: stationAiCore
+ steps:
+ - tool: Screwing
+ doAfter: 2
+ - to: frameWithBrain
+ completed:
+ - !type:SpawnPrototype
+ prototype: SheetRGlass1
+ amount: 2
+ steps:
+ - tool: Prying
+ doAfter: 4
+
+ - node: frameWithoutBrainFinished
+ actions:
+ - !type:AppearanceChange
+ edges:
+ - to: stationAiCore
+ steps:
+ - tool: Screwing
+ doAfter: 2
+ - to: frameWithWires
+ completed:
+ - !type:SpawnPrototype
+ prototype: SheetRGlass1
+ amount: 2
+ steps:
+ - tool: Prying
+ doAfter: 4
+
+ - node: stationAiCore
+ entity: PlayerStationAiEmpty
\ No newline at end of file
canBuildInImpassable: false
conditions:
- !type:TileNotBlocked
+
+- type: construction
+ id: StationAiCore
+ graph: StationAiCore
+ startNode: start
+ targetNode: stationAiCore
+ category: construction-category-structures
+ objectType: Structure
+ placementMode: SnapgridCenter
+ canRotate: false
+ canBuildInImpassable: false
+ conditions:
+ - !type:TileNotBlocked
\ No newline at end of file
- type: Tag
id: StationAi
+- type: Tag
+ id: StationAiCoreElectronics
+
- type: Tag
id: StationMapElectronics
{
"name": "ai_dead"
},
+ {
+ "name": "ai_unpowered"
+ },
{
"name": "ai_empty",
"delays": [
]
]
},
+ {
+ "name": "ai_error",
+ "delays": [
+ [
+ 0.7,
+ 0.7
+ ]
+ ]
+ },
+ {
+ "name": "ai_fuzz",
+ "delays": [
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ]
+ ]
+ },
{
"name": "default",
"directions": 4
},
{
"name": "base"
+ },
+ {
+ "name": "frame_0"
+ },
+ {
+ "name": "frame_1"
+ },
+ {
+ "name": "frame_2"
+ },
+ {
+ "name": "frame_3"
+ },
+ {
+ "name": "frame_3b"
+ },
+ {
+ "name": "frame_4"
}
]
}
--- /dev/null
+{
+ "version": 1,
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Taken from https://github.com/tgstation/tgstation at commit e06b82a7f4b2b09216fb28fd384c95a2e1dc50e5. Edited by chromiumboy.",
+ "states": [
+ {
+ "name": "DamageOverlay_25",
+ "directions": 1
+ },
+ {
+ "name": "DamageOverlay_50",
+ "directions": 1
+ },
+ {
+ "name": "DamageOverlay_75",
+ "directions": 1
+ },
+ {
+ "name": "DamageOverlay_100",
+ "directions": 1
+ },
+ {
+ "name": "DamageOverlay_125",
+ "directions": 1
+ },
+ {
+ "name": "DamageOverlay_150",
+ "directions": 1
+ },
+ {
+ "name": "DamageOverlay_175",
+ "directions": 1
+ }
+ ]
+}
]
]
},
+ {
+ "name": "dead",
+ "delays": [
+ [
+ 0.4,
+ 0.4
+ ]
+ ]
+ },
{
"name": "full",
"delays": [
"y": 32
},
"states": [
+ {
+ "name": "mmi_icon"
+ },
{
"name": "mmi_off"
},
{
"version": 1,
"license": "CC-BY-SA-3.0",
- "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/bd6873fd4dd6a61d7e46f1d75cd4d90f64c40894. comm_syndie made by Veritius, based on comm. generic_panel_open made by Errant, commit https://github.com/space-wizards/space-station-14/pull/32273, comms_wizard and wizard_key by ScarKy0, request- variants transfer made by EmoGarbage404 (github), xenorobot by Samuka-C (github)",
+ "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/bd6873fd4dd6a61d7e46f1d75cd4d90f64c40894. comm_syndie made by Veritius, based on comm. generic_panel_open made by Errant, commit https://github.com/space-wizards/space-station-14/pull/32273, comms_wizard and wizard_key by ScarKy0, request- variants transfer made by EmoGarbage404 (github), xenorobot by Samuka-C (github), ai-fixer-progress and -purge sprites made by chromiumboy",
"size": {
"x": 32,
"y": 32
]
]
},
+ {
+ "name": "ai-fixer-progress-0",
+ "directions": 4,
+ "delays": [
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ]
+ ]
+ },
+ {
+ "name": "ai-fixer-progress-1",
+ "directions": 4,
+ "delays": [
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ]
+ ]
+ },
+ {
+ "name": "ai-fixer-progress-2",
+ "directions": 4,
+ "delays": [
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ]
+ ]
+ },
+ {
+ "name": "ai-fixer-progress-3",
+ "directions": 4,
+ "delays": [
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ]
+ ]
+ },
+ {
+ "name": "ai-fixer-purge-0",
+ "directions": 4,
+ "delays": [
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ]
+ ]
+ },
+ {
+ "name": "ai-fixer-purge-1",
+ "directions": 4,
+ "delays": [
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ]
+ ]
+ },
+ {
+ "name": "ai-fixer-purge-2",
+ "directions": 4,
+ "delays": [
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ]
+ ]
+ },
+ {
+ "name": "ai-fixer-purge-3",
+ "directions": 4,
+ "delays": [
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ]
+ ]
+ },
{
"name": "aiupload",
"directions": 4,