]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
The station AI can be destroyed (#39588)
authorchromiumboy <50505512+chromiumboy@users.noreply.github.com>
Mon, 15 Sep 2025 14:18:32 +0000 (09:18 -0500)
committerGitHub <noreply@github.com>
Mon, 15 Sep 2025 14:18:32 +0000 (16:18 +0200)
* 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>
72 files changed:
Content.Client/Silicons/StationAi/StationAiFixerConsoleBoundUserInterface.cs [new file with mode: 0644]
Content.Client/Silicons/StationAi/StationAiFixerConsoleConfirmationDialog.xaml [new file with mode: 0644]
Content.Client/Silicons/StationAi/StationAiFixerConsoleConfirmationDialog.xaml.cs [new file with mode: 0644]
Content.Client/Silicons/StationAi/StationAiFixerConsoleSystem.cs [new file with mode: 0644]
Content.Client/Silicons/StationAi/StationAiFixerConsoleWindow.xaml [new file with mode: 0644]
Content.Client/Silicons/StationAi/StationAiFixerConsoleWindow.xaml.cs [new file with mode: 0644]
Content.Client/Silicons/StationAi/StationAiSystem.cs
Content.Server/Holopad/HolopadSystem.cs
Content.Server/Silicons/StationAi/StationAiFixerConsoleSystem.cs [new file with mode: 0644]
Content.Server/Silicons/StationAi/StationAiSystem.cs
Content.Server/Spawners/EntitySystems/ContainerSpawnPointSystem.cs
Content.Shared/Silicons/StationAi/SharedStationAiFixerConsoleSystem.cs [new file with mode: 0644]
Content.Shared/Silicons/StationAi/SharedStationAiSystem.Customization.cs
Content.Shared/Silicons/StationAi/SharedStationAiSystem.Held.cs
Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs
Content.Shared/Silicons/StationAi/StationAiCoreComponent.cs
Content.Shared/Silicons/StationAi/StationAiCustomizationComponent.cs
Content.Shared/Silicons/StationAi/StationAiFixerConsoleComponent.cs [new file with mode: 0644]
Resources/Locale/en-US/generic.ftl
Resources/Locale/en-US/recipes/components.ftl
Resources/Locale/en-US/recipes/tags.ftl
Resources/Locale/en-US/silicons/station-ai-fixer-console.ftl [new file with mode: 0644]
Resources/Locale/en-US/silicons/station-ai.ftl
Resources/Prototypes/Catalog/Cargo/cargo_science.yml
Resources/Prototypes/Catalog/Fills/Crates/science.yml
Resources/Prototypes/Catalog/Fills/Lockers/heads.yml
Resources/Prototypes/Chat/notifications.yml
Resources/Prototypes/Entities/Mobs/Player/silicon.yml
Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml
Resources/Prototypes/Entities/Objects/Devices/Electronics/station_ai_core.yml [new file with mode: 0644]
Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml
Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_base.yml
Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
Resources/Prototypes/Recipes/Construction/Graphs/structures/station_ai_core.yml [new file with mode: 0644]
Resources/Prototypes/Recipes/Construction/structures.yml
Resources/Prototypes/tags.yml
Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_dead.png
Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_error.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_fuzz.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_unpowered.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_0.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_1.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_2.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_3.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_3b.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_4.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/station_ai.rsi/meta.json
Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_100.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_125.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_150.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_175.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_25.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_50.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_75.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/meta.json [new file with mode: 0644]
Resources/Textures/Objects/Devices/ai_card.rsi/dead.png [new file with mode: 0644]
Resources/Textures/Objects/Devices/ai_card.rsi/meta.json
Resources/Textures/Objects/Specific/Robotics/mmi.rsi/meta.json
Resources/Textures/Objects/Specific/Robotics/mmi.rsi/mmi_icon.png [new file with mode: 0644]
Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-404.png
Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-empty.png
Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-full.png
Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-0.png [new file with mode: 0644]
Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-1.png [new file with mode: 0644]
Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-2.png [new file with mode: 0644]
Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-3.png [new file with mode: 0644]
Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-0.png [new file with mode: 0644]
Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-1.png [new file with mode: 0644]
Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-2.png [new file with mode: 0644]
Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-3.png [new file with mode: 0644]
Resources/Textures/Structures/Machines/computers.rsi/ai-fixer.png
Resources/Textures/Structures/Machines/computers.rsi/meta.json

diff --git a/Content.Client/Silicons/StationAi/StationAiFixerConsoleBoundUserInterface.cs b/Content.Client/Silicons/StationAi/StationAiFixerConsoleBoundUserInterface.cs
new file mode 100644 (file)
index 0000000..63183c2
--- /dev/null
@@ -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));
+    }
+}
diff --git a/Content.Client/Silicons/StationAi/StationAiFixerConsoleConfirmationDialog.xaml b/Content.Client/Silicons/StationAi/StationAiFixerConsoleConfirmationDialog.xaml
new file mode 100644 (file)
index 0000000..fa61d61
--- /dev/null
@@ -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>
diff --git a/Content.Client/Silicons/StationAi/StationAiFixerConsoleConfirmationDialog.xaml.cs b/Content.Client/Silicons/StationAi/StationAiFixerConsoleConfirmationDialog.xaml.cs
new file mode 100644 (file)
index 0000000..03d3653
--- /dev/null
@@ -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();
+    }
+}
diff --git a/Content.Client/Silicons/StationAi/StationAiFixerConsoleSystem.cs b/Content.Client/Silicons/StationAi/StationAiFixerConsoleSystem.cs
new file mode 100644 (file)
index 0000000..5a7f6ee
--- /dev/null
@@ -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>();
+        }
+    }
+}
diff --git a/Content.Client/Silicons/StationAi/StationAiFixerConsoleWindow.xaml b/Content.Client/Silicons/StationAi/StationAiFixerConsoleWindow.xaml
new file mode 100644 (file)
index 0000000..432d660
--- /dev/null
@@ -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>
diff --git a/Content.Client/Silicons/StationAi/StationAiFixerConsoleWindow.xaml.cs b/Content.Client/Silicons/StationAi/StationAiFixerConsoleWindow.xaml.cs
new file mode 100644 (file)
index 0000000..0c3140a
--- /dev/null
@@ -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));
+    }
+}
index 9b0a9fb7eaeb34e4c7deba4976b58a2cd0065dd7..d4a8b9dbd816ffce1b3332c211ee93e829245bec 100644 (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()
index 884fb3ae7119aae6d0bca20cb7fa3967a4e6f2b9..0cba4824db9583ee27102749aafe2721d6e62fc9 100644 (file)
@@ -8,6 +8,8 @@ using Content.Shared.Chat.TypingIndicator;
 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;
@@ -38,6 +40,7 @@ public sealed class HolopadSystem : SharedHolopadSystem
     [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;
@@ -77,6 +80,8 @@ public sealed class HolopadSystem : SharedHolopadSystem
         SubscribeLocalEvent<HolopadComponent, EntRemovedFromContainerMessage>(OnAiRemove);
         SubscribeLocalEvent<HolopadComponent, EntParentChangedMessage>(OnParentChanged);
         SubscribeLocalEvent<HolopadComponent, PowerChangedEvent>(OnPowerChanged);
+        SubscribeLocalEvent<HolopadUserComponent, MobStateChangedEvent>(OnMobStateChanged);
+
     }
 
     #region: Holopad UI bound user interface messages
@@ -226,7 +231,7 @@ public sealed class HolopadSystem : SharedHolopadSystem
             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);
         }
 
@@ -446,6 +451,17 @@ public sealed class HolopadSystem : SharedHolopadSystem
             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)
@@ -605,25 +621,23 @@ public sealed class HolopadSystem : SharedHolopadSystem
         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);
diff --git a/Content.Server/Silicons/StationAi/StationAiFixerConsoleSystem.cs b/Content.Server/Silicons/StationAi/StationAiFixerConsoleSystem.cs
new file mode 100644 (file)
index 0000000..cc6f54c
--- /dev/null
@@ -0,0 +1,64 @@
+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);
+    }
+}
index 45b3dda4310f3b4fe395f8eeff6baaaf636093f1..73c5670c1ead8bc948612408537ee97b2b134615 100644 (file)
@@ -1,10 +1,34 @@
 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;
@@ -16,19 +40,300 @@ public sealed class StationAiSystem : SharedStationAiSystem
 {
     [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>();
@@ -147,7 +452,7 @@ public sealed class StationAiSystem : SharedStationAiSystem
             if (!TryGetHeld((stationAiCore, stationAiCore.Comp), out var insertedAi))
                 continue;
 
-            hashSet.Add(insertedAi);
+            hashSet.Add(insertedAi.Value);
         }
 
         return hashSet;
index 1a592b99294178d6b8b4c67f5b4aadcb2de67ea8..1763d5f6a121e46a37362b68f9be07a89e2e627c 100644 (file)
@@ -1,8 +1,7 @@
-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;
@@ -87,6 +86,9 @@ public sealed class ContainerSpawnPointSystem : EntitySystem
             if (!_container.Insert(args.SpawnResult.Value, container, containerXform: xform))
                 continue;
 
+            var ev = new ContainerSpawnEvent(args.SpawnResult.Value);
+            RaiseLocalEvent(uid, ref ev);
+
             return;
         }
 
@@ -94,3 +96,9 @@ public sealed class ContainerSpawnPointSystem : EntitySystem
         args.SpawnResult = null;
     }
 }
+
+/// <summary>
+/// Raised on a container when a player is spawned into it.
+/// </summary>
+[ByRefEvent]
+public record struct ContainerSpawnEvent(EntityUid Player);
diff --git a/Content.Shared/Silicons/StationAi/SharedStationAiFixerConsoleSystem.cs b/Content.Shared/Silicons/StationAi/SharedStationAiFixerConsoleSystem.cs
new file mode 100644 (file)
index 0000000..1abafd7
--- /dev/null
@@ -0,0 +1,411 @@
+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);
+        }
+    }
+}
index 7a5131c9a175c81f9a66fe8f0233bad92cb46150..4361b86d1257c69b486cefad719ba925a2bd9d72 100644 (file)
@@ -1,5 +1,9 @@
 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;
 
@@ -8,9 +12,15 @@ public abstract partial class SharedStationAiSystem
     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)
@@ -29,17 +39,53 @@ public abstract partial class SharedStationAiSystem
 
         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))
@@ -62,21 +108,36 @@ public abstract partial class SharedStationAiSystem
     {
         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;
     }
 }
index 1c9c57dccf789cd6e3e2c1986d2b115846a44684..c82e92b4510456d022b2e73f9371b744e7b38ffa 100644 (file)
@@ -5,6 +5,7 @@ using Content.Shared.Popups;
 using Content.Shared.Verbs;
 using Robust.Shared.Serialization;
 using Robust.Shared.Utility;
+using System.Diagnostics.CodeAnalysis;
 
 namespace Content.Shared.Silicons.StationAi;
 
@@ -26,6 +27,7 @@ public abstract partial class SharedStationAiSystem
         SubscribeLocalEvent<StationAiHeldComponent, InteractionAttemptEvent>(OnHeldInteraction);
         SubscribeLocalEvent<StationAiHeldComponent, AttemptRelayActionComponentChangeEvent>(OnHeldRelay);
         SubscribeLocalEvent<StationAiHeldComponent, JumpToCoreEvent>(OnCoreJump);
+
         SubscribeLocalEvent<TryGetIdentityShortInfoEvent>(OnTryGetIdentityShortInfo);
     }
 
@@ -49,20 +51,23 @@ public abstract partial class SharedStationAiSystem
         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;
 
@@ -70,26 +75,32 @@ public abstract partial class SharedStationAiSystem
         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;
index 1a3d4c788eb4298c3fd83f59676e87c0cf16ebc7..e109c23fe6103ae25ebe8ccefeded602b4d42aa4 100644 (file)
@@ -4,6 +4,7 @@ using Content.Shared.Administration.Managers;
 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;
@@ -11,11 +12,14 @@ using Content.Shared.Intellicard;
 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;
@@ -28,36 +32,36 @@ using Robust.Shared.Prototypes;
 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).
@@ -72,8 +76,6 @@ public abstract partial class SharedStationAiSystem : EntitySystem
     private static readonly EntProtoId DefaultAi = "StationAiBrain";
     private readonly ProtoId<ChatNotificationPrototype> _downloadChatNotificationPrototype = "IntellicardDownload";
 
-    private const float MaxVisionMultiplier = 5f;
-
     public override void Initialize()
     {
         base.Initialize();
@@ -102,10 +104,12 @@ public abstract partial class SharedStationAiSystem : EntitySystem
 
         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)
@@ -137,7 +141,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem
             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")),
             });
         }
@@ -271,8 +275,8 @@ public abstract partial class SharedStationAiSystem : EntitySystem
         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)
         {
@@ -290,7 +294,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem
         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)
@@ -298,7 +302,8 @@ public abstract partial class SharedStationAiSystem : EntitySystem
             BreakOnDamage = true,
             BreakOnMove = true,
             NeedHand = true,
-            BreakOnDropItem = true
+            BreakOnDropItem = true,
+            AttemptFrequency = AttemptFrequency.EveryTick,
         };
 
         _doAfter.TryStartDoAfter(doAfterArgs);
@@ -327,7 +332,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem
 
     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)
@@ -342,24 +347,32 @@ public abstract partial class SharedStationAiSystem : EntitySystem
 
     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)
@@ -395,7 +408,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem
             _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;
@@ -420,7 +433,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem
         return true;
     }
 
-    private void ClearEye(Entity<StationAiCoreComponent> ent)
+    protected void ClearEye(Entity<StationAiCoreComponent> ent)
     {
         if (_net.IsClient)
             return;
@@ -428,9 +441,16 @@ public abstract partial class SharedStationAiSystem : EntitySystem
         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;
@@ -467,7 +487,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem
         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;
@@ -475,17 +495,21 @@ public abstract partial class SharedStationAiSystem : EntitySystem
         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;
 
@@ -506,26 +530,49 @@ public abstract partial class SharedStationAiSystem : EntitySystem
         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)
@@ -573,15 +620,16 @@ public sealed partial class JumpToCoreEvent : InstantActionEvent
 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]
@@ -590,5 +638,6 @@ public enum StationAiState : byte
     Empty,
     Occupied,
     Dead,
+    Rebooting,
     Hologram,
 }
index a795c9eda635c7107e9b2b9d3fec638bbb54c182..ec3f308104154a13119413e34ee9aba2d981f095 100644 (file)
@@ -38,11 +38,19 @@ public sealed partial class StationAiCoreComponent : Component
     [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]
index a2b713edfeb4e0502fd57ee731b16116be583a99..520b7f98c5134cf5bfc5b2118a9cc853653df2af 100644 (file)
@@ -15,6 +15,12 @@ public sealed partial class StationAiCustomizationComponent : Component
     /// </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>
@@ -33,6 +39,12 @@ public sealed class StationAiCustomizationMessage : BoundUserInterfaceMessage
     }
 }
 
+/// <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>
diff --git a/Content.Shared/Silicons/StationAi/StationAiFixerConsoleComponent.cs b/Content.Shared/Silicons/StationAi/StationAiFixerConsoleComponent.cs
new file mode 100644 (file)
index 0000000..0b872b1
--- /dev/null
@@ -0,0 +1,144 @@
+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,
+}
+
index cdca0f24934420d3140b17f0d194551f0ed36c0f..c963f0e0fbec659439fcd961501e05ec69af8db6 100644 (file)
@@ -14,6 +14,7 @@ generic-invalid = invalid
 
 generic-hours = hours
 generic-minutes = minutes
+generic-seconds = seconds
 
 generic-playtime-title = Playtime
 
index 236097532cdbea1f5f59e9513a9b859769d0205c..d67c661ecda4eb51dceac0db2536779473131b79 100644 (file)
@@ -5,3 +5,4 @@ construction-graph-component-second-flash = second flash
 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
index 34eadc37d85ab46772d008e40839b2b20fb1b911..96c0729881a96e6ca5762683cfe870bc1c2c7d85 100644 (file)
@@ -103,6 +103,7 @@ construction-graph-tag-ripley-peripherals-control-module = ripley peripherals co
 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
diff --git a/Resources/Locale/en-US/silicons/station-ai-fixer-console.ftl b/Resources/Locale/en-US/silicons/station-ai-fixer-console.ftl
new file mode 100644 (file)
index 0000000..a6940f2
--- /dev/null
@@ -0,0 +1,37 @@
+# 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
index 442782f9a137edd864e85990ed1553b10643095a..11c51ddea402bbe9859c04eda0467cb7f1cdc9e4 100644 (file)
@@ -4,6 +4,10 @@ wire-name-ai-vision-light = AIV
 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
index aa428b7d5508798e5a7b166a0081259966503ae8..cefcca5fabbc948605375cafa0ddb766ffabc9da 100644 (file)
   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
index 6adf5942a43a7efcf3e0aa0dd6ea34bae81053a0..4ed07d607f7031befff37ef220e8485f584454d7 100644 (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
index 6b1efddad1aef0d9d4c76d5b76b22661a42c355f..ae904f7f95a294a054ce3ba3fb169f7cc374a5c5 100644 (file)
     - id: ProtolatheMachineCircuitboard
     - id: ResearchComputerCircuitboard
     - id: CargoRequestScienceComputerCircuitboard
+    - id: StationAiFixerCircuitboard
     - id: RubberStampRd
 
 # Hardsuit table, used for suit storage as well
index c1aee755c687e905b3f5e3fa3c575e4fb1c513b9..cea67fa0ee0d2b8dbe91bdba41b03088e63dd9c1 100644 (file)
   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
index e18100ab8ac1d9df98df6a89d153479a9d40688b..845971be35691e1ad1124f7dd2523a59afbb989e 100644 (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
index 459030d8a9bac9c6afc47514a5c95de98c9a086f..8c90308417c3c843b3c40a1e78915a74cd8c1c6b 100644 (file)
   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
diff --git a/Resources/Prototypes/Entities/Objects/Devices/Electronics/station_ai_core.yml b/Resources/Prototypes/Entities/Objects/Devices/Electronics/station_ai_core.yml
new file mode 100644 (file)
index 0000000..637d7e6
--- /dev/null
@@ -0,0 +1,14 @@
+- 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
index 8f181900b70595211edbbd4fea297206b46c06c1..4d27a0f07abbdaec61e8ca41263215794a768a06 100644 (file)
       proto: robot
     - type: Speech
       speechSounds: Pai
+    - type: Alerts
     - type: MobState
       allowedStates:
       - Alive
+      - Dead
     - type: Appearance
     - type: Tag
       tags:
index 0e412b014bbf8cb0857352f97fa86e8ee38a00cf..f60297d2235491fac01d9927334a9e883e3245c9 100644 (file)
     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
index f6538ba64e9673c3adb12db8dc2dbaab2c9475e6..e275bef0e9ce94586e5b60c51fa552690ab4c0a5 100644 (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
diff --git a/Resources/Prototypes/Recipes/Construction/Graphs/structures/station_ai_core.yml b/Resources/Prototypes/Recipes/Construction/Graphs/structures/station_ai_core.yml
new file mode 100644 (file)
index 0000000..a04c9b0
--- /dev/null
@@ -0,0 +1,144 @@
+- 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
index ed533bcc525db156a2ee9306889dcf80d00337be..1f568a16294efbdbed4e4bb44c9e8d22a07c0e05 100644 (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
index 174374beb8eca711cd00de6c4c7eec350a1465b6..14dfa9499e7bd61e8f5a9dadb8b18a080946a0ff 100644 (file)
 - type: Tag
   id: StationAi
 
+- type: Tag
+  id: StationAiCoreElectronics
+
 - type: Tag
   id: StationMapElectronics
 
index eb74655e027f45f7ce830da6a337dcf2aa74c248..b015ef9a8a438e085de2f9c743585b33ee4caf65 100644 (file)
Binary files a/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_dead.png and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_dead.png differ
diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_error.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_error.png
new file mode 100644 (file)
index 0000000..91e5635
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_error.png differ
diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_fuzz.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_fuzz.png
new file mode 100644 (file)
index 0000000..dcc48a4
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_fuzz.png differ
diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_unpowered.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_unpowered.png
new file mode 100644 (file)
index 0000000..6ce4bde
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_unpowered.png differ
diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_0.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_0.png
new file mode 100644 (file)
index 0000000..dfee825
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_0.png differ
diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_1.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_1.png
new file mode 100644 (file)
index 0000000..668a53a
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_1.png differ
diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_2.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_2.png
new file mode 100644 (file)
index 0000000..21ecc5e
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_2.png differ
diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_3.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_3.png
new file mode 100644 (file)
index 0000000..afddf9f
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_3.png differ
diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_3b.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_3b.png
new file mode 100644 (file)
index 0000000..3780e52
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_3b.png differ
diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_4.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_4.png
new file mode 100644 (file)
index 0000000..70c4834
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_4.png differ
index 867aeb41e8db059c13cad8c11b94fd7872ef9e6d..55bd389b63daf69de1e82d39aa857d8ec6a08c15 100644 (file)
         {
             "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"
         }
     ]
 }
diff --git a/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_100.png b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_100.png
new file mode 100644 (file)
index 0000000..64ef65a
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_100.png differ
diff --git a/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_125.png b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_125.png
new file mode 100644 (file)
index 0000000..642132e
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_125.png differ
diff --git a/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_150.png b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_150.png
new file mode 100644 (file)
index 0000000..f7859d9
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_150.png differ
diff --git a/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_175.png b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_175.png
new file mode 100644 (file)
index 0000000..6a08521
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_175.png differ
diff --git a/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_25.png b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_25.png
new file mode 100644 (file)
index 0000000..36e729d
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_25.png differ
diff --git a/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_50.png b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_50.png
new file mode 100644 (file)
index 0000000..54d2b3b
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_50.png differ
diff --git a/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_75.png b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_75.png
new file mode 100644 (file)
index 0000000..4dea5cb
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_75.png differ
diff --git a/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/meta.json b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/meta.json
new file mode 100644 (file)
index 0000000..4f70fd7
--- /dev/null
@@ -0,0 +1,39 @@
+{
+    "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
+        }
+    ]
+}
diff --git a/Resources/Textures/Objects/Devices/ai_card.rsi/dead.png b/Resources/Textures/Objects/Devices/ai_card.rsi/dead.png
new file mode 100644 (file)
index 0000000..6b20c2a
Binary files /dev/null and b/Resources/Textures/Objects/Devices/ai_card.rsi/dead.png differ
index 140b77fbeea666ac2adb27cc126713405ffcc7f6..8a12aec8cf847c0de6c1137a49fecfac0436d236 100644 (file)
         ]
       ]
     },
+    {
+      "name": "dead",
+      "delays": [
+        [
+          0.4,
+          0.4
+        ]
+      ]
+    },
     {
       "name": "full",
       "delays": [
index fcdd9e1b26e2438d49bd350d267538f1523affb4..1192d1a2089fe34fae7cc3f7991eeba3712f23cc 100644 (file)
@@ -7,6 +7,9 @@
     "y": 32
   },
   "states": [
+    {
+      "name": "mmi_icon"
+    },
     {
       "name": "mmi_off"
     },
diff --git a/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/mmi_icon.png b/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/mmi_icon.png
new file mode 100644 (file)
index 0000000..e109ee0
Binary files /dev/null and b/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/mmi_icon.png differ
index 1bb9234abef2b7fcbffb0e63e3ab6e7e9780bb56..bc60d18319f6bd6c1520bbb7ca10a078a5d221dd 100644 (file)
Binary files a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-404.png and b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-404.png differ
index 2b9c1c1c95e6717332717bd09fe776726a8cb0c4..5409dc51dd1c2e8aadd420fc008c4478875b9b6b 100644 (file)
Binary files a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-empty.png and b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-empty.png differ
index da7937d84d73c0ff45b158a96e5a3f62386ac43b..fe7d222f1cef82ddd9233fd0e90d307e98910e1b 100644 (file)
Binary files a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-full.png and b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-full.png differ
diff --git a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-0.png b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-0.png
new file mode 100644 (file)
index 0000000..1bd832b
Binary files /dev/null and b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-0.png differ
diff --git a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-1.png b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-1.png
new file mode 100644 (file)
index 0000000..5d3b546
Binary files /dev/null and b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-1.png differ
diff --git a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-2.png b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-2.png
new file mode 100644 (file)
index 0000000..4496ead
Binary files /dev/null and b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-2.png differ
diff --git a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-3.png b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-3.png
new file mode 100644 (file)
index 0000000..80dcc81
Binary files /dev/null and b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-3.png differ
diff --git a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-0.png b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-0.png
new file mode 100644 (file)
index 0000000..be2f0f6
Binary files /dev/null and b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-0.png differ
diff --git a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-1.png b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-1.png
new file mode 100644 (file)
index 0000000..af82c55
Binary files /dev/null and b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-1.png differ
diff --git a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-2.png b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-2.png
new file mode 100644 (file)
index 0000000..56dfc2f
Binary files /dev/null and b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-2.png differ
diff --git a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-3.png b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-3.png
new file mode 100644 (file)
index 0000000..4f6dbe4
Binary files /dev/null and b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-3.png differ
index fe98279329681d2a10f92f19fc4c08ad2c5c06cf..eb9cf126679e780bd1912fdeeb2546965166529f 100644 (file)
Binary files a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer.png and b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer.png differ
index 28b6b7fb79ad9fca80570d50130dafd425cbcf3e..ebb9a875dd477ddff0dc1a85285dee15c1e890e1 100644 (file)
@@ -1,7 +1,7 @@
 {
   "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,