The station AI can be destroyed (#39588)

* Initial commit

* Fixing merge conflict

* Merge conflict fixed

* Anchorable entities can now be marked as 'unanchorable'

* Revert "Anchorable entities can now be marked as 'unanchorable'"

This reverts commit 6a502e62a703cf06bd36ed3bdefe655fc074cfc5

This functionality will be made into a separate PR

* Error sprite

* Update AI core appearance with sustained damage, spawn scrap on destroyed

* Added intellicard sprite

* AI damage overlays

* Added fixtures

* AI core accent changes when damaged or low on power

* Bug fix and pop up messages for inserting AIs into inoperable cores

* Updated 'dead' sprite

* Destroying the AI core reduces the number of AI job slots available

* AI battery duration set to 10 minutes

* Initial commit

* Allow MMIs used in the construction of AI cores to take them over

* Initial resources commit

* Initial code commit

* Sprite update

* Bug fixes and updates

* Basic console UI

* Code refactor

* Added lock screen

* Added all outstanding UI features

* Added purge sprites

* Better appearance handling

* Fixed issue with purge sprite

* Finalized UI design

* Major components finalized

* Bit of clean up

* Removed some code that was used for testing

* Tweaked some text

* Removed extra space

* Added the circuitboard to the RD's locker

* Addressed reviewer comments plus tweaks

* Addressed reviewer comments plus tweaks

* Removed instances of granular damage

* Various improvements

* Removed testing code

* Fixed issue with disabled buttons

* Finalized code

* Addressed review comments

* Added a spare Station AI core electronics to the research director's locker

* Fixing build failure

* Addressed review comments

* Addressed review comments

* Added reverse path for construction graph

* Removed unneeded reference

* Parts can be purchased through cargo

* Fixing merge conflict

* Merge conflict resolved

* Fixing merge conflict

* Code update

* Code updates

* Increased AI core health and gave it a sell price to fix test fail

* Added screen static sprite

* Added better support for ghosted AI players plus code tweaks

* Various improvements and clean up

* Increased purge duration to 60 seconds

* Fixed needless complication

* Addressed reviewer comments part 1

* Addressed reviewer comments part 2

* Further fixes

* Trying lower battery values to see if it fixes the test fail

* Adjusted power values again

* Addressed review comments

* Addressed review comments

* Fixed test fail

* Fixed bug with endless rebooting. Using rejuvenation on an AI core revives the AI inside.

* Added pop up text

* Bug fix

* Tweaks and fixes

* Fixed restoration console not updating when the AI finishes rebooting

* Update SharedStationAiSystem.Held.cs

---------

Co-authored-by: ScarKy0 <scarky0@onet.eu>
This commit is contained in:
chromiumboy
2025-09-15 09:18:32 -05:00
committed by GitHub
parent e0fd44da66
commit 7444c8ea4a
72 changed files with 2553 additions and 133 deletions

View File

@@ -0,0 +1,42 @@
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));
}
}

View File

@@ -0,0 +1,22 @@
<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>

View File

@@ -0,0 +1,30 @@
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();
}
}

View File

@@ -0,0 +1,24 @@
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>();
}
}
}

View File

@@ -0,0 +1,172 @@
<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>

View File

@@ -0,0 +1,198 @@
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));
}
}

View File

@@ -81,10 +81,10 @@ public sealed partial class StationAiSystem : SharedStationAiSystem
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()