The station AI can be destroyed (#39588)

* Initial commit

* Fixing merge conflict

* Merge conflict fixed

* Anchorable entities can now be marked as 'unanchorable'

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

This reverts commit 6a502e62a703cf06bd36ed3bdefe655fc074cfc5

This functionality will be made into a separate PR

* Error sprite

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

* Added intellicard sprite

* AI damage overlays

* Added fixtures

* AI core accent changes when damaged or low on power

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

* Updated 'dead' sprite

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

* AI battery duration set to 10 minutes

* Initial commit

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

* Initial resources commit

* Initial code commit

* Sprite update

* Bug fixes and updates

* Basic console UI

* Code refactor

* Added lock screen

* Added all outstanding UI features

* Added purge sprites

* Better appearance handling

* Fixed issue with purge sprite

* Finalized UI design

* Major components finalized

* Bit of clean up

* Removed some code that was used for testing

* Tweaked some text

* Removed extra space

* Added the circuitboard to the RD's locker

* Addressed reviewer comments plus tweaks

* Addressed reviewer comments plus tweaks

* Removed instances of granular damage

* Various improvements

* Removed testing code

* Fixed issue with disabled buttons

* Finalized code

* Addressed review comments

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

* Fixing build failure

* Addressed review comments

* Addressed review comments

* Added reverse path for construction graph

* Removed unneeded reference

* Parts can be purchased through cargo

* Fixing merge conflict

* Merge conflict resolved

* Fixing merge conflict

* Code update

* Code updates

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

* Added screen static sprite

* Added better support for ghosted AI players plus code tweaks

* Various improvements and clean up

* Increased purge duration to 60 seconds

* Fixed needless complication

* Addressed reviewer comments part 1

* Addressed reviewer comments part 2

* Further fixes

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

* Adjusted power values again

* Addressed review comments

* Addressed review comments

* Fixed test fail

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

* Added pop up text

* Bug fix

* Tweaks and fixes

* Fixed restoration console not updating when the AI finishes rebooting

* Update SharedStationAiSystem.Held.cs

---------

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

View File

@@ -0,0 +1,42 @@
using Content.Shared.Silicons.StationAi;
using Robust.Client.UserInterface;
namespace Content.Client.Silicons.StationAi;
public sealed class StationAiFixerConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
private StationAiFixerConsoleWindow? _window;
protected override void Open()
{
base.Open();
_window = this.CreateWindow<StationAiFixerConsoleWindow>();
_window.SetOwner(Owner);
_window.SendStationAiFixerConsoleMessageAction += SendStationAiFixerConsoleMessage;
_window.OpenConfirmationDialogAction += OpenConfirmationDialog;
}
public override void Update()
{
base.Update();
_window?.UpdateState();
}
private void OpenConfirmationDialog()
{
if (_window == null)
return;
_window.ConfirmationDialog?.Close();
_window.ConfirmationDialog = new StationAiFixerConsoleConfirmationDialog();
_window.ConfirmationDialog.OpenCentered();
_window.ConfirmationDialog.SendStationAiFixerConsoleMessageAction += SendStationAiFixerConsoleMessage;
}
private void SendStationAiFixerConsoleMessage(StationAiFixerConsoleAction action)
{
SendPredictedMessage(new StationAiFixerConsoleMessage(action));
}
}

View File

@@ -0,0 +1,22 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'station-ai-fixer-console-window-purge-warning-title'}"
Resizable="False">
<BoxContainer Orientation="Vertical" VerticalExpand="True" SetWidth="400">
<RichTextLabel Name="PurgeWarningLabel1" Margin="20 10 20 0"/>
<RichTextLabel Name="PurgeWarningLabel2" Margin="20 10 20 0"/>
<RichTextLabel Name="PurgeWarningLabel3" Margin="20 10 20 10"/>
<BoxContainer HorizontalExpand="True">
<Button Name="CancelPurge"
Text="{Loc 'station-ai-fixer-console-window-cancel-action'}"
SetWidth="150"
Margin="20 10 0 10"/>
<Control HorizontalExpand="True"/>
<Button Name="ContinuePurge"
Text="{Loc 'station-ai-fixer-console-window-continue-action'}"
SetWidth="150"
Margin="0 10 20 10"/>
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@@ -0,0 +1,30 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.Silicons.StationAi;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Silicons.StationAi;
[GenerateTypedNameReferences]
public sealed partial class StationAiFixerConsoleConfirmationDialog : FancyWindow
{
public event Action<StationAiFixerConsoleAction>? SendStationAiFixerConsoleMessageAction;
public StationAiFixerConsoleConfirmationDialog()
{
RobustXamlLoader.Load(this);
PurgeWarningLabel1.SetMessage(Loc.GetString($"station-ai-fixer-console-window-purge-warning-1"));
PurgeWarningLabel2.SetMessage(Loc.GetString($"station-ai-fixer-console-window-purge-warning-2"));
PurgeWarningLabel3.SetMessage(Loc.GetString($"station-ai-fixer-console-window-purge-warning-3"));
CancelPurge.OnButtonDown += _ => Close();
ContinuePurge.OnButtonDown += _ => OnSendStationAiFixerConsoleMessage(StationAiFixerConsoleAction.Purge);
}
public void OnSendStationAiFixerConsoleMessage(StationAiFixerConsoleAction action)
{
SendStationAiFixerConsoleMessageAction?.Invoke(action);
Close();
}
}

View File

@@ -0,0 +1,24 @@
using Content.Shared.Silicons.StationAi;
using Robust.Client.GameObjects;
namespace Content.Client.Silicons.StationAi;
public sealed partial class StationAiFixerConsoleSystem : SharedStationAiFixerConsoleSystem
{
[Dependency] private readonly SharedUserInterfaceSystem _userInterface = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StationAiFixerConsoleComponent, AppearanceChangeEvent>(OnAppearanceChange);
}
private void OnAppearanceChange(Entity<StationAiFixerConsoleComponent> ent, ref AppearanceChangeEvent args)
{
if (_userInterface.TryGetOpenUi(ent.Owner, StationAiFixerConsoleUiKey.Key, out var bui))
{
bui?.Update<StationAiFixerConsoleBoundUserInterfaceState>();
}
}
}

View File

@@ -0,0 +1,172 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
Title="{Loc 'station-ai-fixer-console-window'}"
Resizable="False">
<BoxContainer Orientation="Vertical" VerticalExpand="True">
<BoxContainer VerticalExpand="True" HorizontalExpand="True" Orientation="Horizontal">
<!-- Left side - AI display -->
<BoxContainer VerticalExpand="True" HorizontalExpand="True" Orientation="Vertical" MinWidth="225" Margin="20 15 20 20">
<!-- AI panel -->
<PanelContainer>
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#1B1B1E" />
</PanelContainer.PanelOverride>
<BoxContainer Orientation="Vertical">
<!-- AI name -->
<Label Name="StationAiNameLabel"
HorizontalAlignment="Center"
Margin="0 5 0 0"
Text="{Loc 'station-ai-fixer-console-window-no-station-ai'}"/>
<!-- AI portrait -->
<AnimatedTextureRect Name="StationAiPortraitTexture" VerticalAlignment="Center" SetSize="128 128" />
</BoxContainer>
</PanelContainer>
<!-- AI status panel-->
<PanelContainer Name="StationAiStatus">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#757575" />
</PanelContainer.PanelOverride>
<!-- AI name -->
<Label Name="StationAiStatusLabel"
HorizontalAlignment="Center"
Text="{Loc 'station-ai-fixer-console-window-no-station-ai-status'}"/>
</PanelContainer>
</BoxContainer>
<!-- Central divider -->
<PanelContainer StyleClasses="LowDivider" VerticalExpand="True" Margin="0 0 0 0" SetWidth="2"/>
<!-- Right side - control panel -->
<BoxContainer VerticalExpand="True" HorizontalExpand="True" Orientation="Vertical" MinWidth="225" Margin="10 10 10 10">
<!-- Locked controls -->
<BoxContainer Name="LockScreen"
VerticalExpand="True"
HorizontalExpand="True"
Orientation="Vertical"
ReservesSpace="False">
<controls:StripeBack VerticalExpand="True" HorizontalExpand="True" Margin="0 0 0 5">
<PanelContainer VerticalExpand="True" HorizontalExpand="True">
<BoxContainer VerticalExpand="True" HorizontalExpand="True" Orientation="Vertical">
<Control VerticalExpand="True"/>
<TextureRect VerticalAlignment="Center"
HorizontalAlignment="Center"
SetSize="64 64"
Stretch="KeepAspectCentered"
TexturePath="/Textures/Interface/VerbIcons/lock.svg.192dpi.png">
</TextureRect>
<Label Text="{Loc 'station-ai-fixer-console-window-controls-locked'}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Margin="0 5 0 0"/>
<Control VerticalExpand="True"/>
</BoxContainer>
</PanelContainer>
</controls:StripeBack>
</BoxContainer>
<!-- Action progress screen -->
<BoxContainer Name="ActionProgressScreen"
VerticalExpand="True"
HorizontalExpand="True"
Orientation="Vertical"
ReservesSpace="False"
Visible="False">
<Control VerticalExpand="True" Margin="0 0 0 0"/>
<Label Name="ActionInProgressLabel" Text="???" HorizontalAlignment="Center"/>
<ProgressBar Name="ActionProgressBar"
MinValue="0"
MaxValue="1"
SetHeight="20"
Margin="5 10 5 10">
</ProgressBar>
<Label Name="ActionProgressEtaLabel" Text="???" HorizontalAlignment="Center"/>
<!-- Cancel button -->
<Button Name="CancelButton" HorizontalExpand="True" Margin="0 20 0 10" SetHeight="40"
Text="{Loc 'station-ai-fixer-console-window-cancel-action'}">
<TextureRect HorizontalAlignment="Left"
VerticalAlignment="Center"
SetSize="24 24"
Stretch="KeepAspectCentered"
TexturePath="/Textures/Interface/Nano/cross.svg.png">
</TextureRect>
</Button>
</BoxContainer>
<!-- Visible controls -->
<BoxContainer Name="MainControls"
VerticalExpand="True"
HorizontalExpand="True"
Orientation="Vertical"
ReservesSpace="False"
Visible="False">
<controls:StripeBack>
<PanelContainer>
<Label Text="{Loc 'Controls'}"
HorizontalExpand="True"
HorizontalAlignment="Center"/>
</PanelContainer>
</controls:StripeBack>
<!-- Eject button -->
<Button Name="EjectButton" HorizontalExpand="True" Margin="0 10 0 0" SetHeight="40"
Text="{Loc 'station-ai-fixer-console-window-station-ai-eject'}">
<TextureRect HorizontalAlignment="Left"
VerticalAlignment="Center"
SetSize="32 32"
Stretch="KeepAspectCentered"
TexturePath="/Textures/Interface/VerbIcons/eject.svg.192dpi.png">
</TextureRect>
</Button>
<!-- Repair button -->
<Button Name="RepairButton" HorizontalExpand="True" Margin="0 10 0 0" SetHeight="40"
Text="{Loc 'station-ai-fixer-console-window-station-ai-repair'}">
<TextureRect HorizontalAlignment="Left"
VerticalAlignment="Center"
SetSize="32 32"
Stretch="KeepAspectCentered"
TexturePath="/Textures/Interface/hammer_scaled.svg.192dpi.png">
</TextureRect>
</Button>
<!-- Purge button -->
<Button Name="PurgeButton" HorizontalExpand="True" Margin="0 10 0 0" SetHeight="40"
Text="{Loc 'station-ai-fixer-console-window-station-ai-purge'}">
<TextureRect HorizontalAlignment="Left"
VerticalAlignment="Center"
SetSize="32 32"
Stretch="KeepAspectCentered"
TexturePath="/Textures/Interface/VerbIcons/delete_transparent.svg.192dpi.png">
</TextureRect>
</Button>
</BoxContainer>
</BoxContainer>
</BoxContainer>
<!-- Footer -->
<BoxContainer Orientation="Vertical">
<PanelContainer StyleClasses="LowDivider" />
<BoxContainer Orientation="Horizontal" Margin="10 2 5 0" VerticalAlignment="Bottom">
<Label Text="{Loc 'station-ai-fixer-console-window-flavor-left'}" StyleClasses="WindowFooterText" />
<Label Text="{Loc 'station-ai-fixer-console-window-flavor-right'}" StyleClasses="WindowFooterText"
HorizontalAlignment="Right" HorizontalExpand="True" Margin="0 0 5 0" />
<TextureRect StyleClasses="NTLogoDark" Stretch="KeepAspectCentered"
VerticalAlignment="Center" HorizontalAlignment="Right" SetSize="19 19"/>
</BoxContainer>
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@@ -0,0 +1,198 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.Lock;
using Content.Shared.Silicons.StationAi;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using System.Numerics;
namespace Content.Client.Silicons.StationAi;
[GenerateTypedNameReferences]
public sealed partial class StationAiFixerConsoleWindow : FancyWindow
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private readonly StationAiFixerConsoleSystem _stationAiFixerConsole;
private readonly SharedStationAiSystem _stationAi;
private EntityUid? _owner;
private readonly SpriteSpecifier.Rsi _emptyPortrait = new(new("Mobs/Silicon/station_ai.rsi"), "ai_empty");
private readonly SpriteSpecifier.Rsi _rebootingPortrait = new(new("Mobs/Silicon/station_ai.rsi"), "ai_fuzz");
private SpriteSpecifier? _currentPortrait;
public event Action<StationAiFixerConsoleAction>? SendStationAiFixerConsoleMessageAction;
public event Action? OpenConfirmationDialogAction;
public StationAiFixerConsoleConfirmationDialog? ConfirmationDialog;
private readonly Dictionary<StationAiState, Color> _statusColors = new()
{
[StationAiState.Empty] = Color.FromHex("#464966"),
[StationAiState.Occupied] = Color.FromHex("#3E6C45"),
[StationAiState.Rebooting] = Color.FromHex("#A5762F"),
[StationAiState.Dead] = Color.FromHex("#BB3232"),
};
public StationAiFixerConsoleWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_stationAiFixerConsole = _entManager.System<StationAiFixerConsoleSystem>();
_stationAi = _entManager.System<StationAiSystem>();
StationAiPortraitTexture.DisplayRect.TextureScale = new Vector2(4f, 4f);
CancelButton.OnButtonDown += _ => OnSendStationAiFixerConsoleMessage(StationAiFixerConsoleAction.Cancel);
EjectButton.OnButtonDown += _ => OnSendStationAiFixerConsoleMessage(StationAiFixerConsoleAction.Eject);
RepairButton.OnButtonDown += _ => OnSendStationAiFixerConsoleMessage(StationAiFixerConsoleAction.Repair);
PurgeButton.OnButtonDown += _ => OnOpenConfirmationDialog();
CancelButton.Label.HorizontalAlignment = HAlignment.Left;
EjectButton.Label.HorizontalAlignment = HAlignment.Left;
RepairButton.Label.HorizontalAlignment = HAlignment.Left;
PurgeButton.Label.HorizontalAlignment = HAlignment.Left;
CancelButton.Label.Margin = new Thickness(40, 0, 0, 0);
EjectButton.Label.Margin = new Thickness(40, 0, 0, 0);
RepairButton.Label.Margin = new Thickness(40, 0, 0, 0);
PurgeButton.Label.Margin = new Thickness(40, 0, 0, 0);
}
public void OnSendStationAiFixerConsoleMessage(StationAiFixerConsoleAction action)
{
SendStationAiFixerConsoleMessageAction?.Invoke(action);
}
public void OnOpenConfirmationDialog()
{
OpenConfirmationDialogAction?.Invoke();
}
public override void Close()
{
base.Close();
ConfirmationDialog?.Close();
}
public void SetOwner(EntityUid owner)
{
_owner = owner;
UpdateState();
}
public void UpdateState()
{
if (!_entManager.TryGetComponent<StationAiFixerConsoleComponent>(_owner, out var stationAiFixerConsole))
return;
var ent = (_owner.Value, stationAiFixerConsole);
var isLocked = _entManager.TryGetComponent<LockComponent>(_owner, out var lockable) && lockable.Locked;
var stationAiHolderInserted = _stationAiFixerConsole.IsStationAiHolderInserted((_owner.Value, stationAiFixerConsole));
var stationAi = stationAiFixerConsole.ActionTarget;
var stationAiState = StationAiState.Empty;
if (_entManager.TryGetComponent<StationAiCustomizationComponent>(stationAi, out var stationAiCustomization))
{
stationAiState = stationAiCustomization.State;
}
// Set subscreen visibility
LockScreen.Visible = isLocked;
MainControls.Visible = !isLocked && !_stationAiFixerConsole.IsActionInProgress(ent);
ActionProgressScreen.Visible = !isLocked && _stationAiFixerConsole.IsActionInProgress(ent);
// Update station AI name
StationAiNameLabel.Text = GetStationAiName(stationAi);
StationAiStatusLabel.Text = Loc.GetString("station-ai-fixer-console-window-no-station-ai-status");
// Update station AI portrait
var portrait = _emptyPortrait;
var statusColor = _statusColors[StationAiState.Empty];
if (stationAiState == StationAiState.Rebooting)
{
portrait = _rebootingPortrait;
StationAiStatusLabel.Text = Loc.GetString("station-ai-fixer-console-window-station-ai-rebooting");
_statusColors.TryGetValue(StationAiState.Rebooting, out statusColor);
}
else if (stationAi != null &&
stationAiCustomization != null &&
_stationAi.TryGetCustomizedAppearanceData((stationAi.Value, stationAiCustomization), out var layerData))
{
StationAiStatusLabel.Text = stationAiState == StationAiState.Occupied ?
Loc.GetString("station-ai-fixer-console-window-station-ai-online") :
Loc.GetString("station-ai-fixer-console-window-station-ai-offline");
if (layerData.TryGetValue(stationAiState.ToString(), out var stateData) && stateData is { RsiPath: not null, State: not null })
{
portrait = new SpriteSpecifier.Rsi(new ResPath(stateData.RsiPath), stateData.State);
}
_statusColors.TryGetValue(stationAiState, out statusColor);
}
if (_currentPortrait == null || !_currentPortrait.Equals(portrait))
{
StationAiPortraitTexture.SetFromSpriteSpecifier(portrait);
_currentPortrait = portrait;
}
StationAiStatus.PanelOverride = new StyleBoxFlat
{
BackgroundColor = statusColor,
};
// Update buttons
EjectButton.Disabled = !stationAiHolderInserted;
RepairButton.Disabled = !stationAiHolderInserted || stationAiState != StationAiState.Dead;
PurgeButton.Disabled = !stationAiHolderInserted || stationAiState == StationAiState.Empty;
// Update progress bar
if (ActionProgressScreen.Visible)
UpdateProgressBar(ent);
}
public void UpdateProgressBar(Entity<StationAiFixerConsoleComponent> ent)
{
ActionInProgressLabel.Text = ent.Comp.ActionType == StationAiFixerConsoleAction.Repair ?
Loc.GetString("station-ai-fixer-console-window-action-progress-repair") :
Loc.GetString("station-ai-fixer-console-window-action-progress-purge");
var fullTimeSpan = ent.Comp.ActionEndTime - ent.Comp.ActionStartTime;
var remainingTimeSpan = ent.Comp.ActionEndTime - _timing.CurTime;
var time = remainingTimeSpan.TotalSeconds > 60 ? remainingTimeSpan.TotalMinutes : remainingTimeSpan.TotalSeconds;
var units = remainingTimeSpan.TotalSeconds > 60 ? Loc.GetString("generic-minutes") : Loc.GetString("generic-seconds");
ActionProgressEtaLabel.Text = Loc.GetString("station-ai-fixer-console-window-action-progress-eta", ("time", (int)time), ("units", units));
ActionProgressBar.Value = 1f - (float)remainingTimeSpan.Divide(fullTimeSpan);
}
private string GetStationAiName(EntityUid? uid)
{
if (_entManager.TryGetComponent<MetaDataComponent>(uid, out var metadata))
{
return metadata.EntityName;
}
return Loc.GetString("station-ai-fixer-console-window-no-station-ai");
}
protected override void FrameUpdate(FrameEventArgs args)
{
if (!ActionProgressScreen.Visible)
return;
if (!_entManager.TryGetComponent<StationAiFixerConsoleComponent>(_owner, out var stationAiFixerConsole))
return;
UpdateProgressBar((_owner.Value, stationAiFixerConsole));
}
}

View File

@@ -81,10 +81,10 @@ public sealed partial class StationAiSystem : SharedStationAiSystem
if (args.Sprite == null) if (args.Sprite == null)
return; return;
if (_appearance.TryGetData<PrototypeLayerData>(entity.Owner, StationAiVisualState.Key, out var layerData, args.Component)) if (_appearance.TryGetData<PrototypeLayerData>(entity.Owner, StationAiVisualLayers.Icon, out var layerData, args.Component))
_sprite.LayerSetData((entity.Owner, args.Sprite), StationAiVisualState.Key, layerData); _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() public override void Shutdown()

View File

@@ -8,6 +8,8 @@ using Content.Shared.Chat.TypingIndicator;
using Content.Shared.Holopad; using Content.Shared.Holopad;
using Content.Shared.IdentityManagement; using Content.Shared.IdentityManagement;
using Content.Shared.Labels.Components; using Content.Shared.Labels.Components;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Systems;
using Content.Shared.Power; using Content.Shared.Power;
using Content.Shared.Silicons.StationAi; using Content.Shared.Silicons.StationAi;
using Content.Shared.Speech; using Content.Shared.Speech;
@@ -38,6 +40,7 @@ public sealed class HolopadSystem : SharedHolopadSystem
[Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly PvsOverrideSystem _pvs = default!; [Dependency] private readonly PvsOverrideSystem _pvs = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
private float _updateTimer = 1.0f; private float _updateTimer = 1.0f;
private const float UpdateTime = 1.0f; private const float UpdateTime = 1.0f;
@@ -77,6 +80,8 @@ public sealed class HolopadSystem : SharedHolopadSystem
SubscribeLocalEvent<HolopadComponent, EntRemovedFromContainerMessage>(OnAiRemove); SubscribeLocalEvent<HolopadComponent, EntRemovedFromContainerMessage>(OnAiRemove);
SubscribeLocalEvent<HolopadComponent, EntParentChangedMessage>(OnParentChanged); SubscribeLocalEvent<HolopadComponent, EntParentChangedMessage>(OnParentChanged);
SubscribeLocalEvent<HolopadComponent, PowerChangedEvent>(OnPowerChanged); SubscribeLocalEvent<HolopadComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<HolopadUserComponent, MobStateChangedEvent>(OnMobStateChanged);
} }
#region: Holopad UI bound user interface messages #region: Holopad UI bound user interface messages
@@ -226,7 +231,7 @@ public sealed class HolopadSystem : SharedHolopadSystem
if (!_stationAiSystem.TryGetHeld((receiver, receiverStationAiCore), out var insertedAi)) if (!_stationAiSystem.TryGetHeld((receiver, receiverStationAiCore), out var insertedAi))
continue; continue;
if (_userInterfaceSystem.TryOpenUi(receiverUid, HolopadUiKey.AiRequestWindow, insertedAi)) if (_userInterfaceSystem.TryOpenUi(receiverUid, HolopadUiKey.AiRequestWindow, insertedAi.Value))
LinkHolopadToUser(entity, args.Actor); LinkHolopadToUser(entity, args.Actor);
} }
@@ -446,6 +451,17 @@ public sealed class HolopadSystem : SharedHolopadSystem
UpdateHolopadControlLockoutStartTime(entity); 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 #endregion
public override void Update(float frameTime) public override void Update(float frameTime)
@@ -605,25 +621,23 @@ public sealed class HolopadSystem : SharedHolopadSystem
if (entity.Comp.Hologram != null) if (entity.Comp.Hologram != null)
DeleteHologram(entity.Comp.Hologram.Value, entity); 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 // Return the AI eye to free roaming
if (TryComp<StationAiHeldComponent>(entity.Comp.User, out var stationAiHeld) && _stationAiSystem.SwitchRemoteEntityMode(stationAiCore, true);
_stationAiSystem.TryGetCore(entity.Comp.User.Value, out var stationAiCore))
// If the AI core is still broadcasting, end its calls
if (TryComp<TelephoneComponent>(stationAiCore, out var stationAiCoreTelephone) &&
_telephoneSystem.IsTelephoneEngaged((stationAiCore.Owner, stationAiCoreTelephone)))
{ {
// Return the AI eye to free roaming _telephoneSystem.EndTelephoneCalls((stationAiCore.Owner, stationAiCoreTelephone));
_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));
}
} }
}
UnlinkHolopadFromUser(entity, entity.Comp.User.Value); else
{
UnlinkHolopadFromUser(entity, entity.Comp.User);
} }
Dirty(entity); Dirty(entity);

View File

@@ -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);
}
}

View File

@@ -1,10 +1,34 @@
using Content.Server.Chat.Systems; 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.Chat.Prototypes;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Damage;
using Content.Shared.Destructible;
using Content.Shared.DeviceNetwork.Components; 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.Silicons.StationAi;
using Content.Shared.Speech.Components;
using Content.Shared.StationAi; using Content.Shared.StationAi;
using Content.Shared.Turrets; using Content.Shared.Turrets;
using Content.Shared.Weapons.Ranged.Events; using Content.Shared.Weapons.Ranged.Events;
using Robust.Server.Containers;
using Robust.Shared.Containers;
using Robust.Shared.Map.Components; using Robust.Shared.Map.Components;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
@@ -16,19 +40,300 @@ public sealed class StationAiSystem : SharedStationAiSystem
{ {
[Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly SharedTransformSystem _xforms = 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 HashSet<Entity<StationAiCoreComponent>> _stationAiCores = new();
private readonly ProtoId<ChatNotificationPrototype> _turretIsAttackingChatNotificationPrototype = "TurretIsAttacking"; private readonly ProtoId<ChatNotificationPrototype> _turretIsAttackingChatNotificationPrototype = "TurretIsAttacking";
private readonly ProtoId<ChatNotificationPrototype> _aiWireSnippedChatNotificationPrototype = "AiWireSnipped"; 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() public override void Initialize()
{ {
base.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<ExpandICChatRecipientsEvent>(OnExpandICChatRecipients);
SubscribeLocalEvent<StationAiTurretComponent, AmmoShotEvent>(OnAmmoShot); 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) private void OnExpandICChatRecipients(ExpandICChatRecipientsEvent ev)
{ {
var xformQuery = GetEntityQuery<TransformComponent>(); var xformQuery = GetEntityQuery<TransformComponent>();
@@ -147,7 +452,7 @@ public sealed class StationAiSystem : SharedStationAiSystem
if (!TryGetHeld((stationAiCore, stationAiCore.Comp), out var insertedAi)) if (!TryGetHeld((stationAiCore, stationAiCore.Comp), out var insertedAi))
continue; continue;
hashSet.Add(insertedAi); hashSet.Add(insertedAi.Value);
} }
return hashSet; return hashSet;

View File

@@ -1,8 +1,7 @@
using Content.Server.GameTicking; using Content.Server.GameTicking;
using Content.Server.Spawners.Components; using Content.Server.Spawners.Components;
using Content.Server.Station.Systems; using Content.Server.Station.Systems;
using Content.Shared.Preferences; using Content.Shared.Preferences;
using Content.Shared.Roles;
using Robust.Server.Containers; using Robust.Server.Containers;
using Robust.Shared.Containers; using Robust.Shared.Containers;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
@@ -87,6 +86,9 @@ public sealed class ContainerSpawnPointSystem : EntitySystem
if (!_container.Insert(args.SpawnResult.Value, container, containerXform: xform)) if (!_container.Insert(args.SpawnResult.Value, container, containerXform: xform))
continue; continue;
var ev = new ContainerSpawnEvent(args.SpawnResult.Value);
RaiseLocalEvent(uid, ref ev);
return; return;
} }
@@ -94,3 +96,9 @@ public sealed class ContainerSpawnPointSystem : EntitySystem
args.SpawnResult = null; args.SpawnResult = null;
} }
} }
/// <summary>
/// Raised on a container when a player is spawned into it.
/// </summary>
[ByRefEvent]
public record struct ContainerSpawnEvent(EntityUid Player);

View File

@@ -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);
}
}
}

View File

@@ -1,5 +1,9 @@
using Content.Shared.Holopad; using Content.Shared.Holopad;
using Content.Shared.Mobs;
using Robust.Shared.Player;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using System.Diagnostics.CodeAnalysis;
namespace Content.Shared.Silicons.StationAi; namespace Content.Shared.Silicons.StationAi;
@@ -8,9 +12,15 @@ public abstract partial class SharedStationAiSystem
private ProtoId<StationAiCustomizationGroupPrototype> _stationAiCoreCustomGroupProtoId = "StationAiCoreIconography"; private ProtoId<StationAiCustomizationGroupPrototype> _stationAiCoreCustomGroupProtoId = "StationAiCoreIconography";
private ProtoId<StationAiCustomizationGroupPrototype> _stationAiHologramCustomGroupProtoId = "StationAiHolograms"; private ProtoId<StationAiCustomizationGroupPrototype> _stationAiHologramCustomGroupProtoId = "StationAiHolograms";
private readonly SpriteSpecifier.Rsi _stationAiRebooting = new(new ResPath("Mobs/Silicon/station_ai.rsi"), "ai_fuzz");
private void InitializeCustomization() private void InitializeCustomization()
{ {
SubscribeLocalEvent<StationAiCoreComponent, StationAiCustomizationMessage>(OnStationAiCustomization); 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) private void OnStationAiCustomization(Entity<StationAiCoreComponent> entity, ref StationAiCustomizationMessage args)
@@ -29,17 +39,53 @@ public abstract partial class SharedStationAiSystem
stationAiCustomization.ProtoIds[args.GroupProtoId] = args.CustomizationProtoId; stationAiCustomization.ProtoIds[args.GroupProtoId] = args.CustomizationProtoId;
Dirty(held, stationAiCustomization); Dirty(held.Value, stationAiCustomization);
// Update hologram // Update hologram
if (groupPrototype.Category == StationAiCustomizationType.Hologram) if (groupPrototype.Category == StationAiCustomizationType.Hologram)
UpdateHolographicAvatar((held, stationAiCustomization)); UpdateHolographicAvatar((held.Value, stationAiCustomization));
// Update core iconography // Update core iconography
if (groupPrototype.Category == StationAiCustomizationType.CoreIconography && TryComp<StationAiHolderComponent>(entity, out var stationAiHolder)) if (groupPrototype.Category == StationAiCustomizationType.CoreIconography && TryComp<StationAiHolderComponent>(entity, out var stationAiHolder))
UpdateAppearance((entity, 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) private void UpdateHolographicAvatar(Entity<StationAiCustomizationComponent> entity)
{ {
if (!TryComp<HolographicAvatarComponent>(entity, out var avatar)) if (!TryComp<HolographicAvatarComponent>(entity, out var avatar))
@@ -62,21 +108,36 @@ public abstract partial class SharedStationAiSystem
{ {
var stationAi = GetInsertedAI(entity); var stationAi = GetInsertedAI(entity);
if (stationAi == null)
{
_appearance.RemoveData(entity.Owner, StationAiVisualState.Key);
return;
}
if (!TryComp<StationAiCustomizationComponent>(stationAi, out var stationAiCustomization) || if (!TryComp<StationAiCustomizationComponent>(stationAi, out var stationAiCustomization) ||
!stationAiCustomization.ProtoIds.TryGetValue(_stationAiCoreCustomGroupProtoId, out var protoId) || !TryGetCustomizedAppearanceData((stationAi.Value, stationAiCustomization), out var layerData) ||
!_protoManager.Resolve(protoId, out var prototype) || !layerData.TryGetValue(state.ToString(), out var stateData))
!prototype.LayerData.TryGetValue(state.ToString(), out var layerData))
{ {
return; return;
} }
// This data is handled manually in the client StationAiSystem // This data is handled manually in the client StationAiSystem
_appearance.SetData(entity.Owner, StationAiVisualState.Key, layerData); _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 false;
}
layerData = prototype.LayerData;
return true;
} }
} }

View File

@@ -5,6 +5,7 @@ using Content.Shared.Popups;
using Content.Shared.Verbs; using Content.Shared.Verbs;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
using Robust.Shared.Utility; using Robust.Shared.Utility;
using System.Diagnostics.CodeAnalysis;
namespace Content.Shared.Silicons.StationAi; namespace Content.Shared.Silicons.StationAi;
@@ -26,6 +27,7 @@ public abstract partial class SharedStationAiSystem
SubscribeLocalEvent<StationAiHeldComponent, InteractionAttemptEvent>(OnHeldInteraction); SubscribeLocalEvent<StationAiHeldComponent, InteractionAttemptEvent>(OnHeldInteraction);
SubscribeLocalEvent<StationAiHeldComponent, AttemptRelayActionComponentChangeEvent>(OnHeldRelay); SubscribeLocalEvent<StationAiHeldComponent, AttemptRelayActionComponentChangeEvent>(OnHeldRelay);
SubscribeLocalEvent<StationAiHeldComponent, JumpToCoreEvent>(OnCoreJump); SubscribeLocalEvent<StationAiHeldComponent, JumpToCoreEvent>(OnCoreJump);
SubscribeLocalEvent<TryGetIdentityShortInfoEvent>(OnTryGetIdentityShortInfo); SubscribeLocalEvent<TryGetIdentityShortInfoEvent>(OnTryGetIdentityShortInfo);
} }
@@ -49,20 +51,23 @@ public abstract partial class SharedStationAiSystem
if (!TryGetCore(ent.Owner, out var core) || core.Comp?.RemoteEntity == null) if (!TryGetCore(ent.Owner, out var core) || core.Comp?.RemoteEntity == null)
return; return;
_xforms.DropNextTo(core.Comp.RemoteEntity.Value, core.Owner) ; _xforms.DropNextTo(core.Comp.RemoteEntity.Value, core.Owner);
} }
/// <summary> /// <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> /// </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; held = EntityUid.Invalid;
if (!Resolve(entity.Owner, ref entity.Comp)) if (!Resolve(entity.Owner, ref entity.Comp))
return false; 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) container.ContainedEntities.Count == 0)
return false; return false;
@@ -70,26 +75,32 @@ public abstract partial class SharedStationAiSystem
return true; return true;
} }
/// <summary>
/// Tries to get the entity held in the AI using StationAiHolder.
/// </summary>
public bool TryGetHeld(Entity<StationAiHolderComponent?> entity, out EntityUid held)
{
TryComp<StationAiCoreComponent>(entity.Owner, out var stationAiCore);
return TryGetHeld((entity.Owner, stationAiCore), out held); /// <summary>
/// Tries to find an AI being held in by an entity using <see cref="StationAiCoreComponent"/>.
/// </summary>
/// <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)
{
held = null;
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) public bool TryGetCore(EntityUid entity, out Entity<StationAiCoreComponent?> core)
{ {
var xform = Transform(entity); if (!_containers.TryGetContainingContainer(entity, out var container) ||
var meta = MetaData(entity);
var ent = new Entity<TransformComponent?, MetaDataComponent?>(entity, xform, meta);
if (!_containers.TryGetContainingContainer(ent, out var container) ||
container.ID != StationAiCoreComponent.Container || container.ID != StationAiCoreComponent.Container ||
!TryComp(container.Owner, out StationAiCoreComponent? coreComp) || !TryComp(container.Owner, out StationAiCoreComponent? coreComp))
coreComp.RemoteEntity == null)
{ {
core = (EntityUid.Invalid, null); core = (EntityUid.Invalid, null);
return false; return false;

View File

@@ -4,6 +4,7 @@ using Content.Shared.Administration.Managers;
using Content.Shared.Chat.Prototypes; using Content.Shared.Chat.Prototypes;
using Content.Shared.Containers.ItemSlots; using Content.Shared.Containers.ItemSlots;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.Destructible;
using Content.Shared.Doors.Systems; using Content.Shared.Doors.Systems;
using Content.Shared.DoAfter; using Content.Shared.DoAfter;
using Content.Shared.Electrocution; using Content.Shared.Electrocution;
@@ -11,11 +12,14 @@ using Content.Shared.Intellicard;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Item.ItemToggle; using Content.Shared.Item.ItemToggle;
using Content.Shared.Mind; using Content.Shared.Mind;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Systems;
using Content.Shared.Movement.Components; using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems; using Content.Shared.Movement.Systems;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Power; using Content.Shared.Power;
using Content.Shared.Power.EntitySystems; using Content.Shared.Power.EntitySystems;
using Content.Shared.Repairable;
using Content.Shared.StationAi; using Content.Shared.StationAi;
using Content.Shared.Verbs; using Content.Shared.Verbs;
using Robust.Shared.Audio.Systems; using Robust.Shared.Audio.Systems;
@@ -28,36 +32,36 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Robust.Shared.Utility; using Robust.Shared.Utility;
using System.Diagnostics.CodeAnalysis;
namespace Content.Shared.Silicons.StationAi; namespace Content.Shared.Silicons.StationAi;
public abstract partial class SharedStationAiSystem : EntitySystem public abstract partial class SharedStationAiSystem : EntitySystem
{ {
[Dependency] private readonly ISharedAdminManager _admin = default!; [Dependency] private readonly ISharedAdminManager _admin = default!;
[Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly INetManager _net = default!; [Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly ItemSlotsSystem _slots = default!; [Dependency] private readonly ItemSlotsSystem _slots = default!;
[Dependency] private readonly ItemToggleSystem _toggles = default!; [Dependency] private readonly ItemToggleSystem _toggles = default!;
[Dependency] private readonly ActionBlockerSystem _blocker = default!; [Dependency] private readonly ActionBlockerSystem _blocker = default!;
[Dependency] private readonly MetaDataSystem _metadata = default!; [Dependency] private readonly MetaDataSystem _metadata = default!;
[Dependency] private readonly SharedAirlockSystem _airlocks = default!; [Dependency] private readonly SharedAirlockSystem _airlocks = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedContainerSystem _containers = default!; [Dependency] private readonly SharedContainerSystem _containers = default!;
[Dependency] private readonly SharedDoorSystem _doors = default!; [Dependency] private readonly SharedDoorSystem _doors = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedElectrocutionSystem _electrify = default!; [Dependency] private readonly SharedElectrocutionSystem _electrify = default!;
[Dependency] private readonly SharedEyeSystem _eye = default!; [Dependency] private readonly SharedEyeSystem _eye = default!;
[Dependency] protected readonly SharedMapSystem Maps = default!; [Dependency] protected readonly SharedMapSystem Maps = default!;
[Dependency] private readonly SharedMindSystem _mind = default!; [Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly SharedMoverController _mover = default!; [Dependency] private readonly SharedMoverController _mover = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedPowerReceiverSystem PowerReceiver = default!; [Dependency] private readonly SharedPowerReceiverSystem PowerReceiver = default!;
[Dependency] private readonly SharedTransformSystem _xforms = default!; [Dependency] private readonly SharedTransformSystem _xforms = default!;
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!; [Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly StationAiVisionSystem _vision = default!; [Dependency] private readonly StationAiVisionSystem _vision = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!; [Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
// StationAiHeld is added to anything inside of an AI core. // StationAiHeld is added to anything inside of an AI core.
// StationAiHolder indicates it can hold an AI positronic brain (e.g. holocard / 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 static readonly EntProtoId DefaultAi = "StationAiBrain";
private readonly ProtoId<ChatNotificationPrototype> _downloadChatNotificationPrototype = "IntellicardDownload"; private readonly ProtoId<ChatNotificationPrototype> _downloadChatNotificationPrototype = "IntellicardDownload";
private const float MaxVisionMultiplier = 5f;
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
@@ -102,10 +104,12 @@ public abstract partial class SharedStationAiSystem : EntitySystem
SubscribeLocalEvent<StationAiCoreComponent, EntInsertedIntoContainerMessage>(OnAiInsert); SubscribeLocalEvent<StationAiCoreComponent, EntInsertedIntoContainerMessage>(OnAiInsert);
SubscribeLocalEvent<StationAiCoreComponent, EntRemovedFromContainerMessage>(OnAiRemove); SubscribeLocalEvent<StationAiCoreComponent, EntRemovedFromContainerMessage>(OnAiRemove);
SubscribeLocalEvent<StationAiCoreComponent, MapInitEvent>(OnAiMapInit);
SubscribeLocalEvent<StationAiCoreComponent, ComponentShutdown>(OnAiShutdown); SubscribeLocalEvent<StationAiCoreComponent, ComponentShutdown>(OnAiShutdown);
SubscribeLocalEvent<StationAiCoreComponent, PowerChangedEvent>(OnCorePower); SubscribeLocalEvent<StationAiCoreComponent, PowerChangedEvent>(OnCorePower);
SubscribeLocalEvent<StationAiCoreComponent, GetVerbsEvent<Verb>>(OnCoreVerbs); SubscribeLocalEvent<StationAiCoreComponent, GetVerbsEvent<Verb>>(OnCoreVerbs);
SubscribeLocalEvent<StationAiCoreComponent, BreakageEventArgs>(OnBroken);
SubscribeLocalEvent<StationAiCoreComponent, RepairedEvent>(OnRepaired);
} }
private void OnCoreVerbs(Entity<StationAiCoreComponent> ent, ref GetVerbsEvent<Verb> args) 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() args.Verbs.Add(new Verb()
{ {
Text = Loc.GetString("station-ai-customization-menu"), 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")), 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)) if (!TryComp(args.Used, out IntellicardComponent? intelliComp))
return; return;
var cardHasAi = _slots.CanEject(ent.Owner, args.User, ent.Comp.Slot); var cardHasAi = ent.Comp.Slot.Item != null;
var coreHasAi = _slots.CanEject(args.Target.Value, args.User, targetHolder.Slot); var coreHasAi = targetHolder.Slot.Item != null;
if (cardHasAi && coreHasAi) if (cardHasAi && coreHasAi)
{ {
@@ -290,7 +294,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem
if (TryGetHeld((args.Target.Value, targetHolder), out var held)) if (TryGetHeld((args.Target.Value, targetHolder), out var held))
{ {
var ev = new ChatNotificationEvent(_downloadChatNotificationPrototype, args.Used, args.User); 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) 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, BreakOnDamage = true,
BreakOnMove = true, BreakOnMove = true,
NeedHand = true, NeedHand = true,
BreakOnDropItem = true BreakOnDropItem = true,
AttemptFrequency = AttemptFrequency.EveryTick,
}; };
_doAfter.TryStartDoAfter(doAfterArgs); _doAfter.TryStartDoAfter(doAfterArgs);
@@ -327,7 +332,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem
private void OnHolderMapInit(Entity<StationAiHolderComponent> ent, ref MapInitEvent args) private void OnHolderMapInit(Entity<StationAiHolderComponent> ent, ref MapInitEvent args)
{ {
UpdateAppearance(ent.Owner); UpdateAppearance((ent.Owner, ent.Comp));
} }
private void OnAiShutdown(Entity<StationAiCoreComponent> ent, ref ComponentShutdown args) private void 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) 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 (args.Powered)
{ {
if (!SetupEye(ent)) KillHeldAi(ent);
return;
AttachEye(ent);
}
else
{
ClearEye(ent);
} }
} }
private void OnAiMapInit(Entity<StationAiCoreComponent> ent, ref MapInitEvent args) private void OnBroken(Entity<StationAiCoreComponent> ent, ref BreakageEventArgs args)
{ {
SetupEye(ent); KillHeldAi(ent);
AttachEye(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) public void SwitchRemoteEntityMode(Entity<StationAiCoreComponent?> entity, bool isRemote)
@@ -395,7 +408,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem
_eye.SetDrawFov(user.Value, !isRemote); _eye.SetDrawFov(user.Value, !isRemote);
} }
private bool SetupEye(Entity<StationAiCoreComponent> ent, EntityCoordinates? coords = null) protected bool SetupEye(Entity<StationAiCoreComponent> ent, EntityCoordinates? coords = null)
{ {
if (_net.IsClient) if (_net.IsClient)
return false; return false;
@@ -420,7 +433,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem
return true; return true;
} }
private void ClearEye(Entity<StationAiCoreComponent> ent) protected void ClearEye(Entity<StationAiCoreComponent> ent)
{ {
if (_net.IsClient) if (_net.IsClient)
return; return;
@@ -428,9 +441,16 @@ public abstract partial class SharedStationAiSystem : EntitySystem
QueueDel(ent.Comp.RemoteEntity); QueueDel(ent.Comp.RemoteEntity);
ent.Comp.RemoteEntity = null; ent.Comp.RemoteEntity = null;
Dirty(ent); 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) if (ent.Comp.RemoteEntity == null)
return; return;
@@ -467,7 +487,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem
return container.ContainedEntities[0]; 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) if (args.Container.ID != StationAiCoreComponent.Container)
return; return;
@@ -475,17 +495,21 @@ public abstract partial class SharedStationAiSystem : EntitySystem
if (_timing.ApplyingState) if (_timing.ApplyingState)
return; return;
ClearEye(ent);
ent.Comp.Remote = true; ent.Comp.Remote = true;
SetupEye(ent);
// Just so text and the likes works properly // Just so text and the likes works properly
_metadata.SetEntityName(ent.Owner, MetaData(args.Entity).EntityName); _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) if (_timing.ApplyingState)
return; return;
@@ -506,26 +530,49 @@ public abstract partial class SharedStationAiSystem : EntitySystem
ClearEye(ent); ClearEye(ent);
} }
private void UpdateAppearance(Entity<StationAiHolderComponent?> entity) protected void UpdateAppearance(Entity<StationAiHolderComponent?> entity)
{ {
if (!Resolve(entity.Owner, ref entity.Comp, false)) if (!Resolve(entity.Owner, ref entity.Comp, false))
return; return;
// Todo: when AIs can die, add a check to see if the AI is in the 'dead' state
var state = StationAiState.Empty; var state = StationAiState.Empty;
if (_containers.TryGetContainer(entity.Owner, StationAiHolderComponent.Container, out var container) && container.Count > 0) // Get what visual state the held AI holder is in
state = StationAiState.Occupied; if (TryGetHeld(entity, out var stationAi) &&
TryComp<StationAiCustomizationComponent>(stationAi, out var customization))
// If the entity is a station AI core, attempt to customize its appearance
if (TryComp<StationAiCoreComponent>(entity, out var stationAiCore))
{ {
CustomizeAppearance((entity, stationAiCore), state); 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; return;
} }
// Otherwise let generic visualizers handle the appearance update // The AI core is empty
_appearance.SetData(entity.Owner, StationAiVisualState.Key, state); if (state == StationAiState.Empty)
{
_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 attempt to set the AI core's appearance
CustomizeAppearance((entity, stationAiCore), state);
} }
public virtual bool SetVisionEnabled(Entity<StationAiVisionComponent> entity, bool enabled, bool announce = false) public 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; public sealed partial class IntellicardDoAfterEvent : SimpleDoAfterEvent;
[Serializable, NetSerializable] [Serializable, NetSerializable]
public enum StationAiVisualState : byte public enum StationAiVisualLayers : byte
{ {
Key, Base,
Icon,
} }
[Serializable, NetSerializable] [Serializable, NetSerializable]
public enum StationAiSpriteState : byte public enum StationAiVisuals : byte
{ {
Key, Broken,
} }
[Serializable, NetSerializable] [Serializable, NetSerializable]
@@ -590,5 +638,6 @@ public enum StationAiState : byte
Empty, Empty,
Occupied, Occupied,
Dead, Dead,
Rebooting,
Hologram, Hologram,
} }

View File

@@ -38,11 +38,19 @@ public sealed partial class StationAiCoreComponent : Component
[DataField(readOnly: true)] [DataField(readOnly: true)]
public EntProtoId? PhysicalEntityProto = "StationAiHoloLocal"; 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"; 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> /// <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> /// </summary>
/// <param name="NewRemoteEntity">The entity UID of the replacement entity</param> /// <param name="NewRemoteEntity">The entity UID of the replacement entity</param>
[ByRefEvent] [ByRefEvent]

View File

@@ -15,6 +15,12 @@ public sealed partial class StationAiCustomizationComponent : Component
/// </summary> /// </summary>
[DataField, AutoNetworkedField] [DataField, AutoNetworkedField]
public Dictionary<ProtoId<StationAiCustomizationGroupPrototype>, ProtoId<StationAiCustomizationPrototype>> ProtoIds = new(); public Dictionary<ProtoId<StationAiCustomizationGroupPrototype>, ProtoId<StationAiCustomizationPrototype>> ProtoIds = new();
/// <summary>
/// The current visual state of the associated entity.
/// </summary>
[DataField, AutoNetworkedField]
public StationAiState State = StationAiState.Occupied;
} }
/// <summary> /// <summary>
@@ -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> /// <summary>
/// Key for opening the station AI customization UI /// Key for opening the station AI customization UI
/// </summary> /// </summary>

View File

@@ -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,
}

View File

@@ -14,6 +14,7 @@ generic-invalid = invalid
generic-hours = hours generic-hours = hours
generic-minutes = minutes generic-minutes = minutes
generic-seconds = seconds
generic-playtime-title = Playtime generic-playtime-title = Playtime

View File

@@ -5,3 +5,4 @@ construction-graph-component-second-flash = second flash
construction-graph-component-power-cell = power cell construction-graph-component-power-cell = power cell
construction-graph-component-apc-electronics = APC electronics construction-graph-component-apc-electronics = APC electronics
construction-graph-component-payload-trigger = trigger construction-graph-component-payload-trigger = trigger
construction-graph-component-borg-brain = MMI or positronic brain

View 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-door-electronics-circuit-board = door electronics circuit board
construction-graph-tag-firelock-electronics-circuit-board = firelock 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-conveyor-belt-assembly = conveyor belt assembly
construction-graph-tag-station-ai-core-electronics = station AI core electronics
# tools # tools
construction-graph-tag-multitool = a multitool construction-graph-tag-multitool = a multitool

View File

@@ -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?

View File

@@ -4,6 +4,10 @@ wire-name-ai-vision-light = AIV
wire-name-ai-act-light = AIA wire-name-ai-act-light = AIA
station-ai-takeover = AI takeover station-ai-takeover = AI takeover
station-ai-eye-name = AI eye - {$name} 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 # Radial actions
ai-open = Open actions ai-open = Open actions

View File

@@ -47,3 +47,13 @@
cost: 2000 cost: 2000
category: cargoproduct-category-name-science category: cargoproduct-category-name-science
group: market 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

View File

@@ -24,3 +24,19 @@
- id: CrewMonitoringServerFlatpack - id: CrewMonitoringServerFlatpack
- id: CrewMonitoringComputerFlatpack - id: CrewMonitoringComputerFlatpack
amount: 3 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

View File

@@ -282,6 +282,7 @@
- id: ProtolatheMachineCircuitboard - id: ProtolatheMachineCircuitboard
- id: ResearchComputerCircuitboard - id: ResearchComputerCircuitboard
- id: CargoRequestScienceComputerCircuitboard - id: CargoRequestScienceComputerCircuitboard
- id: StationAiFixerCircuitboard
- id: RubberStampRd - id: RubberStampRd
# Hardsuit table, used for suit storage as well # Hardsuit table, used for suit storage as well

View File

@@ -19,3 +19,17 @@
color: Pink color: Pink
nextDelay: 12 nextDelay: 12
notifyBySource: true 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

View File

@@ -144,10 +144,12 @@
- type: Appearance - type: Appearance
- type: GenericVisualizer - type: GenericVisualizer
visuals: visuals:
enum.StationAiVisualState.Key: enum.StationAiVisualLayers.Icon:
unshaded: unshaded:
Empty: { state: empty } Empty: { state: empty }
Occupied: { state: full } Occupied: { state: full }
Rebooting: { state: dead }
Dead: { state: dead }
- type: Intellicard - type: Intellicard
- type: entity - type: entity
@@ -161,6 +163,7 @@
- state: ai - state: ai
shader: unshaded shader: unshaded
# Empty AI core
- type: entity - type: entity
id: PlayerStationAiEmpty id: PlayerStationAiEmpty
name: AI Core name: AI Core
@@ -178,23 +181,69 @@
blacklist: blacklist:
tags: tags:
- GhostOnlyWarp - 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 - type: ContainerComp
proto: AiHeld proto: AiHeld
container: station_ai_mind_slot container: station_ai_mind_slot
- type: Damageable
damageModifierSet: StrongMetallic
- type: Repairable
doAfterDelay: 10
allowSelfRepair: false
- type: Destructible - type: Destructible
thresholds: thresholds:
- trigger: - trigger:
!type:DamageTrigger !type:DamageTrigger
damage: 100 damage: 400
behaviors: behaviors:
- !type:PlaySoundBehavior - !type:PlaySoundBehavior
sound: sound:
collection: MetalBreak collection: MetalBreak
- !type:DoActsBehavior - !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 - type: ApcPowerReceiver
powerLoad: 1000 powerLoad: 500
needsPower: false - 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: StationAiCore
- type: StationAiVision - type: StationAiVision
- type: InteractionOutline - type: InteractionOutline
@@ -204,12 +253,26 @@
layers: layers:
- state: base - state: base
- state: ai_empty - state: ai_empty
map: ["enum.StationAiVisualLayers.Base"]
shader: unshaded shader: unshaded
- state: ai - state: ai
map: ["enum.StationAiVisualState.Key"] map: ["enum.StationAiVisualLayers.Icon"]
shader: unshaded shader: unshaded
visible: false visible: false
- state: ai_unpowered
map: ["enum.PowerDeviceVisualLayers.Powered"]
visible: false
- type: Appearance - 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 - type: InteractionPopup
interactSuccessString: petting-success-station-ai interactSuccessString: petting-success-station-ai
interactFailureString: petting-failure-station-ai interactFailureString: petting-failure-station-ai
@@ -234,7 +297,22 @@
type: HolopadBoundUserInterface type: HolopadBoundUserInterface
enum.StationAiCustomizationUiKey.Key: enum.StationAiCustomizationUiKey.Key:
type: StationAiCustomizationBoundUserInterface 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. # The job-ready version of an AI spawn.
- type: entity - type: entity
id: PlayerStationAi id: PlayerStationAi
@@ -245,6 +323,77 @@
containerId: station_ai_mind_slot containerId: station_ai_mind_slot
job: StationAi 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 # The actual brain inside the core
- type: entity - type: entity
id: StationAiBrain id: StationAiBrain
@@ -254,8 +403,6 @@
- type: Sprite - type: Sprite
# Once it's in a core it's pretty much an abstract entity at that point. # Once it's in a core it's pretty much an abstract entity at that point.
visible: false visible: false
- type: BlockMovement
blockInteraction: false
- type: SiliconLawProvider - type: SiliconLawProvider
laws: Crewsimov laws: Crewsimov
- type: SiliconLawBound - type: SiliconLawBound
@@ -277,9 +424,16 @@
drawFov: false drawFov: false
- type: Examiner - type: Examiner
- type: InputMover - type: InputMover
- type: BlockMovement
blockInteraction: false
- type: GhostOnMove
mustBeDead: true
- type: Speech - type: Speech
speechVerb: Robotic speechVerb: Robotic
speechSounds: Borg speechSounds: Borg
- type: DamagedSiliconAccent
startPowerCorruptionAtCharIdx: 4
maxPowerCorruptionAtCharIdx: 20
- type: Tag - type: Tag
tags: tags:
- HideContextMenu - HideContextMenu

View File

@@ -554,9 +554,20 @@
parent: BaseComputerCircuitboard parent: BaseComputerCircuitboard
id: StationAiUploadCircuitboard id: StationAiUploadCircuitboard
name: AI upload console board 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: components:
- type: Sprite - type: Sprite
state: cpu_science state: cpu_science
- type: ComputerBoard - type: ComputerBoard
prototype: StationAiUploadComputer 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

View File

@@ -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

View File

@@ -115,9 +115,11 @@
proto: robot proto: robot
- type: Speech - type: Speech
speechSounds: Pai speechSounds: Pai
- type: Alerts
- type: MobState - type: MobState
allowedStates: allowedStates:
- Alive - Alive
- Dead
- type: Appearance - type: Appearance
- type: Tag - type: Tag
tags: tags:

View File

@@ -125,7 +125,7 @@
fireCost: 100 fireCost: 100
- type: Battery - type: Battery
maxCharge: 2000 maxCharge: 2000
startingCharge: 0 startingCharge: 2000
- type: ApcPowerReceiverBattery - type: ApcPowerReceiverBattery
idleLoad: 5 idleLoad: 5
batteryRechargeRate: 200 batteryRechargeRate: 200
@@ -136,3 +136,5 @@
- type: HTN - type: HTN
rootTask: rootTask:
task: EnergyTurretCompound task: EnergyTurretCompound
- type: StaticPrice
price: 200

View File

@@ -1636,3 +1636,86 @@
containers: containers:
circuit_holder: !type:ContainerSlot circuit_holder: !type:ContainerSlot
board: !type:Container 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

View File

@@ -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

View File

@@ -1315,3 +1315,16 @@
canBuildInImpassable: false canBuildInImpassable: false
conditions: conditions:
- !type:TileNotBlocked - !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

View File

@@ -1343,6 +1343,9 @@
- type: Tag - type: Tag
id: StationAi id: StationAi
- type: Tag
id: StationAiCoreElectronics
- type: Tag - type: Tag
id: StationMapElectronics id: StationMapElectronics

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 B

View File

@@ -195,6 +195,9 @@
{ {
"name": "ai_dead" "name": "ai_dead"
}, },
{
"name": "ai_unpowered"
},
{ {
"name": "ai_empty", "name": "ai_empty",
"delays": [ "delays": [
@@ -204,12 +207,52 @@
] ]
] ]
}, },
{
"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", "name": "default",
"directions": 4 "directions": 4
}, },
{ {
"name": "base" "name": "base"
},
{
"name": "frame_0"
},
{
"name": "frame_1"
},
{
"name": "frame_2"
},
{
"name": "frame_3"
},
{
"name": "frame_3b"
},
{
"name": "frame_4"
} }
] ]
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 B

View File

@@ -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
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

View File

@@ -63,6 +63,15 @@
] ]
] ]
}, },
{
"name": "dead",
"delays": [
[
0.4,
0.4
]
]
},
{ {
"name": "full", "name": "full",
"delays": [ "delays": [

View File

@@ -7,6 +7,9 @@
"y": 32 "y": 32
}, },
"states": [ "states": [
{
"name": "mmi_icon"
},
{ {
"name": "mmi_off" "name": "mmi_off"
}, },

Binary file not shown.

After

Width:  |  Height:  |  Size: 912 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 B

After

Width:  |  Height:  |  Size: 621 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 371 B

After

Width:  |  Height:  |  Size: 909 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 594 B

After

Width:  |  Height:  |  Size: 921 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 635 B

After

Width:  |  Height:  |  Size: 569 B

View File

@@ -1,7 +1,7 @@
{ {
"version": 1, "version": 1,
"license": "CC-BY-SA-3.0", "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": { "size": {
"x": 32, "x": 32,
"y": 32 "y": 32
@@ -75,6 +75,246 @@
] ]
] ]
}, },
{
"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", "name": "aiupload",
"directions": 4, "directions": 4,