diff --git a/Content.Client/Silicons/StationAi/StationAiFixerConsoleBoundUserInterface.cs b/Content.Client/Silicons/StationAi/StationAiFixerConsoleBoundUserInterface.cs new file mode 100644 index 0000000000..63183c2334 --- /dev/null +++ b/Content.Client/Silicons/StationAi/StationAiFixerConsoleBoundUserInterface.cs @@ -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(); + _window.SetOwner(Owner); + + _window.SendStationAiFixerConsoleMessageAction += SendStationAiFixerConsoleMessage; + _window.OpenConfirmationDialogAction += OpenConfirmationDialog; + } + + public override void Update() + { + base.Update(); + _window?.UpdateState(); + } + + private void OpenConfirmationDialog() + { + if (_window == null) + return; + + _window.ConfirmationDialog?.Close(); + _window.ConfirmationDialog = new StationAiFixerConsoleConfirmationDialog(); + _window.ConfirmationDialog.OpenCentered(); + _window.ConfirmationDialog.SendStationAiFixerConsoleMessageAction += SendStationAiFixerConsoleMessage; + } + + private void SendStationAiFixerConsoleMessage(StationAiFixerConsoleAction action) + { + SendPredictedMessage(new StationAiFixerConsoleMessage(action)); + } +} diff --git a/Content.Client/Silicons/StationAi/StationAiFixerConsoleConfirmationDialog.xaml b/Content.Client/Silicons/StationAi/StationAiFixerConsoleConfirmationDialog.xaml new file mode 100644 index 0000000000..fa61d614e0 --- /dev/null +++ b/Content.Client/Silicons/StationAi/StationAiFixerConsoleConfirmationDialog.xaml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Content.Client/Silicons/StationAi/StationAiFixerConsoleWindow.xaml.cs b/Content.Client/Silicons/StationAi/StationAiFixerConsoleWindow.xaml.cs new file mode 100644 index 0000000000..0c3140a13e --- /dev/null +++ b/Content.Client/Silicons/StationAi/StationAiFixerConsoleWindow.xaml.cs @@ -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? SendStationAiFixerConsoleMessageAction; + public event Action? OpenConfirmationDialogAction; + + public StationAiFixerConsoleConfirmationDialog? ConfirmationDialog; + + private readonly Dictionary _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(); + _stationAi = _entManager.System(); + + 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(_owner, out var stationAiFixerConsole)) + return; + + var ent = (_owner.Value, stationAiFixerConsole); + var isLocked = _entManager.TryGetComponent(_owner, out var lockable) && lockable.Locked; + + var stationAiHolderInserted = _stationAiFixerConsole.IsStationAiHolderInserted((_owner.Value, stationAiFixerConsole)); + var stationAi = stationAiFixerConsole.ActionTarget; + var stationAiState = StationAiState.Empty; + + if (_entManager.TryGetComponent(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 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(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(_owner, out var stationAiFixerConsole)) + return; + + UpdateProgressBar((_owner.Value, stationAiFixerConsole)); + } +} diff --git a/Content.Client/Silicons/StationAi/StationAiSystem.cs b/Content.Client/Silicons/StationAi/StationAiSystem.cs index 9b0a9fb7ea..d4a8b9dbd8 100644 --- a/Content.Client/Silicons/StationAi/StationAiSystem.cs +++ b/Content.Client/Silicons/StationAi/StationAiSystem.cs @@ -81,10 +81,10 @@ public sealed partial class StationAiSystem : SharedStationAiSystem if (args.Sprite == null) return; - if (_appearance.TryGetData(entity.Owner, StationAiVisualState.Key, out var layerData, args.Component)) - _sprite.LayerSetData((entity.Owner, args.Sprite), StationAiVisualState.Key, layerData); + if (_appearance.TryGetData(entity.Owner, StationAiVisualLayers.Icon, out var layerData, args.Component)) + _sprite.LayerSetData((entity.Owner, args.Sprite), StationAiVisualLayers.Icon, layerData); - _sprite.LayerSetVisible((entity.Owner, args.Sprite), StationAiVisualState.Key, layerData != null); + _sprite.LayerSetVisible((entity.Owner, args.Sprite), StationAiVisualLayers.Icon, layerData != null); } public override void Shutdown() diff --git a/Content.Server/Holopad/HolopadSystem.cs b/Content.Server/Holopad/HolopadSystem.cs index 884fb3ae71..0cba4824db 100644 --- a/Content.Server/Holopad/HolopadSystem.cs +++ b/Content.Server/Holopad/HolopadSystem.cs @@ -8,6 +8,8 @@ using Content.Shared.Chat.TypingIndicator; using Content.Shared.Holopad; using Content.Shared.IdentityManagement; using Content.Shared.Labels.Components; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Systems; using Content.Shared.Power; using Content.Shared.Silicons.StationAi; using Content.Shared.Speech; @@ -38,6 +40,7 @@ public sealed class HolopadSystem : SharedHolopadSystem [Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly PvsOverrideSystem _pvs = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; private float _updateTimer = 1.0f; private const float UpdateTime = 1.0f; @@ -77,6 +80,8 @@ public sealed class HolopadSystem : SharedHolopadSystem SubscribeLocalEvent(OnAiRemove); SubscribeLocalEvent(OnParentChanged); SubscribeLocalEvent(OnPowerChanged); + SubscribeLocalEvent(OnMobStateChanged); + } #region: Holopad UI bound user interface messages @@ -226,7 +231,7 @@ public sealed class HolopadSystem : SharedHolopadSystem if (!_stationAiSystem.TryGetHeld((receiver, receiverStationAiCore), out var insertedAi)) continue; - if (_userInterfaceSystem.TryOpenUi(receiverUid, HolopadUiKey.AiRequestWindow, insertedAi)) + if (_userInterfaceSystem.TryOpenUi(receiverUid, HolopadUiKey.AiRequestWindow, insertedAi.Value)) LinkHolopadToUser(entity, args.Actor); } @@ -446,6 +451,17 @@ public sealed class HolopadSystem : SharedHolopadSystem UpdateHolopadControlLockoutStartTime(entity); } + private void OnMobStateChanged(Entity ent, ref MobStateChangedEvent args) + { + if (!HasComp(ent)) + return; + + foreach (var holopad in ent.Comp.LinkedHolopads) + { + ShutDownHolopad(holopad); + } + } + #endregion public override void Update(float frameTime) @@ -605,25 +621,23 @@ public sealed class HolopadSystem : SharedHolopadSystem if (entity.Comp.Hologram != null) DeleteHologram(entity.Comp.Hologram.Value, entity); - if (entity.Comp.User != null) + // Check if the associated holopad user is an AI + if (HasComp(entity.Comp.User) && + _stationAiSystem.TryGetCore(entity.Comp.User.Value, out var stationAiCore)) { - // Check if the associated holopad user is an AI - if (TryComp(entity.Comp.User, out var stationAiHeld) && - _stationAiSystem.TryGetCore(entity.Comp.User.Value, out var stationAiCore)) + // Return the AI eye to free roaming + _stationAiSystem.SwitchRemoteEntityMode(stationAiCore, true); + + // If the AI core is still broadcasting, end its calls + if (TryComp(stationAiCore, out var stationAiCoreTelephone) && + _telephoneSystem.IsTelephoneEngaged((stationAiCore.Owner, stationAiCoreTelephone))) { - // Return the AI eye to free roaming - _stationAiSystem.SwitchRemoteEntityMode(stationAiCore, true); - - // If the AI core is still broadcasting, end its calls - if (entity.Owner != stationAiCore.Owner && - TryComp(stationAiCore, out var stationAiCoreTelephone) && - _telephoneSystem.IsTelephoneEngaged((stationAiCore.Owner, stationAiCoreTelephone))) - { - _telephoneSystem.EndTelephoneCalls((stationAiCore.Owner, stationAiCoreTelephone)); - } + _telephoneSystem.EndTelephoneCalls((stationAiCore.Owner, stationAiCoreTelephone)); } - - UnlinkHolopadFromUser(entity, entity.Comp.User.Value); + } + else + { + UnlinkHolopadFromUser(entity, entity.Comp.User); } Dirty(entity); diff --git a/Content.Server/Silicons/StationAi/StationAiFixerConsoleSystem.cs b/Content.Server/Silicons/StationAi/StationAiFixerConsoleSystem.cs new file mode 100644 index 0000000000..cc6f54c446 --- /dev/null +++ b/Content.Server/Silicons/StationAi/StationAiFixerConsoleSystem.cs @@ -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 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); + } +} diff --git a/Content.Server/Silicons/StationAi/StationAiSystem.cs b/Content.Server/Silicons/StationAi/StationAiSystem.cs index 45b3dda431..73c5670c1e 100644 --- a/Content.Server/Silicons/StationAi/StationAiSystem.cs +++ b/Content.Server/Silicons/StationAi/StationAiSystem.cs @@ -1,10 +1,34 @@ using Content.Server.Chat.Systems; +using Content.Server.Construction; +using Content.Server.Destructible; +using Content.Server.Ghost; +using Content.Server.Mind; +using Content.Server.Power.Components; +using Content.Server.Power.EntitySystems; +using Content.Server.Roles; +using Content.Server.Spawners.Components; +using Content.Server.Spawners.EntitySystems; +using Content.Server.Station.Systems; +using Content.Shared.Alert; using Content.Shared.Chat.Prototypes; +using Content.Shared.Containers.ItemSlots; +using Content.Shared.Damage; +using Content.Shared.Destructible; using Content.Shared.DeviceNetwork.Components; +using Content.Shared.DoAfter; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Systems; +using Content.Shared.Popups; +using Content.Shared.Power.Components; +using Content.Shared.Rejuvenate; +using Content.Shared.Roles; using Content.Shared.Silicons.StationAi; +using Content.Shared.Speech.Components; using Content.Shared.StationAi; using Content.Shared.Turrets; using Content.Shared.Weapons.Ranged.Events; +using Robust.Server.Containers; +using Robust.Shared.Containers; using Robust.Shared.Map.Components; using Robust.Shared.Player; using Robust.Shared.Prototypes; @@ -16,19 +40,300 @@ public sealed class StationAiSystem : SharedStationAiSystem { [Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly SharedTransformSystem _xforms = default!; + [Dependency] private readonly ContainerSystem _container = default!; + [Dependency] private readonly MindSystem _mind = default!; + [Dependency] private readonly RoleSystem _roles = default!; + [Dependency] private readonly ItemSlotsSystem _slots = default!; + [Dependency] private readonly GhostSystem _ghost = default!; + [Dependency] private readonly AlertsSystem _alerts = default!; + [Dependency] private readonly DestructibleSystem _destructible = default!; + [Dependency] private readonly BatterySystem _battery = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly SharedPopupSystem _popups = default!; + [Dependency] private readonly StationSystem _station = default!; + [Dependency] private readonly StationJobsSystem _stationJobs = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; private readonly HashSet> _stationAiCores = new(); + private readonly ProtoId _turretIsAttackingChatNotificationPrototype = "TurretIsAttacking"; private readonly ProtoId _aiWireSnippedChatNotificationPrototype = "AiWireSnipped"; + private readonly ProtoId _aiLosingPowerChatNotificationPrototype = "AiLosingPower"; + private readonly ProtoId _aiCriticalPowerChatNotificationPrototype = "AiCriticalPower"; + + private readonly ProtoId _stationAiJob = "StationAi"; + private readonly EntProtoId _stationAiBrain = "StationAiBrain"; + + private readonly ProtoId _batteryAlert = "BorgBattery"; + private readonly ProtoId _damageAlert = "BorgHealth"; public override void Initialize() { base.Initialize(); + SubscribeLocalEvent(AfterConstructionChangeEntity); + SubscribeLocalEvent(OnContainerSpawn); + SubscribeLocalEvent(OnApcBatteryChanged); + SubscribeLocalEvent(OnChargeChanged); + SubscribeLocalEvent(OnDamageChanged); + SubscribeLocalEvent(OnDestruction); + SubscribeLocalEvent>(OnDoAfterAttempt); + SubscribeLocalEvent(OnRejuvenate); + SubscribeLocalEvent(OnExpandICChatRecipients); SubscribeLocalEvent(OnAmmoShot); } + private void AfterConstructionChangeEntity(Entity 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(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 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(ent, out var battery)) + { + _battery.SetCharge(ent, battery.MaxCharge); + } + + if (TryComp(ent, out var damageable)) + { + _damageable.SetAllDamage(ent, damageable, 0); + } + } + + protected override void OnAiInsert(Entity ent, ref EntInsertedIntoContainerMessage args) + { + base.OnAiInsert(ent, ref args); + + UpdateBatteryAlert(ent); + UpdateCoreIntegrityAlert(ent); + UpdateDamagedAccent(ent); + } + + protected override void OnAiRemove(Entity ent, ref EntRemovedFromContainerMessage args) + { + base.OnAiRemove(ent, ref args); + + _alerts.ClearAlert(args.Entity, _batteryAlert); + _alerts.ClearAlert(args.Entity, _damageAlert); + + if (TryComp(args.Entity, out var accent)) + { + accent.OverrideChargeLevel = null; + accent.OverrideTotalDamage = null; + accent.DamageAtMaxCorruption = null; + } + } + + protected override void OnMobStateChanged(Entity 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 ent, ref DestructionEventArgs args) + { + var station = _station.GetOwningStation(ent); + + if (station == null) + return; + + if (!HasComp(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 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 entity, ref ChargeChangedEvent args) + { + UpdateBatteryAlert(entity); + UpdateDamagedAccent(entity); + } + + private void OnDamageChanged(Entity entity, ref DamageChangedEvent args) + { + UpdateCoreIntegrityAlert(entity); + UpdateDamagedAccent(entity); + } + + private void UpdateDamagedAccent(Entity ent) + { + if (!TryGetHeld((ent.Owner, ent.Comp), out var held)) + return; + + if (!TryComp(held, out var accent)) + return; + + if (TryComp(ent, out var battery)) + accent.OverrideChargeLevel = battery.CurrentCharge / battery.MaxCharge; + + if (TryComp(ent, out var damageable)) + accent.OverrideTotalDamage = damageable.TotalDamage; + + if (TryComp(ent, out var destructible)) + accent.DamageAtMaxCorruption = _destructible.DestroyedAt(ent, destructible); + + Dirty(held.Value, accent); + } + + private void UpdateBatteryAlert(Entity ent) + { + if (!TryComp(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(ent, out var apcBattery) && + apcBattery.Enabled && + chargePercent < 0.2) + { + var ev = new ChatNotificationEvent(_aiCriticalPowerChatNotificationPrototype, ent); + RaiseLocalEvent(held.Value, ref ev); + } + } + + private void UpdateCoreIntegrityAlert(Entity ent) + { + if (!TryComp(ent, out var damageable)) + return; + + if (!TryComp(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 ent, ref DoAfterAttemptEvent args) + { + if (TryGetHeld((ent.Owner, ent.Comp), out _)) + return; + + // Prevent AIs from being uploaded into an unpowered or broken AI core. + + if (TryComp(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(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 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(held.Value); + } + + ClearEye(ent); + } + + private void OnRejuvenate(Entity ent, ref RejuvenateEvent args) + { + if (TryGetHeld((ent.Owner, ent.Comp), out var held)) + { + _mobState.ChangeMobState(held.Value, MobState.Alive); + EnsureComp(held.Value); + } + + if (TryComp(ent, out var holder)) + { + _appearance.SetData(ent, StationAiVisuals.Broken, false); + UpdateAppearance((ent, holder)); + } + } + private void OnExpandICChatRecipients(ExpandICChatRecipientsEvent ev) { var xformQuery = GetEntityQuery(); @@ -147,7 +452,7 @@ public sealed class StationAiSystem : SharedStationAiSystem if (!TryGetHeld((stationAiCore, stationAiCore.Comp), out var insertedAi)) continue; - hashSet.Add(insertedAi); + hashSet.Add(insertedAi.Value); } return hashSet; diff --git a/Content.Server/Spawners/EntitySystems/ContainerSpawnPointSystem.cs b/Content.Server/Spawners/EntitySystems/ContainerSpawnPointSystem.cs index 1a592b9929..1763d5f6a1 100644 --- a/Content.Server/Spawners/EntitySystems/ContainerSpawnPointSystem.cs +++ b/Content.Server/Spawners/EntitySystems/ContainerSpawnPointSystem.cs @@ -1,8 +1,7 @@ -using Content.Server.GameTicking; +using Content.Server.GameTicking; using Content.Server.Spawners.Components; using Content.Server.Station.Systems; using Content.Shared.Preferences; -using Content.Shared.Roles; using Robust.Server.Containers; using Robust.Shared.Containers; using Robust.Shared.Prototypes; @@ -87,6 +86,9 @@ public sealed class ContainerSpawnPointSystem : EntitySystem if (!_container.Insert(args.SpawnResult.Value, container, containerXform: xform)) continue; + var ev = new ContainerSpawnEvent(args.SpawnResult.Value); + RaiseLocalEvent(uid, ref ev); + return; } @@ -94,3 +96,9 @@ public sealed class ContainerSpawnPointSystem : EntitySystem args.SpawnResult = null; } } + +/// +/// Raised on a container when a player is spawned into it. +/// +[ByRefEvent] +public record struct ContainerSpawnEvent(EntityUid Player); diff --git a/Content.Shared/Silicons/StationAi/SharedStationAiFixerConsoleSystem.cs b/Content.Shared/Silicons/StationAi/SharedStationAiFixerConsoleSystem.cs new file mode 100644 index 0000000000..1abafd7cb2 --- /dev/null +++ b/Content.Shared/Silicons/StationAi/SharedStationAiFixerConsoleSystem.cs @@ -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; + +/// +/// 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. +/// +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(OnInserted); + SubscribeLocalEvent(OnRemoved); + SubscribeLocalEvent(OnLockToggle); + SubscribeLocalEvent(OnMessage); + SubscribeLocalEvent(OnPowerChanged); + SubscribeLocalEvent(OnExamined); + + SubscribeLocalEvent(OnStationAiCustomizationStateChanged); + } + + private void OnInserted(Entity 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 ent, ref EntRemovedFromContainerMessage args) + { + if (args.Container.ID != ent.Comp.StationAiHolderSlot) + return; + + ent.Comp.ActionTarget = null; + + StopAction(ent); + } + + private void OnLockToggle(Entity ent, ref LockToggledEvent args) + { + if (_userInterface.TryGetOpenUi(ent.Owner, StationAiFixerConsoleUiKey.Key, out var bui)) + bui.Update(); + } + + private void OnMessage(Entity ent, ref StationAiFixerConsoleMessage args) + { + if (TryComp(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 ent, ref PowerChangedEvent args) + { + if (args.Powered) + return; + + StopAction(ent); + } + + private void OnExamined(Entity 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 ent, ref StationAiCustomizationStateChanged args) + { + if (_container.TryGetOuterContainer(ent, Transform(ent), out var outerContainer) && + TryComp(outerContainer.Owner, out var stationAiFixerConsole)) + { + UpdateAppearance((outerContainer.Owner, stationAiFixerConsole)); + } + } + + private void EjectStationAiHolder(Entity ent, EntityUid user) + { + if (!TryComp(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 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 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 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); + } + + /// + /// Initiates an action upon a target entity by the specified console. + /// + /// The console. + /// The action to be enacted on the target. + private void StartAction(Entity 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); + } + + /// + /// Updates the current action being conducted by the specified console. + /// + /// The console. + private void UpdateAction(Entity 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); + } + + /// + /// Terminates any action being conducted by the specified console. + /// + /// The console. + private void StopAction(Entity ent) + { + ent.Comp.ActionType = StationAiFixerConsoleAction.None; + Dirty(ent); + + UpdateAppearance(ent); + } + + /// + /// Finalizes the action being conducted by the specified console + /// (i.e., repairing or purging a target). + /// + /// The console. + protected virtual void FinalizeAction(Entity 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); + } + + /// + /// Updates the appearance of the specified console based on its current state. + /// + /// The console. + private void UpdateAppearance(Entity ent) + { + if (!TryComp(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(target, out var customization) && !EntityManager.IsQueuedForDeletion(target.Value)) + { + state = customization.State; + } + + _appearance.SetData(ent, StationAiFixerConsoleVisuals.Key, state.ToString(), appearance); + } + + /// + /// Calculates the current stage of any in-progress actions. + /// + /// The console. + /// The current stage. + private int CalculateActionStage(Entity ent) + { + var completionPercentage = (_timing.CurTime - ent.Comp.ActionStartTime) / (ent.Comp.ActionEndTime - ent.Comp.ActionStartTime); + + return (int)(completionPercentage * ent.Comp.ActionStageCount); + } + + /// + /// Try to find a valid target being stored inside the specified console. + /// + /// The console. + /// The found target. + /// True if a valid target was found. + public bool TryGetTarget(Entity 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(stationAi)) + return false; + + target = stationAi; + + return !EntityManager.IsQueuedForDeletion(target.Value); + } + + /// + /// Try to find a station AI holder being stored inside the specified console. + /// + /// The console. + /// The found holder. + /// True if a valid holder was found. + public bool TryGetStationAiHolder(Entity 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; + } + + /// + /// Determines if the specified console can act upon its action target. + /// + /// The console. + /// The action to be enacted on the target. + /// True, if the target is valid for the specified console action. + public bool IsTargetValid(Entity 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; + } + + /// + /// Returns whether an station AI holder is inserted into the specified console. + /// + /// The console. + /// True if a station AI holder is inserted. + public bool IsStationAiHolderInserted(Entity ent) + { + return TryGetStationAiHolder(ent, out var _); + } + + /// + /// Returns whether the specified console has an action in progress. + /// + /// The console. + /// Ture, if an action is in progress. + public bool IsActionInProgress(Entity ent) + { + return ent.Comp.ActionType != StationAiFixerConsoleAction.None; + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = AllEntityQuery(); + + while (query.MoveNext(out var uid, out var stationAiFixerConsole)) + { + var ent = (uid, stationAiFixerConsole); + + if (!IsActionInProgress(ent)) + continue; + + UpdateAction(ent); + } + } +} diff --git a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Customization.cs b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Customization.cs index 7a5131c9a1..4361b86d12 100644 --- a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Customization.cs +++ b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Customization.cs @@ -1,5 +1,9 @@ using Content.Shared.Holopad; +using Content.Shared.Mobs; +using Robust.Shared.Player; using Robust.Shared.Prototypes; +using Robust.Shared.Utility; +using System.Diagnostics.CodeAnalysis; namespace Content.Shared.Silicons.StationAi; @@ -8,9 +12,15 @@ public abstract partial class SharedStationAiSystem private ProtoId _stationAiCoreCustomGroupProtoId = "StationAiCoreIconography"; private ProtoId _stationAiHologramCustomGroupProtoId = "StationAiHolograms"; + private readonly SpriteSpecifier.Rsi _stationAiRebooting = new(new ResPath("Mobs/Silicon/station_ai.rsi"), "ai_fuzz"); + private void InitializeCustomization() { SubscribeLocalEvent(OnStationAiCustomization); + + SubscribeLocalEvent(OnPlayerAttached); + SubscribeLocalEvent(OnPlayerDetached); + SubscribeLocalEvent(OnMobStateChanged); } private void OnStationAiCustomization(Entity entity, ref StationAiCustomizationMessage args) @@ -29,17 +39,53 @@ public abstract partial class SharedStationAiSystem stationAiCustomization.ProtoIds[args.GroupProtoId] = args.CustomizationProtoId; - Dirty(held, stationAiCustomization); + Dirty(held.Value, stationAiCustomization); // Update hologram if (groupPrototype.Category == StationAiCustomizationType.Hologram) - UpdateHolographicAvatar((held, stationAiCustomization)); + UpdateHolographicAvatar((held.Value, stationAiCustomization)); // Update core iconography if (groupPrototype.Category == StationAiCustomizationType.CoreIconography && TryComp(entity, out var stationAiHolder)) UpdateAppearance((entity, stationAiHolder)); } + private void OnPlayerAttached(Entity ent, ref PlayerAttachedEvent args) + { + var state = _mobState.IsDead(ent) ? StationAiState.Dead : StationAiState.Occupied; + SetStationAiState(ent, state); + } + + private void OnPlayerDetached(Entity ent, ref PlayerDetachedEvent args) + { + var state = _mobState.IsDead(ent) ? StationAiState.Dead : StationAiState.Rebooting; + SetStationAiState(ent, state); + } + + protected virtual void OnMobStateChanged(Entity ent, ref MobStateChangedEvent args) + { + var state = (args.NewMobState == MobState.Dead) ? StationAiState.Dead : StationAiState.Rebooting; + SetStationAiState(ent, state); + } + + protected void SetStationAiState(Entity 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(container.Owner, out var holder)) + { + UpdateAppearance((container.Owner, holder)); + } + } + private void UpdateHolographicAvatar(Entity entity) { if (!TryComp(entity, out var avatar)) @@ -62,21 +108,36 @@ public abstract partial class SharedStationAiSystem { var stationAi = GetInsertedAI(entity); - if (stationAi == null) - { - _appearance.RemoveData(entity.Owner, StationAiVisualState.Key); - return; - } - if (!TryComp(stationAi, out var stationAiCustomization) || - !stationAiCustomization.ProtoIds.TryGetValue(_stationAiCoreCustomGroupProtoId, out var protoId) || - !_protoManager.Resolve(protoId, out var prototype) || - !prototype.LayerData.TryGetValue(state.ToString(), out var layerData)) + !TryGetCustomizedAppearanceData((stationAi.Value, stationAiCustomization), out var layerData) || + !layerData.TryGetValue(state.ToString(), out var stateData)) { return; } // This data is handled manually in the client StationAiSystem - _appearance.SetData(entity.Owner, StationAiVisualState.Key, layerData); + _appearance.SetData(entity.Owner, StationAiVisualLayers.Icon, stateData); + } + + /// + /// Returns a dictionary containing the station AI's appearance for different states. + /// + /// The station AI. + /// The apperance data, indexed by possible AI states. + /// True if the apperance data was found. + public bool TryGetCustomizedAppearanceData(Entity entity, [NotNullWhen(true)] out Dictionary? 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; } } diff --git a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Held.cs b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Held.cs index 1c9c57dccf..c82e92b451 100644 --- a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Held.cs +++ b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Held.cs @@ -5,6 +5,7 @@ using Content.Shared.Popups; using Content.Shared.Verbs; using Robust.Shared.Serialization; using Robust.Shared.Utility; +using System.Diagnostics.CodeAnalysis; namespace Content.Shared.Silicons.StationAi; @@ -26,6 +27,7 @@ public abstract partial class SharedStationAiSystem SubscribeLocalEvent(OnHeldInteraction); SubscribeLocalEvent(OnHeldRelay); SubscribeLocalEvent(OnCoreJump); + SubscribeLocalEvent(OnTryGetIdentityShortInfo); } @@ -49,20 +51,23 @@ public abstract partial class SharedStationAiSystem if (!TryGetCore(ent.Owner, out var core) || core.Comp?.RemoteEntity == null) return; - _xforms.DropNextTo(core.Comp.RemoteEntity.Value, core.Owner) ; + _xforms.DropNextTo(core.Comp.RemoteEntity.Value, core.Owner); } /// - /// Tries to get the entity held in the AI core using StationAiCore. + /// Tries to find an AI being held in by an entity using . /// - public bool TryGetHeld(Entity entity, out EntityUid held) + /// The station AI holder. + /// The found AI. + /// True if an AI is found. + public bool TryGetHeld(Entity entity, [NotNullWhen(true)] out EntityUid? held) { held = EntityUid.Invalid; if (!Resolve(entity.Owner, ref entity.Comp)) return false; - if (!_containers.TryGetContainer(entity.Owner, StationAiCoreComponent.Container, out var container) || + if (!_containers.TryGetContainer(entity.Owner, StationAiHolderComponent.Container, out var container) || container.ContainedEntities.Count == 0) return false; @@ -70,26 +75,32 @@ public abstract partial class SharedStationAiSystem return true; } - /// - /// Tries to get the entity held in the AI using StationAiHolder. - /// - public bool TryGetHeld(Entity entity, out EntityUid held) - { - TryComp(entity.Owner, out var stationAiCore); - return TryGetHeld((entity.Owner, stationAiCore), out held); + /// + /// Tries to find an AI being held in by an entity using . + /// + /// The station AI core. + /// The found AI. + /// True if an AI is found. + public bool TryGetHeld(Entity entity, [NotNullWhen(true)] out EntityUid? held) + { + held = null; + + return TryComp(entity.Owner, out var holder) && + TryGetHeld((entity, holder), out held); } + /// + /// Tries to find the station AI core holding an AI. + /// + /// The AI. + /// The found AI core. + /// True if an AI core is found. public bool TryGetCore(EntityUid entity, out Entity core) { - var xform = Transform(entity); - var meta = MetaData(entity); - var ent = new Entity(entity, xform, meta); - - if (!_containers.TryGetContainingContainer(ent, out var container) || + if (!_containers.TryGetContainingContainer(entity, out var container) || container.ID != StationAiCoreComponent.Container || - !TryComp(container.Owner, out StationAiCoreComponent? coreComp) || - coreComp.RemoteEntity == null) + !TryComp(container.Owner, out StationAiCoreComponent? coreComp)) { core = (EntityUid.Invalid, null); return false; diff --git a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs index 1a3d4c788e..e109c23fe6 100644 --- a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs +++ b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs @@ -4,6 +4,7 @@ using Content.Shared.Administration.Managers; using Content.Shared.Chat.Prototypes; using Content.Shared.Containers.ItemSlots; using Content.Shared.Database; +using Content.Shared.Destructible; using Content.Shared.Doors.Systems; using Content.Shared.DoAfter; using Content.Shared.Electrocution; @@ -11,11 +12,14 @@ using Content.Shared.Intellicard; using Content.Shared.Interaction; using Content.Shared.Item.ItemToggle; using Content.Shared.Mind; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Systems; using Content.Shared.Movement.Components; using Content.Shared.Movement.Systems; using Content.Shared.Popups; using Content.Shared.Power; using Content.Shared.Power.EntitySystems; +using Content.Shared.Repairable; using Content.Shared.StationAi; using Content.Shared.Verbs; using Robust.Shared.Audio.Systems; @@ -28,36 +32,36 @@ using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Timing; using Robust.Shared.Utility; -using System.Diagnostics.CodeAnalysis; namespace Content.Shared.Silicons.StationAi; public abstract partial class SharedStationAiSystem : EntitySystem { - [Dependency] private readonly ISharedAdminManager _admin = default!; - [Dependency] private readonly IGameTiming _timing = default!; - [Dependency] private readonly INetManager _net = default!; - [Dependency] private readonly ItemSlotsSystem _slots = default!; - [Dependency] private readonly ItemToggleSystem _toggles = default!; - [Dependency] private readonly ActionBlockerSystem _blocker = default!; - [Dependency] private readonly MetaDataSystem _metadata = default!; - [Dependency] private readonly SharedAirlockSystem _airlocks = default!; - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly SharedContainerSystem _containers = default!; - [Dependency] private readonly SharedDoorSystem _doors = default!; - [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; - [Dependency] private readonly SharedElectrocutionSystem _electrify = default!; - [Dependency] private readonly SharedEyeSystem _eye = default!; + [Dependency] private readonly ISharedAdminManager _admin = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly ItemSlotsSystem _slots = default!; + [Dependency] private readonly ItemToggleSystem _toggles = default!; + [Dependency] private readonly ActionBlockerSystem _blocker = default!; + [Dependency] private readonly MetaDataSystem _metadata = default!; + [Dependency] private readonly SharedAirlockSystem _airlocks = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedContainerSystem _containers = default!; + [Dependency] private readonly SharedDoorSystem _doors = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly SharedElectrocutionSystem _electrify = default!; + [Dependency] private readonly SharedEyeSystem _eye = default!; [Dependency] protected readonly SharedMapSystem Maps = default!; - [Dependency] private readonly SharedMindSystem _mind = default!; - [Dependency] private readonly SharedMoverController _mover = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly SharedPowerReceiverSystem PowerReceiver = default!; - [Dependency] private readonly SharedTransformSystem _xforms = default!; - [Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!; - [Dependency] private readonly StationAiVisionSystem _vision = default!; - [Dependency] private readonly IPrototypeManager _protoManager = default!; + [Dependency] private readonly SharedMindSystem _mind = default!; + [Dependency] private readonly SharedMoverController _mover = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedPowerReceiverSystem PowerReceiver = default!; + [Dependency] private readonly SharedTransformSystem _xforms = default!; + [Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!; + [Dependency] private readonly StationAiVisionSystem _vision = default!; + [Dependency] private readonly IPrototypeManager _protoManager = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; // StationAiHeld is added to anything inside of an AI core. // StationAiHolder indicates it can hold an AI positronic brain (e.g. holocard / core). @@ -72,8 +76,6 @@ public abstract partial class SharedStationAiSystem : EntitySystem private static readonly EntProtoId DefaultAi = "StationAiBrain"; private readonly ProtoId _downloadChatNotificationPrototype = "IntellicardDownload"; - private const float MaxVisionMultiplier = 5f; - public override void Initialize() { base.Initialize(); @@ -102,10 +104,12 @@ public abstract partial class SharedStationAiSystem : EntitySystem SubscribeLocalEvent(OnAiInsert); SubscribeLocalEvent(OnAiRemove); - SubscribeLocalEvent(OnAiMapInit); SubscribeLocalEvent(OnAiShutdown); SubscribeLocalEvent(OnCorePower); SubscribeLocalEvent>(OnCoreVerbs); + + SubscribeLocalEvent(OnBroken); + SubscribeLocalEvent(OnRepaired); } private void OnCoreVerbs(Entity ent, ref GetVerbsEvent args) @@ -137,7 +141,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem args.Verbs.Add(new Verb() { Text = Loc.GetString("station-ai-customization-menu"), - Act = () => _uiSystem.TryOpenUi(ent.Owner, StationAiCustomizationUiKey.Key, insertedAi), + Act = () => _uiSystem.TryOpenUi(ent.Owner, StationAiCustomizationUiKey.Key, insertedAi.Value), Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/emotes.svg.192dpi.png")), }); } @@ -271,8 +275,8 @@ public abstract partial class SharedStationAiSystem : EntitySystem if (!TryComp(args.Used, out IntellicardComponent? intelliComp)) return; - var cardHasAi = _slots.CanEject(ent.Owner, args.User, ent.Comp.Slot); - var coreHasAi = _slots.CanEject(args.Target.Value, args.User, targetHolder.Slot); + var cardHasAi = ent.Comp.Slot.Item != null; + var coreHasAi = targetHolder.Slot.Item != null; if (cardHasAi && coreHasAi) { @@ -290,7 +294,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem if (TryGetHeld((args.Target.Value, targetHolder), out var held)) { var ev = new ChatNotificationEvent(_downloadChatNotificationPrototype, args.Used, args.User); - RaiseLocalEvent(held, ref ev); + RaiseLocalEvent(held.Value, ref ev); } var doAfterArgs = new DoAfterArgs(EntityManager, args.User, cardHasAi ? intelliComp.UploadTime : intelliComp.DownloadTime, new IntellicardDoAfterEvent(), args.Target, ent.Owner) @@ -298,7 +302,8 @@ public abstract partial class SharedStationAiSystem : EntitySystem BreakOnDamage = true, BreakOnMove = true, NeedHand = true, - BreakOnDropItem = true + BreakOnDropItem = true, + AttemptFrequency = AttemptFrequency.EveryTick, }; _doAfter.TryStartDoAfter(doAfterArgs); @@ -327,7 +332,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem private void OnHolderMapInit(Entity ent, ref MapInitEvent args) { - UpdateAppearance(ent.Owner); + UpdateAppearance((ent.Owner, ent.Comp)); } private void OnAiShutdown(Entity ent, ref ComponentShutdown args) @@ -342,24 +347,32 @@ public abstract partial class SharedStationAiSystem : EntitySystem private void OnCorePower(Entity 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)) - return; - - AttachEye(ent); - } - else - { - ClearEye(ent); + KillHeldAi(ent); } } - private void OnAiMapInit(Entity ent, ref MapInitEvent args) + private void OnBroken(Entity ent, ref BreakageEventArgs args) { - SetupEye(ent); - AttachEye(ent); + KillHeldAi(ent); + + if (TryComp(ent, out var appearance)) + _appearance.SetData(ent, StationAiVisuals.Broken, true, appearance); + } + + private void OnRepaired(Entity ent, ref RepairedEvent args) + { + if (TryComp(ent, out var appearance)) + _appearance.SetData(ent, StationAiVisuals.Broken, false, appearance); + } + + public virtual void KillHeldAi(Entity ent) + { + if (TryGetHeld((ent.Owner, ent.Comp), out var held)) + { + _mobState.ChangeMobState(held.Value, MobState.Dead); + } } public void SwitchRemoteEntityMode(Entity entity, bool isRemote) @@ -395,7 +408,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem _eye.SetDrawFov(user.Value, !isRemote); } - private bool SetupEye(Entity ent, EntityCoordinates? coords = null) + protected bool SetupEye(Entity ent, EntityCoordinates? coords = null) { if (_net.IsClient) return false; @@ -420,7 +433,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem return true; } - private void ClearEye(Entity ent) + protected void ClearEye(Entity ent) { if (_net.IsClient) return; @@ -428,9 +441,16 @@ public abstract partial class SharedStationAiSystem : EntitySystem QueueDel(ent.Comp.RemoteEntity); ent.Comp.RemoteEntity = null; Dirty(ent); + + if (TryGetHeld((ent, ent.Comp), out var held) && + TryComp(held, out EyeComponent? eyeComp)) + { + _eye.SetDrawFov(held.Value, true, eyeComp); + _eye.SetTarget(held.Value, null, eyeComp); + } } - private void AttachEye(Entity ent) + protected void AttachEye(Entity ent) { if (ent.Comp.RemoteEntity == null) return; @@ -467,7 +487,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem return container.ContainedEntities[0]; } - private void OnAiInsert(Entity ent, ref EntInsertedIntoContainerMessage args) + protected virtual void OnAiInsert(Entity ent, ref EntInsertedIntoContainerMessage args) { if (args.Container.ID != StationAiCoreComponent.Container) return; @@ -475,17 +495,21 @@ public abstract partial class SharedStationAiSystem : EntitySystem if (_timing.ApplyingState) return; + ClearEye(ent); ent.Comp.Remote = true; - SetupEye(ent); // Just so text and the likes works properly _metadata.SetEntityName(ent.Owner, MetaData(args.Entity).EntityName); - AttachEye(ent); + if (SetupEye(ent)) + AttachEye(ent); } - private void OnAiRemove(Entity ent, ref EntRemovedFromContainerMessage args) + protected virtual void OnAiRemove(Entity ent, ref EntRemovedFromContainerMessage args) { + if (args.Container.ID != StationAiCoreComponent.Container) + return; + if (_timing.ApplyingState) return; @@ -506,26 +530,49 @@ public abstract partial class SharedStationAiSystem : EntitySystem ClearEye(ent); } - private void UpdateAppearance(Entity entity) + protected void UpdateAppearance(Entity entity) { if (!Resolve(entity.Owner, ref entity.Comp, false)) return; - // Todo: when AIs can die, add a check to see if the AI is in the 'dead' state var state = StationAiState.Empty; - if (_containers.TryGetContainer(entity.Owner, StationAiHolderComponent.Container, out var container) && container.Count > 0) - state = StationAiState.Occupied; - - // If the entity is a station AI core, attempt to customize its appearance - if (TryComp(entity, out var stationAiCore)) + // Get what visual state the held AI holder is in + if (TryGetHeld(entity, out var stationAi) && + TryComp(stationAi, out var customization)) { - CustomizeAppearance((entity, stationAiCore), state); + state = customization.State; + } + + // If the entity is not an AI core, let generic visualizers handle the appearance update + if (!TryComp(entity, out var stationAiCore)) + { + _appearance.SetData(entity.Owner, StationAiVisualLayers.Icon, state); return; } - // Otherwise let generic visualizers handle the appearance update - _appearance.SetData(entity.Owner, StationAiVisualState.Key, state); + // The AI core is empty + 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 entity, bool enabled, bool announce = false) @@ -573,15 +620,16 @@ public sealed partial class JumpToCoreEvent : InstantActionEvent public sealed partial class IntellicardDoAfterEvent : SimpleDoAfterEvent; [Serializable, NetSerializable] -public enum StationAiVisualState : byte +public enum StationAiVisualLayers : byte { - Key, + Base, + Icon, } [Serializable, NetSerializable] -public enum StationAiSpriteState : byte +public enum StationAiVisuals : byte { - Key, + Broken, } [Serializable, NetSerializable] @@ -590,5 +638,6 @@ public enum StationAiState : byte Empty, Occupied, Dead, + Rebooting, Hologram, } diff --git a/Content.Shared/Silicons/StationAi/StationAiCoreComponent.cs b/Content.Shared/Silicons/StationAi/StationAiCoreComponent.cs index a795c9eda6..ec3f308104 100644 --- a/Content.Shared/Silicons/StationAi/StationAiCoreComponent.cs +++ b/Content.Shared/Silicons/StationAi/StationAiCoreComponent.cs @@ -38,11 +38,19 @@ public sealed partial class StationAiCoreComponent : Component [DataField(readOnly: true)] public EntProtoId? PhysicalEntityProto = "StationAiHoloLocal"; + /// + /// Name of the container slot that holds the inhabiting AI's mind + /// public const string Container = "station_ai_mind_slot"; + + /// + /// Name of the container slot that holds the 'brain' used to construct the AI core + /// + public const string BrainContainer = "station_ai_brain_slot"; } /// -/// 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 /// /// The entity UID of the replacement entity [ByRefEvent] diff --git a/Content.Shared/Silicons/StationAi/StationAiCustomizationComponent.cs b/Content.Shared/Silicons/StationAi/StationAiCustomizationComponent.cs index a2b713edfe..520b7f98c5 100644 --- a/Content.Shared/Silicons/StationAi/StationAiCustomizationComponent.cs +++ b/Content.Shared/Silicons/StationAi/StationAiCustomizationComponent.cs @@ -15,6 +15,12 @@ public sealed partial class StationAiCustomizationComponent : Component /// [DataField, AutoNetworkedField] public Dictionary, ProtoId> ProtoIds = new(); + + /// + /// The current visual state of the associated entity. + /// + [DataField, AutoNetworkedField] + public StationAiState State = StationAiState.Occupied; } /// @@ -33,6 +39,12 @@ public sealed class StationAiCustomizationMessage : BoundUserInterfaceMessage } } +/// +/// Event raised when the station AI customization visual state changes +/// +[ByRefEvent] +public record StationAiCustomizationStateChanged(StationAiState NewState); + /// /// Key for opening the station AI customization UI /// diff --git a/Content.Shared/Silicons/StationAi/StationAiFixerConsoleComponent.cs b/Content.Shared/Silicons/StationAi/StationAiFixerConsoleComponent.cs new file mode 100644 index 0000000000..0b872b1b05 --- /dev/null +++ b/Content.Shared/Silicons/StationAi/StationAiFixerConsoleComponent.cs @@ -0,0 +1,144 @@ +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared.Silicons.StationAi; + +/// +/// This component holds data needed for AI Restoration Consoles to function. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] +[Access(typeof(SharedStationAiFixerConsoleSystem))] +public sealed partial class StationAiFixerConsoleComponent : Component +{ + /// + /// Determines how long a repair takes to complete (in seconds). + /// + [DataField] + public TimeSpan RepairDuration = TimeSpan.FromSeconds(30); + + /// + /// Determines how long a purge takes to complete (in seconds). + /// + [DataField] + public TimeSpan PurgeDuration = TimeSpan.FromSeconds(30); + + /// + /// 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. + /// + [DataField] + public int ActionStageCount = 4; + + /// + /// The time at which the current action commenced. + /// + [DataField, AutoNetworkedField, AutoPausedField] + public TimeSpan ActionStartTime = TimeSpan.FromSeconds(0); + + /// + /// The time at which the current action will end. + /// + [DataField, AutoNetworkedField, AutoPausedField] + public TimeSpan ActionEndTime = TimeSpan.FromSeconds(0); + + /// + /// The type of action that is currently in progress. + /// + [DataField, AutoNetworkedField] + public StationAiFixerConsoleAction ActionType = StationAiFixerConsoleAction.None; + + /// + /// The target of the current action. + /// + [DataField, AutoNetworkedField] + public EntityUid? ActionTarget; + + /// + /// The current stage of the action in progress. + /// + [DataField, AutoNetworkedField] + public int CurrentActionStage; + + /// + /// Sound clip that is played when a repair is completed. + /// + [DataField] + public SoundSpecifier? RepairFinishedSound = new SoundPathSpecifier("/Audio/Items/beep.ogg"); + + /// + /// Sound clip that is played when a repair is completed. + /// + [DataField] + public SoundSpecifier? PurgeFinishedSound = new SoundPathSpecifier("/Audio/Machines/beep.ogg"); + + /// + /// The name of the console slot which is used to contain station AI holders. + /// + [DataField] + public string StationAiHolderSlot = "station_ai_holder"; + + /// + /// The name of the station AI holder slot which actually contains the station AI. + /// + [DataField] + public string StationAiMindSlot = "station_ai_mind_slot"; +} + +/// +/// Message sent from the server to the client to update the UI of AI Restoration Consoles. +/// +[Serializable, NetSerializable] +public sealed class StationAiFixerConsoleBoundUserInterfaceState : BoundUserInterfaceState; + +/// +/// Message sent from the client to the server to handle player UI inputs from AI Restoration Consoles. +/// +[Serializable, NetSerializable] +public sealed class StationAiFixerConsoleMessage : BoundUserInterfaceMessage +{ + public StationAiFixerConsoleAction Action; + + public StationAiFixerConsoleMessage(StationAiFixerConsoleAction action) + { + Action = action; + } +} + +/// +/// Potential actions that AI Restoration Consoles can perform. +/// +[Serializable, NetSerializable] +public enum StationAiFixerConsoleAction +{ + None, + Eject, + Repair, + Purge, + Cancel, +} + +/// +/// Appearance keys for AI Restoration Consoles. +/// +[Serializable, NetSerializable] +public enum StationAiFixerConsoleVisuals : byte +{ + Key, + ActionProgress, + MobState, + RepairProgress, + PurgeProgress, +} + +/// +/// Interactable UI key for AI Restoration Consoles. +/// +[Serializable, NetSerializable] +public enum StationAiFixerConsoleUiKey +{ + Key, +} + diff --git a/Resources/Locale/en-US/generic.ftl b/Resources/Locale/en-US/generic.ftl index cdca0f2493..c963f0e0fb 100644 --- a/Resources/Locale/en-US/generic.ftl +++ b/Resources/Locale/en-US/generic.ftl @@ -14,6 +14,7 @@ generic-invalid = invalid generic-hours = hours generic-minutes = minutes +generic-seconds = seconds generic-playtime-title = Playtime diff --git a/Resources/Locale/en-US/recipes/components.ftl b/Resources/Locale/en-US/recipes/components.ftl index 236097532c..d67c661ecd 100644 --- a/Resources/Locale/en-US/recipes/components.ftl +++ b/Resources/Locale/en-US/recipes/components.ftl @@ -5,3 +5,4 @@ construction-graph-component-second-flash = second flash construction-graph-component-power-cell = power cell construction-graph-component-apc-electronics = APC electronics construction-graph-component-payload-trigger = trigger +construction-graph-component-borg-brain = MMI or positronic brain diff --git a/Resources/Locale/en-US/recipes/tags.ftl b/Resources/Locale/en-US/recipes/tags.ftl index 34eadc37d8..96c0729881 100644 --- a/Resources/Locale/en-US/recipes/tags.ftl +++ b/Resources/Locale/en-US/recipes/tags.ftl @@ -103,6 +103,7 @@ construction-graph-tag-ripley-peripherals-control-module = ripley peripherals co construction-graph-tag-door-electronics-circuit-board = door electronics circuit board construction-graph-tag-firelock-electronics-circuit-board = firelock electronics circuit board construction-graph-tag-conveyor-belt-assembly = conveyor belt assembly +construction-graph-tag-station-ai-core-electronics = station AI core electronics # tools construction-graph-tag-multitool = a multitool diff --git a/Resources/Locale/en-US/silicons/station-ai-fixer-console.ftl b/Resources/Locale/en-US/silicons/station-ai-fixer-console.ftl new file mode 100644 index 0000000000..a6940f2306 --- /dev/null +++ b/Resources/Locale/en-US/silicons/station-ai-fixer-console.ftl @@ -0,0 +1,37 @@ +# System +station-ai-fixer-console-is-locked = The console is locked. +station-ai-fixer-console-station-ai-holder-required = Only AI storage units can be inserted into the console. +station-ai-fixer-console-examination-station-ai-holder-present = There is {INDEFINITE($holder)} [color=cyan]{$holder}[/color] inserted in the console. +station-ai-fixer-console-examination-station-ai-holder-absent = There is an unoccupied slot for an [color=cyan]AI storage unit[/color]. +station-ai-fixer-console-repair-finished = Repair complete. Attempting to reboot AI... +station-ai-fixer-console-repair-successful = Repair complete. AI successfully rebooted. +station-ai-fixer-console-purge-successful = Purge complete. AI successfully deleted. + +# UI +station-ai-fixer-console-window = AI restoration console +station-ai-fixer-console-window-no-station-ai = No AI detected +station-ai-fixer-console-window-no-station-ai-status = Waiting +station-ai-fixer-console-window-station-ai-online = Online +station-ai-fixer-console-window-station-ai-offline = Offline +station-ai-fixer-console-window-station-ai-rebooting = Rebooting... + +station-ai-fixer-console-window-controls-locked = Controls locked + +station-ai-fixer-console-window-station-ai-eject = Eject storage unit +station-ai-fixer-console-window-station-ai-repair = Run repair tool +station-ai-fixer-console-window-station-ai-purge = Initiate AI purge + +station-ai-fixer-console-window-action-progress-repair = Repair in progress... +station-ai-fixer-console-window-action-progress-purge = Purge in progress... +station-ai-fixer-console-window-action-progress-eta = Time remaining: {$time} {$units} + +station-ai-fixer-console-window-flavor-left = Lock this console when it is not in use +station-ai-fixer-console-window-flavor-right = v4.0.4 + +station-ai-fixer-console-window-continue-action = Continue +station-ai-fixer-console-window-cancel-action = Cancel + +station-ai-fixer-console-window-purge-warning-title = Initiating AI purge +station-ai-fixer-console-window-purge-warning-1 = You are about to permanently delete an artifical intelligence. +station-ai-fixer-console-window-purge-warning-2 = Once this operation is complete, the intelligence will be gone and cannot be revived. +station-ai-fixer-console-window-purge-warning-3 = Do you wish to proceed? \ No newline at end of file diff --git a/Resources/Locale/en-US/silicons/station-ai.ftl b/Resources/Locale/en-US/silicons/station-ai.ftl index 442782f9a1..11c51ddea4 100644 --- a/Resources/Locale/en-US/silicons/station-ai.ftl +++ b/Resources/Locale/en-US/silicons/station-ai.ftl @@ -4,6 +4,10 @@ wire-name-ai-vision-light = AIV wire-name-ai-act-light = AIA station-ai-takeover = AI takeover station-ai-eye-name = AI eye - {$name} +station-ai-has-no-power-for-upload = Upload failed - the AI core is unpowered. +station-ai-is-too-damaged-for-upload = Upload failed - the AI core must be repaired. +station-ai-core-losing-power = Your AI core is now running on reserve battery power. +station-ai-core-critical-power = Your AI core is critically low on power. External power must be re-established or severe data corruption may occur! # Radial actions ai-open = Open actions diff --git a/Resources/Prototypes/Catalog/Cargo/cargo_science.yml b/Resources/Prototypes/Catalog/Cargo/cargo_science.yml index aa428b7d55..cefcca5fab 100644 --- a/Resources/Prototypes/Catalog/Cargo/cargo_science.yml +++ b/Resources/Prototypes/Catalog/Cargo/cargo_science.yml @@ -47,3 +47,13 @@ cost: 2000 category: cargoproduct-category-name-science group: market + +- type: cargoProduct + id: StationAiCore + icon: + sprite: Mobs/Silicon/station_ai.rsi + state: frame_4 + product: CrateStationAiCore + cost: 10000 + category: cargoproduct-category-name-science + group: market \ No newline at end of file diff --git a/Resources/Prototypes/Catalog/Fills/Crates/science.yml b/Resources/Prototypes/Catalog/Fills/Crates/science.yml index 6adf5942a4..4ed07d607f 100644 --- a/Resources/Prototypes/Catalog/Fills/Crates/science.yml +++ b/Resources/Prototypes/Catalog/Fills/Crates/science.yml @@ -24,3 +24,19 @@ - id: CrewMonitoringServerFlatpack - id: CrewMonitoringComputerFlatpack amount: 3 + +- type: entity + id: CrateStationAiCore + parent: CrateScienceSecure + name: station AI core crate + description: Contains the components for constructing a station AI core. Positronic brain not included. Requires Science access to open. + components: + - type: StorageFill + contents: + - id: StationAiCoreElectronics + - id: SheetPlasteel1 + amount: 4 + - id: CableApcStack1 + amount: 1 + - id: SheetRGlass1 + amount: 2 \ No newline at end of file diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml b/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml index 6b1efddad1..ae904f7f95 100644 --- a/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml +++ b/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml @@ -282,6 +282,7 @@ - id: ProtolatheMachineCircuitboard - id: ResearchComputerCircuitboard - id: CargoRequestScienceComputerCircuitboard + - id: StationAiFixerCircuitboard - id: RubberStampRd # Hardsuit table, used for suit storage as well diff --git a/Resources/Prototypes/Chat/notifications.yml b/Resources/Prototypes/Chat/notifications.yml index c1aee755c6..cea67fa0ee 100644 --- a/Resources/Prototypes/Chat/notifications.yml +++ b/Resources/Prototypes/Chat/notifications.yml @@ -19,3 +19,17 @@ color: Pink nextDelay: 12 notifyBySource: true + +- type: chatNotification + id: AiLosingPower + message: station-ai-core-losing-power + sound: /Audio/Misc/notice2.ogg + color: Orange + nextDelay: 30 + +- type: chatNotification + id: AiCriticalPower + message: station-ai-core-critical-power + sound: /Audio/Effects/alert.ogg + color: Red + nextDelay: 120 \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml index e18100ab8a..845971be35 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml @@ -144,10 +144,12 @@ - type: Appearance - type: GenericVisualizer visuals: - enum.StationAiVisualState.Key: + enum.StationAiVisualLayers.Icon: unshaded: Empty: { state: empty } Occupied: { state: full } + Rebooting: { state: dead } + Dead: { state: dead } - type: Intellicard - type: entity @@ -161,6 +163,7 @@ - state: ai shader: unshaded +# Empty AI core - type: entity id: PlayerStationAiEmpty name: AI Core @@ -178,23 +181,69 @@ blacklist: tags: - GhostOnlyWarp + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeAabb + bounds: "-0.5,-0.5,0.5,0.5" + mask: + - MachineMask + layer: + - MachineLayer + density: 200 - type: ContainerComp proto: AiHeld container: station_ai_mind_slot + - type: Damageable + damageModifierSet: StrongMetallic + - type: Repairable + doAfterDelay: 10 + allowSelfRepair: false - type: Destructible thresholds: - trigger: !type:DamageTrigger - damage: 100 + damage: 400 behaviors: - !type:PlaySoundBehavior sound: collection: MetalBreak - !type:DoActsBehavior - acts: [ "Destruction" ] + acts: [ "Breakage" ] + - trigger: + !type:DamageTrigger + damage: 800 + behaviors: + - !type:PlaySoundBehavior + sound: + collection: MetalBreak + - !type:SpawnEntitiesBehavior + spawn: + ShardGlassReinforced: + min: 1 + max: 2 + SheetPlasteel: + min: 2 + max: 2 + - !type:DoActsBehavior + acts: ["Destruction"] + - type: DamageVisuals + thresholds: [25, 50, 75, 100, 125, 150, 175] + damageDivisor: 4 + trackAllDamage: true + damageOverlay: + sprite: Mobs/Silicon/station_ai_cracks.rsi - type: ApcPowerReceiver - powerLoad: 1000 - needsPower: false + powerLoad: 500 + - type: ExtensionCableReceiver + - type: Battery + maxCharge: 300000 + startingCharge: 300000 + - type: ApcPowerReceiverBattery + idleLoad: 500 + batteryRechargeRate: 1000 + batteryRechargeEfficiency: 0 # Setting to zero until the light flickering issue associated with dynamic power loads is fixed - type: StationAiCore - type: StationAiVision - type: InteractionOutline @@ -204,12 +253,26 @@ layers: - state: base - state: ai_empty + map: ["enum.StationAiVisualLayers.Base"] shader: unshaded - state: ai - map: ["enum.StationAiVisualState.Key"] + map: ["enum.StationAiVisualLayers.Icon"] shader: unshaded visible: false + - state: ai_unpowered + map: ["enum.PowerDeviceVisualLayers.Powered"] + visible: false - type: Appearance + - type: GenericVisualizer + visuals: + enum.PowerDeviceVisuals.Powered: + enum.PowerDeviceVisualLayers.Powered: + False: { visible: true } + True: { visible: false } + enum.StationAiVisuals.Broken: + enum.StationAiVisualLayers.Base: + False: { state: ai_empty } + True: { state: ai_error } - type: InteractionPopup interactSuccessString: petting-success-station-ai interactFailureString: petting-failure-station-ai @@ -234,7 +297,22 @@ type: HolopadBoundUserInterface enum.StationAiCustomizationUiKey.Key: type: StationAiCustomizationBoundUserInterface - + - type: Construction + graph: StationAiCore + node: stationAiCore + - type: ContainerContainer + containers: + board: !type:Container + station_ai_brain_slot: !type:Container + station_ai_mind_slot: !type:ContainerSlot + showEnts: true + - type: ContainerFill + containers: + board: + - StationAiCoreElectronics + - type: StaticPrice + price: 5000 + # The job-ready version of an AI spawn. - type: entity id: PlayerStationAi @@ -245,6 +323,77 @@ containerId: station_ai_mind_slot job: StationAi +# The station AI core assembly +- type: entity + parent: BaseStructure + id: PlayerStationAiAssembly + name: AI Core Assembly + description: An unfinished computer core for housing an artifical intelligence. + components: + - type: Anchorable + flags: + - Anchorable + - type: Rotatable + - type: Sprite + snapCardinals: true + sprite: Mobs/Silicon/station_ai.rsi + layers: + - state: frame_0 + map: [ "enum.ConstructionVisuals.Layer" ] + - type: Appearance + - type: GenericVisualizer + visuals: + enum.ConstructionVisuals.Key: + enum.ConstructionVisuals.Layer: + frame: { state: frame_0 } + frameWithElectronics: { state: frame_1 } + frameWithSecuredElectronics: { state: frame_2 } + frameWithWires: { state: frame_3 } + frameWithBrain: { state: frame_3b } + frameWithBrainFinished: { state: frame_4 } + frameWithoutBrainFinished: { state: frame_4 } + - type: InteractionOutline + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeAabb + bounds: "-0.5,-0.5,0.5,0.5" + mask: + - MachineMask + layer: + - MachineLayer + density: 200 + - type: Damageable + damageModifierSet: StrongMetallic + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 400 + behaviors: + - !type:PlaySoundBehavior + sound: + collection: MetalBreak + - !type:SpawnEntitiesBehavior + spawn: + SheetPlasteel: + min: 2 + max: 4 + - !type:EmptyContainersBehaviour + containers: + - station_ai_brain_slot + - board + - !type:DoActsBehavior + acts: ["Destruction"] + - type: Construction + graph: StationAiCore + node: frame + - type: ContainerContainer + containers: + board: !type:Container + station_ai_brain_slot: !type:Container + # The actual brain inside the core - type: entity id: StationAiBrain @@ -254,8 +403,6 @@ - type: Sprite # Once it's in a core it's pretty much an abstract entity at that point. visible: false - - type: BlockMovement - blockInteraction: false - type: SiliconLawProvider laws: Crewsimov - type: SiliconLawBound @@ -277,9 +424,16 @@ drawFov: false - type: Examiner - type: InputMover + - type: BlockMovement + blockInteraction: false + - type: GhostOnMove + mustBeDead: true - type: Speech speechVerb: Robotic speechSounds: Borg + - type: DamagedSiliconAccent + startPowerCorruptionAtCharIdx: 4 + maxPowerCorruptionAtCharIdx: 20 - type: Tag tags: - HideContextMenu diff --git a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml index 459030d8a9..8c90308417 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml @@ -554,9 +554,20 @@ parent: BaseComputerCircuitboard id: StationAiUploadCircuitboard name: AI upload console board - description: A computer printed circuit board for a AI upload console. + description: A computer printed circuit board for an AI upload console. components: - type: Sprite state: cpu_science - type: ComputerBoard prototype: StationAiUploadComputer + +- type: entity + parent: BaseComputerCircuitboard + id: StationAiFixerCircuitboard + name: AI restoration console + description: A computer printed circuit board for an AI restoration console console. + components: + - type: Sprite + state: cpu_science + - type: ComputerBoard + prototype: StationAiFixerComputer \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Objects/Devices/Electronics/station_ai_core.yml b/Resources/Prototypes/Entities/Objects/Devices/Electronics/station_ai_core.yml new file mode 100644 index 0000000000..637d7e6a54 --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Devices/Electronics/station_ai_core.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml b/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml index 8f181900b7..4d27a0f07a 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml @@ -115,9 +115,11 @@ proto: robot - type: Speech speechSounds: Pai + - type: Alerts - type: MobState allowedStates: - Alive + - Dead - type: Appearance - type: Tag tags: diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_base.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_base.yml index 0e412b014b..f60297d223 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_base.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_base.yml @@ -125,7 +125,7 @@ fireCost: 100 - type: Battery maxCharge: 2000 - startingCharge: 0 + startingCharge: 2000 - type: ApcPowerReceiverBattery idleLoad: 5 batteryRechargeRate: 200 @@ -136,3 +136,5 @@ - type: HTN rootTask: task: EnergyTurretCompound + - type: StaticPrice + price: 200 \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml index f6538ba64e..e275bef0e9 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml @@ -1636,3 +1636,86 @@ containers: circuit_holder: !type:ContainerSlot board: !type:Container + +- type: entity + id: StationAiFixerComputer + parent: BaseComputer + name: AI restoration console + description: Used to repair damaged artifical intelligences. + components: + - type: Sprite + layers: + - map: [ "computerLayerBody" ] + state: computer + - map: [ "computerLayerKeyboard" ] + state: generic_keyboard + - map: [ "computerLayerScreen" ] + state: ai-fixer-empty + - map: [ "computerLayerKeys" ] + state: rd_key + - map: [ "enum.WiresVisualLayers.MaintenancePanel" ] + state: generic_panel_open + - type: Appearance + - type: GenericVisualizer + visuals: + enum.ComputerVisuals.Powered: + computerLayerScreen: + True: { visible: true, shader: unshaded } + False: { visible: false } + computerLayerKeys: + True: { visible: true, shader: unshaded } + False: { visible: true, shader: shaded } + enum.StationAiFixerConsoleVisuals.Key: + computerLayerScreen: + Repair0: { state: ai-fixer-progress-0 } + Repair1: { state: ai-fixer-progress-1 } + Repair2: { state: ai-fixer-progress-2 } + Repair3: { state: ai-fixer-progress-3 } + Purge0: { state: ai-fixer-purge-0 } + Purge1: { state: ai-fixer-purge-1 } + Purge2: { state: ai-fixer-purge-2 } + Purge3: { state: ai-fixer-purge-3 } + Empty: { state: ai-fixer-empty } + Occupied: { state: ai-fixer-full } + Rebooting: { state: ai-fixer-404 } + Dead: { state: ai-fixer-404 } + enum.WiresVisuals.MaintenancePanelState: + enum.WiresVisualLayers.MaintenancePanel: + True: { visible: false } + False: { visible: true } + - type: ApcPowerReceiver + powerLoad: 1000 + - type: Computer + board: StationAiFixerCircuitboard + - type: AccessReader + access: [ [ "ResearchDirector" ] ] + - type: Lock + unlockOnClick: false + - type: StationAiFixerConsole + - type: ItemSlotsLock + slots: + - station_ai_holder + - type: ItemSlotRequiresPower + - type: ItemSlots + slots: + station_ai_holder: + ejectOnBreak: true + lockedFailPopup: station-ai-fixer-console-is-locked + whitelistFailPopup: station-ai-fixer-console-station-ai-holder-required + whitelist: + requireAll: true + components: + - StationAiHolder + - Item + - type: ContainerContainer + containers: + station_ai_holder: !type:ContainerSlot + board: !type:Container + - type: ActivatableUI + key: enum.StationAiFixerConsoleUiKey.Key + - type: UserInterface + interfaces: + enum.StationAiFixerConsoleUiKey.Key: + type: StationAiFixerConsoleBoundUserInterface + enum.WiresUiKey.Key: + type: WiresBoundUserInterface \ No newline at end of file diff --git a/Resources/Prototypes/Recipes/Construction/Graphs/structures/station_ai_core.yml b/Resources/Prototypes/Recipes/Construction/Graphs/structures/station_ai_core.yml new file mode 100644 index 0000000000..a04c9b009f --- /dev/null +++ b/Resources/Prototypes/Recipes/Construction/Graphs/structures/station_ai_core.yml @@ -0,0 +1,144 @@ +- type: constructionGraph + id: StationAiCore + start: start + graph: + - node: start + edges: + - to: frame + steps: + - material: Plasteel + amount: 4 + doAfter: 4 + + - node: frame + entity: PlayerStationAiAssembly + actions: + - !type:AppearanceChange + edges: + - to: frameWithElectronics + steps: + - tag: StationAiCoreElectronics + name: construction-graph-tag-station-ai-core-electronics + store: board + icon: + sprite: "Objects/Misc/module.rsi" + state: "mainboard" + - to: start + completed: + - !type:SpawnPrototype + prototype: SheetPlasteel1 + amount: 4 + - !type:DeleteEntity {} + steps: + - tool: Welding + doAfter: 8 + + - node: frameWithElectronics + actions: + - !type:AppearanceChange + edges: + - to: frameWithSecuredElectronics + steps: + - tool: Screwing + doAfter: 2 + - to: frame + completed: + - !type:EmptyContainer + container: board + steps: + - tool: Prying + doAfter: 2 + + - node: frameWithSecuredElectronics + actions: + - !type:AppearanceChange + edges: + - to: frameWithWires + steps: + - material: Cable + amount: 1 + doAfter: 1 + - to: frameWithElectronics + steps: + - tool: Screwing + doAfter: 2 + + - node: frameWithWires + actions: + - !type:AppearanceChange + edges: + - to: frameWithBrain + steps: + - component: BorgBrain + name: construction-graph-component-borg-brain + store: station_ai_brain_slot + icon: + sprite: "Objects/Specific/Robotics/mmi.rsi" + state: "mmi_icon" + - to: frameWithoutBrainFinished + steps: + - material: ReinforcedGlass + amount: 2 + doAfter: 2 + - to: frameWithSecuredElectronics + completed: + - !type:SpawnPrototype + prototype: CableApcStack1 + amount: 1 + steps: + - tool: Cutting + doAfter: 2 + + - node: frameWithBrain + actions: + - !type:AppearanceChange + edges: + - to: frameWithBrainFinished + steps: + - material: ReinforcedGlass + amount: 2 + doAfter: 2 + - to: frameWithWires + completed: + - !type:EmptyContainer + container: station_ai_brain_slot + steps: + - tool: Prying + doAfter: 4 + + - node: frameWithBrainFinished + actions: + - !type:AppearanceChange + edges: + - to: stationAiCore + steps: + - tool: Screwing + doAfter: 2 + - to: frameWithBrain + completed: + - !type:SpawnPrototype + prototype: SheetRGlass1 + amount: 2 + steps: + - tool: Prying + doAfter: 4 + + - node: frameWithoutBrainFinished + actions: + - !type:AppearanceChange + edges: + - to: stationAiCore + steps: + - tool: Screwing + doAfter: 2 + - to: frameWithWires + completed: + - !type:SpawnPrototype + prototype: SheetRGlass1 + amount: 2 + steps: + - tool: Prying + doAfter: 4 + + - node: stationAiCore + entity: PlayerStationAiEmpty \ No newline at end of file diff --git a/Resources/Prototypes/Recipes/Construction/structures.yml b/Resources/Prototypes/Recipes/Construction/structures.yml index ed533bcc52..1f568a1629 100644 --- a/Resources/Prototypes/Recipes/Construction/structures.yml +++ b/Resources/Prototypes/Recipes/Construction/structures.yml @@ -1315,3 +1315,16 @@ canBuildInImpassable: false conditions: - !type:TileNotBlocked + +- type: construction + id: StationAiCore + graph: StationAiCore + startNode: start + targetNode: stationAiCore + category: construction-category-structures + objectType: Structure + placementMode: SnapgridCenter + canRotate: false + canBuildInImpassable: false + conditions: + - !type:TileNotBlocked \ No newline at end of file diff --git a/Resources/Prototypes/tags.yml b/Resources/Prototypes/tags.yml index 174374beb8..14dfa9499e 100644 --- a/Resources/Prototypes/tags.yml +++ b/Resources/Prototypes/tags.yml @@ -1343,6 +1343,9 @@ - type: Tag id: StationAi +- type: Tag + id: StationAiCoreElectronics + - type: Tag id: StationMapElectronics diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_dead.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_dead.png index eb74655e02..b015ef9a8a 100644 Binary files a/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_dead.png and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_dead.png differ diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_error.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_error.png new file mode 100644 index 0000000000..91e5635657 Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_error.png differ diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_fuzz.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_fuzz.png new file mode 100644 index 0000000000..dcc48a40ec Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_fuzz.png differ diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_unpowered.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_unpowered.png new file mode 100644 index 0000000000..6ce4bdeedf Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_unpowered.png differ diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_0.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_0.png new file mode 100644 index 0000000000..dfee82552e Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_0.png differ diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_1.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_1.png new file mode 100644 index 0000000000..668a53ad3c Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_1.png differ diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_2.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_2.png new file mode 100644 index 0000000000..21ecc5e71c Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_2.png differ diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_3.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_3.png new file mode 100644 index 0000000000..afddf9f519 Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_3.png differ diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_3b.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_3b.png new file mode 100644 index 0000000000..3780e52715 Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_3b.png differ diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_4.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_4.png new file mode 100644 index 0000000000..70c4834a3e Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_4.png differ diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/meta.json b/Resources/Textures/Mobs/Silicon/station_ai.rsi/meta.json index 867aeb41e8..55bd389b63 100644 --- a/Resources/Textures/Mobs/Silicon/station_ai.rsi/meta.json +++ b/Resources/Textures/Mobs/Silicon/station_ai.rsi/meta.json @@ -195,6 +195,9 @@ { "name": "ai_dead" }, + { + "name": "ai_unpowered" + }, { "name": "ai_empty", "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", "directions": 4 }, { "name": "base" + }, + { + "name": "frame_0" + }, + { + "name": "frame_1" + }, + { + "name": "frame_2" + }, + { + "name": "frame_3" + }, + { + "name": "frame_3b" + }, + { + "name": "frame_4" } ] } diff --git a/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_100.png b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_100.png new file mode 100644 index 0000000000..64ef65ac9b Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_100.png differ diff --git a/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_125.png b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_125.png new file mode 100644 index 0000000000..642132e99e Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_125.png differ diff --git a/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_150.png b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_150.png new file mode 100644 index 0000000000..f7859d9604 Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_150.png differ diff --git a/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_175.png b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_175.png new file mode 100644 index 0000000000..6a08521030 Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_175.png differ diff --git a/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_25.png b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_25.png new file mode 100644 index 0000000000..36e729d4a9 Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_25.png differ diff --git a/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_50.png b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_50.png new file mode 100644 index 0000000000..54d2b3bf9c Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_50.png differ diff --git a/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_75.png b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_75.png new file mode 100644 index 0000000000..4dea5cb6e4 Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_75.png differ diff --git a/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/meta.json b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/meta.json new file mode 100644 index 0000000000..4f70fd7672 --- /dev/null +++ b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/meta.json @@ -0,0 +1,39 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from https://github.com/tgstation/tgstation at commit e06b82a7f4b2b09216fb28fd384c95a2e1dc50e5. Edited by chromiumboy.", + "states": [ + { + "name": "DamageOverlay_25", + "directions": 1 + }, + { + "name": "DamageOverlay_50", + "directions": 1 + }, + { + "name": "DamageOverlay_75", + "directions": 1 + }, + { + "name": "DamageOverlay_100", + "directions": 1 + }, + { + "name": "DamageOverlay_125", + "directions": 1 + }, + { + "name": "DamageOverlay_150", + "directions": 1 + }, + { + "name": "DamageOverlay_175", + "directions": 1 + } + ] +} diff --git a/Resources/Textures/Objects/Devices/ai_card.rsi/dead.png b/Resources/Textures/Objects/Devices/ai_card.rsi/dead.png new file mode 100644 index 0000000000..6b20c2ae9f Binary files /dev/null and b/Resources/Textures/Objects/Devices/ai_card.rsi/dead.png differ diff --git a/Resources/Textures/Objects/Devices/ai_card.rsi/meta.json b/Resources/Textures/Objects/Devices/ai_card.rsi/meta.json index 140b77fbee..8a12aec8cf 100644 --- a/Resources/Textures/Objects/Devices/ai_card.rsi/meta.json +++ b/Resources/Textures/Objects/Devices/ai_card.rsi/meta.json @@ -63,6 +63,15 @@ ] ] }, + { + "name": "dead", + "delays": [ + [ + 0.4, + 0.4 + ] + ] + }, { "name": "full", "delays": [ diff --git a/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/meta.json b/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/meta.json index fcdd9e1b26..1192d1a208 100644 --- a/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/meta.json +++ b/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/meta.json @@ -7,6 +7,9 @@ "y": 32 }, "states": [ + { + "name": "mmi_icon" + }, { "name": "mmi_off" }, diff --git a/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/mmi_icon.png b/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/mmi_icon.png new file mode 100644 index 0000000000..e109ee0f31 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/mmi_icon.png differ diff --git a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-404.png b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-404.png index 1bb9234abe..bc60d18319 100644 Binary files a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-404.png and b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-404.png differ diff --git a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-empty.png b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-empty.png index 2b9c1c1c95..5409dc51dd 100644 Binary files a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-empty.png and b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-empty.png differ diff --git a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-full.png b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-full.png index da7937d84d..fe7d222f1c 100644 Binary files a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-full.png and b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-full.png differ diff --git a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-0.png b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-0.png new file mode 100644 index 0000000000..1bd832bc63 Binary files /dev/null and b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-0.png differ diff --git a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-1.png b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-1.png new file mode 100644 index 0000000000..5d3b546892 Binary files /dev/null and b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-1.png differ diff --git a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-2.png b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-2.png new file mode 100644 index 0000000000..4496ead2f3 Binary files /dev/null and b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-2.png differ diff --git a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-3.png b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-3.png new file mode 100644 index 0000000000..80dcc81ec3 Binary files /dev/null and b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-3.png differ diff --git a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-0.png b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-0.png new file mode 100644 index 0000000000..be2f0f6e32 Binary files /dev/null and b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-0.png differ diff --git a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-1.png b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-1.png new file mode 100644 index 0000000000..af82c550a9 Binary files /dev/null and b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-1.png differ diff --git a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-2.png b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-2.png new file mode 100644 index 0000000000..56dfc2f2d8 Binary files /dev/null and b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-2.png differ diff --git a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-3.png b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-3.png new file mode 100644 index 0000000000..4f6dbe43d1 Binary files /dev/null and b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-3.png differ diff --git a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer.png b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer.png index fe98279329..eb9cf12667 100644 Binary files a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer.png and b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer.png differ diff --git a/Resources/Textures/Structures/Machines/computers.rsi/meta.json b/Resources/Textures/Structures/Machines/computers.rsi/meta.json index 28b6b7fb79..ebb9a875dd 100644 --- a/Resources/Textures/Structures/Machines/computers.rsi/meta.json +++ b/Resources/Textures/Structures/Machines/computers.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/bd6873fd4dd6a61d7e46f1d75cd4d90f64c40894. comm_syndie made by Veritius, based on comm. generic_panel_open made by Errant, commit https://github.com/space-wizards/space-station-14/pull/32273, comms_wizard and wizard_key by ScarKy0, request- variants transfer made by EmoGarbage404 (github), xenorobot by Samuka-C (github)", + "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/bd6873fd4dd6a61d7e46f1d75cd4d90f64c40894. comm_syndie made by Veritius, based on comm. generic_panel_open made by Errant, commit https://github.com/space-wizards/space-station-14/pull/32273, comms_wizard and wizard_key by ScarKy0, request- variants transfer made by EmoGarbage404 (github), xenorobot by Samuka-C (github), ai-fixer-progress and -purge sprites made by chromiumboy", "size": { "x": 32, "y": 32 @@ -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", "directions": 4,