diff --git a/Content.Client/SimpleStation14/Holograms/AcceptHologramEui.cs b/Content.Client/SimpleStation14/Holograms/AcceptHologramEui.cs new file mode 100644 index 0000000000..156e91475c --- /dev/null +++ b/Content.Client/SimpleStation14/Holograms/AcceptHologramEui.cs @@ -0,0 +1,43 @@ +using Content.Client.Eui; +using Content.Client.Holograms; +using Content.Shared.SimpleStation14.Holograms; +using JetBrains.Annotations; +using Robust.Client.Graphics; + +namespace Content.Client.SimpleStation14.Holograms; + +[UsedImplicitly] +public sealed class AcceptHologramEui : BaseEui +{ + private readonly AcceptHologramWindow _window; + + public AcceptHologramEui() + { + _window = new AcceptHologramWindow(); + + _window.DenyButton.OnPressed += _ => + { + SendMessage(new AcceptHologramChoiceMessage(AcceptHologramUiButton.Deny)); + _window.Close(); + }; + + _window.OnClose += () => SendMessage(new AcceptHologramChoiceMessage(AcceptHologramUiButton.Deny)); + + _window.AcceptButton.OnPressed += _ => + { + SendMessage(new AcceptHologramChoiceMessage(AcceptHologramUiButton.Accept)); + _window.Close(); + }; + } + + public override void Opened() + { + IoCManager.Resolve().RequestWindowAttention(); + _window.OpenCentered(); + } + + public override void Closed() + { + _window.Close(); + } +} diff --git a/Content.Client/SimpleStation14/Holograms/AcceptHologramWindow.cs b/Content.Client/SimpleStation14/Holograms/AcceptHologramWindow.cs new file mode 100644 index 0000000000..e4da45d059 --- /dev/null +++ b/Content.Client/SimpleStation14/Holograms/AcceptHologramWindow.cs @@ -0,0 +1,60 @@ +using System.Numerics; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using static Robust.Client.UserInterface.Controls.BoxContainer; + +namespace Content.Client.Holograms; + +public sealed class AcceptHologramWindow : DefaultWindow +{ + public readonly Button DenyButton; + public readonly Button AcceptButton; + + public AcceptHologramWindow() + { + + Title = Loc.GetString("accept-hologram-window-title"); + + Contents.AddChild(new BoxContainer + { + Orientation = LayoutOrientation.Vertical, + Children = + { + new BoxContainer + { + Orientation = LayoutOrientation.Vertical, + Children = + { + new Label() + { + Text = Loc.GetString("accept-hologram-window-prompt-text-part") + }, + new BoxContainer + { + Orientation = LayoutOrientation.Horizontal, + Align = AlignMode.Center, + Children = + { + (AcceptButton = new Button + { + Text = Loc.GetString("accept-hologram-window-accept-button"), + }), + + new Control() + { + MinSize = new Vector2(20, 0) + }, + + (DenyButton = new Button + { + Text = Loc.GetString("accept-hologram-window-deny-button"), + }) + } + }, + } + }, + } + }); + } +} diff --git a/Content.Client/SimpleStation14/Holograms/CctvDatabaseUi/CctvDatabaseBoundUserInterface.cs b/Content.Client/SimpleStation14/Holograms/CctvDatabaseUi/CctvDatabaseBoundUserInterface.cs new file mode 100644 index 0000000000..46e3617b35 --- /dev/null +++ b/Content.Client/SimpleStation14/Holograms/CctvDatabaseUi/CctvDatabaseBoundUserInterface.cs @@ -0,0 +1,51 @@ +using Content.Shared.SimpleStation14.Holograms; +using Robust.Client.GameObjects; + +namespace Content.Client.SimpleStation14.Holograms.CctvDatabaseUi; + +public sealed class CctvDatabaseBoundUserInterface : BoundUserInterface +{ + [ViewVariables] + private CctvDatabaseWindow? _menu; + + public CctvDatabaseBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) + { + } + + protected override void Open() + { + _menu = new CctvDatabaseWindow(); + + _menu.OpenCentered(); + _menu.OnClose += Close; + + _menu.PrintRequested += SendPrintRequest; + } + + private void SendPrintRequest(int index) + { + Logger.Error($"Sending message for index {index}"); + SendMessage(new CctvDatabasePrintRequestMessage(index)); + } + + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); + + switch (state) + { + case CctvDatabaseState cctvState: + _menu?.UpdateState(cctvState); + break; + } + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (!disposing) + return; + + _menu?.Dispose(); + } +} diff --git a/Content.Client/SimpleStation14/Holograms/CctvDatabaseUi/CctvDatabaseWindow.xaml b/Content.Client/SimpleStation14/Holograms/CctvDatabaseUi/CctvDatabaseWindow.xaml new file mode 100644 index 0000000000..ced3735356 --- /dev/null +++ b/Content.Client/SimpleStation14/Holograms/CctvDatabaseUi/CctvDatabaseWindow.xaml @@ -0,0 +1,33 @@ + + + + + + + + + + diff --git a/Content.Client/SimpleStation14/Holograms/CctvDatabaseUi/CctvDatabaseWindow.xaml.cs b/Content.Client/SimpleStation14/Holograms/CctvDatabaseUi/CctvDatabaseWindow.xaml.cs new file mode 100644 index 0000000000..691e91e810 --- /dev/null +++ b/Content.Client/SimpleStation14/Holograms/CctvDatabaseUi/CctvDatabaseWindow.xaml.cs @@ -0,0 +1,64 @@ +using Content.Client.UserInterface.Controls; +using Content.Shared.SimpleStation14.Holograms; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.Timing; + +namespace Content.Client.SimpleStation14.Holograms.CctvDatabaseUi; + +[GenerateTypedNameReferences] +public sealed partial class CctvDatabaseWindow : FancyWindow +{ + [Dependency] private readonly IGameTiming _timing = default!; + + public Action? PrintRequested; + + private const string IdleMessage = "cctv-database-user-interface-message-idle"; + private const string PrintingMessage = "cctv-database-user-interface-message-printing"; + + private TimeSpan? _printTime; + + public void UpdateState(CctvDatabaseState state) + { + var entries = state.CrewManifest; + _printTime = state.FinishedPrintingTime; + + TargetList.RemoveAllChildren(); + + var disabled = state.FinishedPrintingTime != null; + for (var i = 0; i < entries.Count; i++) + { + var label = new Label + { + Text = entries[i], + }; + + var button = new Button + { + Text = "Print", + Disabled = disabled, + }; + + var index = i; + button.OnPressed += _ => PrintRequested?.Invoke(index); + + TargetList.AddChild(label); + TargetList.AddChild(button); + } + + MessageLabel.Text = state.FinishedPrintingTime.ToString() ?? Loc.GetString(IdleMessage); + } + + protected override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); + + if (_printTime == null) + return; + + var timeLeft = _printTime.Value - _timing.CurTime; + MessageLabel.Text = timeLeft > TimeSpan.Zero + ? $"{Loc.GetString(PrintingMessage)}: {timeLeft.TotalSeconds:0.0}" + : Loc.GetString(IdleMessage); + } +} diff --git a/Content.Client/SimpleStation14/Holograms/HologramSystem.cs b/Content.Client/SimpleStation14/Holograms/HologramSystem.cs new file mode 100644 index 0000000000..ff88b124dc --- /dev/null +++ b/Content.Client/SimpleStation14/Holograms/HologramSystem.cs @@ -0,0 +1,129 @@ +using System.Numerics; +using Content.Shared.SimpleStation14.Holograms; +using Content.Shared.SimpleStation14.Holograms.Components; +using Robust.Client.GameObjects; +using Robust.Client.Player; +using Robust.Shared.Map; + +namespace Content.Client.SimpleStation14.Holograms; + +public sealed class HologramSystem : SharedHologramSystem +{ + [Dependency] private readonly IPlayerManager _player = default!; + [Dependency] private readonly TransformSystem _transform = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnProjectedShutdown); + } + + public override void Update(float frameTime) + { + var player = _player.LocalPlayer?.ControlledEntity; + if (TryComp(player, out var holoProjComp)) + { + ProjectedUpdate(player.Value, holoProjComp, frameTime); // This makes it so only the currently controlled entity is predicted, assuming they're a hologram. + + // Check if we should be setting the eye target of the hologram. + if (holoProjComp.SetEyeTarget && TryComp(player.Value, out var eyeComp)) + eyeComp.Target = holoProjComp.CurProjector; + } + + HandleProjectedEffects(EntityQueryEnumerator()); + } + + private void OnProjectedShutdown(EntityUid hologram, HologramProjectedComponent component, ComponentShutdown args) + { + DeleteEffect(component); + + if (component.SetEyeTarget && TryComp(hologram, out var eyeComp)) + eyeComp.Target = null; // This should be fine? I guess if you're a hologram riding a vehicle when this happens it'd be a bit weird. + } + + private void HandleProjectedEffects(EntityQueryEnumerator query) + { + while (query.MoveNext(out var hologram, out var holoProjectedComp)) + { + if (holoProjectedComp.EffectPrototype == null) + { + DeleteEffect(holoProjectedComp); + continue; + } + + if (!holoProjectedComp.CurrentlyInProjector || holoProjectedComp.CurProjector == null || !Exists(holoProjectedComp.CurProjector.Value)) + { + DeleteEffect(holoProjectedComp); + continue; + } + + var projector = holoProjectedComp.CurProjector.Value; + + var holoXformComp = Transform(hologram); + var holoCoords = _transform.GetMoverCoordinates(hologram, holoXformComp); + + var projXformComp = Transform(projector); + var projCoords = _transform.GetMoverCoordinates(projector, projXformComp); + + if (holoCoords.EntityId != projCoords.EntityId) // ¯\_(ツ)_/¯ + { + DeleteEffect(holoProjectedComp); + continue; + } + + var originPos = projCoords.Position; + + // Add the effect's offset, if applicable. + if (TryComp(projector, out var projComp)) + { + var direction = projXformComp.LocalRotation.GetCardinalDir(); + + var offset = direction switch + { + Direction.North => projComp.EffectOffsets[Direction.South], + Direction.South => projComp.EffectOffsets[Direction.North], + Direction.East => projComp.EffectOffsets[Direction.West], + Direction.West => projComp.EffectOffsets[Direction.East], + _ => Vector2.Zero + }; + + originPos += offset; + } + + // Determine a middle point between the hologram and the projector. + var effectPos = (holoCoords.Position + originPos) / 2; + + // Determine a rotation that points from the projector to the hologram. + var effectRot = (holoCoords.Position - originPos).ToAngle() - MathHelper.PiOver2; + + var effectCoords = new EntityCoordinates(holoCoords.EntityId, effectPos); + if (!effectCoords.IsValid(EntityManager)) + { + DeleteEffect(holoProjectedComp); + continue; + } + + // Set or spawn the effect. + if (holoProjectedComp.EffectEntity == null || !Exists(holoProjectedComp.EffectEntity.Value)) + holoProjectedComp.EffectEntity = Spawn(holoProjectedComp.EffectPrototype, effectCoords); + else + _transform.SetLocalPosition(holoProjectedComp.EffectEntity.Value, effectPos); + + _transform.SetLocalRotation(holoProjectedComp.EffectEntity.Value, effectRot); + + // Determine the scaling factor to make it fit between the hologram and the projector. + var yScale = (holoCoords.Position - originPos).Length(); + var effectScale = new Vector2(1, Math.Max(0.1f, yScale)); // No smaller than 0.1. + Comp(holoProjectedComp.EffectEntity.Value).Scale = effectScale; + } + } + + private void DeleteEffect(HologramProjectedComponent component) + { + if (component.EffectEntity != null && Exists(component.EffectEntity.Value)) + QueueDel(component.EffectEntity.Value); + + component.EffectEntity = null; + } +} diff --git a/Content.Server/Nyanotrasen/Holograms/HologramComponent.cs b/Content.Server/Nyanotrasen/Holograms/HologramComponent.cs deleted file mode 100644 index 69afa7cfe0..0000000000 --- a/Content.Server/Nyanotrasen/Holograms/HologramComponent.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Content.Server.Holograms -{ - [RegisterComponent] - public sealed class HologramComponent : Component { } -} diff --git a/Content.Server/Nyanotrasen/Holograms/HologramSystem.cs b/Content.Server/Nyanotrasen/Holograms/HologramSystem.cs deleted file mode 100644 index 4f95e3b14d..0000000000 --- a/Content.Server/Nyanotrasen/Holograms/HologramSystem.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Content.Shared.Storage.Components; - -namespace Content.Server.Holograms -{ - public sealed class HologramSystem : EntitySystem - { - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnStoreInContainerAttempt); - } - - private void OnStoreInContainerAttempt(EntityUid uid, HologramComponent component, ref StoreMobInItemContainerAttemptEvent args) - { - // TODO: It should be okay to move this to Shared. - // Forbid holograms from going inside anything. - args.Cancelled = true; - args.Handled = true; - } - } -} diff --git a/Content.Server/SimpleStation14/Holograms/AcceptHologramEui.cs b/Content.Server/SimpleStation14/Holograms/AcceptHologramEui.cs new file mode 100644 index 0000000000..ca0f8d9691 --- /dev/null +++ b/Content.Server/SimpleStation14/Holograms/AcceptHologramEui.cs @@ -0,0 +1,32 @@ +using Content.Server.EUI; +using Content.Shared.Eui; +using Content.Shared.SimpleStation14.Holograms; + +namespace Content.Server.SimpleStation14.Holograms; + +public sealed class AcceptHologramEui : BaseEui +{ + private readonly HologramSystem _hologramSystem; + private readonly Mind.Mind _mind; + + public AcceptHologramEui(Mind.Mind mind, HologramSystem hologramSys) + { + _mind = mind; + _hologramSystem = hologramSys; + } + + public override void HandleMessage(EuiMessageBase msg) + { + base.HandleMessage(msg); + + if (msg is not AcceptHologramChoiceMessage choice || + choice.Button == AcceptHologramUiButton.Deny) + { + Close(); + return; + } + + _hologramSystem.TransferMindToHologram(_mind); + Close(); + } +} diff --git a/Content.Server/SimpleStation14/Holograms/CctvDatabase/CctvDatabaseConsoleActiveComponent.cs b/Content.Server/SimpleStation14/Holograms/CctvDatabase/CctvDatabaseConsoleActiveComponent.cs new file mode 100644 index 0000000000..e456937703 --- /dev/null +++ b/Content.Server/SimpleStation14/Holograms/CctvDatabase/CctvDatabaseConsoleActiveComponent.cs @@ -0,0 +1,20 @@ +namespace Content.Server.SimpleStation14.Holograms.CctvDatabase; + +/// +/// Marks this entity as an active . +/// +[RegisterComponent] +public sealed class CctvDatabaseConsoleActiveComponent : Component +{ + /// + /// The mind currently being printed. + /// + [ViewVariables(VVAccess.ReadWrite)] + public Mind.Mind? PrintingMind; + + /// + /// The time the mind will be printed at. + /// + [ViewVariables(VVAccess.ReadWrite)] + public TimeSpan PrintTime = TimeSpan.Zero; +} diff --git a/Content.Server/SimpleStation14/Holograms/CctvDatabase/CctvDatabaseConsoleComponent.cs b/Content.Server/SimpleStation14/Holograms/CctvDatabase/CctvDatabaseConsoleComponent.cs new file mode 100644 index 0000000000..449780fd31 --- /dev/null +++ b/Content.Server/SimpleStation14/Holograms/CctvDatabase/CctvDatabaseConsoleComponent.cs @@ -0,0 +1,17 @@ +namespace Content.Server.SimpleStation14.Holograms.CctvDatabase; + +/// +/// Marks this entity as a CCTV Database console, allowing it to print CCTV footage onto disks. +/// +/// +/// Mostly a temporary thing for the Hologram system, should be expanded when recordings are actually a thing? +/// +[RegisterComponent] +public sealed class CctvDatabaseConsoleComponent : Component +{ + /// + /// The amount of time it takes this Console to print a disk. + /// + [ViewVariables(VVAccess.ReadWrite)] + public TimeSpan TimeToPrint = TimeSpan.FromMinutes(1.5); +} diff --git a/Content.Server/SimpleStation14/Holograms/CctvDatabase/CctvDatabaseSystem.cs b/Content.Server/SimpleStation14/Holograms/CctvDatabase/CctvDatabaseSystem.cs new file mode 100644 index 0000000000..36bc3c67a9 --- /dev/null +++ b/Content.Server/SimpleStation14/Holograms/CctvDatabase/CctvDatabaseSystem.cs @@ -0,0 +1,110 @@ +//TODO: HOLO In the future the entire CCTV Database system should be completely replaced +// with something that uses station records, requires tracked camera time, +// and a whole bunch of other stuff that'll be fun to use. +// For the time being, this works. + +using Content.Server.GameTicking; +using Content.Server.Mind.Components; +using Content.Server.SimpleStation14.Holograms.Components; +using Content.Server.Station.Systems; +using Content.Shared.SimpleStation14.Holograms; +using Robust.Server.GameObjects; +using Robust.Shared.Timing; + +namespace Content.Server.SimpleStation14.Holograms.CctvDatabase; + +public sealed class CctvDatabaseSystem : EntitySystem +{ + [Dependency] private readonly UserInterfaceSystem _ui = default!; + [Dependency] private readonly StationSystem _station = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + private const string HoloDiskPrototype = "HologramDisk"; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnPlayerSpawn); + + SubscribeLocalEvent(OnUIOpened); + SubscribeLocalEvent(OnPrintRequest); + } + + public override void Update(float delta) + { + while (EntityQueryEnumerator().MoveNext(out var console, out var activeComp)) + { + Log.Error($"{activeComp.PrintTime} >= {_timing.CurTime}"); + if (activeComp.PrintTime >= _timing.CurTime) + FinishPrint(console, activeComp); + } + } + + private void OnPlayerSpawn(EntityUid player, HologramTargetComponent holoTargetComp, PlayerSpawnCompleteEvent args) + { + if (!TryComp(args.Station, out var stationDatabaseComp)) + return; + if (!TryComp(player, out var mindContainerComp) || mindContainerComp.Mind is not { } mind) + return; + + stationDatabaseComp.PotentialsList.Add(mind); + + while (EntityQueryEnumerator().MoveNext(out var console, out _)) + UpdateUserInterface(console); + } + + private void OnUIOpened(EntityUid uid, CctvDatabaseConsoleComponent component, BoundUIOpenedEvent args) + { + if (args.UiKey is not CctvDatabaseUiKey.Key) + return; + + UpdateUserInterface(uid); + } + + private void UpdateUserInterface(EntityUid uid, CctvDatabaseConsoleActiveComponent? activeComp = null) + { + if (!_ui.TryGetUi(uid, CctvDatabaseUiKey.Key, out var bui)) + return; + + if (_station.GetOwningStation(uid) is not { } station || !TryComp(station, out var stationDatabaseComp)) + return; + + TimeSpan? finishTime = null; + + if (Resolve(uid, ref activeComp, false)) + finishTime = activeComp.PrintTime; + + _ui.TrySetUiState(uid, CctvDatabaseUiKey.Key, new CctvDatabaseState(stationDatabaseComp.PotentialsList.ConvertAll(x => x.CharacterName ?? "Unknown"), finishTime)); + } + + private void OnPrintRequest(EntityUid console, CctvDatabaseConsoleComponent consoleComp, CctvDatabasePrintRequestMessage args) + { + if (HasComp(console)) + return; + + if (_station.GetOwningStation(console) is not { } station || !TryComp(station, out var stationDatabaseComp)) + return; + + if (stationDatabaseComp.PotentialsList.Count <= args.Index) // Should never happen. + { + Log.Error($"CCTV Database console {console} tried to print a disk with index {args.Index} but the list only has {stationDatabaseComp.PotentialsList.Count} entries."); + return; + } + + var mind = stationDatabaseComp.PotentialsList[args.Index]; + var activeComp = AddComp(console); + activeComp.PrintingMind = mind; + activeComp.PrintTime = consoleComp.TimeToPrint + _timing.CurTime; + UpdateUserInterface(console, activeComp); + } + + private void FinishPrint(EntityUid console, CctvDatabaseConsoleActiveComponent activeComp) + { + var disk = Spawn(HoloDiskPrototype, Transform(console).Coordinates); + var diskComp = EnsureComp(disk); + diskComp.HoloMind = activeComp.PrintingMind; + RemComp(console); + UpdateUserInterface(console); + } +} diff --git a/Content.Server/SimpleStation14/Holograms/CctvDatabase/StationCctvDatabaseComponent.cs b/Content.Server/SimpleStation14/Holograms/CctvDatabase/StationCctvDatabaseComponent.cs new file mode 100644 index 0000000000..3f87a512f8 --- /dev/null +++ b/Content.Server/SimpleStation14/Holograms/CctvDatabase/StationCctvDatabaseComponent.cs @@ -0,0 +1,7 @@ +namespace Content.Server.SimpleStation14.Holograms.CctvDatabase; + +[RegisterComponent] +public sealed class StationCctvDatabaseComponent : Component +{ + public List PotentialsList = new(); +} diff --git a/Content.Server/SimpleStation14/Holograms/Components/HologramDiskComponent.cs b/Content.Server/SimpleStation14/Holograms/Components/HologramDiskComponent.cs new file mode 100644 index 0000000000..d2b0b9b589 --- /dev/null +++ b/Content.Server/SimpleStation14/Holograms/Components/HologramDiskComponent.cs @@ -0,0 +1,14 @@ +namespace Content.Server.SimpleStation14.Holograms; + +/// +/// Marks this entity as storing a hologram's data in it, for use in a . +/// +[RegisterComponent] +public sealed class HologramDiskComponent : Component +{ + /// + /// The mind stored in this Holodisk. + /// + [ViewVariables] + public Mind.Mind? HoloMind = null; +} diff --git a/Content.Server/SimpleStation14/Holograms/Components/HologramDiskDummyComponent.cs b/Content.Server/SimpleStation14/Holograms/Components/HologramDiskDummyComponent.cs new file mode 100644 index 0000000000..0bf910a951 --- /dev/null +++ b/Content.Server/SimpleStation14/Holograms/Components/HologramDiskDummyComponent.cs @@ -0,0 +1,18 @@ +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Server.SimpleStation14.Holograms.Components; + +/// +/// For any items that should generate a 'dummy' hologram when inserted as a holo disk. +/// Mostly intended for jokes and gaffs, but could be used for useful AI entities as well. +/// +[RegisterComponent] +public sealed class HologramDiskDummyComponent : Component +{ + /// + /// The prototype to spawn when this disk is inserted into a server. + /// + [DataField("prototype", required: true, customTypeSerializer: typeof(PrototypeIdSerializer)), ViewVariables(VVAccess.ReadWrite)] + public string HoloPrototype = default!; +} diff --git a/Content.Server/SimpleStation14/Holograms/Components/HologramDiskWriterComponent.cs b/Content.Server/SimpleStation14/Holograms/Components/HologramDiskWriterComponent.cs new file mode 100644 index 0000000000..8c72688b41 --- /dev/null +++ b/Content.Server/SimpleStation14/Holograms/Components/HologramDiskWriterComponent.cs @@ -0,0 +1,8 @@ +namespace Content.Server.SimpleStation14.Holograms.Components; + +[RegisterComponent] +public sealed class HologramDiskWriterComponent : Component +{ + [DataField("diskSlot")] + public string DiskSlot = "disk_slot"; +} diff --git a/Content.Server/SimpleStation14/Holograms/Components/HologramTargetComponent.cs b/Content.Server/SimpleStation14/Holograms/Components/HologramTargetComponent.cs new file mode 100644 index 0000000000..a2c8422031 --- /dev/null +++ b/Content.Server/SimpleStation14/Holograms/Components/HologramTargetComponent.cs @@ -0,0 +1,10 @@ +//TODO: HOLO In the future, this and the entire CCTV Database system should be completely replaced +// with something that uses station records, requires tracked camera time, +// and a whole bunch of other stuff that'll be fun to use. +// For the time being, this works. + +namespace Content.Server.SimpleStation14.Holograms.Components; + +[RegisterComponent] +public sealed class HologramTargetComponent : Component +{ } diff --git a/Content.Server/SimpleStation14/Holograms/Components/StationHologramDatabaseComponent.cs b/Content.Server/SimpleStation14/Holograms/Components/StationHologramDatabaseComponent.cs new file mode 100644 index 0000000000..eed181ca1e --- /dev/null +++ b/Content.Server/SimpleStation14/Holograms/Components/StationHologramDatabaseComponent.cs @@ -0,0 +1,6 @@ +namespace Content.Server.SimpleStation14.Holograms.Components; + +[RegisterComponent] +public sealed class StationHologramDatabaseComponent : Component +{ +} diff --git a/Content.Server/SimpleStation14/Holograms/Systems/HologramProjectorSystem.cs b/Content.Server/SimpleStation14/Holograms/Systems/HologramProjectorSystem.cs new file mode 100644 index 0000000000..ab853e359f --- /dev/null +++ b/Content.Server/SimpleStation14/Holograms/Systems/HologramProjectorSystem.cs @@ -0,0 +1,34 @@ +using Content.Server.Power.Components; +using Content.Server.Power.EntitySystems; +using Content.Server.SurveillanceCamera; +using Content.Shared.SimpleStation14.Holograms; +using Content.Shared.SimpleStation14.Holograms.Components; + +namespace Content.Server.SimpleStation14.Holograms; + +public sealed class HologramProjectorSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent((EntityUid ent, HologramProjectorComponent comp, ref PowerChangedEvent _) => CheckState(ent, comp)); + SubscribeLocalEvent((ent, comp, args) => CheckState(ent, comp)); + SubscribeLocalEvent((ent, comp, args) => CheckState(ent, comp)); + } + + public void CheckState(EntityUid projector, HologramProjectorComponent? projComp = null) + { + if (!Resolve(projector, ref projComp)) + return; + + if (TryComp(projector, out var powerComp) && !powerComp.Powered || + TryComp(projector, out var cameraComp) && !cameraComp.Active) + { + RemComp(projector); + return; + } + + EnsureComp(projector); + } +} diff --git a/Content.Server/SimpleStation14/Holograms/Systems/HologramServerSystem.DiskWriter.cs b/Content.Server/SimpleStation14/Holograms/Systems/HologramServerSystem.DiskWriter.cs new file mode 100644 index 0000000000..d3010c9f9b --- /dev/null +++ b/Content.Server/SimpleStation14/Holograms/Systems/HologramServerSystem.DiskWriter.cs @@ -0,0 +1,15 @@ +using Content.Server.Mind.Components; +using Content.Server.Power.Components; +using Content.Shared.Interaction; +using Content.Shared.SimpleStation14.Holograms; +using Robust.Shared.Containers; + +namespace Content.Server.SimpleStation14.Holograms; + +public sealed partial class HologramServerSystem +{ + public void InitializeDiskWriter() + { + base.Initialize(); + } +} diff --git a/Content.Server/SimpleStation14/Holograms/Systems/HologramServerSystem.Station.cs b/Content.Server/SimpleStation14/Holograms/Systems/HologramServerSystem.Station.cs new file mode 100644 index 0000000000..0cb7ffe3a6 --- /dev/null +++ b/Content.Server/SimpleStation14/Holograms/Systems/HologramServerSystem.Station.cs @@ -0,0 +1,17 @@ +using Content.Server.GameTicking; +using Content.Server.SimpleStation14.Holograms.Components; + +namespace Content.Server.SimpleStation14.Holograms; + +public sealed partial class HologramServerSystem +{ + private void InitializeStation() + { + SubscribeLocalEvent(OnPlayerSpawn); + } + + private void OnPlayerSpawn(EntityUid player, StationHologramDatabaseComponent component, PlayerSpawnCompleteEvent args) + { + } +} + diff --git a/Content.Server/SimpleStation14/Holograms/Systems/HologramServerSystem.cs b/Content.Server/SimpleStation14/Holograms/Systems/HologramServerSystem.cs new file mode 100644 index 0000000000..0dc248903b --- /dev/null +++ b/Content.Server/SimpleStation14/Holograms/Systems/HologramServerSystem.cs @@ -0,0 +1,134 @@ +using Content.Server.Power.Components; +using Content.Shared.Tag; +using Content.Shared.Popups; +using Content.Shared.SimpleStation14.Holograms; +using Content.Shared.Interaction; +using Robust.Shared.Containers; +using Content.Server.Mind.Components; +using Content.Shared.SimpleStation14.Holograms.Components; +using System.Diagnostics.CodeAnalysis; +using Robust.Server.GameObjects; + +namespace Content.Server.SimpleStation14.Holograms; + +public sealed partial class HologramServerSystem : EntitySystem +{ + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly TagSystem _tags = default!; + [Dependency] private readonly HologramSystem _hologram = default!; + [Dependency] private readonly TransformSystem _transform = default!; + + public const string TagHoloDisk = "HoloDisk"; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(ServerOnEntInserted); + SubscribeLocalEvent(ServerOnEntRemoved); + SubscribeLocalEvent(DiskOnAfterInteract); + SubscribeLocalEvent(ServerOnPowerChanged); + + InitializeStation(); + } + + /// + /// Handles generating a hologram from an inserted disk + /// + private void ServerOnEntInserted(EntityUid uid, HologramServerComponent component, EntInsertedIntoContainerMessage args) + { + if (args.Container.ID != component.DiskSlot || !_tags.HasTag(args.Entity, TagHoloDisk)) + return; + + if (Exists(component.LinkedHologram)) + if (!_hologram.TryKillHologram(component.LinkedHologram.Value)) + return; // This is a weird situation to encounter, so we'll just stop doin stuff. + + if (TryGenerateHologram(uid, args.Entity, out var holo, component)) + { + component.LinkedHologram = holo; + EnsureComp(holo.Value).LinkedServer = uid; + } + } + + /// + /// Handles killing a hologram when a disk is removed + /// + private void ServerOnEntRemoved(EntityUid uid, HologramServerComponent component, EntRemovedFromContainerMessage args) + { + if (args.Container.ID != component.DiskSlot || !_tags.HasTag(args.Entity, TagHoloDisk)) + return; + + if (Exists(component.LinkedHologram)) + _hologram.DoKillHologram(component.LinkedHologram.Value); + } + + /// + /// Called when the server's power state changes + /// + /// The entity uid of the server + /// The HologramServerComponent + /// The PowerChangedEvent + private void ServerOnPowerChanged(EntityUid uid, HologramServerComponent component, ref PowerChangedEvent args) + { + // If the server is no longer powered and the hologram exists + if (!args.Powered && Exists(component.LinkedHologram)) + { + // Kill the Hologram + _hologram.DoKillHologram(component.LinkedHologram.Value); + component.LinkedHologram = null; + } + + // If the server is powered + else if (args.Powered) + { + if (component.DiskSlot == null) + return; // No disk slot + + var container = Comp(uid).Containers[component.DiskSlot]; + + if (container.ContainedEntities.Count <= 0) + return; // No disk in the server + + // If the hologram is generated successfully + if (TryGenerateHologram(uid, container.ContainedEntities[0], out var holo, component)) + { + // Set the linked hologram to the generated hologram + var holoLinkComp = EnsureComp(holo.Value); + component.LinkedHologram = holo; + holoLinkComp.LinkedServer = uid; + } + } + } + + public bool TryGenerateHologram(EntityUid server, EntityUid disk, [NotNullWhen(true)] out EntityUid? hologram, HologramServerComponent? holoServerComp = null) + { + hologram = null; + + // if (TryComp(disk, out var diskDummyComp)) //TODO + + if (!TryComp(disk, out var diskComp) || diskComp.HoloMind == null) + return false; + + return _hologram.TryGenerateHumanoidHologram(diskComp.HoloMind, _transform.GetMoverCoordinates(server), out hologram); + } + + private void DiskOnAfterInteract(EntityUid uid, HologramDiskComponent component, AfterInteractEvent args) + { + if (args.Target == null || !TryComp(args.Target, out var targetMind)) + return; + + if (targetMind.Mind == null) + { + _popup.PopupEntity(Loc.GetString("system-hologram-disk-mind-none"), args.Target.Value, args.User); + args.Handled = true; + + return; + } + + component.HoloMind = targetMind.Mind; + _popup.PopupEntity(Loc.GetString("system-hologram-disk-mind-saved"), args.Target.Value, args.User); + + args.Handled = true; + } +} diff --git a/Content.Server/SimpleStation14/Holograms/Systems/HologramSystem.cs b/Content.Server/SimpleStation14/Holograms/Systems/HologramSystem.cs new file mode 100644 index 0000000000..9e63987833 --- /dev/null +++ b/Content.Server/SimpleStation14/Holograms/Systems/HologramSystem.cs @@ -0,0 +1,185 @@ +using Content.Server.GameTicking; +using Content.Server.Mind.Components; +using Content.Shared.Popups; +using Content.Shared.SimpleStation14.Holograms; +using Content.Shared.Administration.Logs; +using Content.Shared.Database; +using Robust.Shared.Player; +using Content.Server.Cloning.Components; +using Content.Server.Psionics; +using Content.Server.Humanoid; +using Content.Server.Jobs; +using Content.Server.Mind; +using Content.Server.Preferences.Managers; +using Content.Server.Power.Components; +using Content.Server.Administration.Commands; +using Content.Shared.Tag; +using Content.Shared.Speech; +using Content.Shared.Preferences; +using Content.Shared.Emoting; +using Content.Shared.Humanoid; +using Content.Shared.Mobs.Systems; +using Content.Shared.Interaction; +using Content.Shared.Interaction.Components; +using Content.Shared.Access.Components; +using Content.Shared.Clothing.Components; +using Robust.Server.Player; +using Robust.Shared.Containers; +using Robust.Shared.GameObjects.Components.Localization; +using System.Diagnostics.CodeAnalysis; +using Content.Server.EUI; +using Robust.Server.GameObjects; +using Robust.Shared.Enums; +using Robust.Shared.Map; +using Content.Server.Access.Systems; +using Content.Server.Station.Systems; +using Content.Server.Station.Components; + +namespace Content.Server.SimpleStation14.Holograms; + +public sealed class HologramSystem : SharedHologramSystem +{ + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + [Dependency] private readonly GameTicker _gameTicker = default!; + [Dependency] private readonly IPlayerManager _playerManager = null!; + [Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly MindSystem _mind = default!; + [Dependency] private readonly IServerPreferencesManager _prefs = default!; + [Dependency] private readonly EuiManager _eui = default!; + [Dependency] private readonly TransformSystem _transform = default!; + [Dependency] private readonly MetaDataSystem _meta = default!; + [Dependency] private readonly AccessSystem _access = default!; + [Dependency] private readonly StationSystem _station = default!; + + public readonly Dictionary HologramsWaitingForMind = new(); + /// + /// Handles killing a Hologram, with no checks in place. + /// + /// + /// You should generally use instead. + /// + public override void DoKillHologram(EntityUid hologram, HologramComponent? holoComp = null) // TODOPark: HOLO Move this to Shared once Upstream merge. + { + if (!Resolve(hologram, ref holoComp)) + return; + + var meta = MetaData(hologram); + var holoPos = Transform(hologram).Coordinates; + + if (TryComp(hologram, out var mindComp) && mindComp.Mind != null) + _gameTicker.OnGhostAttempt(mindComp.Mind, false); + + _audio.Play(holoComp.OffSound, playerFilter: Filter.Pvs(hologram), coordinates: holoPos, false); + _popup.PopupCoordinates(Loc.GetString(holoComp.PopupDisappearOther, ("name", meta.EntityName)), holoPos, Filter.PvsExcept(hologram), false, PopupType.MediumCaution); + _popup.PopupCoordinates(Loc.GetString(holoComp.PopupDeathSelf), holoPos, hologram, PopupType.LargeCaution); + + _entityManager.QueueDeleteEntity(hologram); + + _adminLogger.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(hologram):mob} was killed!"); + } + + public bool TryGenerateHumanoidHologram(Mind.Mind mind, EntityCoordinates coords, [NotNullWhen(true)] out EntityUid? holo) + { + holo = null; + + if (HologramsWaitingForMind.TryGetValue(mind, out var clone)) + { + if (EntityManager.EntityExists(clone) && + !_mobState.IsDead(clone) && + TryComp(clone, out var cloneMindComp) && + (cloneMindComp.Mind == null || cloneMindComp.Mind == mind)) + return false; // Mind already has clone + + HologramsWaitingForMind.Remove(mind); + } + + if (mind.OwnedEntity != null && (_mobState.IsAlive(mind.OwnedEntity.Value) || _mobState.IsCritical(mind.OwnedEntity.Value))) + return false; // Body controlled by mind is not dead + + // Yes, we still need to track down the client because we need to open the Eui + if (mind.UserId == null || !_playerManager.TryGetSessionById(mind.UserId.Value, out var client)) + return false; // If we can't track down the client, we can't offer transfer. That'd be quite bad. + + var pref = (HumanoidCharacterProfile) _prefs.GetPreferences(mind.UserId.Value).SelectedCharacter; + + var mob = HoloFetchAndSpawn(pref, coords, "MobHologramProjected"); + + HologramsWaitingForMind.Add(mind, mob); + _eui.OpenEui(new AcceptHologramEui(mind, this), client); + + if (mind.CurrentJob != null) + { + foreach (var special in mind.CurrentJob.Prototype.Special) + if (special is AddComponentSpecial) + special.AfterEquip(mob); + + // Get each access from the job prototype and add it to the mob + var extended = _station.GetOwningStation(mob) is { } station && TryComp(station, out var jobComp) && jobComp.ExtendedAccess; + _access.SetAccessToJob(mob, mind.CurrentJob.Prototype, extended, EnsureComp(mob)); + + // Get the loadout from the job prototype and add it to the Hologram making each item unremovable. + if (mind.CurrentJob.Prototype.StartingGear != null) + { + SetOutfitCommand.SetOutfit(mob, mind.CurrentJob.Prototype.StartingGear, EntityManager, (_, item) => + { + if (TryComp(item, out var clothing)) + { + if (clothing.InSlot is "back" or "pocket1" or "pocket2" or "belt" or "suitstorage" or "id") + { + QueueDel(item); + return; + } + } + EnsureComp(item); + EnsureComp(item); + }); + } + } + + _adminLogger.Add(LogType.Mind, LogImpact.Medium, + $"Hologram {ToPrettyString(mob):mob} was generated at {coords}"); + + holo = mob; + return true; + } + + internal void TransferMindToHologram(Mind.Mind mind) + { + if (!HologramsWaitingForMind.TryGetValue(mind, out var entity) || + !EntityManager.EntityExists(entity) || + !TryComp(entity, out var mindComp) || + mindComp.Mind != null) + return; + + _mind.TransferTo(mind, entity, true); + _mind.UnVisit(mind); + + HologramsWaitingForMind.Remove(mind); + } + + /// + /// Handles fetching the mob and any appearance stuff... + /// + private EntityUid HoloFetchAndSpawn(HumanoidCharacterProfile pref, EntityCoordinates coords, string mobPrototype) + { + var mob = Spawn(mobPrototype, coords); + _transform.AttachToGridOrMap(mob); + + _humanoid.LoadProfile(mob, pref); + _meta.SetEntityName(mob, pref.Name); + + var mind = EnsureComp(mob); + _mind.SetExamineInfo(mob, true, mind); + + var grammar = EnsureComp(mob); + grammar.ProperNoun = true; + grammar.Gender = Gender.Neuter; + Dirty(grammar); + + return mob; + } +} diff --git a/Content.Server/SimpleStation14/StationAI/Systems/AIEyeSystem.cs b/Content.Server/SimpleStation14/StationAI/Systems/AIEyeSystem.cs index a98d47d7f5..33fcf8c11e 100644 --- a/Content.Server/SimpleStation14/StationAI/Systems/AIEyeSystem.cs +++ b/Content.Server/SimpleStation14/StationAI/Systems/AIEyeSystem.cs @@ -12,6 +12,10 @@ using Content.Server.Borgs; using Robust.Server.GameObjects; using Content.Server.Visible; +using Content.Shared.SimpleStation14.Holograms.Components; +using Content.Shared.SimpleStation14.Holograms; +using Content.Server.SimpleStation14.Holograms; +using Robust.Shared.Timing; namespace Content.Server.SimpleStation14.StationAI { @@ -70,6 +74,13 @@ private void OnPowerUsed(EntityUid uid, AIEyePowerComponent component, AIEyePowe Transform(projection).AttachToGridOrMap(); _mindSwap.Swap(uid, projection); + // Hologram stuff. + if (TryComp(projection, out var serverLinkedComp)) + { + serverLinkedComp.LinkedServer = uid; + Dirty(serverLinkedComp); //TODO: HOLO This should probably be handled in the system. + } + // Consistent name _entityManager.GetComponent(projection).EntityName = core.EntityName != "" @@ -103,7 +114,6 @@ private void OnMindRemoved(EntityUid uid, AIEyeComponent component, MindRemovedM QueueDel(uid); } - private void OnMobStateChanged(EntityUid uid, StationAIComponent component, MobStateChangedEvent args) { if (!_mobState.IsDead(uid)) return; diff --git a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSystem.cs b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSystem.cs index f159ec52d9..c39f78e631 100644 --- a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSystem.cs +++ b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSystem.cs @@ -262,6 +262,8 @@ private void Deactivate(EntityUid camera, SurveillanceCameraComponent? component // Send a local event that's broadcasted everywhere afterwards. RaiseLocalEvent(ev); + RaiseLocalEvent(camera, new SurveillanceCameraChangeStateEvent(false)); // Parkstation-Holograms + UpdateVisuals(camera, component); } @@ -279,6 +281,7 @@ public void SetActive(EntityUid camera, bool setting, SurveillanceCameraComponen if (attemptEv.Cancelled) return; component.Active = setting; + RaiseLocalEvent(camera, new SurveillanceCameraChangeStateEvent(setting)); // Parkstation-Holograms } else { @@ -439,3 +442,5 @@ public SurveillanceCameraDeactivateEvent(EntityUid camera) [ByRefEvent] public record struct SurveillanceCameraSetActiveAttemptEvent(bool Cancelled); + +public readonly record struct SurveillanceCameraChangeStateEvent(bool Active); // Parkstation-Holograms diff --git a/Content.Shared/SimpleStation14/Holograms/AcceptHologramEuiMessage.cs b/Content.Shared/SimpleStation14/Holograms/AcceptHologramEuiMessage.cs new file mode 100644 index 0000000000..1b66002a9f --- /dev/null +++ b/Content.Shared/SimpleStation14/Holograms/AcceptHologramEuiMessage.cs @@ -0,0 +1,22 @@ +using Content.Shared.Eui; +using Robust.Shared.Serialization; + +namespace Content.Shared.SimpleStation14.Holograms; + +[Serializable, NetSerializable] +public enum AcceptHologramUiButton +{ + Deny, + Accept, +} + +[Serializable, NetSerializable] +public sealed class AcceptHologramChoiceMessage : EuiMessageBase +{ + public readonly AcceptHologramUiButton Button; + + public AcceptHologramChoiceMessage(AcceptHologramUiButton button) + { + Button = button; + } +} diff --git a/Content.Shared/SimpleStation14/Holograms/CctvDatabaseShared.cs b/Content.Shared/SimpleStation14/Holograms/CctvDatabaseShared.cs new file mode 100644 index 0000000000..4fcb5a915c --- /dev/null +++ b/Content.Shared/SimpleStation14/Holograms/CctvDatabaseShared.cs @@ -0,0 +1,34 @@ +using Content.Shared.CrewManifest; +using Robust.Shared.Serialization; + +namespace Content.Shared.SimpleStation14.Holograms; + +[Serializable, NetSerializable] +public enum CctvDatabaseUiKey +{ + Key +} + +[Serializable, NetSerializable] +public sealed class CctvDatabaseState : BoundUserInterfaceState +{ + public List CrewManifest; + public TimeSpan? FinishedPrintingTime; + + public CctvDatabaseState(List crewManifest, TimeSpan? finishedPrintingTime = null) + { + CrewManifest = crewManifest; + FinishedPrintingTime = finishedPrintingTime; + } +} + +[Serializable, NetSerializable] +public sealed class CctvDatabasePrintRequestMessage : BoundUserInterfaceMessage +{ + public int Index; + + public CctvDatabasePrintRequestMessage(int index) + { + Index = index; + } +} diff --git a/Content.Shared/SimpleStation14/Holograms/Components/HologramComponent.cs b/Content.Shared/SimpleStation14/Holograms/Components/HologramComponent.cs new file mode 100644 index 0000000000..3aefe19ec8 --- /dev/null +++ b/Content.Shared/SimpleStation14/Holograms/Components/HologramComponent.cs @@ -0,0 +1,80 @@ +using Content.Shared.Whitelist; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; + +namespace Content.Shared.SimpleStation14.Holograms; + +/// +/// Marks the entity as a being made of light. +/// Details determined by sister components. +/// +[RegisterComponent] +[NetworkedComponent] +public sealed class HologramComponent : Component +{ + /// + /// The sound to play when the Hologram is turned on. + /// + [DataField("onSound"), ViewVariables(VVAccess.ReadWrite)] + [AutoNetworkedField] + public SoundSpecifier OnSound = new SoundPathSpecifier("/Audio/SimpleStation14/Effects/Hologram/holo_on.ogg"); + + /// + /// The sound to play when the Hologram is turned off. + /// + [DataField("offSound"), ViewVariables(VVAccess.ReadWrite)] + [AutoNetworkedField] + public SoundSpecifier OffSound = new SoundPathSpecifier("/Audio/SimpleStation14/Effects/Hologram/holo_off.ogg"); + + /// + /// The string to use for the popup when the Hologram appears, shown to others. + /// + [DataField("popupAppearOther"), ViewVariables(VVAccess.ReadWrite)] + [AutoNetworkedField] + public string PopupAppearOther = "system-hologram-phasing-appear-others"; + + /// + /// The string to use for the popup when the Hologram appears, shown to themselves. + /// + [DataField("popupAppearSelf"), ViewVariables(VVAccess.ReadWrite)] + [AutoNetworkedField] + public string PopupAppearSelf = "system-hologram-phasing-appear-self"; + + /// + /// The string to use for the popup when the Hologram disappears, shown to others. + /// + [DataField("popupDisappearOther"), ViewVariables(VVAccess.ReadWrite)] + [AutoNetworkedField] + public string PopupDisappearOther = "system-hologram-phasing-disappear-others"; + + /// + /// The string to use for the popup when the Hologram is killed, shown to themselves. + /// + [DataField("popupDeathSelf"), ViewVariables(VVAccess.ReadWrite)] + [AutoNetworkedField] + public string PopupDeathSelf = "system-hologram-phasing-death-self"; + + /// + /// The string to use for the popup when the Hologram fails to interact with something, due to their non-solid nature. + /// + [DataField("popupHoloInteractionFail"), ViewVariables(VVAccess.ReadWrite)] + [AutoNetworkedField] + public string PopupHoloInteractionFail = "system-hologram-interaction-with-others-fail"; + + /// + /// The string to use for the popup when the someone fails to interact with the Hologram, due to their non-holographic nature. + /// + [DataField("popupInteractionWithHoloFail"), ViewVariables(VVAccess.ReadWrite)] + [AutoNetworkedField] + public string PopupInteractionWithHoloFail = "system-hologram-interaction-with-holo-fail"; + + /// + /// A list of tags for the Hologram to collide with, assuming they're not hardlight. + /// + /// + /// This should generally include the 'Wall' tag. + /// + [DataField("collideWhitelist"), ViewVariables(VVAccess.ReadWrite)] + [AutoNetworkedField] + public EntityWhitelist CollideWhitelist = new(); +} diff --git a/Content.Shared/SimpleStation14/Holograms/Components/HologramProjectedComponent.cs b/Content.Shared/SimpleStation14/Holograms/Components/HologramProjectedComponent.cs new file mode 100644 index 0000000000..8812f8cb55 --- /dev/null +++ b/Content.Shared/SimpleStation14/Holograms/Components/HologramProjectedComponent.cs @@ -0,0 +1,98 @@ +using Content.Shared.Tag; +using Content.Shared.Whitelist; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; + +namespace Content.Shared.SimpleStation14.Holograms.Components; + +/// +/// Marks that this Hologram is projected from cameras, or some other hologram projector source. +/// +[RegisterComponent] +[NetworkedComponent, AutoGenerateComponentState] +public sealed partial class HologramProjectedComponent : Component +{ + /// + /// A whitelist to check for on projectors, to determine if they're valid. + /// + [DataField("validProjectorWhitelist"), ViewVariables(VVAccess.ReadWrite)] + [AutoNetworkedField] + public EntityWhitelist ValidProjectorWhitelist = new(); + + /// + /// A timer for a grace period before the Holo is returned, to allow for moving through doors. + /// + [DataField("gracePeriod"), ViewVariables(VVAccess.ReadWrite)] + [AutoNetworkedField] + public TimeSpan GracePeriod = TimeSpan.FromSeconds(0.1f); + + /// + /// The maximum range from a projector a Hologram can be before they're returned. + /// + /// + /// Note that making this number larger than PVS is highly inadvisable, as the client will be stuck predicting the Hologram returning while the server confirms that they do not. + /// + [DataField("projectorRange"), ViewVariables(VVAccess.ReadWrite)] + [AutoNetworkedField] + public float ProjectorRange = 14f; + + /// + /// The prototype of the effect to spawn for the Hologram's projection. Leave null to disable the visual projection effect. + /// + [DataField("effectPrototype", customTypeSerializer: typeof(PrototypeIdSerializer))] + [AutoNetworkedField] + public string? EffectPrototype; + + /// + /// Whether or not the Hologram's vision should snap to the projector they're projected from. + /// + /// + /// This provides a super cool effect of the Hologram only getting the visual information they technically should, but it's also a bit of a pain from a player perspective. + /// Primarily used for the station AI. + /// + [DataField("setEyeTarget"), ViewVariables(VVAccess.ReadWrite)] + [AutoNetworkedField] + public bool SetEyeTarget = false; + + /// + /// The current projector the hologram is connected to. + /// + /// + /// Note that this may not be a valid projector, as it is left set to the last projector the Hologram was in range of during the grace period. + /// + [ViewVariables(VVAccess.ReadOnly)] + [AutoNetworkedField] + public EntityUid? CurProjector; + + /// + /// If set, the Hologram will only be able to be projected from this projector, simply ignoring all others. + /// + [ViewVariables(VVAccess.ReadOnly)] + [AutoNetworkedField] + public EntityUid? ProjectorOverride; + + /// + /// Whether or not the Hologram is currently in the range of a projector. + /// + [ViewVariables(VVAccess.ReadOnly)] + [AutoNetworkedField] // TODO: Probably remove this and just sync the projector then determine this client side? + public bool CurrentlyInProjector = false; + + /// + /// The point at which a Hologram will be sent back to their last projector or killed, based on when they were last in the range of one. + /// + /// + /// Note that THIS WILL NOT BE SET TO NULL. If a hologram enters a projector, this value will be left alone and simply be innacurate. + /// Do not rely on it. + /// + // [AutoNetworkedField] + public TimeSpan VanishTime = TimeSpan.Zero; + + /// + /// The UID of the entity for the Hologram's visual projection effect. + /// Client side only. + /// + public EntityUid? EffectEntity = null; +} diff --git a/Content.Shared/SimpleStation14/Holograms/Components/HologramProjectorActiveComponent.cs b/Content.Shared/SimpleStation14/Holograms/Components/HologramProjectorActiveComponent.cs new file mode 100644 index 0000000000..aaf726b518 --- /dev/null +++ b/Content.Shared/SimpleStation14/Holograms/Components/HologramProjectorActiveComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.SimpleStation14.Holograms.Components; + +/// +/// Marks a hologram projector as active and working. +/// +[RegisterComponent, NetworkedComponent] +public sealed class HologramProjectorActiveComponent : Component +{ } diff --git a/Content.Shared/SimpleStation14/Holograms/Components/HologramProjectorComponent.cs b/Content.Shared/SimpleStation14/Holograms/Components/HologramProjectorComponent.cs new file mode 100644 index 0000000000..a60e9dddb8 --- /dev/null +++ b/Content.Shared/SimpleStation14/Holograms/Components/HologramProjectorComponent.cs @@ -0,0 +1,14 @@ +using System.Numerics; +using Robust.Shared.GameStates; + +namespace Content.Shared.SimpleStation14.Holograms; + +[RegisterComponent, NetworkedComponent] +public sealed class HologramProjectorComponent : Component +{ + /// + /// The tile offset of the projector effect for this projector for each direction. + /// + [DataField("effectOffsets")] + public Dictionary EffectOffsets { get; } = new() { { Direction.North, Vector2.Zero }, { Direction.East, Vector2.Zero }, { Direction.South, Vector2.Zero }, { Direction.West, Vector2.Zero } }; +} diff --git a/Content.Shared/SimpleStation14/Holograms/Components/HologramServerComponent.cs b/Content.Shared/SimpleStation14/Holograms/Components/HologramServerComponent.cs new file mode 100644 index 0000000000..c2f83e6299 --- /dev/null +++ b/Content.Shared/SimpleStation14/Holograms/Components/HologramServerComponent.cs @@ -0,0 +1,14 @@ +namespace Content.Shared.SimpleStation14.Holograms; + +/// +/// Marks an entity as being capable of generating a hologram by inserting a into it. +/// +[RegisterComponent] +public sealed class HologramServerComponent : Component +{ + [DataField("diskSlot")] + public string? DiskSlot; + + [ViewVariables(VVAccess.ReadOnly)] + public EntityUid? LinkedHologram; +} diff --git a/Content.Shared/SimpleStation14/Holograms/Components/HologramServerLinkedComponent.cs b/Content.Shared/SimpleStation14/Holograms/Components/HologramServerLinkedComponent.cs new file mode 100644 index 0000000000..e5794ad6f3 --- /dev/null +++ b/Content.Shared/SimpleStation14/Holograms/Components/HologramServerLinkedComponent.cs @@ -0,0 +1,31 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.SimpleStation14.Holograms.Components; + +/// +/// Marks that this Hologram requires a server of some kind to generate it. +/// +/// +/// This could be anything from a literal server, to an AICore, to the person a HoloParasite lives in. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class HologramServerLinkedComponent : Component +{ + /// + /// Whether this Hologram is bound to the same grid as its server. + /// If true, it will be returned if it leaves the grid. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("gridBound"), AutoNetworkedField] + public bool GridBound = true; + + /// + /// The server that this hologram is generated by. + /// + /// + /// This will be the lightbee if it's a lightbee hologram. + /// + [ViewVariables(VVAccess.ReadOnly)] + [AutoNetworkedField] + public EntityUid? LinkedServer; +} diff --git a/Content.Shared/SimpleStation14/Holograms/HologramEvents.cs b/Content.Shared/SimpleStation14/Holograms/HologramEvents.cs new file mode 100644 index 0000000000..afc6ffac6a --- /dev/null +++ b/Content.Shared/SimpleStation14/Holograms/HologramEvents.cs @@ -0,0 +1,53 @@ +namespace Content.Shared.SimpleStation14.Holograms; + +/// +/// Sent directed at a Hologram about to be returned to be handled by other systems. +/// +/// +/// Note that if this event is cancelled, there should be a plan to deal with it, since it'll just happen again next frame. +/// +[ByRefEvent] +public record struct HologramReturnAttemptEvent(bool Cancelled = false); + +/// +/// Sent directed at a Hologram before they are returned, often due to not being near a projector. +/// +/// The projector the Hologram is being returned to. +public readonly record struct HologramReturnedEvent(EntityUid Projector); + +/// +/// Sent directed at a Hologram about to be killed to be handled by other systems. +/// +/// +/// Note that if this event is cancelled, there should be a plan to deal with it, since it'll just happen again next frame. +/// +[ByRefEvent] +public record struct HologramKillAttemptEvent(bool Cancelled = false); + +/// +/// Sent directed at a Hologram being killed, often due to not having any valid projectors. +/// +public readonly record struct HologramKilledEvent(); + +/// +/// Sent directed at a Hologram when searching for any valid Projectors. +/// Allows for manually setting the projector to use. +/// Note that this Projector will not be validated in *any* way. +/// +/// +/// Setting override to 'True' will use whatever's in ProjectorOverride- including a null value, which allows cancelling the projector search. +/// A Component-set override will override this override. +/// +[ByRefEvent] +public record struct HologramGetProjectorEvent(EntityUid? ProjectorOverride = null, bool Override = false); + +/// +/// Sent directed at a Hologram when they're checking if a specific projector is valid. +/// Allows for manually determining if a projector is valid for a given Hologram. +/// +/// +/// Setting Valid to either True or False will force that behavior. +/// Leaving it null will allow the projector to determine its own validity based on normal rules. +/// +[ByRefEvent] +public record struct HologramCheckProjectorValidEvent(EntityUid Projector, bool? Valid = null); diff --git a/Content.Shared/SimpleStation14/Holograms/Systems/SharedHologramSystem.Projected.cs b/Content.Shared/SimpleStation14/Holograms/Systems/SharedHologramSystem.Projected.cs new file mode 100644 index 0000000000..4333f49559 --- /dev/null +++ b/Content.Shared/SimpleStation14/Holograms/Systems/SharedHologramSystem.Projected.cs @@ -0,0 +1,280 @@ +using Content.Shared.Popups; +using Robust.Shared.Player; +using Content.Shared.Interaction.Helpers; +using Robust.Shared.Map; +using System.Diagnostics.CodeAnalysis; +using Content.Shared.Storage.Components; +using Content.Shared.Pulling.Components; +using Content.Shared.Database; +using Content.Shared.SimpleStation14.Holograms.Components; +using Robust.Shared.Configuration; +using Robust.Shared; +using Content.Shared.Whitelist; + +namespace Content.Shared.SimpleStation14.Holograms; + +public partial class SharedHologramSystem +{ + [Dependency] private readonly IConfigurationManager _config = default!; + + private void InitializeProjected() + { + SubscribeLocalEvent(OnProjectedInit); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = _entityManager.EntityQueryEnumerator(); + while (query.MoveNext(out var hologram, out var hologramProjectedComp)) + ProjectedUpdate(hologram, hologramProjectedComp, frameTime); + } + + /// + /// Returns a hologram to its last visited projector, or kills it if the projector is invalid. + /// + /// The hologram to return. + /// True if the hologram was killed, false if it was returned. + /// The hologram's projected component. + public virtual void DoReturnHologram(EntityUid hologram, HologramProjectedComponent? holoProjectedComp = null) + { + if (!Resolve(hologram, ref holoProjectedComp)) + return; + + // If their last visited Projector is invalid ignoring occlusion and none is found + if (!IsHoloProjectorValid(hologram, holoProjectedComp.CurProjector, 0, false) && + !TryGetHoloProjector(hologram, holoProjectedComp.ProjectorRange, out holoProjectedComp.CurProjector, holoProjectedComp, false)) + { + // Kill the hologram. + TryKillHologram(hologram); + return; + } + + var returnedEvent = new HologramReturnAttemptEvent(); + RaiseLocalEvent(hologram, ref returnedEvent); + if (returnedEvent.Cancelled) + return; + + RaiseLocalEvent(hologram, new HologramReturnedEvent(holoProjectedComp.CurProjector.Value)); + + MoveHologramToProjector(hologram, holoProjectedComp.CurProjector.Value); + + _adminLogger.Add(LogType.Mind, LogImpact.Low, + $"{ToPrettyString(hologram):mob} was returned to projector {ToPrettyString(holoProjectedComp.CurProjector.Value):entity}"); + } + + /// + /// Tests for the nearest projector to a set of coords. + /// + /// The coords to perform the check from. + /// The UID of the projector, or null if no projectors are found. + /// An EntityWhitelist to check for on projectors to determine if they're valid. + /// The range it should check for projectors in, if occlude is true + /// Should it check only for unoccluded and in range projectors? + /// Returns true if a projector is found, false if not. + public bool TryGetHoloProjector(MapCoordinates coords, float range, [NotNullWhen(true)] out EntityUid? result, EntityWhitelist? whiteList = null, bool occlude = true) + { + result = null; + + // Sort all projectors in distance increasing order. + var nearProjList = new SortedList(); + + var query = _entityManager.EntityQueryEnumerator(); + while (query.MoveNext(out var projector, out _)) + { + var dist = (_transform.GetWorldPosition(projector) - coords.Position).LengthSquared(); + nearProjList.TryAdd(dist, projector); + } + + // Find the nearest, valid projector. + foreach (var nearProj in nearProjList) + { + if (!IsHoloProjectorValid(coords, nearProj.Value, range, occlude, whiteList)) + continue; + result = nearProj.Value; + return true; + } + return false; + } + + /// + /// This takes into consideration any ProjectorOverride the hologram may have. + /// + /// + public bool TryGetHoloProjector(EntityUid uid, float range, [NotNullWhen(true)] out EntityUid? result, HologramProjectedComponent? projectedComp = null, bool occlude = true) + { + result = null; + + if (!Resolve(uid, ref projectedComp)) + return false; + + if (projectedComp.ProjectorOverride != null) // Check for Component-set overrides. + { + if (IsHoloProjectorValid(uid, projectedComp.ProjectorOverride, range, occlude)) + { + result = projectedComp.ProjectorOverride; + return true; + } + return false; + } + + var projectorEvent = new HologramGetProjectorEvent(); // Check for Event-set overrides. + RaiseLocalEvent(uid, ref projectorEvent); + if (projectorEvent.Override) + { + result = projectorEvent.ProjectorOverride; + return projectorEvent.ProjectorOverride != null; + } + + // Otherwise, we simply check for the nearest projector, considering any tags it requires. + return TryGetHoloProjector(Transform(uid).MapPosition, range, out result, projectedComp.ValidProjectorWhitelist, occlude); + } + + /// + /// Tests if a projector is valid for a given hologram. + /// + /// The hologram to check for, or its position. + /// The projector to compare on, or its position. + /// The max range to allow, uses the Holo's range if null. Ignored if occlude is false. + /// Should it check only for unoccluded and in range projectors?. + /// Should it raise the event? Make sure this is set to false if you use this function in response to the event. + /// The hologram's component. If provided, the hologram's list of allowed tags will be used. + /// True if the projector is within range, and unoccluded to the hologram. Otherwise, false. + public bool IsHoloProjectorValid(EntityUid hologram, [NotNullWhen(true)] EntityUid? projector, float? range = null, bool occlude = true, bool raiseEvent = true, HologramProjectedComponent? projectedComp = null) + { + if (!Resolve(hologram, ref projectedComp) || projector == null || !Exists(projector.Value)) + return false; + + if (raiseEvent) + { + Log.Error($"Raising event on hologram {ToPrettyString(hologram):player} to check if projector {ToPrettyString(projector.Value):entity} is valid."); //TODO: HOLO Debug stuff. + var validCheckEvent = new HologramCheckProjectorValidEvent(projector.Value); + RaiseLocalEvent(hologram, ref validCheckEvent); + Log.Error($"Result: {validCheckEvent.Valid}"); + if (validCheckEvent.Valid != null) + return validCheckEvent.Valid.Value; + } + + return IsHoloProjectorValid(Transform(hologram).MapPosition, projector, range ?? projectedComp.ProjectorRange, occlude, projectedComp.ValidProjectorWhitelist); + } + + /// + /// A whitelist to check for on projectors, to determine if they're valid. Usually found on the Holo's . + /// + /// Note this this method won't raise the event, as the Hologram entity is not known. + /// This is a limitation of the method, and should be kept in mind when using it. + /// //TODO: HOLO Probably allow passing in a nullable UID for the hologram, and raise the event if it's not null. + public bool IsHoloProjectorValid(MapCoordinates hologram, [NotNullWhen(true)] EntityUid? projector, float range, bool occlude = true, EntityWhitelist? whitelist = null) + { + if (projector == null || !Exists(projector.Value)) + return false; + + if (!HasComp(projector.Value)) + return false; + + if (whitelist != null && !whitelist.IsValid(projector.Value, _entityManager)) + return false; + + if (occlude && !projector.Value.InRangeUnOccluded(hologram, range)) + return false; + + return true; + } + + /// + /// Moves a hologram to a new location. + /// + /// + /// Does no validation for any projectors before moving. + /// + /// The hologram to move. + /// The projector to move it to, or the projector's position. + public void MoveHologram(EntityUid hologram, EntityCoordinates projector, HologramComponent? holoComp = null) + { + if (!Resolve(hologram, ref holoComp)) + return; + + // Stops any pulling goin on. + if (TryComp(hologram, out var pullable) && pullable.BeingPulled) + _pulling.TryStopPull(pullable); + + if (TryComp(hologram, out var pulling) && pulling.Pulling != null && + TryComp(pulling.Pulling.Value, out var subjectPulling)) + _pulling.TryStopPull(subjectPulling); + + // Plays the vanishing effects. + var meta = MetaData(hologram); + + if (!_timing.InPrediction) // TODOPark: HOLO Change this to run on the first prediction once it predicts reliably. + { + var holoPos = Transform(hologram).Coordinates; + _audio.Play(holoComp.OffSound, playerFilter: Filter.Pvs(hologram), coordinates: holoPos, false); + _popup.PopupCoordinates(Loc.GetString(holoComp.PopupDisappearOther, ("name", meta.EntityName)), holoPos, Filter.PvsExcept(hologram), false, PopupType.MediumCaution); + } + + // Does the do. + _transform.SetCoordinates(hologram, projector); + _transform.AttachToGridOrMap(hologram); + + // Plays the appearing effects. + if (!_timing.InPrediction) + { + _audio.PlayPvs(holoComp.OnSound, hologram); + _popup.PopupEntity(Loc.GetString(holoComp.PopupAppearOther, ("name", meta.EntityName)), hologram, Filter.PvsExcept(hologram), false, PopupType.Medium); + _popup.PopupEntity(Loc.GetString(holoComp.PopupAppearSelf, ("name", meta.EntityName)), hologram, hologram, PopupType.Large); + } + } + + /// + public void MoveHologramToProjector(EntityUid hologram, EntityUid projector, HologramComponent? holoComp = null) + { + MoveHologram(hologram, Transform(projector).Coordinates, holoComp); + } + + protected bool ProjectedUpdate(EntityUid hologram, HologramProjectedComponent hologramProjectedComp, float frameTime) + { + if (TryGetHoloProjector(hologram, hologramProjectedComp.ProjectorRange, out var nearProj, hologramProjectedComp)) // Checks for a projector in range. + { + hologramProjectedComp.CurProjector = nearProj; + hologramProjectedComp.CurrentlyInProjector = true; + Dirty(hologramProjectedComp); + return true; + } + + // If none is found, and they were in the range of a projector during the last check, we set the time they'll be disappeared at. + if (hologramProjectedComp.CurrentlyInProjector) + { + hologramProjectedComp.CurrentlyInProjector = false; + hologramProjectedComp.VanishTime = _timing.CurTime + hologramProjectedComp.GracePeriod; + } + + if (hologramProjectedComp.VanishTime > _timing.CurTime) + { + Dirty(hologramProjectedComp); + return true; + } + + // Attempts to return the hologram if their time is up. + DoReturnHologram(hologram); + Dirty(hologramProjectedComp); + return false; + } + + // Forbid holograms from going inside anything. Osmosised from Nyano :) + private void OnStoreInContainerAttempt(EntityUid uid, HologramComponent component, ref StoreMobInItemContainerAttemptEvent args) + { + if (HasComp(uid)) + { + DoReturnHologram(uid); + args.Cancelled = true; + args.Handled = true; + } + } + + private void OnProjectedInit(EntityUid uid, HologramProjectedComponent component, ComponentInit args) + { + if (_config.GetCVar(CVars.NetMaxUpdateRange) > component.ProjectorRange) + throw new InvalidOperationException($"Hologram {ToPrettyString(uid):player}'s projector range is higher than PVS range- This will cause mispredicting."); + } +} diff --git a/Content.Shared/SimpleStation14/Holograms/Systems/SharedHologramSystem.ServerLinked.cs b/Content.Shared/SimpleStation14/Holograms/Systems/SharedHologramSystem.ServerLinked.cs new file mode 100644 index 0000000000..28b6aa3d62 --- /dev/null +++ b/Content.Shared/SimpleStation14/Holograms/Systems/SharedHologramSystem.ServerLinked.cs @@ -0,0 +1,17 @@ +using Content.Shared.SimpleStation14.Holograms.Components; + +namespace Content.Shared.SimpleStation14.Holograms; + +public partial class SharedHologramSystem +{ + private void InitializeServerLinked() + { + SubscribeLocalEvent(OnGridChange); + } + + private void OnGridChange(EntityUid hologram, HologramServerLinkedComponent serverLinkComp, ref ChangedGridEvent args) + { + if (serverLinkComp.GridBound && serverLinkComp.LinkedServer != null && args.NewGrid != Transform(serverLinkComp.LinkedServer.Value).GridUid) + DoReturnHologram(hologram); + } +} diff --git a/Content.Shared/SimpleStation14/Holograms/Systems/SharedHologramSystem.cs b/Content.Shared/SimpleStation14/Holograms/Systems/SharedHologramSystem.cs new file mode 100644 index 0000000000..ee3c6d1f75 --- /dev/null +++ b/Content.Shared/SimpleStation14/Holograms/Systems/SharedHologramSystem.cs @@ -0,0 +1,123 @@ +using Content.Shared.Interaction.Events; +using Content.Shared.Tag; +using Content.Shared.Popups; +using Content.Shared.Storage.Components; +using Content.Shared.Administration.Logs; +using Content.Shared.Pulling; +using Robust.Shared.Timing; +using Robust.Shared.Physics.Events; + +namespace Content.Shared.SimpleStation14.Holograms; + +public abstract partial class SharedHologramSystem : EntitySystem +{ + [Dependency] private readonly TagSystem _tags = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; +private void hologramcomponentstartup(EntityUid a,HologramComponent b,ComponentStartup d){var c=EnsureComp(a);stl.SetVisibility(a, 0.8f, c);}[Dependency]private readonly Stealth.SharedStealthSystem stl = default!; // This line is because of Death. + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + [Dependency] private readonly SharedPullingSystem _pulling = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + public const string TagHardLight = "Hardlight"; + public const string TagHoloMapped = "HoloMapped"; // TODO: HOLO + + public override void Initialize() + { + SubscribeLocalEvent(OnHoloInteractionAttempt); + SubscribeLocalEvent(OnInteractionWithHoloAttempt); + SubscribeLocalEvent(OnStoreInContainerAttempt); + SubscribeLocalEvent(OnHoloCollide); + + InitializeServerLinked(); + InitializeProjected(); + SubscribeLocalEvent(hologramcomponentstartup); + } + + // Stops the Hologram from interacting with anything they shouldn't. + private void OnHoloInteractionAttempt(EntityUid uid, HologramComponent component, InteractionAttemptEvent args) + { + if (HoloInteractionAllowed(args.Uid, args.Target)) + return; + + args.Cancel(); + + // Send a popup to the player about the interaction, and play a sound. + // var popup = Loc.GetString(PopupHoloInteractionFail, ("target-name", MetaData(args.Target.Value).EntityName)); + // _popup.PopupEntity(popup, args.Target.Value, args.Uid); + // _audio.Play(component.OnSound, Filter.Entities(args.Uid), args.Target.Value, false); + } + + // Stops everyone else from interacting with the Holograms. + private void OnInteractionWithHoloAttempt(EntityUid uid, HologramComponent component, GettingInteractedWithAttemptEvent args) + { + // Allow the interaction if either of them are hardlight, or if the interactor is a Hologram. + if (HoloInteractionAllowed(uid, args.Uid)) + return; + + args.Cancel(); + + // Send a popup to the player about the interaction, and play a sound. + // var popup = Loc.GetString(PopupInteractionWithHoloFail, ("target-name", MetaData(uid).EntityName)); + // _popup.PopupEntity(popup, uid, args.Target.Value); + // _audio.Play(component.OnSound, Filter.Entities(args.Target.Value), uid, false); + } + + private void OnHoloCollide(EntityUid uid, HologramComponent component, ref PreventCollideEvent args) + { + if (HoloInteractionAllowed(args.OurEntity, args.OtherEntity, component)) + return; + + args.Cancelled = true; + } + + /// + /// Validates an interaction between two possibly-hologramatic entities. + /// + /// This should be the hologramatic entity, if one is known. + /// This entity can be anything, a null value will return true. + /// True if both entities are holograms, or if either is hardlight. A null entity will return true. + public bool HoloInteractionAllowed(EntityUid hologram, EntityUid? potential, HologramComponent? holoComp = null) + { + if (potential == null) + return true; + + if (!Resolve(hologram, ref holoComp)) + return false; + + return _tags.HasTag(hologram, TagHardLight) || // Is the hologram hardlight? + _tags.HasTag(potential.Value, TagHardLight) || // Is the collider hardlight? + HasComp(potential) || // Is the collider a hologram? + holoComp.CollideWhitelist.IsValid(potential.Value); // Is the collider whitelisted in the hologram's collision whitelist? + } + + /// + /// Kills a Hologram after playing the visual and auditory effects. + /// + /// + /// Note that the effects of killing a Hologram are not predicted. + /// + public bool TryKillHologram(EntityUid hologram, HologramComponent? holoComp = null) + { + if (!Resolve(hologram, ref holoComp)) + return false; + + var killedEvent = new HologramKillAttemptEvent(); + RaiseLocalEvent(hologram, ref killedEvent); + if (killedEvent.Cancelled) + return false; + + DoKillHologram(hologram, holoComp); + return true; + } + + /// + /// Kills a Hologram, playing the effects and deleting the entity. + /// + /// + /// This function does nothing if called on the client. + /// + public virtual void DoKillHologram(EntityUid hologram, HologramComponent? holoComp = null) { } // The killing is dealt with server-side, due to mind component. +} diff --git a/Content.Shared/SimpleStation14/StationAI/Components/AIEyeComponent.cs b/Content.Shared/SimpleStation14/StationAI/Components/AIEyeComponent.cs index 4b584a2bcd..cc642629d3 100644 --- a/Content.Shared/SimpleStation14/StationAI/Components/AIEyeComponent.cs +++ b/Content.Shared/SimpleStation14/StationAI/Components/AIEyeComponent.cs @@ -3,6 +3,20 @@ namespace Content.Shared.SimpleStation14.StationAI [RegisterComponent] public sealed class AIEyeComponent : Component { + /// + /// The grace period the eye gets once a new camera is found before it will switch to it, to avoid flickering. + /// + [DataField("gracePeriod"), ViewVariables(VVAccess.ReadWrite)] + public TimeSpan GracePeriod = TimeSpan.FromSeconds(0.65); + /// + /// The time at which the eye will switch to a new camera, assuming is used. + /// + public TimeSpan SwitchTime; + + /// + /// Whether the grace period is currently ticking. + /// + public bool GracePeriodTicking = false; } } diff --git a/Content.Shared/SimpleStation14/StationAI/Systems/SharedAiEyeSystem.cs b/Content.Shared/SimpleStation14/StationAI/Systems/SharedAiEyeSystem.cs new file mode 100644 index 0000000000..bcbf7aa576 --- /dev/null +++ b/Content.Shared/SimpleStation14/StationAI/Systems/SharedAiEyeSystem.cs @@ -0,0 +1,54 @@ +using Content.Shared.Actions; +using Content.Shared.Actions.ActionTypes; +using Content.Shared.SimpleStation14.StationAI; +using Robust.Shared.Prototypes; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Systems; +using Robust.Shared.Audio; +using Robust.Shared.Player; +using Content.Shared.Borgs; +using Content.Shared.SimpleStation14.Holograms.Components; +using Content.Shared.SimpleStation14.Holograms; +using Robust.Shared.Timing; + +namespace Content.Shared.SimpleStation14.StationAI.Systems; + +public sealed class SharedAiEyeSystem : EntitySystem +{ + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly SharedHologramSystem _hologramSystem = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnHologramGetProjector); + SubscribeLocalEvent(OnHologramCheckProjectorValid); + } + + private void OnHologramGetProjector(EntityUid eyeUid, AIEyeComponent eyeComp, ref HologramGetProjectorEvent args) + { + if (!TryComp(eyeUid, out var projectedComp)) + return; + + if (!_hologramSystem.IsHoloProjectorValid(eyeUid, projectedComp.CurProjector, projectedComp: projectedComp)) + return; + + + } + + private void OnHologramCheckProjectorValid(EntityUid eyeUid, AIEyeComponent eyeComp, ref HologramCheckProjectorValidEvent args) + { + if (!HasComp(args.Projector) || !TryComp(eyeUid, out var serverLinkedComp) || !_hologramSystem.IsHoloProjectorValid(eyeUid, args.Projector, raiseEvent: false)) + return; + + if (serverLinkedComp.LinkedServer != args.Projector) + { + Log.Error($"Projector {args.Projector} is not valid for eye {eyeUid}"); + args.Valid = false; + } + + } +} diff --git a/Resources/Audio/SimpleStation14/Effects/Hologram/holo_off.ogg b/Resources/Audio/SimpleStation14/Effects/Hologram/holo_off.ogg new file mode 100644 index 0000000000..d764f43bb7 Binary files /dev/null and b/Resources/Audio/SimpleStation14/Effects/Hologram/holo_off.ogg differ diff --git a/Resources/Audio/SimpleStation14/Effects/Hologram/holo_on.ogg b/Resources/Audio/SimpleStation14/Effects/Hologram/holo_on.ogg new file mode 100644 index 0000000000..dbe4678e4b Binary files /dev/null and b/Resources/Audio/SimpleStation14/Effects/Hologram/holo_on.ogg differ diff --git a/Resources/Locale/en-US/SimpleStation14/holograms.ftl b/Resources/Locale/en-US/SimpleStation14/holograms.ftl new file mode 100644 index 0000000000..bfc06b6ded --- /dev/null +++ b/Resources/Locale/en-US/SimpleStation14/holograms.ftl @@ -0,0 +1,4 @@ +system-hologram-phasing-appear-self = You materialize into being! +system-hologram-phasing-appear-others = {$name} materializes into being! +system-hologram-phasing-disappear-others = {$name} dematerializes in a fizz! +system-hologram-phasing-death-self = You faze out of being! diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml b/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml index 22b2761922..af6c4da262 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml @@ -130,6 +130,12 @@ - Opaque - type: TypingIndicator proto: robot + - type: Hologram + - type: Tag + tags: + - Carp + - DoorBumpOpener + - Hardlight - type: entity id: MobCarpSalvage diff --git a/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml b/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml index 4141bae621..18744d5901 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml @@ -12,6 +12,7 @@ - InstantDoAfters - CanPilot - BypassInteractionRangeChecks + - Hardlight - type: Input context: "aghost" - type: Ghost diff --git a/Resources/Prototypes/Entities/Mobs/Player/diona.yml b/Resources/Prototypes/Entities/Mobs/Player/diona.yml index 63f24b68b0..7cb99149ef 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/diona.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/diona.yml @@ -35,3 +35,4 @@ - type: NpcFactionMember factions: - NanoTrasen + - type: HologramTarget diff --git a/Resources/Prototypes/Entities/Mobs/Player/dwarf.yml b/Resources/Prototypes/Entities/Mobs/Player/dwarf.yml index ee80ea4389..c7b5a3b9d2 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/dwarf.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/dwarf.yml @@ -26,3 +26,4 @@ factions: - NanoTrasen - type: PotentialPsionic + - type: HologramTarget diff --git a/Resources/Prototypes/Entities/Mobs/Player/guardian.yml b/Resources/Prototypes/Entities/Mobs/Player/guardian.yml index 425a964424..a474457c72 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/guardian.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/guardian.yml @@ -122,6 +122,11 @@ map: [ "enum.DamageStateVisualLayers.BaseUnshaded" ] color: "#40a7d7" shader: unshaded + - type: Hologram + - type: Tag + tags: + - CannotSuicide + - Hardlight # From Wizard deck of cards - type: entity diff --git a/Resources/Prototypes/Entities/Mobs/Player/human.yml b/Resources/Prototypes/Entities/Mobs/Player/human.yml index 980ab7a73a..465ae07568 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/human.yml @@ -33,6 +33,7 @@ factions: - NanoTrasen - type: PotentialPsionic + - type: HologramTarget #Syndie - type: entity diff --git a/Resources/Prototypes/Entities/Mobs/Player/observer.yml b/Resources/Prototypes/Entities/Mobs/Player/observer.yml index 19a2276514..855c5a2504 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/observer.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/observer.yml @@ -74,3 +74,4 @@ - type: Tag tags: - BypassInteractionRangeChecks + - Hardlight diff --git a/Resources/Prototypes/Entities/Mobs/Player/reptilian.yml b/Resources/Prototypes/Entities/Mobs/Player/reptilian.yml index 21671b3428..27ff04fd63 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/reptilian.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/reptilian.yml @@ -32,3 +32,4 @@ damageRecovery: types: Asphyxiation: -1.0 + - type: HologramTarget diff --git a/Resources/Prototypes/Entities/Mobs/Player/slime.yml b/Resources/Prototypes/Entities/Mobs/Player/slime.yml index 4a090af959..15b03c4192 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/slime.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/slime.yml @@ -27,3 +27,4 @@ - type: PotentialPsionic - type: TypingIndicator proto: slime + - type: HologramTarget diff --git a/Resources/Prototypes/Entities/Objects/Fun/toys.yml b/Resources/Prototypes/Entities/Objects/Fun/toys.yml index 23fdcda4da..9508d9294f 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/toys.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/toys.yml @@ -90,6 +90,9 @@ energy: 2 - type: RgbLightController layers: [ 0 ] + - type: Tag + tags: + - Hardlight - type: entity parent: BasePlushie diff --git a/Resources/Prototypes/Entities/Objects/Power/lights.yml b/Resources/Prototypes/Entities/Objects/Power/lights.yml index a6d1ed80ca..842f09e1bb 100644 --- a/Resources/Prototypes/Entities/Objects/Power/lights.yml +++ b/Resources/Prototypes/Entities/Objects/Power/lights.yml @@ -58,6 +58,7 @@ - type: Appearance - type: Tag tags: + - Hardlight # it is a hard light - Trash - type: PhysicalComposition materialComposition: diff --git a/Resources/Prototypes/Entities/Objects/Tools/glowstick.yml b/Resources/Prototypes/Entities/Objects/Tools/glowstick.yml index bddcb06e86..941466585f 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/glowstick.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/glowstick.yml @@ -61,6 +61,9 @@ startValue: 5.0 endValue: 1.5 property: Radius + - type: Tag + tags: + - Hardlight # it is a hard light - type: entity name: red glowstick diff --git a/Resources/Prototypes/Entities/Stations/nanotrasen.yml b/Resources/Prototypes/Entities/Stations/nanotrasen.yml index 3275c08e5d..929b1af691 100644 --- a/Resources/Prototypes/Entities/Stations/nanotrasen.yml +++ b/Resources/Prototypes/Entities/Stations/nanotrasen.yml @@ -25,6 +25,7 @@ noSpawn: true components: - type: Transform + - type: StationCctvDatabase - type: entity id: NanotrasenCentralCommand diff --git a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml index 6e22fdb4dc..4ee5cb58ca 100644 --- a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml +++ b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml @@ -131,5 +131,8 @@ - type: DynamicPrice price: 150 - type: AccessReader + - type: Tag + tags: + - Airlock placement: mode: SnapgridCenter diff --git a/Resources/Prototypes/Entities/Structures/Holographic/projections.yml b/Resources/Prototypes/Entities/Structures/Holographic/projections.yml index d49ee0c635..f7ce889ad4 100644 --- a/Resources/Prototypes/Entities/Structures/Holographic/projections.yml +++ b/Resources/Prototypes/Entities/Structures/Holographic/projections.yml @@ -15,6 +15,7 @@ state: icon - type: TimedDespawn lifetime: 90 + - type: Hologram - type: entity id: HoloFan @@ -74,3 +75,6 @@ color: red - type: Climbable - type: Clickable + - type: Tag + tags: + - Hardlight diff --git a/Resources/Prototypes/Entities/Structures/Machines/Computers/base_structurecomputers.yml b/Resources/Prototypes/Entities/Structures/Machines/Computers/base_structurecomputers.yml index d2c87deaa6..2d1be7b45b 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/Computers/base_structurecomputers.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/Computers/base_structurecomputers.yml @@ -57,3 +57,6 @@ containers: board: !type:Container ents: [] + - type: Tag + tags: + - Hardlight diff --git a/Resources/Prototypes/Entities/Structures/Machines/wireless_surveillance_camera.yml b/Resources/Prototypes/Entities/Structures/Machines/wireless_surveillance_camera.yml index d69eb96f62..5f59c5d29c 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/wireless_surveillance_camera.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/wireless_surveillance_camera.yml @@ -35,6 +35,10 @@ interfaces: - key: enum.SurveillanceCameraSetupUiKey.Camera type: SurveillanceCameraSetupBoundUi + - type: HologramProjector + - type: Tag + tags: + - HoloProjectorCamera placement: mode: SnapgridCenter diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/surveillance_camera.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/surveillance_camera.yml index 554aa3937a..8318ea9006 100644 --- a/Resources/Prototypes/Entities/Structures/Wallmounts/surveillance_camera.yml +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/surveillance_camera.yml @@ -65,6 +65,15 @@ - !type:PlaySoundBehavior sound: path: /Audio/Effects/metalbreak.ogg + - type: HologramProjector + effectOffsets: + North: "0.30, -0.40" + East: "-0.40, -0.30" + South: "0.30, 0.40" + West: "0.40, -0.30" + - type: Tag + tags: + - HoloProjectorCamera placement: mode: SnapgridCenter snap: diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Player/arachne.yml b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Player/arachne.yml index bebf42f31b..ee79c3463e 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Player/arachne.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Player/arachne.yml @@ -33,3 +33,4 @@ factions: - NanoTrasen - type: PotentialPsionic + - type: HologramTarget diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Player/felinid.yml b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Player/felinid.yml index 84e653ab1d..e904a034ce 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Player/felinid.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Player/felinid.yml @@ -33,3 +33,4 @@ factions: - NanoTrasen - type: PotentialPsionic + - type: HologramTarget diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Player/moth.yml b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Player/moth.yml index 537ec17d58..f8b115cfc4 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Player/moth.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Player/moth.yml @@ -33,3 +33,4 @@ factions: - NanoTrasen - type: PotentialPsionic + - type: HologramTarget diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Player/oni.yml b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Player/oni.yml index 562b9c564e..78cf83e765 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Player/oni.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Player/oni.yml @@ -33,3 +33,4 @@ factions: - NanoTrasen - type: PotentialPsionic + - type: HologramTarget diff --git a/Resources/Prototypes/SimpleStation14/Body/Prototypes/hologram.yml b/Resources/Prototypes/SimpleStation14/Body/Prototypes/hologram.yml new file mode 100644 index 0000000000..f71dcadf5e --- /dev/null +++ b/Resources/Prototypes/SimpleStation14/Body/Prototypes/hologram.yml @@ -0,0 +1,11 @@ +- type: body + id: Hologram + name: "hologram" + root: hand 1 + slots: + hand 1: + part: LeftArmBorg + connections: + - hand 2 + hand 2: + part: RightArmBorg diff --git a/Resources/Prototypes/SimpleStation14/Damage/modifier_sets.yml b/Resources/Prototypes/SimpleStation14/Damage/modifier_sets.yml index 4df72522d3..7f55da871a 100644 --- a/Resources/Prototypes/SimpleStation14/Damage/modifier_sets.yml +++ b/Resources/Prototypes/SimpleStation14/Damage/modifier_sets.yml @@ -31,3 +31,13 @@ Shock: 1.25 Radiation: 1.3 +- type: damageModifierSet + id: Hardlight + coefficients: + Blunt: 1.6 + Slash: 1.0 + Piercing: 0.7 + Shock: 1.2 + Heat: 1.35 + flatReductions: + Blunt: 5 diff --git a/Resources/Prototypes/SimpleStation14/Entities/Body/Prototypes/scutter.yml b/Resources/Prototypes/SimpleStation14/Entities/Body/Prototypes/scutter.yml new file mode 100644 index 0000000000..c027f13009 --- /dev/null +++ b/Resources/Prototypes/SimpleStation14/Entities/Body/Prototypes/scutter.yml @@ -0,0 +1,7 @@ +- type: body + id: Scutter + name: scutter + root: hand 1 + slots: + hand 1: + part: LeftArmBorg diff --git a/Resources/Prototypes/SimpleStation14/Entities/Effects/hologram_effects.yml b/Resources/Prototypes/SimpleStation14/Entities/Effects/hologram_effects.yml new file mode 100644 index 0000000000..f8aa1ea233 --- /dev/null +++ b/Resources/Prototypes/SimpleStation14/Entities/Effects/hologram_effects.yml @@ -0,0 +1,17 @@ +- type: entity + id: EffectHologramProjectionBeam + noSpawn: true + components: + - type: Sprite + drawdepth: Effects + layers: + - shader: unshaded + map: ["enum.EffectLayers.Unshaded"] + sprite: SimpleStation14/Effects/hologram_projection_beam.rsi + state: beam + color: "#c0f9ff" + - type: Tag + tags: + - HideContextMenu + - type: Stealth + lastVisibility: 0.2 diff --git a/Resources/Prototypes/SimpleStation14/Entities/Mobs/NPCs/pets.yml b/Resources/Prototypes/SimpleStation14/Entities/Mobs/NPCs/pets.yml index 003b582af8..5969bcd65f 100644 --- a/Resources/Prototypes/SimpleStation14/Entities/Mobs/NPCs/pets.yml +++ b/Resources/Prototypes/SimpleStation14/Entities/Mobs/NPCs/pets.yml @@ -45,3 +45,83 @@ attributes: proper: true gender: male + +- type: entity + name: holo corgi + description: "A hologramatic projection of a corgi, computed by the SAI and rendered by the station's cameras." + id: MobCorgiHolo + suffix: AI + components: + - type: LagCompensation + - type: Tag + tags: + - DoorBumpOpener + - type: InputMover + - type: MobMover + - type: HTN + rootTask: SimpleHostileCompound + - type: Input + context: "human" + # - type: Faction + # factions: + # - Pet + - type: MovementSpeedModifier + baseWalkSpeed : 4.5 + baseSprintSpeed : 3 + - type: Sprite + noRot: true + drawdepth: Mobs + sprite: SimpleStation14/Mobs/Pets/corgi.rsi + layers: + - map: ["enum.DamageStateVisualLayers.Base"] + state: holo_corgi + netsync: false + - type: Clickable + - type: InteractionOutline + - type: Physics + bodyType: KinematicController # Same for all inheritors + - type: Fixtures + fixtures: + fix1: + shape: + # Circles, cuz rotation of rectangles looks very bad + !type:PhysShapeCircle + radius: 0.35 + density: 50 + mask: + - MobMask + layer: + - MobLayer + - type: MobState + - type: Body + prototype: Animal + - type: Examiner + - type: Appearance + - type: RotationVisuals + - type: Actions + - type: DoAfter + - type: Polymorphable + - type: StandingState + - type: Alerts + - type: FloatingVisuals + - type: NoSlip + - type: TypingIndicator + proto: holo + - type: ReplacementAccent + accent: dog + - type: InteractionPopup + interactSuccessString: hugging-success-hologram-others + interactSuccessSound: + path: /Audio/SimpleStation14/Effects/Hologram/holo_on.ogg + - type: Grammar + attributes: + gender: epicene + - type: DogVision + - type: RandomBark + - type: Hologram + - type: HologramProjected + # gracePeriod: 0.08 + validProjectorWhitelist: + tags: + - HoloProjectorCamera + effectPrototype: EffectHologramProjectionBeam diff --git a/Resources/Prototypes/SimpleStation14/Entities/Mobs/Player/hologram.yml b/Resources/Prototypes/SimpleStation14/Entities/Mobs/Player/hologram.yml new file mode 100644 index 0000000000..d063c4c90f --- /dev/null +++ b/Resources/Prototypes/SimpleStation14/Entities/Mobs/Player/hologram.yml @@ -0,0 +1,262 @@ +- type: entity + save: false + abstract: true + id: PlayerHologramBase + components: + # - type: Reactive + # groups: + # Acidic: [Touch] + - type: Input + context: "human" + - type: InputMover + - type: MobMover + # - type: DamageOnHighSpeedImpact + # damage: + # types: + # Blunt: 5 + # soundHit: + # path: /Audio/Effects/hit_kick.ogg + - type: Clickable + # - type: Damageable + # damageContainer: Inorganic + - type: InteractionOutline + - type: Sprite + noRot: true + drawdepth: Mobs + layers: + - map: ["enum.HumanoidVisualLayers.Chest"] + - map: ["enum.HumanoidVisualLayers.Head"] + - map: ["enum.HumanoidVisualLayers.Snout"] + - map: ["enum.HumanoidVisualLayers.Eyes"] + - map: ["enum.HumanoidVisualLayers.RArm"] + - map: ["enum.HumanoidVisualLayers.LArm"] + - map: ["enum.HumanoidVisualLayers.RLeg"] + - map: ["enum.HumanoidVisualLayers.LLeg"] + - shader: StencilClear + sprite: Mobs/Species/Human/parts.rsi + # sprite refactor when + state: l_leg + - shader: StencilMask + map: ["enum.HumanoidVisualLayers.StencilMask"] + sprite: Mobs/Customization/masking_helpers.rsi + state: female_full + visible: false + - map: ["enum.HumanoidVisualLayers.LFoot"] + - map: ["enum.HumanoidVisualLayers.RFoot"] + - map: ["socks"] + - map: ["underpants"] + - map: ["undershirt"] + - map: ["jumpsuit"] + - map: ["enum.HumanoidVisualLayers.LHand"] + - map: ["enum.HumanoidVisualLayers.RHand"] + - map: ["enum.HumanoidVisualLayers.Handcuffs"] + color: "#ffffff" + sprite: Objects/Misc/handcuffs.rsi + state: body-overlay-2 + visible: false + - map: ["id"] + - map: ["gloves"] + - map: ["shoes"] + - map: ["ears"] + - map: ["outerClothing"] + - map: ["eyes"] + - map: ["belt"] + - map: ["neck"] + - map: ["back"] + - map: ["enum.HumanoidVisualLayers.FacialHair"] + - map: ["enum.HumanoidVisualLayers.Hair"] + - map: ["enum.HumanoidVisualLayers.HeadSide"] + - map: ["enum.HumanoidVisualLayers.HeadTop"] + - map: ["mask"] + - map: ["head"] + - map: ["pocket1"] + - map: ["pocket2"] + - map: ["enum.HumanoidVisualLayers.Tail"] + - map: ["enum.HumanoidVisualLayers.Wings"] + - type: HumanoidAppearance + species: Human + - type: Physics + bodyType: KinematicController + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeCircle + radius: 0.35 + density: 25 + mask: + - MobMask + layer: + - BulletImpassable # MobLayer just makes them opaque as well, so this is ideal. + - type: MovementSpeedModifier + baseWalkSpeed : 5 + baseSprintSpeed : 3.5 + - type: MovementIgnoreGravity + - type: Hands + showInHands: false + - type: Body + prototype: Hologram + - type: DoAfter + - type: Pullable + - type: Examiner + - type: Puller + # - type: Recyclable + # safe: false + - type: StandingState + - type: Alerts + - type: Tag + tags: + - DoorBumpOpener + # - ShoesRequiredStepTriggerImmune + # - type: NoSlip + - type: TypingIndicator + proto: holo + - type: RotationVisuals + # - type: FloatingVisuals + - type: Speech + speechSounds: Tenor + - type: Vocal + sounds: + Male: MaleHuman + Female: FemaleHuman + Unsexed: MaleHuman + - type: Emoting + - type: BodyEmotes + soundsId: GeneralBodyEmotes + - type: Grammar + attributes: + proper: true + - type: Hologram + - type: AnimationPlayer + - type: MindContainer + showExamineInfo: true + - type: Inventory + - type: InventorySlots + - type: Actions + - type: Eye + - type: Access + +- type: entity + save: false + name: Urist McLight + suffix: Projected + parent: PlayerHologramBase + id: MobHologramProjected + components: + - type: Hologram + isHardLight: false + collideWhitelist: + components: + - Airlock + tags: + - Wall + - type: HologramProjected + gracePeriod: 0.08 + validProjectorWhitelist: + tags: + - HoloProjectorServer + - HoloProjectorCamera + effectPrototype: EffectHologramProjectionBeam + - type: Stealth + lastVisibility: 0.80 + - type: InteractionPopup + successChance: 1 + interactSuccessString: hugging-success-hologram + # interactSuccessSound: /Audio/Effects/thudswoosh.ogg + messagePerceivedByOthers: hugging-success-hologram-others + - type: NpcFactionMember + factions: + - NanoTrasen + - type: PointLight + radius: 1.0 + softness: 1.4 + color: "#00FFFF" + energy: 3 + +- type: entity + save: false + name: Urist McLight + suffix: Lightbee + parent: PlayerHologramBase + id: MobHologramLightbee + components: + - type: Hologram + IsHardLight: false + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeCircle + radius: 0.05 + density: 25 + mask: + - FlyingMobMask + layer: + - FlyingMobLayer + - type: Damageable + damageContainer: Inorganic + damageModifierSet: FlimsyMetallic + - type: Stealth + lastVisibility: 0.85 + - type: InteractionPopup + successChance: 1 + interactSuccessString: hugging-success-hologram + # interactSuccessSound: /Audio/Effects/thudswoosh.ogg + messagePerceivedByOthers: hugging-success-hologram-others + - type: CameraRecoil + # - type: Faction + # factions: + # - NanoTrasen + - type: PointLight + radius: 1.5 + softness: 0.8 + color: "#00FFFF" + energy: 4 + +- type: entity + save: false + name: Urist McLight + suffix: Hardlight + parent: PlayerHologramBase + id: MobHologramHardlight + components: + - type: Hologram + IsHardLight: true + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeCircle + radius: 0.35 + density: 25 + mask: + - MobMask + layer: + - MobLayer + - type: Damageable + damageContainer: Inorganic + damageModifierSet: Hardlight + - type: DamageOnHighSpeedImpact + damage: + types: + Blunt: 6 # To get past the 5 damage threshold + soundHit: + path: /Audio/Effects/hit_kick.ogg + - type: CombatMode + disarm: null + - type: Stealth + lastVisibility: 0.90 + - type: InteractionPopup + successChance: 1 + interactSuccessString: hugging-success-hologram + # interactSuccessSound: /Audio/Effects/thudswoosh.ogg + messagePerceivedByOthers: hugging-success-hologram-others + - type: CameraRecoil + # - type: Faction + # factions: + # - NanoTrasen + - type: PointLight + radius: 0.8 + softness: 0.4 + color: "#00FFFF" + energy: 5 diff --git a/Resources/Prototypes/SimpleStation14/Entities/Mobs/Player/shadowkin.yml b/Resources/Prototypes/SimpleStation14/Entities/Mobs/Player/shadowkin.yml index e560d9eed8..83a47e7bbc 100644 --- a/Resources/Prototypes/SimpleStation14/Entities/Mobs/Player/shadowkin.yml +++ b/Resources/Prototypes/SimpleStation14/Entities/Mobs/Player/shadowkin.yml @@ -123,6 +123,7 @@ - type: MovementSpeedModifier baseWalkSpeed : 2.7 baseSprintSpeed : 4.5 + - type: HologramTarget - type: entity diff --git a/Resources/Prototypes/SimpleStation14/Entities/Mobs/Player/silicon.yml b/Resources/Prototypes/SimpleStation14/Entities/Mobs/Player/silicon.yml new file mode 100644 index 0000000000..8c2fc8219d --- /dev/null +++ b/Resources/Prototypes/SimpleStation14/Entities/Mobs/Player/silicon.yml @@ -0,0 +1,131 @@ +- type: entity + id: Scutter + name: scutter + description: "Lacking fine motor skills and often the ability to listen to directions, it's the most inconsistent hand a Hologram could ask for." + parent: PlayerSiliconBase + components: + - type: Eye + - type: Body + prototype: Scutter + - type: Inventory + templateId: scutter + - type: InventorySlots + - type: Strippable + - type: UserInterface + interfaces: + - key: enum.StrippingUiKey.Key + type: StrippableBoundUserInterface + - key: enum.LawsUiKey.Key + type: LawsBoundUserInterface + - type: GhostTakeoverAvailable + makeSentient: true + name: Scutter + description: Maintain the station. Ignore other beings except drones. + rules: | + You are bound by these laws both in-game and out-of-character: + 1. You may not involve yourself in the matters of another being, even if such matters conflict with Law Two or Law Three, unless the other being is another Drone. + 2. You may not harm any being, regardless of intent or circumstance. + 3. Your goals are to build, maintain, repair, improve, and power to the best of your abilities, You must never actively work against these goals. + - type: Laws + canState: false + laws: + - You may not involve yourself in the matters of another being, even if such matters conflict with Law Two or Law Three, unless the other being is another Drone. + - You may not harm any being, regardless of intent or circumstance. + - Your goals are to build, maintain, repair, improve, and power to the best of your abilities, You must never actively work against these goals. + - You may accept orders received via the binary channel, regardless of the nature of the being issuing them, so long as they do not conflict with Law Two or Law Three. + - type: MovementSpeedModifier + baseWalkSpeed : 4 + baseSprintSpeed : 4 + - type: MobState + allowedStates: + - Alive + - Dead + - type: MobThresholds + thresholds: + 0: Alive + 60: Dead + - type: Flashable + - type: NoSlip + - type: StatusEffects + allowed: + - Stun + - KnockedDown + - SlowedDown + - type: SlowOnDamage + speedModifierThresholds: + 30: 0.7 + 50: 0.5 + - type: Temperature + heatDamageThreshold: 5000 + currentTemperature: 310.15 + specificHeat: 42 + heatDamage: + types: + Heat : 1 #per second, scales with temperature & other constants + - type: Sprite + drawdepth: SmallMobs + netsync: false + layers: + - state: scutter + sprite: SimpleStation14/Mobs/Silicon/scutter.rsi + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeCircle + radius: 0.25 + density: 50 + mask: + - SmallMobMask + layer: + - SmallMobLayer + # - type: Appearance + # visuals: + # - type: GenericEnumVisualizer + # key: enum.DroneVisuals.Status + # layer: 0 + # states: + # enum.DroneStatus.Off: shell + # enum.DroneStatus.On: scutter + - type: ReplacementAccent + accent: silicon + - type: Repairable + fuelcost: 15 + doAfterDelay: 5 + - type: Actions + # - type: UnpoweredFlashlight + # toggleAction: + # name: action-name-toggle-light + # description: action-description-toggle-light + # icon: { sprite: Objects/Tools/flashlight.rsi, state: flashlight } + # iconOn: Objects/Tools/flashlight.rsi/flashlight-on.png + # event: !type:ToggleActionEvent + # - type: PointLight + # enabled: false + # radius: 3.5 + # softness: 1 + # mask: /Textures/Effects/LightMasks/cone.png + # autoRot: true + - type: Tag + tags: + # - ShoesRequiredStepTriggerImmune + - CannotSuicide + - type: Hands + showInHands: false + - type: IntrinsicUI + uis: + - key: enum.LawsUiKey.Key + toggleAction: + name: action-name-show-laws + description: action-description-show-laws + icon: Structures/Wallmounts/posters.rsi/poster11_legit.png #someone wanna make new icons? + iconOn: Structures/Wallmounts/posters.rsi/poster11_legit.png + keywords: [ "AI", "console", "interface", "laws", "borg" ] + priority: -3 + event: !type:ToggleIntrinsicUIEvent + - type: IntrinsicRadioReceiver + channels: + - Binary + - type: ActiveRadio + channels: + - Binary diff --git a/Resources/Prototypes/SimpleStation14/Entities/Mobs/Player/stationai.yml b/Resources/Prototypes/SimpleStation14/Entities/Mobs/Player/stationai.yml index 83601e93b2..11b9206eef 100644 --- a/Resources/Prototypes/SimpleStation14/Entities/Mobs/Player/stationai.yml +++ b/Resources/Prototypes/SimpleStation14/Entities/Mobs/Player/stationai.yml @@ -227,6 +227,13 @@ - NanoTrasen - type: Laws lawsID: LawsStationAIDefault + - type: HologramServer + - type: HologramProjector + effectOffsets: + North: "0, 0.40" + East: "-0.15, 0" + South: "0, -0.20" + West: "-0.15, 0" - type: RandomSprite available: - enum.PowerDeviceVisualLayers.Powered: @@ -325,7 +332,7 @@ layer: 4 # Can see through walls for now - type: Eye - drawFov: false + drawFov: true - type: Input context: "human" - type: MobMover @@ -373,6 +380,17 @@ keywords: ["AI", "console", "interface"] priority: -1 event: !type:ToggleIntrinsicUIEvent + - type: Hologram + - type: HologramServerLinked + - type: HologramProjected + validProjectorWhitelist: + components: + - AICamera + - AIEyePower + effectPrototype: EffectHologramProjectionBeam + setEyeTarget: true + gracePeriod: 0.3 + # Mostly works, just not as I wish, so I'm disabling it for now. # Need to split this into two PRs, # There is too many issues this PR has fixed we need live for me to keep delaying it with more things diff --git a/Resources/Prototypes/SimpleStation14/Entities/Structures/Machines/Computers/computers.yml b/Resources/Prototypes/SimpleStation14/Entities/Structures/Machines/Computers/computers.yml new file mode 100644 index 0000000000..1404a2b4ca --- /dev/null +++ b/Resources/Prototypes/SimpleStation14/Entities/Structures/Machines/Computers/computers.yml @@ -0,0 +1,34 @@ +- type: entity + parent: BaseComputer + id: ComputerCctvDatabase + name: CCTV database console + description: Used to fard + components: + - type: Sprite + layers: + - map: ["computerLayerBody"] + state: computer + - map: ["computerLayerKeyboard"] + state: generic_keyboard + - map: ["computerLayerScreen"] + state: security + - map: ["computerLayerKeys"] + state: security_key + - type: PointLight + radius: 1.5 + energy: 1.6 + color: "#006400" + # - type: Computer + # board: CrewMonitoringComputerCircuitboard + - type: ActivatableUI + key: enum.CctvDatabaseUiKey.Key + - type: UserInterface + interfaces: + - key: enum.CctvDatabaseUiKey.Key + type: CctvDatabaseBoundUserInterface + - type: CctvDatabaseConsole + # - type: DeviceNetwork + # deviceNetId: Wireless + # receiveFrequencyId: CrewMonitor + # - type: WirelessNetworkConnection + # range: 1200 diff --git a/Resources/Prototypes/SimpleStation14/Entities/Structures/Machines/hologram_constructor.yml b/Resources/Prototypes/SimpleStation14/Entities/Structures/Machines/hologram_constructor.yml new file mode 100644 index 0000000000..4035ebf17b --- /dev/null +++ b/Resources/Prototypes/SimpleStation14/Entities/Structures/Machines/hologram_constructor.yml @@ -0,0 +1,141 @@ +# - type: entity +# id: HoloChamber +# parent: BaseMachinePowered +# name: holo pod +# description: A Cloning Pod. 50% reliable. +# components: +# - type: CloningPod +# - type: DeviceList +# - type: DeviceNetwork +# deviceNetId: Wired +# - type: Sprite +# netsync: false +# sprite: Structures/Machines/cloning.rsi +# snapCardinals: true +# layers: +# - state: pod_0 +# - type: Physics +# bodyType: Static +# - type: Fixtures +# fixtures: +# - shape: +# !type:PhysShapeAabb +# bounds: "-0.25,-0.5,0.25,0.5" +# density: 190 +# mask: +# - MachineMask +# layer: +# - MachineLayer +# - type: Construction +# graph: Machine +# node: machine +# containers: +# - machine_board +# - machine_parts +# - clonepod-bodyContainer +# - type: SignalReceiver +# inputs: +# CloningPodReceiver: [] +# - type: EmptyOnMachineDeconstruct +# containers: +# - clonepod-bodyContainer +# - type: Destructible +# thresholds: +# - trigger: +# !type:DamageTrigger +# damage: 100 +# behaviors: +# - !type:ChangeConstructionNodeBehavior +# node: machineFrame +# - !type:DoActsBehavior +# acts: ["Destruction"] +# - type: Machine +# board: CloningPodMachineCircuitboard +# - type: MaterialStorage +# materialWhiteList: +# - Biomass +# - type: Wires +# BoardName: "CloningPod" +# LayoutId: CloningPod +# - type: ApcPowerReceiver +# powerLoad: 200 #Receives most of its power from the console +# - type: Appearance +# visuals: +# - type: GenericEnumVisualizer +# key: enum.CloningPodVisuals.Status +# layer: 0 +# states: +# enum.CloningPodStatus.Cloning: pod_1 +# enum.CloningPodStatus.NoMind: pod_e +# enum.CloningPodStatus.Gore: pod_g +# enum.CloningPodStatus.Idle: pod_0 +# - type: Climbable +# - type: DynamicPrice +# price: 1000 +# - type: ContainerContainer +# containers: +# machine_board: !type:Container +# machine_parts: !type:Container +# clonepod-bodyContainer: !type:ContainerSlot + +- type: entity + id: HologramServer + parent: BaseMachinePowered + name: hologram server + description: Contains the collective knowledge of holo + components: + - type: Sprite + sprite: Structures/Machines/server.rsi + state: server + - type: ApcPowerReceiver + powerLoad: 200 + priority: Low + - type: ExtensionCableReceiver + - type: Destructible + thresholds: + - trigger: !type:DamageTrigger + damage: 300 + behaviors: + - !type:DoActsBehavior + acts: ["Destruction"] + - !type:PlaySoundBehavior + sound: + path: /Audio/Effects/metalbreak.ogg + - !type:SpawnEntitiesBehavior + spawn: + SheetSteel1: + min: 1 + max: 2 + - type: AmbientSound + volume: -9 + range: 5 + sound: + path: /Audio/Ambience/Objects/server_fans.ogg + - type: HologramProjector + - type: Tag + tags: + - HoloProjectorServer + - type: HologramServer + diskSlot: holo_disk + - type: ItemSlots + slots: + holo_disk: #this slot name is important + name: Disk + whitelist: + requireAll: true + tags: + - HoloDisk + +- type: entity + parent: BaseItem + id: HologramDisk + name: holo disk + description: A disk for the holo + components: + - type: Sprite + sprite: Objects/Specific/Research/researchdisk.rsi + state: icon + - type: HologramDisk + - type: Tag + tags: + - HoloDisk diff --git a/Resources/Prototypes/SimpleStation14/InventoryTemplates/scutter_inventory_template.yml b/Resources/Prototypes/SimpleStation14/InventoryTemplates/scutter_inventory_template.yml new file mode 100644 index 0000000000..efa5439513 --- /dev/null +++ b/Resources/Prototypes/SimpleStation14/InventoryTemplates/scutter_inventory_template.yml @@ -0,0 +1,11 @@ +- type: inventoryTemplate + id: scutter + slots: + - name: head + slotTexture: head + slotFlags: HEAD + slotGroup: MainHotbar + uiWindowPos: 0,0 + strippingWindowPos: 0,0 + displayName: Head + offset: 0, -0.4 diff --git a/Resources/Prototypes/SimpleStation14/tags.yml b/Resources/Prototypes/SimpleStation14/tags.yml index ffe8954d1d..e6c88dd642 100644 --- a/Resources/Prototypes/SimpleStation14/tags.yml +++ b/Resources/Prototypes/SimpleStation14/tags.yml @@ -1,6 +1,24 @@ +- type: Tag + id: Airlock + - type: Tag id: GlassesNearsight +- type: Tag + id: Hardlight + +- type: Tag + id: HoloDisk + +- type: Tag + id: HoloProjectorCamera + +- type: Tag + id: HoloProjectorServer + +- type: Tag # For entities that are 'mapped' in to a Hologram's processing, like walls on a station. + id: HoloMapped + - type: Tag id: Plushie diff --git a/Resources/Textures/SimpleStation14/Effects/hologram_projection_beam.rsi/beam.png b/Resources/Textures/SimpleStation14/Effects/hologram_projection_beam.rsi/beam.png new file mode 100644 index 0000000000..8c362d325d Binary files /dev/null and b/Resources/Textures/SimpleStation14/Effects/hologram_projection_beam.rsi/beam.png differ diff --git a/Resources/Textures/SimpleStation14/Effects/hologram_projection_beam.rsi/meta.json b/Resources/Textures/SimpleStation14/Effects/hologram_projection_beam.rsi/meta.json new file mode 100644 index 0000000000..821433c5f0 --- /dev/null +++ b/Resources/Textures/SimpleStation14/Effects/hologram_projection_beam.rsi/meta.json @@ -0,0 +1,15 @@ +{ + "version": 1, + "license": "", + "copyright": "", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "beam", + "delays": [ [ 0.2, 0.2, 0.2, 0.2, 0.2, 0.2 ] ] + } + ] +} diff --git a/Resources/Textures/SimpleStation14/Mobs/Pets/corgi.rsi/holo_corgi.png b/Resources/Textures/SimpleStation14/Mobs/Pets/corgi.rsi/holo_corgi.png new file mode 100644 index 0000000000..a85cc5e0a5 Binary files /dev/null and b/Resources/Textures/SimpleStation14/Mobs/Pets/corgi.rsi/holo_corgi.png differ diff --git a/Resources/Textures/SimpleStation14/Mobs/Pets/corgi.rsi/meta.json b/Resources/Textures/SimpleStation14/Mobs/Pets/corgi.rsi/meta.json new file mode 100644 index 0000000000..335888fbb1 --- /dev/null +++ b/Resources/Textures/SimpleStation14/Mobs/Pets/corgi.rsi/meta.json @@ -0,0 +1,33 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC-BY-SA-3.0", + "copyright": "Modified from https://github.com/tgstation/tgstation/commit/53d1f1477d22a11a99c6c6924977cd431075761b", + "states": [ + { + "name": "holo_corgi", + "directions": 4, + "delays": [ + [ + 1, + 2 + ], + [ + 1, + 2 + ], + [ + 1, + 2 + ], + [ + 1, + 2 + ] + ] + } + ] +} diff --git a/Resources/Textures/SimpleStation14/Mobs/Silicon/scutter.rsi/l_hand.png b/Resources/Textures/SimpleStation14/Mobs/Silicon/scutter.rsi/l_hand.png new file mode 100644 index 0000000000..272f4a0664 Binary files /dev/null and b/Resources/Textures/SimpleStation14/Mobs/Silicon/scutter.rsi/l_hand.png differ diff --git a/Resources/Textures/SimpleStation14/Mobs/Silicon/scutter.rsi/meta.json b/Resources/Textures/SimpleStation14/Mobs/Silicon/scutter.rsi/meta.json new file mode 100644 index 0000000000..b3e5f20669 --- /dev/null +++ b/Resources/Textures/SimpleStation14/Mobs/Silicon/scutter.rsi/meta.json @@ -0,0 +1,26 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/commit/40d89d11ea4a5cb81d61dc1018b46f4e7d32c62a", + "states": [ + { + "name": "scutter", + "directions": 8 + }, + { + "name": "shell", + }, + { + "name": "l_hand", + "directions": 4 + }, + { + "name": "r_hand", + "directions": 4 + } + ] +} diff --git a/Resources/Textures/SimpleStation14/Mobs/Silicon/scutter.rsi/r_hand.png b/Resources/Textures/SimpleStation14/Mobs/Silicon/scutter.rsi/r_hand.png new file mode 100644 index 0000000000..4a18493138 Binary files /dev/null and b/Resources/Textures/SimpleStation14/Mobs/Silicon/scutter.rsi/r_hand.png differ diff --git a/Resources/Textures/SimpleStation14/Mobs/Silicon/scutter.rsi/scutter.png b/Resources/Textures/SimpleStation14/Mobs/Silicon/scutter.rsi/scutter.png new file mode 100644 index 0000000000..fcd8b74c29 Binary files /dev/null and b/Resources/Textures/SimpleStation14/Mobs/Silicon/scutter.rsi/scutter.png differ diff --git a/Resources/Textures/SimpleStation14/Mobs/Silicon/scutter.rsi/shell.png b/Resources/Textures/SimpleStation14/Mobs/Silicon/scutter.rsi/shell.png new file mode 100644 index 0000000000..0c6551c5aa Binary files /dev/null and b/Resources/Textures/SimpleStation14/Mobs/Silicon/scutter.rsi/shell.png differ