diff --git a/Content.Server/Silicons/StationAi/StationAiSystem.cs b/Content.Server/Silicons/StationAi/StationAiSystem.cs index 846497387d..a10833dc63 100644 --- a/Content.Server/Silicons/StationAi/StationAiSystem.cs +++ b/Content.Server/Silicons/StationAi/StationAiSystem.cs @@ -1,10 +1,11 @@ using System.Linq; using Content.Server.Chat.Managers; -using Content.Server.Chat.Systems; using Content.Shared.Chat; +using Content.Shared.Mind; +using Content.Shared.Roles; using Content.Shared.Silicons.StationAi; using Content.Shared.StationAi; -using Robust.Shared.Audio.Systems; +using Robust.Shared.Audio; using Robust.Shared.Map.Components; using Robust.Shared.Player; @@ -14,6 +15,8 @@ public sealed class StationAiSystem : SharedStationAiSystem { [Dependency] private readonly IChatManager _chats = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly SharedMindSystem _mind = default!; + [Dependency] private readonly SharedRoleSystem _roles = default!; private readonly HashSet> _ais = new(); @@ -43,6 +46,19 @@ public sealed class StationAiSystem : SharedStationAiSystem return true; } + public override void AnnounceIntellicardUsage(EntityUid uid, SoundSpecifier? cue = null) + { + if (!TryComp(uid, out var actor)) + return; + + var msg = Loc.GetString("ai-consciousness-download-warning"); + var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", msg)); + _chats.ChatMessageToOne(ChatChannel.Server, msg, wrappedMessage, default, false, actor.PlayerSession.Channel, colorOverride: Color.Red); + + if (cue != null && _mind.TryGetMind(uid, out var mindId, out _)) + _roles.MindPlaySound(mindId, cue); + } + private void AnnounceSnip(EntityUid entity) { var xform = Transform(entity); diff --git a/Content.Shared/Intellicard/Components/IntellicardComponent.cs b/Content.Shared/Intellicard/Components/IntellicardComponent.cs new file mode 100644 index 0000000000..e27174977f --- /dev/null +++ b/Content.Shared/Intellicard/Components/IntellicardComponent.cs @@ -0,0 +1,39 @@ +using Robust.Shared.Audio; +using Robust.Shared.GameStates; + +namespace Content.Shared.Intellicard; + +/// +/// Allows this entity to download the station AI onto an AiHolderComponent. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class IntellicardComponent : Component +{ + /// + /// The duration it takes to download the AI from an AiHolder. + /// + [DataField, AutoNetworkedField] + public int DownloadTime = 15; + + /// + /// The duration it takes to upload the AI to an AiHolder. + /// + [DataField, AutoNetworkedField] + public int UploadTime = 3; + + /// + /// The sound that plays for the AI + /// when they are being downloaded + /// + [DataField, AutoNetworkedField] + public SoundSpecifier? WarningSound = new SoundPathSpecifier("/Audio/Misc/notice2.ogg"); + + /// + /// The delay before allowing the warning to play again in seconds. + /// + [DataField, AutoNetworkedField] + public TimeSpan WarningDelay = TimeSpan.FromSeconds(8); + + [ViewVariables] + public TimeSpan NextWarningAllowed = TimeSpan.Zero; +} diff --git a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Held.cs b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Held.cs index e067cf3efa..c9279b0215 100644 --- a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Held.cs +++ b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Held.cs @@ -54,7 +54,7 @@ public abstract partial class SharedStationAiSystem } /// - /// Tries to get the entity held in the AI core. + /// Tries to get the entity held in the AI core using StationAiCore. /// private bool TryGetHeld(Entity entity, out EntityUid held) { @@ -71,6 +71,24 @@ public abstract partial class SharedStationAiSystem return true; } + /// + /// Tries to get the entity held in the AI using StationAiHolder. + /// + private bool TryGetHeldFromHolder(Entity entity, out EntityUid held) + { + held = EntityUid.Invalid; + + if (!Resolve(entity.Owner, ref entity.Comp)) + return false; + + if (!_containers.TryGetContainer(entity.Owner, StationAiHolderComponent.Container, out var container) || + container.ContainedEntities.Count == 0) + return false; + + held = container.ContainedEntities[0]; + return true; + } + private bool TryGetCore(EntityUid ent, out Entity core) { if (!_containers.TryGetContainingContainer(ent, out var container) || diff --git a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs index f88df9eea6..189515635a 100644 --- a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs +++ b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs @@ -4,7 +4,9 @@ using Content.Shared.Administration.Managers; using Content.Shared.Containers.ItemSlots; using Content.Shared.Database; using Content.Shared.Doors.Systems; +using Content.Shared.DoAfter; using Content.Shared.Electrocution; +using Content.Shared.Intellicard; using Content.Shared.Interaction; using Content.Shared.Item.ItemToggle; using Content.Shared.Mind; @@ -15,6 +17,7 @@ using Content.Shared.Power; using Content.Shared.Power.EntitySystems; using Content.Shared.StationAi; using Content.Shared.Verbs; +using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.Map.Components; @@ -40,6 +43,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem [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!; @@ -87,6 +91,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem SubscribeLocalEvent(OnHolderMapInit); SubscribeLocalEvent(OnHolderConInsert); SubscribeLocalEvent(OnHolderConRemove); + SubscribeLocalEvent(OnIntellicardDoAfter); SubscribeLocalEvent(OnAiInsert); SubscribeLocalEvent(OnAiRemove); @@ -197,15 +202,22 @@ public abstract partial class SharedStationAiSystem : EntitySystem args.InRange = _vision.IsAccessible((targetXform.GridUid.Value, broadphase, grid), targetTile); } - private void OnHolderInteract(Entity ent, ref AfterInteractEvent args) + + private void OnIntellicardDoAfter(Entity ent, ref IntellicardDoAfterEvent args) { - if (!TryComp(args.Target, out StationAiHolderComponent? targetHolder)) + if (args.Cancelled) + return; + + if (args.Handled) + return; + + if (!TryComp(args.Args.Target, out StationAiHolderComponent? targetHolder)) return; // Try to insert our thing into them if (_slots.CanEject(ent.Owner, args.User, ent.Comp.Slot)) { - if (!_slots.TryInsert(args.Target.Value, targetHolder.Slot, ent.Comp.Slot.Item!.Value, args.User, excludeUserAudio: true)) + if (!_slots.TryInsert(args.Args.Target.Value, targetHolder.Slot, ent.Comp.Slot.Item!.Value, args.User, excludeUserAudio: true)) { return; } @@ -215,7 +227,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem } // Otherwise try to take from them - if (_slots.CanEject(args.Target.Value, args.User, targetHolder.Slot)) + if (_slots.CanEject(args.Args.Target.Value, args.User, targetHolder.Slot)) { if (!_slots.TryInsert(ent.Owner, ent.Comp.Slot, targetHolder.Slot.Item!.Value, args.User, excludeUserAudio: true)) { @@ -226,6 +238,55 @@ public abstract partial class SharedStationAiSystem : EntitySystem } } + private void OnHolderInteract(Entity ent, ref AfterInteractEvent args) + { + if (args.Handled || !args.CanReach || args.Target == null) + return; + + if (!TryComp(args.Target, out StationAiHolderComponent? targetHolder)) + return; + + //Don't want to download/upload between several intellicards. You can just pick it up at that point. + if (HasComp(args.Target)) + return; + + 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); + + if (cardHasAi && coreHasAi) + { + _popup.PopupClient(Loc.GetString("intellicard-core-occupied"), args.User, args.User, PopupType.Medium); + args.Handled = true; + return; + } + if (!cardHasAi && !coreHasAi) + { + _popup.PopupClient(Loc.GetString("intellicard-core-empty"), args.User, args.User, PopupType.Medium); + args.Handled = true; + return; + } + + if (TryGetHeldFromHolder((args.Target.Value, targetHolder), out var held) && _timing.CurTime > intelliComp.NextWarningAllowed) + { + intelliComp.NextWarningAllowed = _timing.CurTime + intelliComp.WarningDelay; + AnnounceIntellicardUsage(held, intelliComp.WarningSound); + } + + var doAfterArgs = new DoAfterArgs(EntityManager, args.User, cardHasAi ? intelliComp.UploadTime : intelliComp.DownloadTime, new IntellicardDoAfterEvent(), args.Target, ent.Owner) + { + BreakOnDamage = true, + BreakOnMove = true, + NeedHand = true, + BreakOnDropItem = true + }; + + _doAfter.TryStartDoAfter(doAfterArgs); + args.Handled = true; + } + private void OnHolderInit(Entity ent, ref ComponentInit args) { _slots.AddItemSlot(ent.Owner, StationAiHolderComponent.Container, ent.Comp.Slot); @@ -378,6 +439,8 @@ public abstract partial class SharedStationAiSystem : EntitySystem _appearance.SetData(entity.Owner, StationAiVisualState.Key, StationAiState.Occupied); } + public virtual void AnnounceIntellicardUsage(EntityUid uid, SoundSpecifier? cue = null) { } + public virtual bool SetVisionEnabled(Entity entity, bool enabled, bool announce = false) { if (entity.Comp.Enabled == enabled) @@ -419,6 +482,10 @@ public sealed partial class JumpToCoreEvent : InstantActionEvent } +[Serializable, NetSerializable] +public sealed partial class IntellicardDoAfterEvent : SimpleDoAfterEvent; + + [Serializable, NetSerializable] public enum StationAiVisualState : byte { diff --git a/Resources/Locale/en-US/intellicard/intellicard.ftl b/Resources/Locale/en-US/intellicard/intellicard.ftl new file mode 100644 index 0000000000..aed155a120 --- /dev/null +++ b/Resources/Locale/en-US/intellicard/intellicard.ftl @@ -0,0 +1,3 @@ +# General +intellicard-core-occupied = The AI core is already occupied by another digital consciousness. +intellicard-core-empty = The AI core has no digital consciousness to download. \ 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 7d9db3f6dc..76c30eb101 100644 --- a/Resources/Locale/en-US/silicons/station-ai.ftl +++ b/Resources/Locale/en-US/silicons/station-ai.ftl @@ -20,3 +20,5 @@ electrify-door-off = Disable overcharge toggle-light = Toggle light ai-device-not-responding = Device is not responding + +ai-consciousness-download-warning = Your consciousness is being downloaded. diff --git a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml index 39750b470f..e787ef59f0 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml @@ -301,6 +301,7 @@ unshaded: Empty: { state: empty } Occupied: { state: full } + - type: Intellicard - type: entity id: PlayerStationAiEmpty