using Content.Shared.ActionBlocker; using Content.Shared.Actions; 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; 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.StationAi; using Content.Shared.Verbs; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Network; using Robust.Shared.Physics; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Timing; 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] 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!; // StationAiHeld is added to anything inside of an AI core. // StationAiHolder indicates it can hold an AI positronic brain (e.g. holocard / core). // StationAiCore holds functionality related to the core itself. // StationAiWhitelist is a general whitelist to stop it being able to interact with anything // StationAiOverlay handles the static overlay. It also handles interaction blocking on client and server // for anything under it. private EntityQuery _broadphaseQuery; private EntityQuery _gridQuery; private const float MaxVisionMultiplier = 5f; public override void Initialize() { base.Initialize(); _broadphaseQuery = GetEntityQuery(); _gridQuery = GetEntityQuery(); InitializeAirlock(); InitializeHeld(); InitializeLight(); SubscribeLocalEvent(OnAiBuiCheck); SubscribeLocalEvent(OnAiAccessible); SubscribeLocalEvent(OnAiInRange); SubscribeLocalEvent(OnAiMenu); SubscribeLocalEvent(OnHolderInit); SubscribeLocalEvent(OnHolderRemove); SubscribeLocalEvent(OnHolderInteract); SubscribeLocalEvent(OnHolderMapInit); SubscribeLocalEvent(OnHolderConInsert); SubscribeLocalEvent(OnHolderConRemove); SubscribeLocalEvent(OnIntellicardDoAfter); SubscribeLocalEvent(OnAiInsert); SubscribeLocalEvent(OnAiRemove); SubscribeLocalEvent(OnAiMapInit); SubscribeLocalEvent(OnAiShutdown); SubscribeLocalEvent(OnCorePower); SubscribeLocalEvent>(OnCoreVerbs); } private void OnCoreVerbs(Entity ent, ref GetVerbsEvent args) { if (!_admin.IsAdmin(args.User) || TryGetHeld((ent.Owner, ent.Comp), out _)) { return; } var user = args.User; } private void OnAiAccessible(Entity ent, ref AccessibleOverrideEvent args) { args.Handled = true; // Hopefully AI never needs storage if (_containers.TryGetContainingContainer(args.Target, out var targetContainer)) { return; } if (!_containers.IsInSameOrTransparentContainer(args.User, args.Target, otherContainer: targetContainer)) { return; } args.Accessible = true; } private void OnAiMenu(Entity ent, ref MenuVisibilityEvent args) { args.Visibility &= ~MenuVisibility.NoFov; } private void OnAiBuiCheck(Entity ent, ref BoundUserInterfaceCheckRangeEvent args) { if (!HasComp(args.Actor)) return; args.Result = BoundUserInterfaceRangeResult.Fail; // Similar to the inrange check but more optimised so server doesn't die. var targetXform = Transform(args.Target); // No cross-grid if (targetXform.GridUid != args.Actor.Comp.GridUid) { return; } if (!_broadphaseQuery.TryComp(targetXform.GridUid, out var broadphase) || !_gridQuery.TryComp(targetXform.GridUid, out var grid)) { return; } var targetTile = Maps.LocalToTile(targetXform.GridUid.Value, grid, targetXform.Coordinates); lock (_vision) { if (_vision.IsAccessible((targetXform.GridUid.Value, broadphase, grid), targetTile, fastPath: true)) { args.Result = BoundUserInterfaceRangeResult.Pass; } } } private void OnAiInRange(Entity ent, ref InRangeOverrideEvent args) { args.Handled = true; var targetXform = Transform(args.Target); // No cross-grid if (targetXform.GridUid != Transform(args.User).GridUid) { return; } // Validate it's in camera range yes this is expensive. // Yes it needs optimising if (!_broadphaseQuery.TryComp(targetXform.GridUid, out var broadphase) || !_gridQuery.TryComp(targetXform.GridUid, out var grid)) { return; } var targetTile = Maps.LocalToTile(targetXform.GridUid.Value, grid, targetXform.Coordinates); args.InRange = _vision.IsAccessible((targetXform.GridUid.Value, broadphase, grid), targetTile); } private void OnIntellicardDoAfter(Entity ent, ref IntellicardDoAfterEvent args) { 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.Args.Target.Value, targetHolder.Slot, ent.Comp.Slot.Item!.Value, args.User, excludeUserAudio: true)) { return; } args.Handled = true; return; } // Otherwise try to take from them 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)) { return; } args.Handled = true; } } 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 (TryGetHeld((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); } private void OnHolderRemove(Entity ent, ref ComponentRemove args) { _slots.RemoveItemSlot(ent.Owner, ent.Comp.Slot); } private void OnHolderConInsert(Entity ent, ref EntInsertedIntoContainerMessage args) { UpdateAppearance((ent.Owner, ent.Comp)); } private void OnHolderConRemove(Entity ent, ref EntRemovedFromContainerMessage args) { UpdateAppearance((ent.Owner, ent.Comp)); } private void OnHolderMapInit(Entity ent, ref MapInitEvent args) { UpdateAppearance(ent.Owner); } private void OnAiShutdown(Entity ent, ref ComponentShutdown args) { // TODO: Tryqueuedel if (_net.IsClient) return; QueueDel(ent.Comp.RemoteEntity); ent.Comp.RemoteEntity = null; } 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 (!SetupEye(ent)) return; AttachEye(ent); } else { ClearEye(ent); } } private void OnAiMapInit(Entity ent, ref MapInitEvent args) { SetupEye(ent); AttachEye(ent); } public void SwitchRemoteEntityMode(Entity entity, bool isRemote) { if (entity.Comp?.Remote == null || entity.Comp.Remote == isRemote) return; var ent = new Entity(entity.Owner, entity.Comp); ent.Comp.Remote = isRemote; EntityCoordinates? coords = ent.Comp.RemoteEntity != null ? Transform(ent.Comp.RemoteEntity.Value).Coordinates : null; // Attach new eye ClearEye(ent); if (SetupEye(ent, coords)) AttachEye(ent); // Adjust user FoV var user = GetInsertedAI(ent); if (TryComp(user, out var eye)) _eye.SetDrawFov(user.Value, !isRemote); } private bool SetupEye(Entity ent, EntityCoordinates? coords = null) { if (_net.IsClient) return false; if (ent.Comp.RemoteEntity != null) return false; var proto = ent.Comp.RemoteEntityProto; if (coords == null) coords = Transform(ent.Owner).Coordinates; if (!ent.Comp.Remote) proto = ent.Comp.PhysicalEntityProto; if (proto != null) { ent.Comp.RemoteEntity = SpawnAtPosition(proto, coords.Value); Dirty(ent); } return true; } private void ClearEye(Entity ent) { if (_net.IsClient) return; QueueDel(ent.Comp.RemoteEntity); ent.Comp.RemoteEntity = null; Dirty(ent); } private void AttachEye(Entity ent) { if (ent.Comp.RemoteEntity == null) return; if (!_containers.TryGetContainer(ent.Owner, StationAiHolderComponent.Container, out var container) || container.ContainedEntities.Count != 1) { return; } // Attach them to the portable eye that can move around. var user = container.ContainedEntities[0]; if (TryComp(user, out EyeComponent? eyeComp)) { _eye.SetDrawFov(user, false, eyeComp); _eye.SetTarget(user, ent.Comp.RemoteEntity.Value, eyeComp); } _mover.SetRelay(user, ent.Comp.RemoteEntity.Value); } private EntityUid? GetInsertedAI(Entity ent) { if (!_containers.TryGetContainer(ent.Owner, StationAiHolderComponent.Container, out var container) || container.ContainedEntities.Count != 1) { return null; } return container.ContainedEntities[0]; } private void OnAiInsert(Entity ent, ref EntInsertedIntoContainerMessage args) { if (args.Container.ID != StationAiCoreComponent.Container) return; if (_timing.ApplyingState) return; ent.Comp.Remote = true; SetupEye(ent); // Just so text and the likes works properly _metadata.SetEntityName(ent.Owner, MetaData(args.Entity).EntityName); AttachEye(ent); } private void OnAiRemove(Entity ent, ref EntRemovedFromContainerMessage args) { if (_timing.ApplyingState) return; ent.Comp.Remote = true; // Reset name to whatever _metadata.SetEntityName(ent.Owner, Prototype(ent.Owner)?.Name ?? string.Empty); // Remove eye relay RemCompDeferred(args.Entity); if (TryComp(args.Entity, out EyeComponent? eyeComp)) { _eye.SetDrawFov(args.Entity, true, eyeComp); _eye.SetTarget(args.Entity, null, eyeComp); } ClearEye(ent); } private void UpdateAppearance(Entity entity) { if (!Resolve(entity.Owner, ref entity.Comp, false)) return; if (!_containers.TryGetContainer(entity.Owner, StationAiHolderComponent.Container, out var container) || container.Count == 0) { _appearance.SetData(entity.Owner, StationAiVisualState.Key, StationAiState.Empty); return; } _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) return false; entity.Comp.Enabled = enabled; Dirty(entity); return true; } public virtual bool SetWhitelistEnabled(Entity entity, bool value, bool announce = false) { if (entity.Comp.Enabled == value) return false; entity.Comp.Enabled = value; Dirty(entity); return true; } /// /// BUI validation for ai interactions. /// private bool ValidateAi(Entity entity) { if (!Resolve(entity.Owner, ref entity.Comp, false)) { return false; } return _blocker.CanComplexInteract(entity.Owner); } } 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 StationAiState : byte { Empty, Occupied, Dead, }