From 72036ce0072cb692a16f166b2c1a8455264a9a0f Mon Sep 17 00:00:00 2001 From: SpicyDarkFox Date: Tue, 13 Aug 2024 15:18:58 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=98=D0=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Administration/AdminNameOverlay.cs | 42 ++ .../_Backmen/StationAI/AiCamOverlay.cs | 127 ++++++ .../_Backmen/StationAI/AiEnemySystem.cs | 42 ++ .../_Backmen/StationAI/AiVisualizerSystem.cs | 99 +++++ .../_Backmen/StationAI/StationAiSystem.cs | 112 +++++ .../_Backmen/StationAI/UI/AICameraList.xaml | 71 ++++ .../StationAI/UI/AICameraList.xaml.cs | 73 ++++ .../UI/AICameraListBoundUserInterface.cs | 51 +++ .../StationAI/UI/CameraNavMapControl.cs | 17 + .../Tests/PostMapInitTest.cs | 3 +- .../Administration/Systems/AdminSystem.cs | 5 + .../CommunicationsConsoleSystem.cs | 6 + .../_Backmen/StationAI/AiEyeMover.cs | 93 +++++ .../_Backmen/StationAI/InnateItemSystem.cs | 117 ++++++ .../StationAI/Systems/AICameraSystem.cs | 233 +++++++++++ .../_Backmen/StationAI/Systems/AIEyeSystem.cs | 264 ++++++++++++ .../StationAI/Systems/AiEnemySystem.cs | 80 ++++ .../StationAI/Systems/StationAiSystem.cs | 383 ++++++++++++++++++ Content.Shared/Eye/VisibilityFlags.cs | 3 +- Content.Shared/Mind/SharedMindSystem.cs | 12 + .../StationAI/Components/AICameraComponent.cs | 34 ++ .../Components/AIEnemyNTComponent.cs | 10 + .../StationAI/Components/AIEyeComponent.cs | 52 +++ .../Components/AIEyePowerComponent.cs | 16 + .../StationAI/Components/BorgAINTComponent.cs | 9 + .../Components/CanBeBorgNTEnemyComponent.cs | 9 + .../Components/InnateItemComponent.cs | 22 + .../Components/StationAIComponent.cs | 39 ++ .../StationAI/Events/StationAIEvents.cs | 21 + .../StationAI/Systems/AICameraSystem.cs | 34 ++ .../StationAI/Systems/AiEnemySystem.cs | 49 +++ .../Systems/SharedStationAISystem.cs | 73 ++++ Content.Shared/_Backmen/StationAI/UI/Key.cs | 23 ++ 33 files changed, 2222 insertions(+), 2 deletions(-) create mode 100644 Content.Client/_Backmen/StationAI/AiCamOverlay.cs create mode 100644 Content.Client/_Backmen/StationAI/AiEnemySystem.cs create mode 100644 Content.Client/_Backmen/StationAI/AiVisualizerSystem.cs create mode 100644 Content.Client/_Backmen/StationAI/StationAiSystem.cs create mode 100644 Content.Client/_Backmen/StationAI/UI/AICameraList.xaml create mode 100644 Content.Client/_Backmen/StationAI/UI/AICameraList.xaml.cs create mode 100644 Content.Client/_Backmen/StationAI/UI/AICameraListBoundUserInterface.cs create mode 100644 Content.Client/_Backmen/StationAI/UI/CameraNavMapControl.cs create mode 100644 Content.Server/_Backmen/StationAI/AiEyeMover.cs create mode 100644 Content.Server/_Backmen/StationAI/InnateItemSystem.cs create mode 100644 Content.Server/_Backmen/StationAI/Systems/AICameraSystem.cs create mode 100644 Content.Server/_Backmen/StationAI/Systems/AIEyeSystem.cs create mode 100644 Content.Server/_Backmen/StationAI/Systems/AiEnemySystem.cs create mode 100644 Content.Server/_Backmen/StationAI/Systems/StationAiSystem.cs create mode 100644 Content.Shared/_Backmen/StationAI/Components/AICameraComponent.cs create mode 100644 Content.Shared/_Backmen/StationAI/Components/AIEnemyNTComponent.cs create mode 100644 Content.Shared/_Backmen/StationAI/Components/AIEyeComponent.cs create mode 100644 Content.Shared/_Backmen/StationAI/Components/AIEyePowerComponent.cs create mode 100644 Content.Shared/_Backmen/StationAI/Components/BorgAINTComponent.cs create mode 100644 Content.Shared/_Backmen/StationAI/Components/CanBeBorgNTEnemyComponent.cs create mode 100644 Content.Shared/_Backmen/StationAI/Components/InnateItemComponent.cs create mode 100644 Content.Shared/_Backmen/StationAI/Components/StationAIComponent.cs create mode 100644 Content.Shared/_Backmen/StationAI/Events/StationAIEvents.cs create mode 100644 Content.Shared/_Backmen/StationAI/Systems/AICameraSystem.cs create mode 100644 Content.Shared/_Backmen/StationAI/Systems/AiEnemySystem.cs create mode 100644 Content.Shared/_Backmen/StationAI/Systems/SharedStationAISystem.cs create mode 100644 Content.Shared/_Backmen/StationAI/UI/Key.cs diff --git a/Content.Client/Administration/AdminNameOverlay.cs b/Content.Client/Administration/AdminNameOverlay.cs index c21ba2e32c..0337ddda88 100644 --- a/Content.Client/Administration/AdminNameOverlay.cs +++ b/Content.Client/Administration/AdminNameOverlay.cs @@ -29,6 +29,48 @@ public AdminNameOverlay(AdminSystem system, IEntityManager entityManager, IEyeMa public override OverlaySpace Space => OverlaySpace.ScreenSpace; + // start _Backmen: AI + + private void DrawVisitedOwner(in Content.Shared.Administration.PlayerInfo playerInfo, in OverlayDrawArgs args) + { + var entity = _entityManager.GetEntity(playerInfo.NetEntity); + + // Otherwise the entity can not exist yet + if (entity == null || !_entityManager.EntityExists(entity)) + { + return; + } + + // if not on the same map, continue + if (_entityManager.GetComponent(entity.Value).MapID != _eyeManager.CurrentMap) + { + return; + } + + var aabb = _entityLookup.GetWorldAABB(entity.Value); + + // if not on screen, continue + if (!aabb.Intersects(in args.WorldAABB)) + { + return; + } + + var lineoffset = new Vector2(0f, 11f); + var screenCoordinates = _eyeManager.WorldToScreen(aabb.Center + + new Angle(-_eyeManager.CurrentEye.Rotation).RotateVec( + aabb.TopRight - aabb.Center)) + new Vector2(1f, 7f); + if (playerInfo.Antag) + { + args.ScreenHandle.DrawString(_font, screenCoordinates + (lineoffset * 3), "ANTAG", Color.OrangeRed); + } + args.ScreenHandle.DrawString(_font, screenCoordinates + lineoffset, playerInfo.Username, playerInfo.Connected ? Color.Yellow : Color.White); + args.ScreenHandle.DrawString(_font, screenCoordinates, playerInfo.CharacterName, playerInfo.Connected ? Color.Aquamarine : Color.White); + + args.ScreenHandle.DrawString(_font, screenCoordinates + (lineoffset * 2), "(In REMOTE)", Color.Cyan); + } + + // end _Backmen: AI + protected override void Draw(in OverlayDrawArgs args) { var viewport = args.WorldAABB; diff --git a/Content.Client/_Backmen/StationAI/AiCamOverlay.cs b/Content.Client/_Backmen/StationAI/AiCamOverlay.cs new file mode 100644 index 0000000000..4f06b99686 --- /dev/null +++ b/Content.Client/_Backmen/StationAI/AiCamOverlay.cs @@ -0,0 +1,127 @@ +using System.Numerics; +using Content.Client.Parallax; +using Content.Client.Weather; +using Content.Shared._Backmen.StationAI; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Shared.Enums; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +namespace Content.Client._Backmen.StationAI; + +public sealed class AiCamOverlay : Overlay +{ + [Dependency] private readonly IClyde _clyde = default!; + [Dependency] private readonly IEntityManager _entManager = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly IPrototypeManager _protoManager = default!; + [Dependency] private readonly IPlayerManager _player = default!; + + private readonly ParallaxSystem _parallax; + private readonly SharedTransformSystem _transform; + private readonly SpriteSystem _sprite; + private readonly StationAiSystem _aiSystem; + + + public override OverlaySpace Space => OverlaySpace.WorldSpace; + + private IRenderTexture? _blep; + + private readonly ShaderInstance _shader; + + public AiCamOverlay(ParallaxSystem parallax, SharedTransformSystem transform, SpriteSystem sprite, StationAiSystem aiSystem) + { + ZIndex = ParallaxSystem.ParallaxZIndex + 1; + _parallax = parallax; + _transform = transform; + _sprite = sprite; + _aiSystem = aiSystem; + IoCManager.InjectDependencies(this); + _shader = _protoManager.Index("WorldGradientCircle").InstanceUnique(); + } + + protected override void Draw(in OverlayDrawArgs args) + { + var playerUid = _player.LocalSession?.AttachedEntity; + if (playerUid is not { Valid: true }) + return; + + if (!_entManager.TryGetComponent(playerUid, out var eyeComponent)) + { + return; + } + + var invMatrix = args.Viewport.GetWorldToLocalMatrix(); + + if (_blep?.Texture.Size != args.Viewport.Size) + { + _blep?.Dispose(); + _blep = _clyde.CreateRenderTarget(args.Viewport.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "aicam-stencil"); + } + + if (eyeComponent.Camera != null) + { + DrawRestrictedRange(args, eyeComponent, invMatrix); + } + + args.WorldHandle.UseShader(null); + args.WorldHandle.SetTransform(Matrix3.Identity); + } + + private void DrawRestrictedRange(in OverlayDrawArgs args, AIEyeComponent eyeComponent, Matrix3 invMatrix) + { + var pos = _transform.GetMapCoordinates(eyeComponent.Camera!.Value); + + var worldHandle = args.WorldHandle; + var renderScale = args.Viewport.RenderScale.X; + // TODO: This won't handle non-standard zooms so uhh yeah, not sure how to structure it on the shader side. + var zoom = args.Viewport.Eye?.Zoom ?? Vector2.One; + var length = zoom.X; + var bufferRange = MathF.Min(10f, SharedStationAISystem.CameraEyeRange); + + var pixelCenter = pos.Position;//Vector2.Transform(pos.Position, invMatrix); + // Something something offset? + var vertical = args.Viewport.Size.Y; + + var pixelMaxRange = SharedStationAISystem.CameraEyeRange * renderScale / length * EyeManager.PixelsPerMeter; + var pixelBufferRange = bufferRange * renderScale / length * EyeManager.PixelsPerMeter; + var pixelMinRange = pixelMaxRange - pixelBufferRange; + + _shader.SetParameter("position", new Vector2(pixelCenter.X, vertical - pixelCenter.Y)); + _shader.SetParameter("maxRange", pixelMaxRange); + _shader.SetParameter("minRange", pixelMinRange); + _shader.SetParameter("bufferRange", pixelBufferRange); + _shader.SetParameter("gradient", 0.80f); + + var worldAABB = args.WorldAABB; + var worldBounds = args.WorldBounds; + var position = args.Viewport.Eye?.Position.Position ?? Vector2.Zero; + var localAABB = invMatrix.TransformBox(worldAABB); + + // Cut out the irrelevant bits via stencil + // This is why we don't just use parallax; we might want specific tiles to get drawn over + // particularly for planet maps or stations. + worldHandle.RenderInRenderTarget(_blep!, + () => + { + worldHandle.UseShader(_shader); + worldHandle.DrawRect(localAABB, Color.White); + }, + Color.Transparent); + + worldHandle.SetTransform(Matrix3.Identity); + worldHandle.UseShader(_protoManager.Index("StencilMask").Instance()); + worldHandle.DrawTextureRect(_blep!.Texture, worldBounds); + var curTime = _timing.RealTime; + var sprite = _sprite.GetFrame(new SpriteSpecifier.Texture(new ResPath("/Textures/Parallaxes/aicam.png")), curTime); + + // Draw the fog + worldHandle.UseShader(_protoManager.Index("StencilDraw").Instance()); + _parallax.DrawParallax(worldHandle, worldAABB, sprite, curTime, position, new Vector2(0.5f, 0f)); + } +} diff --git a/Content.Client/_Backmen/StationAI/AiEnemySystem.cs b/Content.Client/_Backmen/StationAI/AiEnemySystem.cs new file mode 100644 index 0000000000..ea19c4230f --- /dev/null +++ b/Content.Client/_Backmen/StationAI/AiEnemySystem.cs @@ -0,0 +1,42 @@ +using Content.Shared._Backmen.StationAI; +using Content.Shared._Backmen.StationAI.Components; +using Content.Shared._Backmen.StationAI.Systems; +using Content.Shared.Ghost; +using Content.Shared.Silicons.Borgs.Components; +using Content.Shared.StatusIcon; +using Content.Shared.StatusIcon.Components; +using Robust.Client.Player; +using Robust.Shared.Prototypes; + +namespace Content.Client._Backmen.StationAI; + +public sealed class AiEnemySystem : SharedAiEnemySystem +{ + [Dependency] private readonly IPlayerManager _player = default!; + [Dependency] private readonly IPrototypeManager _prototype = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(GetIcon); + } + + protected override void ToggleEnemy(EntityUid u, EntityUid target) + { + //noop + } + + [ValidatePrototypeId] + private const string AiEnemyStatus = "AiIconEnemyTarget"; + private void GetIcon(Entity target, ref GetStatusIconsEvent args) + { + var ent = _player.LocalSession?.AttachedEntity ?? EntityUid.Invalid; + + if (!EntityQuery.HasComponent(ent)) + { + return; + } + args.StatusIcons.Add(_prototype.Index(AiEnemyStatus)); + } +} diff --git a/Content.Client/_Backmen/StationAI/AiVisualizerSystem.cs b/Content.Client/_Backmen/StationAI/AiVisualizerSystem.cs new file mode 100644 index 0000000000..460ff2ac65 --- /dev/null +++ b/Content.Client/_Backmen/StationAI/AiVisualizerSystem.cs @@ -0,0 +1,99 @@ +using System.Linq; +using Content.Client.Power; +using Content.Shared._Backmen.StationAI; +using Content.Shared._Backmen.StationAI.UI; +using Content.Shared.Power; +using Robust.Client.GameObjects; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; + +namespace Content.Client._Backmen.StationAI; + +public sealed class AiVisualizerSystem : VisualizerSystem +{ + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly ISerializationManager _serialization = default!; + private EntityQuery _aiEye; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnUpdate); + + _aiEye = GetEntityQuery(); + } + + protected override void OnAppearanceChange(EntityUid uid, StationAIComponent component, ref AppearanceChangeEvent args) + { + if (_aiEye.HasComp(uid)) + { + base.OnAppearanceChange(uid, component, ref args); + return; + } + + UpdateAppearance(uid, component, args.Component, args.Sprite); + } + + private void OnUpdate(Entity ent, ref AfterAutoHandleStateEvent args) + { + if (_aiEye.HasComp(ent)) + { + return; + } + if ( + !ent.Comp.Layers.ContainsKey(ent.Comp.SelectedLayer) || + !TryComp(ent, out var metaDataComponent) || + metaDataComponent.EntityPrototype == null || + !_prototypeManager.TryIndex(metaDataComponent.EntityPrototype.ID, out var proto) || + !proto.TryGetComponent(out var spriteProtoComponent) + ) + { + return; + } + + var spriteComponent = (SpriteComponent) _serialization.CreateCopy(spriteProtoComponent, notNullableOverride: true); + + var layers = ent.Comp.Layers[ent.Comp.SelectedLayer]; + + foreach (var layer in layers) + { + spriteComponent.AddLayer(layer); + } + + AddComp(ent, spriteComponent, true); + + UpdateAppearance(ent, ent, sprite: spriteComponent); + } + + private void UpdateAppearance(EntityUid id, StationAIComponent sign, AppearanceComponent? appearance = null, + SpriteComponent? sprite = null) + { + if (!Resolve(id, ref appearance, ref sprite)) + return; + + if (_aiEye.HasComp(id)) + { + return; + } + + AppearanceSystem.TryGetData(id, PowerDeviceVisuals.Powered, out var powered, appearance); + AppearanceSystem.TryGetData(id, AiVisuals.Dead, out var dead, appearance); + AppearanceSystem.TryGetData(id, AiVisuals.InEye, out var inEye, appearance); + + if (sprite.LayerMapTryGet(AiVisualLayers.NotInEye, out var eyeLayer)) + { + sprite.LayerSetVisible(eyeLayer, powered && !inEye && !dead); + } + + if (sprite.LayerMapTryGet(AiVisualLayers.Dead, out var deadLayer)) + { + sprite.LayerSetVisible(deadLayer, powered && dead); + } + + if (sprite.LayerMapTryGet(PowerDeviceVisualLayers.Powered, out var poweredLayer)) + { + sprite.LayerSetVisible(poweredLayer, powered && !dead); + } + } +} diff --git a/Content.Client/_Backmen/StationAI/StationAiSystem.cs b/Content.Client/_Backmen/StationAI/StationAiSystem.cs new file mode 100644 index 0000000000..1c9b2cc8be --- /dev/null +++ b/Content.Client/_Backmen/StationAI/StationAiSystem.cs @@ -0,0 +1,112 @@ +using System.Linq; +// using Content.Client._Backmen.CartridgeLoader.Cartridges; +using Content.Client._Backmen.StationAI.UI; +using Content.Client.Parallax; +using Content.Client.Storage.Components; +using Content.Client.UserInterface.Fragments; +using Content.Shared._Backmen.StationAI; +using Content.Shared._Backmen.StationAI.UI; +using Content.Shared.Interaction.Events; +using Content.Shared.Item; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Client.UserInterface; + +namespace Content.Client._Backmen.StationAI; + +public sealed class StationAiSystem : SharedStationAISystem +{ + [Dependency] private readonly IPlayerManager _player = default!; + [Dependency] private readonly IOverlayManager _overlay = default!; + private AiCamOverlay _aiCamOverlay = default!; + + public override void Initialize() + { + base.Initialize(); + + _aiCamOverlay = new( + EntityManager.System(), + EntityManager.System(), + EntityManager.System(), + this); + + SubscribeLocalEvent(CanInteraction); + SubscribeLocalEvent(OnCamUpdate); + + _player.LocalPlayerAttached += PlayerOnLocalPlayerAttached; + _player.LocalPlayerDetached += PlayerOnLocalPlayerDetached; + } + + private void PlayerOnLocalPlayerDetached(EntityUid obj) + { + if (_overlay.HasOverlay()) + _overlay.RemoveOverlay(); + } + + private void PlayerOnLocalPlayerAttached(EntityUid obj) + { + if (!HasComp(obj)) + return; + _overlay.AddOverlay(_aiCamOverlay); + } + + public override void Shutdown() + { + base.Shutdown(); + + _player.LocalPlayerAttached -= PlayerOnLocalPlayerAttached; + _player.LocalPlayerDetached -= PlayerOnLocalPlayerDetached; + } + + private void OnCamUpdate(Entity ent, ref AfterAutoHandleStateEvent args) + { + if (!TryComp(ent, out var userInterface) || + !userInterface.OpenInterfaces.TryGetValue(AICameraListUiKey.Key, out var ui1) || + ui1 is not AICameraListBoundUserInterface ui) + { + return; + } + ui.Update(); + } + + private void CanInteraction(Entity ent, ref InteractionAttemptEvent args) + { + var core = ent; + if (TryComp(ent, out var eye)) + { + if (eye.AiCore == null) + { + args.Cancel(); + return; + } + + core = eye.AiCore.Value; + } + + if (!core.Owner.Valid) + { + args.Cancel(); + return; + } + + if (args.Target != null && Transform(core).GridUid != Transform(args.Target.Value).GridUid) + { + args.Cancel(); + return; + } + + + if (HasComp(args.Target)) + { + args.Cancel(); + return; + } + + if (HasComp(args.Target)) + { + args.Cancel(); + return; + } + } +} diff --git a/Content.Client/_Backmen/StationAI/UI/AICameraList.xaml b/Content.Client/_Backmen/StationAI/UI/AICameraList.xaml new file mode 100644 index 0000000000..45e391098e --- /dev/null +++ b/Content.Client/_Backmen/StationAI/UI/AICameraList.xaml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Content.Client/_Backmen/StationAI/UI/AICameraList.xaml.cs b/Content.Client/_Backmen/StationAI/UI/AICameraList.xaml.cs new file mode 100644 index 0000000000..892920b2aa --- /dev/null +++ b/Content.Client/_Backmen/StationAI/UI/AICameraList.xaml.cs @@ -0,0 +1,73 @@ +using System.Linq; +using System.Net.Mime; +using System.Numerics; +using Content.Client.Pinpointer.UI; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.XAML; +using Content.Shared._Backmen.StationAI; +using Robust.Client.UserInterface.Controls; +using Content.Client.UserInterface.Controls; +using Robust.Client.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Utility; + +namespace Content.Client._Backmen.StationAI.UI; + +[GenerateTypedNameReferences] +public sealed partial class AICameraList : FancyWindow +{ + [Dependency] private readonly IEntityManager _entityManager = default!; + public event Action? WarpToCamera; + + private readonly EntityUid _owner; + public AICameraList(EntityUid? mapUid, EntityUid? trackedEntity) + { + _owner = trackedEntity ?? EntityUid.Invalid; + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + + NavMapScreen.MapUid = mapUid; + + var msg = new FormattedMessage(); + if (_entityManager.TryGetComponent(mapUid, out var metadata)) + { + msg.AddText(metadata.EntityName); + } + else + { + msg.AddText(Loc.GetString("ai-warp-menu-no-cameras")); + } + StationName.SetMessage(msg); + + NavMapScreen.TrackedEntitySelectedAction += ItemSelected; + UpdateCameras(); + + + + + } + + public void UpdateCameras() + { + if (!_entityManager.TryGetComponent(_owner, out var eyeComponent)) + return; + + NavMapScreen.TrackedEntities.Clear(); + + foreach (var (camera,pos) in eyeComponent.FollowsCameras) + { + var texture = _entityManager.System().Frame0(new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png"))); + var isSame = _entityManager.TryGetEntity(camera, out var camUid) && camUid == eyeComponent.Camera; + var blip = new NavMapBlip(_entityManager.GetCoordinates(pos), texture, isSame ? Color.DarkRed : Color.Cyan, isSame, true); + NavMapScreen.TrackedEntities[camera] = blip; + } + + NavMapScreen.ForceNavMapUpdate(); + } + + private void ItemSelected(NetEntity? obj) + { + if(obj != null) + WarpToCamera?.Invoke(obj.Value); + } +} diff --git a/Content.Client/_Backmen/StationAI/UI/AICameraListBoundUserInterface.cs b/Content.Client/_Backmen/StationAI/UI/AICameraListBoundUserInterface.cs new file mode 100644 index 0000000000..0738c0c6f4 --- /dev/null +++ b/Content.Client/_Backmen/StationAI/UI/AICameraListBoundUserInterface.cs @@ -0,0 +1,51 @@ +using Content.Shared._Backmen.StationAI; +using Content.Shared._Backmen.StationAI.Events; +using JetBrains.Annotations; + +namespace Content.Client._Backmen.StationAI.UI; + +/// +/// Initializes a and updates it when new server messages are received. +/// +[UsedImplicitly] +public sealed class AICameraListBoundUserInterface : BoundUserInterface +{ + public AICameraList? Window; + + public AICameraListBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) + { + } + + protected override void Open() + { + base.Open(); + Window?.Close(); + EntityUid? gridUid = null; + + if (EntMan.TryGetComponent(Owner, out var xform)) + { + gridUid = xform.GridUid; + } + + Window = new AICameraList(gridUid, Owner); + Window.OpenCentered(); + Window.OnClose += Close; + Window.WarpToCamera += WindowOnWarpToCamera; + } + + private void WindowOnWarpToCamera(NetEntity obj) + { + SendMessage(new EyeMoveToCam { Entity = EntMan.GetNetEntity(Owner), Uid = obj }); + } + + public void Update() + { + Window?.UpdateCameras(); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + Window?.Dispose(); + } +} diff --git a/Content.Client/_Backmen/StationAI/UI/CameraNavMapControl.cs b/Content.Client/_Backmen/StationAI/UI/CameraNavMapControl.cs new file mode 100644 index 0000000000..714bc57141 --- /dev/null +++ b/Content.Client/_Backmen/StationAI/UI/CameraNavMapControl.cs @@ -0,0 +1,17 @@ +using Content.Client.Pinpointer.UI; +using Content.Shared._Backmen.StationAI; +using Robust.Client.Graphics; +using Robust.Shared.Map.Components; + +namespace Content.Client._Backmen.StationAI.UI; + +public sealed class CameraNavMapControl : NavMapControl +{ + public CameraNavMapControl() : base() + { + // Set colors + TileColor = new Color(30, 57, 67); + WallColor = new Color(102, 164, 217); + BackgroundColor = Color.FromSrgb(TileColor.WithAlpha(BackgroundOpacity)); + } +} diff --git a/Content.IntegrationTests/Tests/PostMapInitTest.cs b/Content.IntegrationTests/Tests/PostMapInitTest.cs index 07a6ddd89f..6585e39e9c 100644 --- a/Content.IntegrationTests/Tests/PostMapInitTest.cs +++ b/Content.IntegrationTests/Tests/PostMapInitTest.cs @@ -238,7 +238,8 @@ await server.WaitPost(() => // This is done inside gamemap test because loading the map takes ages and we already have it. var jobList = entManager.GetComponent(station).RoundStartJobList .Where(x => x.Value != 0) - .Select(x => x.Key); + .Select(x => x.Key) + .Where(x => x != "SAI"); var spawnPoints = entManager.EntityQuery() .Where(spawnpoint => spawnpoint.SpawnType == SpawnPointType.Job) .Select(spawnpoint => spawnpoint.Job.ID) diff --git a/Content.Server/Administration/Systems/AdminSystem.cs b/Content.Server/Administration/Systems/AdminSystem.cs index 82bbe6e266..7c1a5847b2 100644 --- a/Content.Server/Administration/Systems/AdminSystem.cs +++ b/Content.Server/Administration/Systems/AdminSystem.cs @@ -234,6 +234,11 @@ private PlayerInfo GetPlayerInfo(SessionData data, ICommonSession? session) overallPlaytime = playTime; } + // start _Backmen: AI + var parentAttachedEntity = CompOrNull( + CompOrNull(session?.AttachedEntity)?.MindId)?.OwnedEntity; + // end _Backmen: AI + return new PlayerInfo(name, entityName, identityName, startingRole, antag, GetNetEntity(session?.AttachedEntity), data.UserId, connected, _roundActivePlayers.Contains(data.UserId), overallPlaytime); } diff --git a/Content.Server/Communications/CommunicationsConsoleSystem.cs b/Content.Server/Communications/CommunicationsConsoleSystem.cs index b68a952105..0efe7e2c5d 100644 --- a/Content.Server/Communications/CommunicationsConsoleSystem.cs +++ b/Content.Server/Communications/CommunicationsConsoleSystem.cs @@ -27,6 +27,7 @@ using Content.Server.Announcements.Systems; using Robust.Shared.Player; using Content.Server.Station.Components; +using Content.Shared._Backmen.StationAI; namespace Content.Server.Communications { @@ -262,6 +263,11 @@ private void OnAnnounceMessage(EntityUid uid, CommunicationsConsoleComponent com { author = $"{id.Comp.FullName} ({CultureInfo.CurrentCulture.TextInfo.ToTitleCase(id.Comp.JobTitle ?? string.Empty)})".Trim(); } + + if (HasComp(mob)) + { + author = MetaData(mob).EntityName; + } } comp.AnnouncementCooldownRemaining = comp.Delay; diff --git a/Content.Server/_Backmen/StationAI/AiEyeMover.cs b/Content.Server/_Backmen/StationAI/AiEyeMover.cs new file mode 100644 index 0000000000..7de40a0a97 --- /dev/null +++ b/Content.Server/_Backmen/StationAI/AiEyeMover.cs @@ -0,0 +1,93 @@ +using System.Threading; +using System.Threading.Tasks; +using Content.Server._Backmen.StationAI.Systems; +using Content.Server.SurveillanceCamera; +using Content.Shared._Backmen.StationAI; +using Content.Shared.Tag; +using Robust.Server.GameObjects; +using Robust.Shared.CPUJob.JobQueues; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Timing; + +namespace Content.Server._Backmen.StationAI; + +public sealed class AiEyeMover : Job +{ + private readonly AICameraSystem _cameraSystem; + private readonly EntityLookupSystem _lookup; + private readonly SharedTransformSystem _transform; + private readonly EntityManager _entityManager; + private readonly MapSystem _map; + private readonly TagSystem _tag; + + public AiEyeMover(EntityManager entityManager, AICameraSystem cameraSystem, EntityLookupSystem lookup, SharedTransformSystem transform, MapSystem map, TagSystem tag, double maxTime, CancellationToken cancellation = default) : base(maxTime, cancellation) + { + _cameraSystem = cameraSystem; + _lookup = lookup; + _transform = transform; + _entityManager = entityManager; + _map = map; + _tag = tag; + } + + public Entity Eye { get; set; } + public EntityCoordinates NewPosition { get; set; } + + + private readonly HashSet> _cameraComponents = new(); + + protected override async Task Process() + { + try + { + if (!Eye.Comp.AiCore.HasValue) + { + _entityManager.QueueDeleteEntity(Eye); + return null; + } + + if (!NewPosition.EntityId.IsValid()) + { + _entityManager.QueueDeleteEntity(Eye); + return null; + } + + var core = Eye.Comp.AiCore.Value; + + + var gridUid = _transform.GetParentUid(NewPosition.EntityId); + + if ( + //gridUid == null || //эта строка всегда false + _transform.GetParentUid(_transform.GetMoverCoordinates(core).EntityId) != gridUid || + !_entityManager.TryGetComponent(gridUid, out var grid)) + { + _entityManager.QueueDeleteEntity(Eye); + return null; + } + + var mapPos = _transform.ToMapCoordinates(NewPosition); + + // cache + if (_cameraSystem.IsCameraActive(Eye, mapPos)) + return null; + + foreach (var uid in _map.GetAnchoredEntities(gridUid, grid, NewPosition)) + { + if (_tag.HasAnyTag(uid, "Wall", "Window", "Airlock", "GlassAirlock")) + return null; + } + + await WaitAsyncTask(Task.Run(() => + _lookup.GetEntitiesInRange(mapPos, SharedStationAISystem.CameraEyeRange, _cameraComponents, LookupFlags.Sensors))); + + _cameraSystem.HandleMove(Eye, mapPos, _cameraComponents); + } + finally + { + Eye.Comp.IsProcessingMoveEvent = false; + } + return null; + } +} diff --git a/Content.Server/_Backmen/StationAI/InnateItemSystem.cs b/Content.Server/_Backmen/StationAI/InnateItemSystem.cs new file mode 100644 index 0000000000..2fc0cbae2a --- /dev/null +++ b/Content.Server/_Backmen/StationAI/InnateItemSystem.cs @@ -0,0 +1,117 @@ +using Content.Shared.Actions; +using Content.Shared._Backmen.StationAI; +using Content.Shared._Backmen.StationAI.Events; +using Content.Shared.Interaction; +using Content.Shared.Mind.Components; +using Content.Shared.Mobs.Systems; +using Content.Shared.Tag; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Server._Backmen.StationAI; + +public sealed partial class InnateItemSystem : EntitySystem +{ + [Dependency] private readonly SharedActionsSystem _actionsSystem = default!; + [Dependency] private readonly TagSystem _tagSystem = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMindAdded); + SubscribeLocalEvent(OnPlayerAttached); + SubscribeLocalEvent(StartAfterInteract); + SubscribeLocalEvent(StartBeforeInteract); + } + + private void OnPlayerAttached(Entity ent, ref PlayerAttachedEvent args) + { + if (!ent.Comp.AlreadyInitialized) + RefreshItems(ent, ent); + + ent.Comp.AlreadyInitialized = true; + } + + private void OnMindAdded(EntityUid uid, InnateItemComponent component, MindAddedMessage args) + { + if (!component.AlreadyInitialized) + RefreshItems(uid, component); + + component.AlreadyInitialized = true; + } + + private void RefreshItems(EntityUid uid, InnateItemComponent component) + { + int priority = component.StartingPriority ?? 0; + foreach (var (key, sourceItem) in component.Slots) + { + /*if (_tagSystem.HasTag(sourceItem, "NoAction")) + continue;*/ + + var actionId = component.Actions.FirstOrNull(x => x.Key == key)?.Value; + _actionsSystem.AddAction(uid, ref actionId, sourceItem); + component.Actions[key] = actionId!.Value; + if (_actionsSystem.TryGetActionData(actionId, out var action)) + { + action.Priority = priority; + Dirty(actionId.Value, action); + } + priority--; + } + } + + private void StartAfterInteract(EntityUid uid, InnateItemComponent component, InnateAfterInteractActionEvent args) + { + EnsureItem(uid, component, args.Item); + if (!component.Items.ContainsKey(args.Item)) + return; + var ev = new AfterInteractEvent(args.Performer, component.Items[args.Item], args.Target, Transform(args.Target).Coordinates, true); + RaiseLocalEvent(component.Items[args.Item], ev, false); + } + + private void StartBeforeInteract(EntityUid uid, InnateItemComponent component, InnateBeforeInteractActionEvent args) + { + var tarPos = Transform(args.Target); + + if (TryComp(uid, out var aiEyeComponent)) + { + if (!aiEyeComponent.AiCore.HasValue || TerminatingOrDeleted(aiEyeComponent.AiCore.Value)) + { + return; + } + if (Transform(aiEyeComponent.AiCore.Value).GridUid != tarPos.GridUid) + { + return; + } + if (_mobState.IsDead(aiEyeComponent.AiCore.Value)) + return; + } + else + { + if (_mobState.IsDead(uid)) + return; + } + + EnsureItem(uid, component, args.Item); + if (!component.Items.ContainsKey(args.Item)) + return; + var ev = new BeforeRangedInteractEvent(args.Performer, component.Items[args.Item], args.Target, tarPos.Coordinates, true); + RaiseLocalEvent(component.Items[args.Item], ev, false); + } + + private void EnsureItem(EntityUid uid, InnateItemComponent component, EntProtoId args) + { + if (!component.Items.ContainsKey(args) && args != "") + { + if (!TrySpawnInContainer(args, uid, "tools", out var item)) + { + Logger.Error($"Невозможно заспавнить tools {args}"); + return; + } + component.Items[args] = item.Value; + } + } +} diff --git a/Content.Server/_Backmen/StationAI/Systems/AICameraSystem.cs b/Content.Server/_Backmen/StationAI/Systems/AICameraSystem.cs new file mode 100644 index 0000000000..816b731f57 --- /dev/null +++ b/Content.Server/_Backmen/StationAI/Systems/AICameraSystem.cs @@ -0,0 +1,233 @@ +using System.Numerics; +using Content.Server.Interaction; +using Content.Server.Lightning; +using Content.Server.Popups; +using Content.Server.SurveillanceCamera; +using Content.Server.Weapons.Ranged.Systems; +using Content.Shared._Backmen.StationAI; +using Content.Shared.Eye.Blinding.Components; +using Content.Shared.Popups; +using Content.Shared.Tag; +using Content.Shared.Traits.Assorted; +using Content.Shared.Weapons.Ranged; +using Content.Shared.Weapons.Ranged.Components; +using Robust.Server.GameObjects; +using Robust.Shared.Audio.Systems; +using Robust.Shared.CPUJob.JobQueues.Queues; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; + +namespace Content.Server._Backmen.StationAI.Systems; + +public sealed class AICameraSystem : EntitySystem +{ + [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly InteractionSystem _interaction = default!; + [Dependency] private readonly SurveillanceCameraSystem _cameraSystem = default!; + [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly GunSystem _gun = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly MapSystem _map = default!; + [Dependency] private readonly TagSystem _tag = default!; + + + + private const double MoverJobTime = 0.005; + private readonly JobQueue _moveJobQueue = new(MoverJobTime); + + + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(HandleCameraStartup); + + SubscribeLocalEvent(OnEyeMove); + SubscribeLocalEvent(OnActiveCameraDisable); + SubscribeLocalEvent(OnRemove); + SubscribeLocalEvent(OnMoveToCam); + SubscribeLocalEvent(OnShootCam); + } + + private void OnRemove(Entity ent, ref EntityTerminatingEvent args) + { + OnCameraOffline(ent); + } + + [ValidatePrototypeId] + private const string LPPBulletDisablerBorg = "LPPBulletDisablerBorg"; + + private void OnShootCam(Entity ent, ref AIEyeCampShootActionEvent args) + { + if (!ent.Comp.AiCore.HasValue || ent.Comp.Camera == null) + return; + + var camMapPos = _transform.GetMapCoordinates(ent.Comp.Camera.Value); + var camPos = Transform(ent.Comp.Camera.Value).Coordinates; + if (args.Target.GetGridUid(EntityManager) != Transform(ent.Comp.AiCore.Value).GridUid) + return; + + args.Handled = true; + var targetPos = args.Target.ToMap(EntityManager, _transform); + + var ammo = Spawn(LPPBulletDisablerBorg, camPos); + _gun.ShootProjectile(ammo, targetPos.Position - camMapPos.Position, Vector2.One, ent.Comp.Camera.Value, args.Performer); + _audio.PlayPvs("/Audio/LPP/Weapons/AILaser.ogg", ent.Comp.Camera.Value); + } + + private void OnMoveToCam(Entity ent, ref EyeMoveToCam args) + { + if (!TryGetEntity(args.Uid, out var uid) || !ent.Comp.AiCore.HasValue) + return; + var camPos = Transform(uid.Value); + if (Transform(uid.Value).GridUid != Transform(ent.Comp.AiCore.Value).GridUid) + return; + + if (!TryComp(uid, out var camera)) + return; + + if (!camera.Active) + { + _popup.PopupCursor("камера не работает!", ent, PopupType.LargeCaution); + return; + } + _transform.SetCoordinates(ent, camPos.Coordinates); + _transform.AttachToGridOrMap(ent); + } + + private void OnActiveCameraDisable(Entity ent, ref SurveillanceCameraDeactivateEvent args) + { + OnCameraOffline(ent); + } + + private void OnCameraOffline(Entity ent) + { + foreach (var viewer in ent.Comp.ActiveViewers) + { + if (TerminatingOrDeleted(viewer) || !TryComp(viewer, out var aiEyeComponent)) + { + continue; + } + + RemoveActiveCamera((viewer, aiEyeComponent)); + EnsureComp(viewer); + } + ent.Comp.ActiveViewers.Clear(); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + _moveJobQueue.Process(); + } + + private void OnEyeMove(Entity ent, ref MoveEvent args) + { + if (ent.Comp.IsProcessingMoveEvent) + return; + + ent.Comp.IsProcessingMoveEvent = true; + + var job = new AiEyeMover(EntityManager, this, _lookup, _transform, _map, _tag, MoverJobTime) + { + Eye = ent, + //OldPosition = args.OldPosition, + NewPosition = args.NewPosition + }; + + _moveJobQueue.EnqueueJob(job); + } + + public bool IsCameraActive(Entity eye, MapCoordinates eyeCoordinates) + { + if (!eye.Comp.Camera.HasValue) + return false; + + + return _interaction.InRangeUnobstructed(eyeCoordinates, eye.Comp.Camera.Value, SharedStationAISystem.CameraEyeRange); + } + + public void RemoveActiveCamera(Entity eye) + { + if (!eye.Comp.Camera.HasValue) + { + return; + } + + if (!TryComp(eye.Comp.Camera.Value, out var camera)) + { + return; + } + var v = camera.ActiveViewers; + v.Remove(eye); + _cameraSystem.SetActive(eye.Comp.Camera.Value, false, camera); + EnsureComp(eye.Comp.Camera.Value).ActiveViewers.Remove(eye); + eye.Comp.Camera = null; + Dirty(eye, eye.Comp); + } + private void ChangeActiveCamera(Entity eye, EntityUid camUid, SurveillanceCameraComponent? cameraComponent = null) + { + if (!Resolve(camUid, ref cameraComponent, false)) + { + return; + } + + if (eye.Comp.Camera.HasValue && eye.Comp.Camera != camUid) + { + RemoveActiveCamera(eye); + } + + if (eye.Comp.Camera.HasValue) + { + return; + } + + var v = cameraComponent.ActiveViewers; + + eye.Comp.Camera = camUid; + Dirty(eye, eye.Comp); + v.Add(eye); + _cameraSystem.SetActive(camUid, true, cameraComponent); + EnsureComp(camUid).ActiveViewers.Add(eye); + } + + public void HandleMove(Entity eye, MapCoordinates eyeCoordinates, HashSet> cameraComponents) + { + foreach (var cameraComponent in cameraComponents) + { + var v = cameraComponent.Comp.ActiveViewers; + if (!cameraComponent.Comp.Active) + continue; + + if (!_interaction.InRangeUnobstructed(eyeCoordinates, cameraComponent, SharedStationAISystem.CameraEyeRange)) + continue; + + RemCompDeferred(eye); + ChangeActiveCamera(eye, cameraComponent, cameraComponent.Comp); + return; + } + + if (eye.Comp.Camera.HasValue) + { + RemoveActiveCamera(eye); + } + + if (eye.Owner is { Valid: true }) + EnsureComp(eye); + } + + private void HandleCameraStartup(EntityUid uid, AICameraComponent component, ComponentStartup args) + { + if (!TryComp(uid, out var camera)) + return; + + component.CameraName = camera.CameraId; + component.CameraCategories = camera.AvailableNetworks; + component.Enabled = true; + + Dirty(uid, component); + } +} diff --git a/Content.Server/_Backmen/StationAI/Systems/AIEyeSystem.cs b/Content.Server/_Backmen/StationAI/Systems/AIEyeSystem.cs new file mode 100644 index 0000000000..79f9a9daa2 --- /dev/null +++ b/Content.Server/_Backmen/StationAI/Systems/AIEyeSystem.cs @@ -0,0 +1,264 @@ +using Content.Server._Backmen.StationAI.Systems; +using Content.Server.Mind; +using Content.Server.Power.Components; +using Content.Server.Speech.Components; +using Content.Server.SurveillanceCamera; +using Content.Shared.Actions; +using Content.Shared._Backmen.StationAI; +using Content.Shared._Backmen.StationAI.UI; +using Content.Shared.Eye; +using Content.Shared.Mind; +using Content.Shared.Mind.Components; +using Robust.Shared.Prototypes; +using Content.Shared.Mobs.Systems; +using Content.Shared.Random.Helpers; +using Content.Shared.Silicons.Laws; +using Content.Shared.Silicons.Laws.Components; +using Robust.Server.GameObjects; +using Robust.Shared.Player; + +namespace Content.Server._Backmen.StationAI; + +public sealed class AIEyePowerSystem : EntitySystem +{ + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly MindSystem _mindSystem = default!; + [Dependency] private readonly TransformSystem _transformSystem = default!; + + [Dependency] private readonly VisibilitySystem _visibilitySystem = default!; + [Dependency] private readonly MetaDataSystem _metaDataSystem = default!; + [Dependency] private readonly SharedEyeSystem _sharedEyeSystem = default!; + + [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; + + [Dependency] private readonly AICameraSystem _cameraSystem = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnPowerUsed); + + SubscribeLocalEvent(OnPowerReturnUsed); + SubscribeLocalEvent(OnEyeRemove); + + SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnMindRemoved); + SubscribeLocalEvent(OnMindRemoved2); + + SubscribeLocalEvent(OnGetLaws); + + SubscribeLocalEvent(OnPowerChange); + + SubscribeLocalEvent(OnOpenUiCams); + } + + private void OnOpenUiCams(Entity ent, ref AIEyeCampActionEvent args) + { + if (!TryComp(ent, out var actorComponent)) + { + return; + } + + _uiSystem.TryToggleUi(ent.Owner, AICameraListUiKey.Key, actorComponent.PlayerSession); + } + + private void OnEyeRemove(Entity ent, ref ComponentShutdown args) + { + if (_uiSystem.TryCloseAll(ent.Owner)) + _cameraSystem.RemoveActiveCamera(ent); + } + + private void OnPowerChange(EntityUid uid, StationAIComponent component, ref PowerChangedEvent args) + { + if (HasComp(uid) || TerminatingOrDeleted(uid)) + { + return; + } + + foreach (var (actionId, action) in _actions.GetActions(uid)) + { + _actions.SetEnabled(actionId, args.Powered); + } + + if (!args.Powered && component.ActiveEye.IsValid()) + { + QueueDel(component.ActiveEye); + component.ActiveEye = EntityUid.Invalid; + } + + if (!args.Powered) + { + EnsureComp(uid).Accent = "dwarf"; + _uiSystem.TryCloseAll(uid); + } + else + { + RemCompDeferred(uid); + } + } + + [ValidatePrototypeId] + private const string DefaultAIRule = "Asimovpp"; + private void OnGetLaws(Entity ent, ref GetSiliconLawsEvent args) + { + if (ent.Comp.SelectedLaw == null) + { + var selectedLaw = _prototypeManager.Index(ent.Comp.LawsId).Pick(); + if (_prototypeManager.TryIndex(selectedLaw, out var newLaw)) + { + ent.Comp.SelectedLaw = newLaw; + } + else + { + ent.Comp.SelectedLaw = _prototypeManager.Index(DefaultAIRule); + } + } + + foreach (var law in ent.Comp.SelectedLaw.Laws) + { + args.Laws.Laws.Add(_prototypeManager.Index(law)); + } + + args.Handled = true; + } + + private void OnInit(EntityUid uid, AIEyePowerComponent component, ComponentInit args) + { + if (!HasComp(uid)) + return; + + _actions.AddAction(uid, ref component.EyePowerAction, component.PrototypeAction); + } + + private void OnShutdown(EntityUid uid, AIEyePowerComponent component, ComponentShutdown args) + { + if (!HasComp(uid)) + return; + + if (component.EyePowerAction != null) + _actions.RemoveAction(uid, component.EyePowerAction); + } + + private void OnPowerReturnUsed(EntityUid uid, AIEyeComponent component, AIEyePowerReturnActionEvent args) + { + if ( + !TryComp(args.Performer, out var mindId) || + mindId!.MindId == null || + !TryComp(mindId.MindId.Value, out var mind) + ) + return; + + ClearState(args.Performer); + args.Handled = true; + } + + private void OnPowerUsed(EntityUid uid, AIEyePowerComponent component, AIEyePowerActionEvent args) + { + if (_mobState.IsDead(args.Performer)) + return; + + if (!_mindSystem.TryGetMind(args.Performer, out var mindId, out var mind)) + return; + + if (!TryComp(uid, out var ai)) + return; + + var coords = Transform(uid).Coordinates; + var projection = EntityManager.CreateEntityUninitialized(component.Prototype, coords); + ai.ActiveEye = projection; + EnsureComp(projection).AiCore = (uid, ai); + var eyeStation = EnsureComp(projection); + eyeStation.SelectedLaw = ai.SelectedLaw; + eyeStation.SelectedLayer = ai.SelectedLayer; + EnsureComp(projection); + var core = MetaData(uid); + // Consistent name + _metaDataSystem.SetEntityName(projection, core.EntityName != "" ? core.EntityName : "Invalid AI"); + EntityManager.InitializeAndStartEntity(projection, coords.GetMapId(EntityManager)); + + _transformSystem.AttachToGridOrMap(projection); + + _appearance.SetData(uid, AiVisuals.InEye, true); + _mindSystem.Visit(mindId, projection, mind); // Mind swap + + args.Handled = true; + } + + + private void OnStartup(EntityUid uid, AIEyeComponent component, ComponentStartup args) + { + if (!HasComp(uid) || + !TryComp(uid, out var visibility) || + !TryComp(uid, out var eye)) + return; + + _sharedEyeSystem.SetVisibilityMask(uid, eye.VisibilityMask | (int) VisibilityFlags.AIEye, eye); + _visibilitySystem.AddLayer((uid, visibility), (int) VisibilityFlags.AIEye); + _actions.AddAction(uid, ref component.ReturnActionUid, component.ReturnAction); + _actions.AddAction(uid, ref component.CamListUid, component.CamListAction); + _actions.AddAction(uid, ref component.CamShootUid, component.CamShootAction); + + + + var pos = Transform(uid).GridUid; + var cams = EntityQueryEnumerator(); + while (cams.MoveNext(out var camUid, out var cam, out var transformComponent)) + { + if (transformComponent.GridUid != pos) + continue; + component.FollowsCameras.Add((GetNetEntity(camUid), GetNetCoordinates(transformComponent.Coordinates))); + } + Dirty(uid, component); + } + + private void OnMindRemoved(EntityUid uid, AIEyeComponent component, MindRemovedMessage args) + { + RemoveMind(uid, component); + } + private void OnMindRemoved2(EntityUid uid, AIEyeComponent component, MindUnvisitedMessage args) + { + RemoveMind(uid, component); + } + + private void RemoveMind(EntityUid uid, AIEyeComponent component) // поскольку нет разницы по какой причине это вызвано, то код вынесен + { + if (_uiSystem.TryCloseAll(uid)) + { + QueueDel(uid); + if (component.AiCore.HasValue) + OnReturnToCore(component.AiCore.Value); + } + } + + private void ClearState(EntityUid uid, AIEyeComponent? component = null) + { + if (!Resolve(uid, ref component)) + { + return; + } + + QueueDel(uid); + if (!component.AiCore.HasValue) + return; + + if (_mindSystem.TryGetMind(component.AiCore.Value, out var mindId, out var mind)) + { + _mindSystem.UnVisit(mindId, mind); + } + + OnReturnToCore(component.AiCore.Value); + } + + private void OnReturnToCore(Entity ent) + { + ent.Comp.ActiveEye = EntityUid.Invalid; + _uiSystem.TryCloseAll(ent.Owner); + _appearance.SetData(ent, AiVisuals.InEye, false); + } +} diff --git a/Content.Server/_Backmen/StationAI/Systems/AiEnemySystem.cs b/Content.Server/_Backmen/StationAI/Systems/AiEnemySystem.cs new file mode 100644 index 0000000000..3c41ac6694 --- /dev/null +++ b/Content.Server/_Backmen/StationAI/Systems/AiEnemySystem.cs @@ -0,0 +1,80 @@ +using Content.Server.NPC.Components; +using Content.Server.NPC.Systems; +using Content.Shared._Backmen.StationAI; +using Content.Shared._Backmen.StationAI.Components; +using Content.Shared._Backmen.StationAI.Systems; +using Content.Shared.Examine; +using Content.Shared.Mobs.Systems; +using Content.Shared.NPC; +using Content.Shared.Verbs; + +namespace Content.Server._Backmen.StationAI.Systems; + +public sealed class AiEnemySystem : SharedAiEnemySystem +{ + [Dependency] private readonly NpcFactionSystem _faction = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnAdd); + SubscribeLocalEvent(OnRemove); + } + + protected override void ToggleEnemy(EntityUid u, EntityUid target) + { + if (!EntityQuery.HasComponent(u)) + return; + if (!HasComp(u)) + return; + + if (_mobState.IsDead(u)) + return; + + var core = u; + if (TryComp(core, out var eyeComponent)) + { + if (eyeComponent.AiCore == null) + return; + core = eyeComponent.AiCore.Value.Owner; + } + + if (!core.Valid) + { + return; + } + + var xform = Transform(core); + if (xform.GridUid != Transform(target).GridUid || !xform.Anchored) + { + return; + } + + if (HasComp(target)) + RemCompDeferred(target); + else + EnsureComp(target).Source = core; + } + + [ValidatePrototypeId] + private const string AiEnemyFaction = "AiEnemy"; + + private void OnRemove(Entity ent, ref ComponentShutdown args) + { + if (TryComp(ent, out var npcFactionMemberComponent)) + { + _faction.RemoveFaction(ent.Owner, AiEnemyFaction); + } + + } + + private void OnAdd(Entity ent, ref MapInitEvent args) + { + if (TryComp(ent, out var npcFactionMemberComponent)) + { + _faction.AddFaction(ent.Owner, AiEnemyFaction); + } + } +} diff --git a/Content.Server/_Backmen/StationAI/Systems/StationAiSystem.cs b/Content.Server/_Backmen/StationAI/Systems/StationAiSystem.cs new file mode 100644 index 0000000000..fbac277113 --- /dev/null +++ b/Content.Server/_Backmen/StationAI/Systems/StationAiSystem.cs @@ -0,0 +1,383 @@ +using System.Linq; +using Content.Server.Administration; +using Content.Server.Mind; +using Content.Server.Nuke; +using Content.Server.Popups; +using Content.Server.Power.Components; +using Content.Server.Roles; +using Content.Server.Storage.Components; +using Content.Shared.Actions; +using Content.Shared.Administration; +using Content.Shared._Backmen.StationAI; +using Content.Shared._Backmen.StationAI.Events; +using Content.Shared._Backmen.StationAI.UI; +using Content.Shared.Destructible; +using Content.Shared.Hands.Components; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Interaction.Events; +using Content.Shared.Item; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Systems; +using Content.Shared.Nuke; +using Content.Shared.Roles; +using Content.Shared.Roles.Jobs; +using Content.Shared.Silicons.Laws; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Console; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Server._Backmen.StationAI.Systems; + +public sealed class StationAiSystem : SharedStationAISystem +{ + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly SharedHandsSystem _hands = default!; + + [Dependency] private readonly PopupSystem _popup = default!; + + [Dependency] private readonly NukeSystem _nuke = default!; + [Dependency] private readonly RoleSystem _role = default!; + [Dependency] private readonly MindSystem _mind = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly SharedAudioSystem _audioSystem = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly IRobustRandom _robust = default!; + [Dependency] private readonly IConsoleHost _console = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnTerminated); + + SubscribeLocalEvent(CanInteraction); + + SubscribeLocalEvent(OnToggleNuke); + SubscribeLocalEvent(OnRoleUpdateIfNeed); + + //SubscribeLocalEvent(OnDestruction); + SubscribeLocalEvent(OnMobStateChanged); + + _console.RegisterCommand("aiset_screen", AiSetScreen, AiSetScreenCompletion); + _console.RegisterCommand("aiset_law", AiSetLaw, AiSetLawCompletion); + } + + #region aiset_law + + private CompletionResult AiSetLawCompletion(IConsoleShell shell, string[] args) + { + return args.Length switch + { + 1 => CompletionResult.FromHintOptions( + StationAiComponents(args[^1]), + "ИИ"), + 2 => CompletionResult.FromHintOptions( + CompletionHelper.PrototypeIDs(), + "Закон"), + _ => CompletionResult.Empty + }; + } + + [AdminCommand(AdminFlags.Fun)] + private void AiSetLaw(IConsoleShell shell, string argstr, string[] args) + { + if (args.Length != 2) + { + shell.WriteError(Loc.GetString("shell-wrong-arguments-number")); + return; + } + + if (!NetEntity.TryParse(args[0], out var netEntity) || !TryGetEntity(netEntity, out var ent)) + { + shell.WriteError(Loc.GetString("shell-entity-uid-must-be-number")); + return; + } + + if (!TryComp(ent, out var stationAiComponent) || + HasComp(ent) || + !_prototypeManager.TryIndex(args[1], out var lawsetPrototype) + ) + return; + + stationAiComponent.SelectedLaw = lawsetPrototype; + + if (stationAiComponent.ActiveEye.IsValid() && !TerminatingOrDeleted(stationAiComponent.ActiveEye)) + EnsureComp(stationAiComponent.ActiveEye).SelectedLaw = lawsetPrototype; + + shell.WriteLine("OK!"); + } + + #endregion + + #region aiset_screen + + private CompletionResult AiSetScreenCompletion(IConsoleShell shell, string[] args) + { + return args.Length switch + { + 1 => CompletionResult.FromHintOptions( + StationAiComponents(args[^1]), + "ИИ"), + 2 => CompletionResult.FromHint("Экран ии"), + _ => CompletionResult.Empty + }; + } + + [AdminCommand(AdminFlags.Fun)] + private void AiSetScreen(IConsoleShell shell, string argstr, string[] args) + { + if (args.Length < 1) + { + shell.WriteError(Loc.GetString("shell-wrong-arguments-number")); + return; + } + + if (!NetEntity.TryParse(args[0], out var netEntity) || !TryGetEntity(netEntity, out var ent)) + { + shell.WriteError(Loc.GetString("shell-entity-uid-must-be-number")); + return; + } + + if (!TryComp(ent, out var stationAiComponent) || + HasComp(ent) + ) + return; + + if (args.Length != 2 || !stationAiComponent.Layers.ContainsKey(args[1])) + { + shell.WriteError("Текущий экран: " + stationAiComponent.SelectedLayer); + shell.WriteError("Доступные экраны: " + string.Join(", ", stationAiComponent.Layers.Keys)); + return; + } + + stationAiComponent.SelectedLayer = args[1]; + Dirty(ent.Value, stationAiComponent); + + if (stationAiComponent.ActiveEye.IsValid() && !TerminatingOrDeleted(stationAiComponent.ActiveEye)) + { + var comp = EnsureComp(stationAiComponent.ActiveEye); + comp.SelectedLayer = args[1]; + Dirty(stationAiComponent.ActiveEye, comp); + } + + shell.WriteLine("OK!"); + } + + #endregion + + + [ValidatePrototypeId] + private const string SAIJob = "SAI"; + + private void OnRoleUpdateIfNeed(Entity ent, ref PlayerAttachedEvent args) + { + if (!_mind.TryGetMind(args.Player, out var mindId, out var mind)) + { + return; + } + + var job = CompOrNull(mindId); + if (job != null && job.Prototype == SAIJob) + { + return; + } + + if (job != null) + { + _role.MindRemoveRole(mindId); + } + + _role.MindAddRole(mindId, + new JobComponent + { + Prototype = SAIJob + }, + mind); + } + + private void OnToggleNuke(Entity ent, ref ToggleArmNukeEvent args) + { + if (_mobState.IsDead(ent)) + return; + + if (!TryComp(ent, out var nuke)) + { + return; + } + + if (nuke.Status == NukeStatus.COOLDOWN) + { + _popup.PopupCursor("На перезарядке!", ent); + return; + } + + if (nuke.DiskSlot.Item != null) + { + _popup.PopupCursor("Невозможно управлять бомбой при вставленном диске!", ent); + return; + } + + args.Handled = true; + if (nuke.Status == NukeStatus.ARMED) + { + _nuke.DisarmBomb(ent, nuke); + } + else + { + _nuke.ArmBomb(ent, nuke); + } + } + + private void CanInteraction(Entity ent, ref InteractionAttemptEvent args) + { + var core = ent; + if (TryComp(ent, out var eye)) + { + if (eye.AiCore == null) + { + QueueDel(ent); + return; + } + + core = eye.AiCore.Value; + } + + if (!core.Owner.Valid) + { + args.Cancel(); + return; + } + + if (_mobState.IsDead(core)) + return; + + if (args.Target != null && Transform(core).GridUid != Transform(args.Target.Value).GridUid) + { + args.Cancel(); + return; + } + + if (!TryComp(core, out var power)) + { + args.Cancel(); + return; + } + + if (power is { NeedsPower: true, Powered: false }) + { + args.Cancel(); + return; + } + + if (HasComp(args.Target)) + { + args.Cancel(); + return; + } + + if (HasComp(args.Target)) + { + args.Cancel(); + return; + } + + if (TryComp(args.Target, out var targetPower) && targetPower.NeedsPower && + !targetPower.Powered) + { + args.Cancel(); + return; + } + + if (HasComp(args.Target)) + { + args.Cancel(); + return; + } + } + + private void OnTerminated(Entity ent, ref EntityTerminatingEvent args) + { + if (!ent.Comp.ActiveEye.IsValid()) + { + return; + } + + QueueDel(ent.Comp.ActiveEye); + } + + private void OnStartup(EntityUid uid, StationAIComponent component, MapInitEvent args) + { + if (component.Layers.Count == 0) + { + return; + } + + _actions.AddAction(uid, ref component.ActionId, component.Action); + _hands.AddHand(uid, "SAI", HandLocation.Middle); + + if (!HasComp(uid)) + { + component.SelectedLayer = _robust.Pick(component.Layers.Keys); + Dirty(uid, component); + + _appearance.SetData(uid, AiVisuals.Dead, false); + _appearance.SetData(uid, AiVisuals.InEye, false); + if (TryComp(uid, out var nuke)) + { + (nuke as dynamic).Status = NukeStatus.COOLDOWN; + (nuke as dynamic).CooldownTime = 1; + _actions.AddAction(uid, ref component.NukeToggleId, component.NukeToggle); + } + } + } + + private void OnShutdown(EntityUid uid, StationAIComponent component, ComponentShutdown args) + { + _actions.RemoveAction(uid, component.ActionId); + _actions.RemoveAction(uid, component.NukeToggleId); + } + + private static readonly SoundSpecifier AiDeath = + new SoundPathSpecifier("/Audio/LPP/IPC/borg_deathsound.ogg"); + + private void OnMobStateChanged(EntityUid uid, StationAIComponent component, MobStateChangedEvent args) + { + if (HasComp(uid)) + return; + + if (args.OldMobState != MobState.Dead && args.NewMobState == MobState.Dead) + { + SetAsDead((uid, component), true); + } + else if (args.OldMobState != MobState.Alive && args.NewMobState == MobState.Alive) + { + SetAsDead((uid, component), false); + } + } + + private void SetAsDead(Entity ent, bool state) + { + if (state) + { + if (ent.Comp.ActiveEye.IsValid()) + { + QueueDel(ent.Comp.ActiveEye); + } + + _audioSystem.PlayPvs(AiDeath, ent); + _appearance.SetData(ent, AiVisuals.Dead, true); + } + else + { + _appearance.SetData(ent, AiVisuals.Dead, false); + _mobState.ChangeMobState(ent, MobState.Alive); + } + } +} diff --git a/Content.Shared/Eye/VisibilityFlags.cs b/Content.Shared/Eye/VisibilityFlags.cs index 7e2dd33d7d..de25a7c060 100644 --- a/Content.Shared/Eye/VisibilityFlags.cs +++ b/Content.Shared/Eye/VisibilityFlags.cs @@ -10,6 +10,7 @@ public enum VisibilityFlags : int Normal = 1 << 0, Ghost = 1 << 1, PsionicInvisibility = 1 << 2, //Nyano - Summary: adds Psionic Invisibility as a visibility layer. Currently does nothing. - TelegnosticProjection = 5, + AIEye = 1 << 3, + TelegnosticProjection = 5 } } diff --git a/Content.Shared/Mind/SharedMindSystem.cs b/Content.Shared/Mind/SharedMindSystem.cs index 1898126d80..88462a106a 100644 --- a/Content.Shared/Mind/SharedMindSystem.cs +++ b/Content.Shared/Mind/SharedMindSystem.cs @@ -154,6 +154,16 @@ private void OnExamined(EntityUid uid, MindContainerComponent mindContainer, Exa return; var dead = _mobState.IsDead(uid); + + // start _Backmen: AI + var mind = CompOrNull(mindContainer.Mind); + var remoteMind = CompOrNull(uid); + if (remoteMind != null) + { + mind = CompOrNull(remoteMind.MindId); + } + // end _Backmen: AI + var hasUserId = CompOrNull(mindContainer.Mind)?.UserId; var hasSession = CompOrNull(mindContainer.Mind)?.Session; @@ -167,6 +177,8 @@ private void OnExamined(EntityUid uid, MindContainerComponent mindContainer, Exa args.PushMarkup($"[color=mediumpurple]{Loc.GetString("comp-mind-examined-catatonic", ("ent", uid))}[/color]"); else if (hasSession == null) args.PushMarkup($"[color=yellow]{Loc.GetString("comp-mind-examined-ssd", ("ent", uid))}[/color]"); + + if (remoteMind != null) args.PushMarkup($"[color=red]{Loc.GetString("comp-mind-examined-remote-controlled")}[/color]"); // _Backmen: AI } private void OnSuicide(EntityUid uid, MindContainerComponent component, SuicideEvent args) diff --git a/Content.Shared/_Backmen/StationAI/Components/AICameraComponent.cs b/Content.Shared/_Backmen/StationAI/Components/AICameraComponent.cs new file mode 100644 index 0000000000..300a0cda96 --- /dev/null +++ b/Content.Shared/_Backmen/StationAI/Components/AICameraComponent.cs @@ -0,0 +1,34 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared._Backmen.StationAI; + +[RegisterComponent, NetworkedComponent] +public sealed partial class AICameraComponent : Component +{ + [DataField("enabled"), ViewVariables(VVAccess.ReadWrite)] + public bool Enabled = false; + + [DataField("cameraName"), ViewVariables(VVAccess.ReadWrite)] + public string CameraName = "Unnamed"; + + [DataField("cameraCategory"), ViewVariables(VVAccess.ReadWrite)] + public List CameraCategories = new List() + { + "Uncategorized" + }; + + [ViewVariables] + public HashSet ActiveViewers { get; } = new(); +} + +[Serializable, NetSerializable] +public sealed class AICameraComponentState : ComponentState +{ + public bool Enabled { get; init; } + public string CameraName { get; init; } = "Unnamed"; + public List CameraCategories { get; init; } = new List() + { + "Uncategorized" + }; +} diff --git a/Content.Shared/_Backmen/StationAI/Components/AIEnemyNTComponent.cs b/Content.Shared/_Backmen/StationAI/Components/AIEnemyNTComponent.cs new file mode 100644 index 0000000000..fb97451fe8 --- /dev/null +++ b/Content.Shared/_Backmen/StationAI/Components/AIEnemyNTComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared._Backmen.StationAI.Components; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class AIEnemyNTComponent : Component +{ + [AutoNetworkedField] + public EntityUid? Source; +} diff --git a/Content.Shared/_Backmen/StationAI/Components/AIEyeComponent.cs b/Content.Shared/_Backmen/StationAI/Components/AIEyeComponent.cs new file mode 100644 index 0000000000..fcbd8ff944 --- /dev/null +++ b/Content.Shared/_Backmen/StationAI/Components/AIEyeComponent.cs @@ -0,0 +1,52 @@ +using Content.Shared.Actions; +using Robust.Shared.GameStates; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared._Backmen.StationAI; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] +public sealed partial class AIEyeComponent : Component +{ + public Entity? AiCore; + + public EntProtoId ReturnAction = "AIEyeReturnAction"; + public EntityUid? ReturnActionUid; + [ViewVariables, AutoNetworkedField] + public EntityUid? Camera; + public bool IsProcessingMoveEvent = false; + + public override bool SendOnlyToOwner => true; + + [ViewVariables, AutoNetworkedField] + public HashSet<(NetEntity, NetCoordinates)> FollowsCameras = new (); + + public EntityUid? CamListUid; + public EntProtoId CamListAction = "AIEyeCamAction"; + + public EntityUid? CamShootUid; + public EntProtoId CamShootAction = "AIEyeCamShootAction"; +} + +public sealed partial class AIEyeCampShootActionEvent : WorldTargetActionEvent +{ +} + +public sealed partial class AIEyeCampActionEvent : InstantActionEvent +{ +} + +public sealed partial class AIEyePowerActionEvent : InstantActionEvent +{ +} + +public sealed partial class AIEyePowerReturnActionEvent : InstantActionEvent +{ +} + +[Serializable, NetSerializable] +public sealed class EyeMoveToCam : BoundUserInterfaceMessage +{ + public NetEntity Uid; +} diff --git a/Content.Shared/_Backmen/StationAI/Components/AIEyePowerComponent.cs b/Content.Shared/_Backmen/StationAI/Components/AIEyePowerComponent.cs new file mode 100644 index 0000000000..852c3c1471 --- /dev/null +++ b/Content.Shared/_Backmen/StationAI/Components/AIEyePowerComponent.cs @@ -0,0 +1,16 @@ +using Content.Shared.Actions; +using Robust.Shared.Prototypes; + +namespace Content.Shared._Backmen.StationAI; + +[RegisterComponent] +public sealed partial class AIEyePowerComponent : Component +{ + [DataField("prototype")] + public EntProtoId Prototype = "AIEye"; + + [DataField("prototypeAction")] + public EntProtoId PrototypeAction = "AIEyeAction"; + + public EntityUid? EyePowerAction = null; +} diff --git a/Content.Shared/_Backmen/StationAI/Components/BorgAINTComponent.cs b/Content.Shared/_Backmen/StationAI/Components/BorgAINTComponent.cs new file mode 100644 index 0000000000..6e0ff24bbc --- /dev/null +++ b/Content.Shared/_Backmen/StationAI/Components/BorgAINTComponent.cs @@ -0,0 +1,9 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared._Backmen.StationAI.Components; + +[RegisterComponent, NetworkedComponent] +public sealed partial class BorgAINTComponent : Component +{ + +} diff --git a/Content.Shared/_Backmen/StationAI/Components/CanBeBorgNTEnemyComponent.cs b/Content.Shared/_Backmen/StationAI/Components/CanBeBorgNTEnemyComponent.cs new file mode 100644 index 0000000000..5d66116144 --- /dev/null +++ b/Content.Shared/_Backmen/StationAI/Components/CanBeBorgNTEnemyComponent.cs @@ -0,0 +1,9 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared._Backmen.StationAI.Components; + +[RegisterComponent,NetworkedComponent] +public sealed partial class CanBeBorgNTEnemyComponent : Component +{ + +} diff --git a/Content.Shared/_Backmen/StationAI/Components/InnateItemComponent.cs b/Content.Shared/_Backmen/StationAI/Components/InnateItemComponent.cs new file mode 100644 index 0000000000..a21e084e4c --- /dev/null +++ b/Content.Shared/_Backmen/StationAI/Components/InnateItemComponent.cs @@ -0,0 +1,22 @@ +using Robust.Shared.Prototypes; + +namespace Content.Server._Backmen.StationAI; + +[RegisterComponent] +public sealed partial class InnateItemComponent : Component +{ + public bool AlreadyInitialized = false; + + [DataField("afterInteract")] + public bool AfterInteract = true; + + [DataField("startingPriority")] + public int? StartingPriority = null; + + [DataField("slots")] + public Dictionary Slots = new Dictionary(); + + public Dictionary Actions = new Dictionary(); + + public Dictionary Items = new Dictionary(); +} diff --git a/Content.Shared/_Backmen/StationAI/Components/StationAIComponent.cs b/Content.Shared/_Backmen/StationAI/Components/StationAIComponent.cs new file mode 100644 index 0000000000..a2ca622ca8 --- /dev/null +++ b/Content.Shared/_Backmen/StationAI/Components/StationAIComponent.cs @@ -0,0 +1,39 @@ +using Content.Shared.Actions; +using Robust.Shared.Prototypes; +using Content.Shared.Random; +using Content.Shared.Silicons.Laws; +using Robust.Shared.GameStates; + +namespace Content.Shared._Backmen.StationAI; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] +public sealed partial class StationAIComponent : Component +{ + [DataField("action")] + public EntProtoId Action; + + public EntityUid? ActionId; + + [ViewVariables(VVAccess.ReadOnly)] + public EntityUid ActiveEye = EntityUid.Invalid; + + [DataField("lawsId")] + public ProtoId LawsId = "LawsStationAIDefault"; + + [ViewVariables(VVAccess.ReadWrite)] + public SiliconLawsetPrototype? SelectedLaw; + + [DataField("nukeToggle")] + public EntProtoId NukeToggle = "AIToggleArmNuke"; + + public EntityUid? NukeToggleId; + + [DataField("layers", required: true)] + [ViewVariables] + public Dictionary Layers = new(); + + [DataField("defaultLayer", required: true)] + [AutoNetworkedField] + [ViewVariables(VVAccess.ReadWrite)] + public string SelectedLayer = "blue"; +} diff --git a/Content.Shared/_Backmen/StationAI/Events/StationAIEvents.cs b/Content.Shared/_Backmen/StationAI/Events/StationAIEvents.cs new file mode 100644 index 0000000000..2cc0a32a83 --- /dev/null +++ b/Content.Shared/_Backmen/StationAI/Events/StationAIEvents.cs @@ -0,0 +1,21 @@ +using Content.Shared.Actions; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared._Backmen.StationAI.Events; +public sealed partial class ToggleArmNukeEvent : InstantActionEvent +{ + +} + +public sealed partial class InnateAfterInteractActionEvent : EntityTargetActionEvent +{ + [DataField("item", required:true)] + public EntProtoId Item; +} + +public sealed partial class InnateBeforeInteractActionEvent : EntityTargetActionEvent +{ + [DataField("item", required:true)] + public EntProtoId Item; +} diff --git a/Content.Shared/_Backmen/StationAI/Systems/AICameraSystem.cs b/Content.Shared/_Backmen/StationAI/Systems/AICameraSystem.cs new file mode 100644 index 0000000000..23a449dc23 --- /dev/null +++ b/Content.Shared/_Backmen/StationAI/Systems/AICameraSystem.cs @@ -0,0 +1,34 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared._Backmen.StationAI.Systems; + +public sealed class AICameraSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(GetCompState); + SubscribeLocalEvent(HandleCompState); + } + + private void GetCompState(EntityUid uid, AICameraComponent component, ref ComponentGetState args) + { + args.State = new AICameraComponentState + { + Enabled = component.Enabled, + CameraName = component.CameraName, + CameraCategories = component.CameraCategories + }; + } + + private void HandleCompState(EntityUid uid, AICameraComponent component, ref ComponentHandleState args) + { + if (args.Current is not AICameraComponentState camera) + return; + + component.Enabled = camera.Enabled; + component.CameraName = camera.CameraName; + component.CameraCategories = camera.CameraCategories; + } +} diff --git a/Content.Shared/_Backmen/StationAI/Systems/AiEnemySystem.cs b/Content.Shared/_Backmen/StationAI/Systems/AiEnemySystem.cs new file mode 100644 index 0000000000..6a55abd3a7 --- /dev/null +++ b/Content.Shared/_Backmen/StationAI/Systems/AiEnemySystem.cs @@ -0,0 +1,49 @@ +using Content.Shared._Backmen.StationAI.Components; +using Content.Shared.Examine; +using Content.Shared.Verbs; + +namespace Content.Shared._Backmen.StationAI.Systems; + +public abstract class SharedAiEnemySystem : EntitySystem +{ + + protected EntityQuery EntityQuery; + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnExamine); + SubscribeLocalEvent>(OnMarkAsTarget); + EntityQuery = GetEntityQuery(); + } + + private void OnExamine(Entity ent, ref ExaminedEvent args) + { + var source = ent.Comp.Source ?? EntityUid.Invalid; + if(TerminatingOrDeleted(source)) + source = EntityUid.Invalid; + args.PushMarkup(Loc.GetString("aienemy-examine", ("name", source))); + } + + protected abstract void ToggleEnemy(EntityUid u, EntityUid target); + + private void OnMarkAsTarget(Entity ent, ref GetVerbsEvent args) + { + if (!EntityQuery.HasComponent(args.User)) + return; + if (!HasComp(args.User)) + return; + + var u = args.User; + AlternativeVerb verb = new() + { + Act = () => + { + ToggleEnemy(u, ent); + }, + Text = Loc.GetString("sai-enemy-verb"), + Priority = 2 + }; + + args.Verbs.Add(verb); + } +} diff --git a/Content.Shared/_Backmen/StationAI/Systems/SharedStationAISystem.cs b/Content.Shared/_Backmen/StationAI/Systems/SharedStationAISystem.cs new file mode 100644 index 0000000000..0448789247 --- /dev/null +++ b/Content.Shared/_Backmen/StationAI/Systems/SharedStationAISystem.cs @@ -0,0 +1,73 @@ +using Content.Shared.Throwing; +using Content.Shared.Item; +using Content.Shared.Strip.Components; +using Content.Shared.Hands; +using Content.Shared.Interaction.Events; +using Content.Shared.Inventory.Events; +using Content.Shared.Movement.Events; +using Robust.Shared.Console; + +namespace Content.Shared._Backmen.StationAI; + +public abstract class SharedStationAISystem : EntitySystem +{ + public const float CameraEyeRange = 10f; + + public override void Initialize() + { + base.Initialize(); + + + SubscribeLocalEvent(OnAttempt); + SubscribeLocalEvent(OnAttempt); + SubscribeLocalEvent(OnAttempt); + SubscribeLocalEvent(OnAttempt); + SubscribeLocalEvent(OnAttempt); + SubscribeLocalEvent(OnAttempt); + SubscribeLocalEvent(OnAttempt); + SubscribeLocalEvent(OnUpdateCanMove); + SubscribeLocalEvent(OnUpdateCanMove); + + SubscribeLocalEvent(OnStripEvent); + } + + public static IEnumerable StationAiComponents(string text, IEntityManager? entManager = null) + { + IoCManager.Resolve(ref entManager); + + var query = entManager.AllEntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out _, out var metadata)) + { + if (!entManager.TryGetNetEntity(uid, out var netEntity, metadata: metadata)) + continue; + + if (entManager.HasComponent(uid)) + continue; + + var netString = netEntity.Value.ToString(); + + if (!netString.StartsWith(text)) + continue; + + yield return new CompletionOption(netString, metadata.EntityName); + } + } + + + private void OnAttempt(EntityUid uid, StationAIComponent component, CancellableEntityEventArgs args) + { + args.Cancel(); + } + + private void OnUpdateCanMove(EntityUid uid, StationAIComponent component, CancellableEntityEventArgs args) + { + if (!HasComp(uid)) + args.Cancel(); + } + + private void OnStripEvent(EntityUid uid, Component component, StrippingSlotButtonPressed args) + { + return; + } +} diff --git a/Content.Shared/_Backmen/StationAI/UI/Key.cs b/Content.Shared/_Backmen/StationAI/UI/Key.cs new file mode 100644 index 0000000000..500267e704 --- /dev/null +++ b/Content.Shared/_Backmen/StationAI/UI/Key.cs @@ -0,0 +1,23 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared._Backmen.StationAI.UI; + +[Serializable, NetSerializable] +public enum AICameraListUiKey : byte +{ + Key, +} + +[Serializable, NetSerializable] +public enum AiVisuals : byte +{ + InEye, + Dead +} + +[Serializable, NetSerializable] +public enum AiVisualLayers : byte +{ + Dead, + NotInEye +}