Station AI customizations (#34501)

Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
This commit is contained in:
chromiumboy
2025-05-13 03:50:43 -05:00
committed by GitHub
parent 0eeb46cf9e
commit 7ac67ebf8c
29 changed files with 974 additions and 101 deletions

View File

@@ -9,5 +9,5 @@ public sealed partial class HolographicAvatarComponent : Component
/// The prototype sprite layer data for the hologram
/// </summary>
[DataField, AutoNetworkedField]
public PrototypeLayerData[] LayerData;
public PrototypeLayerData[]? LayerData = null;
}

View File

@@ -0,0 +1,82 @@
using Content.Shared.Holopad;
using Robust.Shared.Prototypes;
namespace Content.Shared.Silicons.StationAi;
public abstract partial class SharedStationAiSystem
{
private ProtoId<StationAiCustomizationGroupPrototype> _stationAiCoreCustomGroupProtoId = "StationAiCoreIconography";
private ProtoId<StationAiCustomizationGroupPrototype> _stationAiHologramCustomGroupProtoId = "StationAiHolograms";
private void InitializeCustomization()
{
SubscribeLocalEvent<StationAiCoreComponent, StationAiCustomizationMessage>(OnStationAiCustomization);
}
private void OnStationAiCustomization(Entity<StationAiCoreComponent> entity, ref StationAiCustomizationMessage args)
{
if (!_protoManager.TryIndex(args.GroupProtoId, out var groupPrototype) || !_protoManager.TryIndex(args.CustomizationProtoId, out var customizationProto))
return;
if (!TryGetHeld((entity, entity.Comp), out var held))
return;
if (!TryComp<StationAiCustomizationComponent>(held, out var stationAiCustomization))
return;
if (stationAiCustomization.ProtoIds.TryGetValue(args.GroupProtoId, out var protoId) && protoId == args.CustomizationProtoId)
return;
stationAiCustomization.ProtoIds[args.GroupProtoId] = args.CustomizationProtoId;
Dirty(held, stationAiCustomization);
// Update hologram
if (groupPrototype.Category == StationAiCustomizationType.Hologram)
UpdateHolographicAvatar((held, stationAiCustomization));
// Update core iconography
if (groupPrototype.Category == StationAiCustomizationType.CoreIconography && TryComp<StationAiHolderComponent>(entity, out var stationAiHolder))
UpdateAppearance((entity, stationAiHolder));
}
private void UpdateHolographicAvatar(Entity<StationAiCustomizationComponent> entity)
{
if (!TryComp<HolographicAvatarComponent>(entity, out var avatar))
return;
if (!entity.Comp.ProtoIds.TryGetValue(_stationAiHologramCustomGroupProtoId, out var protoId))
return;
if (!_protoManager.TryIndex(protoId, out var prototype))
return;
if (!prototype.LayerData.TryGetValue(StationAiState.Hologram.ToString(), out var layerData))
return;
avatar.LayerData = [layerData];
Dirty(entity, avatar);
}
private void CustomizeAppearance(Entity<StationAiCoreComponent> entity, StationAiState state)
{
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.TryIndex(protoId, out var prototype) ||
!prototype.LayerData.TryGetValue(state.ToString(), out var layerData))
{
return;
}
// This data is handled manually in the client StationAiSystem
_appearance.SetData(entity.Owner, StationAiVisualState.Key, layerData);
}
}

View File

@@ -28,6 +28,7 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Utility;
namespace Content.Shared.Silicons.StationAi;
@@ -56,6 +57,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem
[Dependency] private readonly SharedTransformSystem _xforms = default!;
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly StationAiVisionSystem _vision = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
// StationAiHeld is added to anything inside of an AI core.
// StationAiHolder indicates it can hold an AI positronic brain (e.g. holocard / core).
@@ -82,6 +84,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem
InitializeAirlock();
InitializeHeld();
InitializeLight();
InitializeCustomization();
SubscribeLocalEvent<StationAiWhitelistComponent, BoundUserInterfaceCheckRangeEvent>(OnAiBuiCheck);
@@ -107,25 +110,35 @@ public abstract partial class SharedStationAiSystem : EntitySystem
private void OnCoreVerbs(Entity<StationAiCoreComponent> ent, ref GetVerbsEvent<Verb> args)
{
if (!_admin.IsAdmin(args.User) ||
TryGetHeld((ent.Owner, ent.Comp), out _))
{
return;
}
var user = args.User;
args.Verbs.Add(new Verb()
// Admin option to take over the station AI core
if (_admin.IsAdmin(args.User) &&
!TryGetHeld((ent.Owner, ent.Comp), out _))
{
Text = Loc.GetString("station-ai-takeover"),
Category = VerbCategory.Debug,
Act = () =>
args.Verbs.Add(new Verb()
{
var brain = SpawnInContainerOrDrop(DefaultAi, ent.Owner, StationAiCoreComponent.Container);
_mind.ControlMob(user, brain);
},
Impact = LogImpact.High,
});
Text = Loc.GetString("station-ai-takeover"),
Category = VerbCategory.Debug,
Act = () =>
{
var brain = SpawnInContainerOrDrop(DefaultAi, ent.Owner, StationAiCoreComponent.Container);
_mind.ControlMob(user, brain);
},
Impact = LogImpact.High,
});
}
// Option to open the station AI customization menu
if (TryGetHeld((ent, ent.Comp), out var insertedAi) && insertedAi == user)
{
args.Verbs.Add(new Verb()
{
Text = Loc.GetString("station-ai-customization-menu"),
Act = () => _uiSystem.TryOpenUi(ent.Owner, StationAiCustomizationUiKey.Key, insertedAi),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/emotes.svg.192dpi.png")),
});
}
}
private void OnAiAccessible(Entity<StationAiOverlayComponent> ent, ref AccessibleOverrideEvent args)
@@ -494,14 +507,21 @@ public abstract partial class SharedStationAiSystem : EntitySystem
if (!Resolve(entity.Owner, ref entity.Comp, false))
return;
if (!_containers.TryGetContainer(entity.Owner, StationAiHolderComponent.Container, out var container) ||
container.Count == 0)
// 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))
{
_appearance.SetData(entity.Owner, StationAiVisualState.Key, StationAiState.Empty);
CustomizeAppearance((entity, stationAiCore), state);
return;
}
_appearance.SetData(entity.Owner, StationAiVisualState.Key, StationAiState.Occupied);
// Otherwise let generic visualizers handle the appearance update
_appearance.SetData(entity.Owner, StationAiVisualState.Key, state);
}
public virtual void AnnounceIntellicardUsage(EntityUid uid, SoundSpecifier? cue = null) { }
@@ -550,17 +570,23 @@ public sealed partial class JumpToCoreEvent : InstantActionEvent
[Serializable, NetSerializable]
public sealed partial class IntellicardDoAfterEvent : SimpleDoAfterEvent;
[Serializable, NetSerializable]
public enum StationAiVisualState : byte
{
Key,
}
[Serializable, NetSerializable]
public enum StationAiSpriteState : byte
{
Key,
}
[Serializable, NetSerializable]
public enum StationAiState : byte
{
Empty,
Occupied,
Dead,
Hologram,
}

View File

@@ -0,0 +1,53 @@
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Silicons.StationAi;
/// <summary>
/// Holds data for altering the appearance of station AIs.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class StationAiCustomizationComponent : Component
{
/// <summary>
/// Dictionary of the prototype data used for customizing the appearance of the entity.
/// </summary>
[DataField, AutoNetworkedField]
public Dictionary<ProtoId<StationAiCustomizationGroupPrototype>, ProtoId<StationAiCustomizationPrototype>> ProtoIds = new();
}
/// <summary>
/// Message sent to server that contains a station AI customization that the client has selected
/// </summary>
[Serializable, NetSerializable]
public sealed class StationAiCustomizationMessage : BoundUserInterfaceMessage
{
public readonly ProtoId<StationAiCustomizationGroupPrototype> GroupProtoId;
public readonly ProtoId<StationAiCustomizationPrototype> CustomizationProtoId;
public StationAiCustomizationMessage(ProtoId<StationAiCustomizationGroupPrototype> groupProtoId, ProtoId<StationAiCustomizationPrototype> customizationProtoId)
{
GroupProtoId = groupProtoId;
CustomizationProtoId = customizationProtoId;
}
}
/// <summary>
/// Key for opening the station AI customization UI
/// </summary>
[Serializable, NetSerializable]
public enum StationAiCustomizationUiKey : byte
{
Key,
}
/// <summary>
/// The different catagories of station Ai customizations available
/// </summary>
[Serializable, NetSerializable]
public enum StationAiCustomizationType : byte
{
CoreIconography,
Hologram,
}

View File

@@ -0,0 +1,31 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.Silicons.StationAi;
/// <summary>
/// Holds data for customizing the appearance of station AIs.
/// </summary>
[Prototype]
public sealed partial class StationAiCustomizationGroupPrototype : IPrototype
{
[IdDataField]
public string ID { get; } = string.Empty;
/// <summary>
/// The localized name of the customization.
/// </summary>
[DataField(required: true)]
public LocId Name;
/// <summary>
/// The type of customization that is associated with this group.
/// </summary>
[DataField]
public StationAiCustomizationType Category = StationAiCustomizationType.CoreIconography;
/// <summary>
/// The list of prototypes associated with the customization group.
/// </summary>
[DataField(required: true)]
public List<ProtoId<StationAiCustomizationPrototype>> ProtoIds = new();
}

View File

@@ -0,0 +1,54 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array;
using Robust.Shared.Utility;
namespace Content.Shared.Silicons.StationAi;
/// <summary>
/// Holds data for customizing the appearance of station AIs.
/// </summary>
[Prototype]
public sealed partial class StationAiCustomizationPrototype : IPrototype, IInheritingPrototype
{
[IdDataField]
public string ID { get; } = string.Empty;
/// <summary>
/// The (unlocalized) name of the customization.
/// </summary>
[DataField(required: true)]
public LocId Name;
/// <summary>
/// Stores the data which is used to modify the appearance of the station AI.
/// </summary>
[DataField(required: true)]
public Dictionary<string, PrototypeLayerData> LayerData = new();
/// <summary>
/// Key used to index the prototype layer data and extract a preview of the customization (for menus, etc)
/// </summary>
[DataField]
public string PreviewKey = string.Empty;
/// <summary>
/// Specifies a background to use for previewing the customization (for menus, etc)
/// </summary>
[DataField]
public SpriteSpecifier? PreviewBackground;
/// <summary>
/// The prototype we inherit from.
/// </summary>
[ViewVariables]
[ParentDataFieldAttribute(typeof(AbstractPrototypeIdArraySerializer<StationAiCustomizationPrototype>))]
public string[]? Parents { get; }
/// <summary>
/// Specifies whether the prototype is abstract.
/// </summary>
[ViewVariables]
[NeverPushInheritance]
[AbstractDataField]
public bool Abstract { get; }
}