The station AI can be destroyed (#39588)

* Initial commit

* Fixing merge conflict

* Merge conflict fixed

* Anchorable entities can now be marked as 'unanchorable'

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

This reverts commit 6a502e62a703cf06bd36ed3bdefe655fc074cfc5

This functionality will be made into a separate PR

* Error sprite

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

* Added intellicard sprite

* AI damage overlays

* Added fixtures

* AI core accent changes when damaged or low on power

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

* Updated 'dead' sprite

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

* AI battery duration set to 10 minutes

* Initial commit

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

* Initial resources commit

* Initial code commit

* Sprite update

* Bug fixes and updates

* Basic console UI

* Code refactor

* Added lock screen

* Added all outstanding UI features

* Added purge sprites

* Better appearance handling

* Fixed issue with purge sprite

* Finalized UI design

* Major components finalized

* Bit of clean up

* Removed some code that was used for testing

* Tweaked some text

* Removed extra space

* Added the circuitboard to the RD's locker

* Addressed reviewer comments plus tweaks

* Addressed reviewer comments plus tweaks

* Removed instances of granular damage

* Various improvements

* Removed testing code

* Fixed issue with disabled buttons

* Finalized code

* Addressed review comments

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

* Fixing build failure

* Addressed review comments

* Addressed review comments

* Added reverse path for construction graph

* Removed unneeded reference

* Parts can be purchased through cargo

* Fixing merge conflict

* Merge conflict resolved

* Fixing merge conflict

* Code update

* Code updates

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

* Added screen static sprite

* Added better support for ghosted AI players plus code tweaks

* Various improvements and clean up

* Increased purge duration to 60 seconds

* Fixed needless complication

* Addressed reviewer comments part 1

* Addressed reviewer comments part 2

* Further fixes

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

* Adjusted power values again

* Addressed review comments

* Addressed review comments

* Fixed test fail

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

* Added pop up text

* Bug fix

* Tweaks and fixes

* Fixed restoration console not updating when the AI finishes rebooting

* Update SharedStationAiSystem.Held.cs

---------

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

View File

@@ -0,0 +1,411 @@
using Content.Shared.Administration.Logs;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Database;
using Content.Shared.Examine;
using Content.Shared.Lock;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Power;
using Robust.Shared.Containers;
using Robust.Shared.Timing;
using System.Diagnostics.CodeAnalysis;
namespace Content.Shared.Silicons.StationAi;
/// <summary>
/// This system is used to handle the actions of AI Restoration Consoles.
/// These consoles can be used to revive dead station AIs, or destroy them.
/// </summary>
public abstract partial class SharedStationAiFixerConsoleSystem : EntitySystem
{
[Dependency] private readonly SharedUserInterfaceSystem _userInterface = default!;
[Dependency] private readonly ItemSlotsSystem _itemSlots = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StationAiFixerConsoleComponent, EntInsertedIntoContainerMessage>(OnInserted);
SubscribeLocalEvent<StationAiFixerConsoleComponent, EntRemovedFromContainerMessage>(OnRemoved);
SubscribeLocalEvent<StationAiFixerConsoleComponent, LockToggledEvent>(OnLockToggle);
SubscribeLocalEvent<StationAiFixerConsoleComponent, StationAiFixerConsoleMessage>(OnMessage);
SubscribeLocalEvent<StationAiFixerConsoleComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<StationAiFixerConsoleComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<StationAiCustomizationComponent, StationAiCustomizationStateChanged>(OnStationAiCustomizationStateChanged);
}
private void OnInserted(Entity<StationAiFixerConsoleComponent> ent, ref EntInsertedIntoContainerMessage args)
{
if (args.Container.ID != ent.Comp.StationAiHolderSlot)
return;
if (TryGetTarget(ent, out var target))
{
ent.Comp.ActionTarget = target;
Dirty(ent);
}
UpdateAppearance(ent);
}
private void OnRemoved(Entity<StationAiFixerConsoleComponent> ent, ref EntRemovedFromContainerMessage args)
{
if (args.Container.ID != ent.Comp.StationAiHolderSlot)
return;
ent.Comp.ActionTarget = null;
StopAction(ent);
}
private void OnLockToggle(Entity<StationAiFixerConsoleComponent> ent, ref LockToggledEvent args)
{
if (_userInterface.TryGetOpenUi(ent.Owner, StationAiFixerConsoleUiKey.Key, out var bui))
bui.Update<StationAiFixerConsoleBoundUserInterfaceState>();
}
private void OnMessage(Entity<StationAiFixerConsoleComponent> ent, ref StationAiFixerConsoleMessage args)
{
if (TryComp<LockComponent>(ent, out var lockable) && lockable.Locked)
return;
switch (args.Action)
{
case StationAiFixerConsoleAction.Eject:
EjectStationAiHolder(ent, args.Actor);
break;
case StationAiFixerConsoleAction.Repair:
RepairStationAi(ent, args.Actor);
break;
case StationAiFixerConsoleAction.Purge:
PurgeStationAi(ent, args.Actor);
break;
case StationAiFixerConsoleAction.Cancel:
CancelAction(ent, args.Actor);
break;
}
}
private void OnPowerChanged(Entity<StationAiFixerConsoleComponent> ent, ref PowerChangedEvent args)
{
if (args.Powered)
return;
StopAction(ent);
}
private void OnExamined(Entity<StationAiFixerConsoleComponent> ent, ref ExaminedEvent args)
{
var message = TryGetStationAiHolder(ent, out var holder) ?
Loc.GetString("station-ai-fixer-console-examination-station-ai-holder-present", ("holder", Name(holder.Value))) :
Loc.GetString("station-ai-fixer-console-examination-station-ai-holder-absent");
args.PushMarkup(message);
}
private void OnStationAiCustomizationStateChanged(Entity<StationAiCustomizationComponent> ent, ref StationAiCustomizationStateChanged args)
{
if (_container.TryGetOuterContainer(ent, Transform(ent), out var outerContainer) &&
TryComp<StationAiFixerConsoleComponent>(outerContainer.Owner, out var stationAiFixerConsole))
{
UpdateAppearance((outerContainer.Owner, stationAiFixerConsole));
}
}
private void EjectStationAiHolder(Entity<StationAiFixerConsoleComponent> ent, EntityUid user)
{
if (!TryComp<ItemSlotsComponent>(ent, out var slots))
return;
if (!_itemSlots.TryGetSlot(ent, ent.Comp.StationAiHolderSlot, out var holderSlot, slots))
return;
if (_itemSlots.TryEjectToHands(ent, holderSlot, user, true))
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(user):user} ejected a station AI holder from AI restoration console ({ToPrettyString(ent.Owner)})");
}
private void RepairStationAi(Entity<StationAiFixerConsoleComponent> ent, EntityUid user)
{
if (ent.Comp.ActionTarget == null)
return;
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(user):user} started a repair of {ToPrettyString(ent.Comp.ActionTarget)} using an AI restoration console ({ToPrettyString(ent.Owner)})");
StartAction(ent, StationAiFixerConsoleAction.Repair);
}
private void PurgeStationAi(Entity<StationAiFixerConsoleComponent> ent, EntityUid user)
{
if (ent.Comp.ActionTarget == null)
return;
_adminLogger.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(user):user} started a purge of {ToPrettyString(ent.Comp.ActionTarget)} using {ToPrettyString(ent.Owner)}");
StartAction(ent, StationAiFixerConsoleAction.Purge);
}
private void CancelAction(Entity<StationAiFixerConsoleComponent> ent, EntityUid user)
{
if (!IsActionInProgress(ent))
return;
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(user):user} canceled operation involving {ToPrettyString(ent.Comp.ActionTarget)} and {ToPrettyString(ent.Owner)} ({ent.Comp.ActionType} action)");
StopAction(ent);
}
/// <summary>
/// Initiates an action upon a target entity by the specified console.
/// </summary>
/// <param name="ent">The console.</param>
/// <param name="actionType">The action to be enacted on the target.</param>
private void StartAction(Entity<StationAiFixerConsoleComponent> ent, StationAiFixerConsoleAction actionType)
{
if (IsActionInProgress(ent))
{
StopAction(ent);
}
if (IsTargetValid(ent, actionType))
{
var duration = actionType == StationAiFixerConsoleAction.Repair ?
ent.Comp.RepairDuration :
ent.Comp.PurgeDuration;
ent.Comp.ActionType = actionType;
ent.Comp.ActionStartTime = _timing.CurTime;
ent.Comp.ActionEndTime = _timing.CurTime + duration;
ent.Comp.CurrentActionStage = 0;
Dirty(ent);
}
UpdateAppearance(ent);
}
/// <summary>
/// Updates the current action being conducted by the specified console.
/// </summary>
/// <param name="ent">The console.</param>
private void UpdateAction(Entity<StationAiFixerConsoleComponent> ent)
{
if (IsActionInProgress(ent))
{
if (ent.Comp.ActionTarget == null)
{
StopAction(ent);
return;
}
if (_timing.CurTime >= ent.Comp.ActionEndTime)
{
FinalizeAction(ent);
return;
}
var currentStage = CalculateActionStage(ent);
if (currentStage != ent.Comp.CurrentActionStage)
{
ent.Comp.CurrentActionStage = currentStage;
Dirty(ent);
}
}
UpdateAppearance(ent);
}
/// <summary>
/// Terminates any action being conducted by the specified console.
/// </summary>
/// <param name="ent">The console.</param>
private void StopAction(Entity<StationAiFixerConsoleComponent> ent)
{
ent.Comp.ActionType = StationAiFixerConsoleAction.None;
Dirty(ent);
UpdateAppearance(ent);
}
/// <summary>
/// Finalizes the action being conducted by the specified console
/// (i.e., repairing or purging a target).
/// </summary>
/// <param name="ent">The console.</param>
protected virtual void FinalizeAction(Entity<StationAiFixerConsoleComponent> ent)
{
if (IsActionInProgress(ent) && ent.Comp.ActionTarget != null)
{
if (ent.Comp.ActionType == StationAiFixerConsoleAction.Repair)
{
_mobState.ChangeMobState(ent.Comp.ActionTarget.Value, MobState.Alive);
}
else if (ent.Comp.ActionType == StationAiFixerConsoleAction.Purge &&
TryGetStationAiHolder(ent, out var holder))
{
_container.RemoveEntity(holder.Value, ent.Comp.ActionTarget.Value, force: true);
PredictedQueueDel(ent.Comp.ActionTarget);
ent.Comp.ActionTarget = null;
Dirty(ent);
}
}
StopAction(ent);
}
/// <summary>
/// Updates the appearance of the specified console based on its current state.
/// </summary>
/// <param name="ent">The console.</param>
private void UpdateAppearance(Entity<StationAiFixerConsoleComponent> ent)
{
if (!TryComp<AppearanceComponent>(ent, out var appearance))
return;
if (IsActionInProgress(ent))
{
var currentStage = ent.Comp.ActionType + ent.Comp.CurrentActionStage.ToString();
if (!_appearance.TryGetData(ent, StationAiFixerConsoleVisuals.Key, out string oldStage, appearance) ||
oldStage != currentStage)
{
_appearance.SetData(ent, StationAiFixerConsoleVisuals.Key, currentStage, appearance);
}
return;
}
var target = ent.Comp.ActionTarget;
var state = StationAiState.Empty;
if (TryComp<StationAiCustomizationComponent>(target, out var customization) && !EntityManager.IsQueuedForDeletion(target.Value))
{
state = customization.State;
}
_appearance.SetData(ent, StationAiFixerConsoleVisuals.Key, state.ToString(), appearance);
}
/// <summary>
/// Calculates the current stage of any in-progress actions.
/// </summary>
/// <param name="ent">The console.</param>
/// <returns>The current stage.</returns>
private int CalculateActionStage(Entity<StationAiFixerConsoleComponent> ent)
{
var completionPercentage = (_timing.CurTime - ent.Comp.ActionStartTime) / (ent.Comp.ActionEndTime - ent.Comp.ActionStartTime);
return (int)(completionPercentage * ent.Comp.ActionStageCount);
}
/// <summary>
/// Try to find a valid target being stored inside the specified console.
/// </summary>
/// <param name="ent">The console.</param>
/// <param name="target">The found target.</param>
/// <returns>True if a valid target was found.</returns>
public bool TryGetTarget(Entity<StationAiFixerConsoleComponent> ent, [NotNullWhen(true)] out EntityUid? target)
{
target = null;
if (!TryGetStationAiHolder(ent, out var holder))
return false;
if (!_container.TryGetContainer(holder.Value, ent.Comp.StationAiMindSlot, out var stationAiMindSlot) || stationAiMindSlot.Count == 0)
return false;
var stationAi = stationAiMindSlot.ContainedEntities[0];
if (!HasComp<MobStateComponent>(stationAi))
return false;
target = stationAi;
return !EntityManager.IsQueuedForDeletion(target.Value);
}
/// <summary>
/// Try to find a station AI holder being stored inside the specified console.
/// </summary>
/// <param name="ent">The console.</param>
/// <param name="holder">The found holder.</param>
/// <returns>True if a valid holder was found.</returns>
public bool TryGetStationAiHolder(Entity<StationAiFixerConsoleComponent> ent, [NotNullWhen(true)] out EntityUid? holder)
{
holder = null;
if (!_container.TryGetContainer(ent, ent.Comp.StationAiHolderSlot, out var holderContainer) ||
holderContainer.Count == 0)
{
return false;
}
holder = holderContainer.ContainedEntities[0];
return true;
}
/// <summary>
/// Determines if the specified console can act upon its action target.
/// </summary>
/// <param name="ent">The console.</param>
/// <param name="actionType">The action to be enacted on the target.</param>
/// <returns>True, if the target is valid for the specified console action.</returns>
public bool IsTargetValid(Entity<StationAiFixerConsoleComponent> ent, StationAiFixerConsoleAction actionType)
{
if (ent.Comp.ActionTarget == null)
return false;
if (actionType == StationAiFixerConsoleAction.Purge)
return true;
if (actionType == StationAiFixerConsoleAction.Repair &&
_mobState.IsDead(ent.Comp.ActionTarget.Value))
{
return true;
}
return false;
}
/// <summary>
/// Returns whether an station AI holder is inserted into the specified console.
/// </summary>
/// <param name="ent">The console.</param>
/// <returns>True if a station AI holder is inserted.</returns>
public bool IsStationAiHolderInserted(Entity<StationAiFixerConsoleComponent> ent)
{
return TryGetStationAiHolder(ent, out var _);
}
/// <summary>
/// Returns whether the specified console has an action in progress.
/// </summary>
/// <param name="ent">The console.</param>
/// <returns>Ture, if an action is in progress.</returns>
public bool IsActionInProgress(Entity<StationAiFixerConsoleComponent> ent)
{
return ent.Comp.ActionType != StationAiFixerConsoleAction.None;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = AllEntityQuery<StationAiFixerConsoleComponent>();
while (query.MoveNext(out var uid, out var stationAiFixerConsole))
{
var ent = (uid, stationAiFixerConsole);
if (!IsActionInProgress(ent))
continue;
UpdateAction(ent);
}
}
}

View File

@@ -1,5 +1,9 @@
using Content.Shared.Holopad;
using Content.Shared.Mobs;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using System.Diagnostics.CodeAnalysis;
namespace Content.Shared.Silicons.StationAi;
@@ -8,9 +12,15 @@ public abstract partial class SharedStationAiSystem
private ProtoId<StationAiCustomizationGroupPrototype> _stationAiCoreCustomGroupProtoId = "StationAiCoreIconography";
private ProtoId<StationAiCustomizationGroupPrototype> _stationAiHologramCustomGroupProtoId = "StationAiHolograms";
private readonly SpriteSpecifier.Rsi _stationAiRebooting = new(new ResPath("Mobs/Silicon/station_ai.rsi"), "ai_fuzz");
private void InitializeCustomization()
{
SubscribeLocalEvent<StationAiCoreComponent, StationAiCustomizationMessage>(OnStationAiCustomization);
SubscribeLocalEvent<StationAiCustomizationComponent, PlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<StationAiCustomizationComponent, PlayerDetachedEvent>(OnPlayerDetached);
SubscribeLocalEvent<StationAiCustomizationComponent, MobStateChangedEvent>(OnMobStateChanged);
}
private void OnStationAiCustomization(Entity<StationAiCoreComponent> entity, ref StationAiCustomizationMessage args)
@@ -29,17 +39,53 @@ public abstract partial class SharedStationAiSystem
stationAiCustomization.ProtoIds[args.GroupProtoId] = args.CustomizationProtoId;
Dirty(held, stationAiCustomization);
Dirty(held.Value, stationAiCustomization);
// Update hologram
if (groupPrototype.Category == StationAiCustomizationType.Hologram)
UpdateHolographicAvatar((held, stationAiCustomization));
UpdateHolographicAvatar((held.Value, stationAiCustomization));
// Update core iconography
if (groupPrototype.Category == StationAiCustomizationType.CoreIconography && TryComp<StationAiHolderComponent>(entity, out var stationAiHolder))
UpdateAppearance((entity, stationAiHolder));
}
private void OnPlayerAttached(Entity<StationAiCustomizationComponent> ent, ref PlayerAttachedEvent args)
{
var state = _mobState.IsDead(ent) ? StationAiState.Dead : StationAiState.Occupied;
SetStationAiState(ent, state);
}
private void OnPlayerDetached(Entity<StationAiCustomizationComponent> ent, ref PlayerDetachedEvent args)
{
var state = _mobState.IsDead(ent) ? StationAiState.Dead : StationAiState.Rebooting;
SetStationAiState(ent, state);
}
protected virtual void OnMobStateChanged(Entity<StationAiCustomizationComponent> ent, ref MobStateChangedEvent args)
{
var state = (args.NewMobState == MobState.Dead) ? StationAiState.Dead : StationAiState.Rebooting;
SetStationAiState(ent, state);
}
protected void SetStationAiState(Entity<StationAiCustomizationComponent> ent, StationAiState state)
{
if (ent.Comp.State != state)
{
ent.Comp.State = state;
Dirty(ent);
var ev = new StationAiCustomizationStateChanged(state);
RaiseLocalEvent(ent, ref ev);
}
if (_containers.TryGetContainingContainer(ent.Owner, out var container) &&
TryComp<StationAiHolderComponent>(container.Owner, out var holder))
{
UpdateAppearance((container.Owner, holder));
}
}
private void UpdateHolographicAvatar(Entity<StationAiCustomizationComponent> entity)
{
if (!TryComp<HolographicAvatarComponent>(entity, out var avatar))
@@ -62,21 +108,36 @@ public abstract partial class SharedStationAiSystem
{
var stationAi = GetInsertedAI(entity);
if (stationAi == null)
{
_appearance.RemoveData(entity.Owner, StationAiVisualState.Key);
return;
}
if (!TryComp<StationAiCustomizationComponent>(stationAi, out var stationAiCustomization) ||
!stationAiCustomization.ProtoIds.TryGetValue(_stationAiCoreCustomGroupProtoId, out var protoId) ||
!_protoManager.Resolve(protoId, out var prototype) ||
!prototype.LayerData.TryGetValue(state.ToString(), out var layerData))
!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);
}
/// <summary>
/// Returns a dictionary containing the station AI's appearance for different states.
/// </summary>
/// <param name="entity">The station AI.</param>
/// <param name="layerData">The apperance data, indexed by possible AI states.</param>
/// <returns>True if the apperance data was found.</returns>
public bool TryGetCustomizedAppearanceData(Entity<StationAiCustomizationComponent> entity, [NotNullWhen(true)] out Dictionary<string, PrototypeLayerData>? layerData)
{
layerData = null;
if (!entity.Comp.ProtoIds.TryGetValue(_stationAiCoreCustomGroupProtoId, out var protoId) ||
!_protoManager.Resolve(protoId, out var prototype) ||
prototype.LayerData.Count == 0)
{
return false;
}
layerData = prototype.LayerData;
return true;
}
}

View File

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

View File

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

View File

@@ -38,11 +38,19 @@ public sealed partial class StationAiCoreComponent : Component
[DataField(readOnly: true)]
public EntProtoId? PhysicalEntityProto = "StationAiHoloLocal";
/// <summary>
/// Name of the container slot that holds the inhabiting AI's mind
/// </summary>
public const string Container = "station_ai_mind_slot";
/// <summary>
/// Name of the container slot that holds the 'brain' used to construct the AI core
/// </summary>
public const string BrainContainer = "station_ai_brain_slot";
}
/// <summary>
/// This event is raised on a station AI 'eye' that is being replaced with a new one
/// This event is raised on a station AI 'eye' that is being replaced with a new one
/// </summary>
/// <param name="NewRemoteEntity">The entity UID of the replacement entity</param>
[ByRefEvent]

View File

@@ -15,6 +15,12 @@ public sealed partial class StationAiCustomizationComponent : Component
/// </summary>
[DataField, AutoNetworkedField]
public Dictionary<ProtoId<StationAiCustomizationGroupPrototype>, ProtoId<StationAiCustomizationPrototype>> ProtoIds = new();
/// <summary>
/// The current visual state of the associated entity.
/// </summary>
[DataField, AutoNetworkedField]
public StationAiState State = StationAiState.Occupied;
}
/// <summary>
@@ -33,6 +39,12 @@ public sealed class StationAiCustomizationMessage : BoundUserInterfaceMessage
}
}
/// <summary>
/// Event raised when the station AI customization visual state changes
/// </summary>
[ByRefEvent]
public record StationAiCustomizationStateChanged(StationAiState NewState);
/// <summary>
/// Key for opening the station AI customization UI
/// </summary>

View File

@@ -0,0 +1,144 @@
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Silicons.StationAi;
/// <summary>
/// This component holds data needed for AI Restoration Consoles to function.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
[Access(typeof(SharedStationAiFixerConsoleSystem))]
public sealed partial class StationAiFixerConsoleComponent : Component
{
/// <summary>
/// Determines how long a repair takes to complete (in seconds).
/// </summary>
[DataField]
public TimeSpan RepairDuration = TimeSpan.FromSeconds(30);
/// <summary>
/// Determines how long a purge takes to complete (in seconds).
/// </summary>
[DataField]
public TimeSpan PurgeDuration = TimeSpan.FromSeconds(30);
/// <summary>
/// The number of stages that a console action (repair or purge)
/// progresses through before it concludes. Each stage has an equal
/// duration. The appearance data of the entity is updated with
/// each new stage reached.
/// </summary>
[DataField]
public int ActionStageCount = 4;
/// <summary>
/// The time at which the current action commenced.
/// </summary>
[DataField, AutoNetworkedField, AutoPausedField]
public TimeSpan ActionStartTime = TimeSpan.FromSeconds(0);
/// <summary>
/// The time at which the current action will end.
/// </summary>
[DataField, AutoNetworkedField, AutoPausedField]
public TimeSpan ActionEndTime = TimeSpan.FromSeconds(0);
/// <summary>
/// The type of action that is currently in progress.
/// </summary>
[DataField, AutoNetworkedField]
public StationAiFixerConsoleAction ActionType = StationAiFixerConsoleAction.None;
/// <summary>
/// The target of the current action.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? ActionTarget;
/// <summary>
/// The current stage of the action in progress.
/// </summary>
[DataField, AutoNetworkedField]
public int CurrentActionStage;
/// <summary>
/// Sound clip that is played when a repair is completed.
/// </summary>
[DataField]
public SoundSpecifier? RepairFinishedSound = new SoundPathSpecifier("/Audio/Items/beep.ogg");
/// <summary>
/// Sound clip that is played when a repair is completed.
/// </summary>
[DataField]
public SoundSpecifier? PurgeFinishedSound = new SoundPathSpecifier("/Audio/Machines/beep.ogg");
/// <summary>
/// The name of the console slot which is used to contain station AI holders.
/// </summary>
[DataField]
public string StationAiHolderSlot = "station_ai_holder";
/// <summary>
/// The name of the station AI holder slot which actually contains the station AI.
/// </summary>
[DataField]
public string StationAiMindSlot = "station_ai_mind_slot";
}
/// <summary>
/// Message sent from the server to the client to update the UI of AI Restoration Consoles.
/// </summary>
[Serializable, NetSerializable]
public sealed class StationAiFixerConsoleBoundUserInterfaceState : BoundUserInterfaceState;
/// <summary>
/// Message sent from the client to the server to handle player UI inputs from AI Restoration Consoles.
/// </summary>
[Serializable, NetSerializable]
public sealed class StationAiFixerConsoleMessage : BoundUserInterfaceMessage
{
public StationAiFixerConsoleAction Action;
public StationAiFixerConsoleMessage(StationAiFixerConsoleAction action)
{
Action = action;
}
}
/// <summary>
/// Potential actions that AI Restoration Consoles can perform.
/// </summary>
[Serializable, NetSerializable]
public enum StationAiFixerConsoleAction
{
None,
Eject,
Repair,
Purge,
Cancel,
}
/// <summary>
/// Appearance keys for AI Restoration Consoles.
/// </summary>
[Serializable, NetSerializable]
public enum StationAiFixerConsoleVisuals : byte
{
Key,
ActionProgress,
MobState,
RepairProgress,
PurgeProgress,
}
/// <summary>
/// Interactable UI key for AI Restoration Consoles.
/// </summary>
[Serializable, NetSerializable]
public enum StationAiFixerConsoleUiKey
{
Key,
}