Files
space-station-14/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs
chromiumboy 7780b867ac Holopads (#32711)
* Initial resources commit

* Initial code commit

* Added additional resources

* Continuing to build holopad and telephone systems

* Added hologram shader

* Added hologram system and entity

* Holo calls now have a hologram of the user appear on them

* Initial implementation of holopads transmitting nearby chatter

* Added support for linking across multiple telephones/holopads/entities

* Fixed a bunch of bugs

* Tried simplifying holopad entity dependence, added support for mid-call user switching

* Replaced PVS expansion with manually networked sprite states

* Adjusted volume of ring tone

* Added machine board

* Minor features and tweaks

* Resolving merge conflict

* Recommit audio attributions

* Telephone chat adjustments

* Added support for AI interactions with holopads

* Building the holopad UI

* Holopad UI finished

* Further UI tweaks

* Station AI can hear local chatter when being projected from a holopad

* Minor bug fixes

* Added wire panels to holopads

* Basic broadcasting

* Start of emergency broadcasting code

* Fixing issues with broadcasting

* More work on emergency broadcasting

* Updated holopad visuals

* Added cooldown text to emergency broadcast and control lock out screen

* Code clean up

* Fixed issue with timing

* Broadcasting now requires command access

* Fixed some bugs

* Added multiple holopad prototypes with different ranges

* The AI no longer requires power to interact with holopads

* Fixed some additional issues

* Addressing more issues

* Added emote support for holograms

* Changed the broadcast lockout durations to their proper values

* Added AI vision wire to holopads

* Bug fixes

* AI vision and interaction wires can be added to the same wire panel

* Fixed error

* More bug fixes

* Fixed test fail

* Embellished the emergency call lock out window

* Holopads play borg sounds when speaking

* Borg and AI names are listed as the caller ID on the holopad

* Borg chassis can now be seen on holopad holograms

* Holopad returns to a machine frame when badly damaged

* Clarified some text

* Fix merge conflict

* Fixed merge conflict

* Fixing merge conflict

* Fixing merge conflict

* Fixing merge conflict

* Offset menu on open

* AI can alt click on holopads to activate the projector

* Bug fixes for intellicard interactions

* Fixed speech issue with intellicards

* The UI automatically opens for the AI when it alt-clicks on the holopad

* Simplified shader math

* Telephones will auto hang up 60 seconds after the last person on a call stops speaking

* Added better support for AI requests when multiple AI cores are on the station

* The call controls pop up for the AI when they accept a summons from a holopad

* Compatibility mode fix for the hologram shader

* Further shader fixes for compatibility mode

* File clean up

* More cleaning up

* Removed access requirements from quantum holopads so they can used by nukies

* The title of the holopad window now reflects the name of the device

* Linked telephones will lose their connection if both move out of range of each other
2024-12-17 20:18:15 +01:00

587 lines
19 KiB
C#

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.GameObjects;
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<BroadphaseComponent> _broadphaseQuery;
private EntityQuery<MapGridComponent> _gridQuery;
[ValidatePrototypeId<EntityPrototype>]
private static readonly EntProtoId DefaultAi = "StationAiBrain";
private const float MaxVisionMultiplier = 5f;
public override void Initialize()
{
base.Initialize();
_broadphaseQuery = GetEntityQuery<BroadphaseComponent>();
_gridQuery = GetEntityQuery<MapGridComponent>();
InitializeAirlock();
InitializeHeld();
InitializeLight();
SubscribeLocalEvent<StationAiWhitelistComponent, BoundUserInterfaceCheckRangeEvent>(OnAiBuiCheck);
SubscribeLocalEvent<StationAiOverlayComponent, AccessibleOverrideEvent>(OnAiAccessible);
SubscribeLocalEvent<StationAiOverlayComponent, InRangeOverrideEvent>(OnAiInRange);
SubscribeLocalEvent<StationAiOverlayComponent, MenuVisibilityEvent>(OnAiMenu);
SubscribeLocalEvent<StationAiHolderComponent, ComponentInit>(OnHolderInit);
SubscribeLocalEvent<StationAiHolderComponent, ComponentRemove>(OnHolderRemove);
SubscribeLocalEvent<StationAiHolderComponent, AfterInteractEvent>(OnHolderInteract);
SubscribeLocalEvent<StationAiHolderComponent, MapInitEvent>(OnHolderMapInit);
SubscribeLocalEvent<StationAiHolderComponent, EntInsertedIntoContainerMessage>(OnHolderConInsert);
SubscribeLocalEvent<StationAiHolderComponent, EntRemovedFromContainerMessage>(OnHolderConRemove);
SubscribeLocalEvent<StationAiHolderComponent, IntellicardDoAfterEvent>(OnIntellicardDoAfter);
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);
}
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()
{
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,
});
}
private void OnAiAccessible(Entity<StationAiOverlayComponent> 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<StationAiOverlayComponent> ent, ref MenuVisibilityEvent args)
{
args.Visibility &= ~MenuVisibility.NoFov;
}
private void OnAiBuiCheck(Entity<StationAiWhitelistComponent> ent, ref BoundUserInterfaceCheckRangeEvent args)
{
if (!HasComp<StationAiHeldComponent>(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<StationAiOverlayComponent> 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<StationAiHolderComponent> 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<StationAiHolderComponent> 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<IntellicardComponent>(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<StationAiHolderComponent> ent, ref ComponentInit args)
{
_slots.AddItemSlot(ent.Owner, StationAiHolderComponent.Container, ent.Comp.Slot);
}
private void OnHolderRemove(Entity<StationAiHolderComponent> ent, ref ComponentRemove args)
{
_slots.RemoveItemSlot(ent.Owner, ent.Comp.Slot);
}
private void OnHolderConInsert(Entity<StationAiHolderComponent> ent, ref EntInsertedIntoContainerMessage args)
{
UpdateAppearance((ent.Owner, ent.Comp));
}
private void OnHolderConRemove(Entity<StationAiHolderComponent> ent, ref EntRemovedFromContainerMessage args)
{
UpdateAppearance((ent.Owner, ent.Comp));
}
private void OnHolderMapInit(Entity<StationAiHolderComponent> ent, ref MapInitEvent args)
{
UpdateAppearance(ent.Owner);
}
private void OnAiShutdown(Entity<StationAiCoreComponent> ent, ref ComponentShutdown args)
{
// TODO: Tryqueuedel
if (_net.IsClient)
return;
QueueDel(ent.Comp.RemoteEntity);
ent.Comp.RemoteEntity = null;
}
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 (!SetupEye(ent))
return;
AttachEye(ent);
}
else
{
ClearEye(ent);
}
}
private void OnAiMapInit(Entity<StationAiCoreComponent> ent, ref MapInitEvent args)
{
SetupEye(ent);
AttachEye(ent);
}
public void SwitchRemoteEntityMode(Entity<StationAiCoreComponent> ent, bool isRemote)
{
if (isRemote == ent.Comp.Remote)
return;
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<EyeComponent>(user, out var eye))
_eye.SetDrawFov(user.Value, !isRemote);
}
private bool SetupEye(Entity<StationAiCoreComponent> 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<StationAiCoreComponent> ent)
{
if (_net.IsClient)
return;
QueueDel(ent.Comp.RemoteEntity);
ent.Comp.RemoteEntity = null;
Dirty(ent);
}
private void AttachEye(Entity<StationAiCoreComponent> 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<StationAiCoreComponent> 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<StationAiCoreComponent> 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<StationAiCoreComponent> 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<RelayInputMoverComponent>(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<StationAiHolderComponent?> 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<StationAiVisionComponent> 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<StationAiWhitelistComponent> entity, bool value, bool announce = false)
{
if (entity.Comp.Enabled == value)
return false;
entity.Comp.Enabled = value;
Dirty(entity);
return true;
}
/// <summary>
/// BUI validation for ai interactions.
/// </summary>
private bool ValidateAi(Entity<StationAiHeldComponent?> entity)
{
if (!Resolve(entity.Owner, ref entity.Comp, false))
{
return false;
}
return _blocker.CanComplexInteract(entity.Owner);
}
public bool TryGetStationAiCore(Entity<StationAiHeldComponent?> ent, [NotNullWhen(true)] out Entity<StationAiCoreComponent>? parentEnt)
{
parentEnt = null;
var parent = Transform(ent).ParentUid;
if (!parent.IsValid())
return false;
if (!TryComp<StationAiCoreComponent>(parent, out var stationAiCore))
return false;
parentEnt = new Entity<StationAiCoreComponent>(parent, stationAiCore);
return true;
}
public bool TryGetInsertedAI(Entity<StationAiCoreComponent> ent, [NotNullWhen(true)] out Entity<StationAiHeldComponent>? insertedAi)
{
insertedAi = null;
var insertedEnt = GetInsertedAI(ent);
if (TryComp<StationAiHeldComponent>(insertedEnt, out var stationAiHeld))
{
insertedAi = (insertedEnt.Value, stationAiHeld);
return true;
}
return false;
}
}
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,
}