]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Station AI (#30944)
authormetalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Wed, 28 Aug 2024 00:57:12 +0000 (10:57 +1000)
committerGitHub <noreply@github.com>
Wed, 28 Aug 2024 00:57:12 +0000 (10:57 +1000)
* Station AI overlay

* implement

* Bunch of ports

* Fix a heap of bugs and basic scouting

* helldivers

* Shuffle interactions a bit

* navmap stuff

* Revert "navmap stuff"

This reverts commit d1f89dd4be83233e22cf5dd062b2581f3c6da062.

* AI wires implemented

* Fix examines

* Optimise the overlay significantly

* Back to old static

* BUI radial working

* lots of work

* Saving work

* thanks fork

* alright

* pc

* AI upload console

* AI upload

* stuff

* Fix copy-paste shitcode

* AI actions

* navmap work

* Fixes

* first impressions

* a

* reh

* Revert "navmap work"

This reverts commit 6f63fea6e9245e189f368f97be3e32e9b210580e.

# Conflicts:
# Content.Client/Silicons/StationAi/StationAiOverlay.cs

* OD

* radar

* weh

* Fix examines

* scoop mine eyes

* fixes

* reh

* Optimise

* Final round of optimisations

* Fixes

* fixes

153 files changed:
Content.Client/Chat/UI/EmotesMenu.xaml.cs
Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml
Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml.cs
Content.Client/Silicons/Laws/SiliconLawEditUi/SiliconLawEui.cs
Content.Client/Silicons/StationAi/StationAiBoundUserInterface.cs [new file with mode: 0644]
Content.Client/Silicons/StationAi/StationAiMenu.xaml [new file with mode: 0644]
Content.Client/Silicons/StationAi/StationAiMenu.xaml.cs [new file with mode: 0644]
Content.Client/Silicons/StationAi/StationAiOverlay.cs
Content.Client/Silicons/StationAi/StationAiSystem.Airlock.cs [new file with mode: 0644]
Content.Client/Silicons/StationAi/StationAiSystem.Light.cs [new file with mode: 0644]
Content.Client/Silicons/StationAi/StationAiSystem.cs
Content.Client/Verbs/VerbSystem.cs
Content.Server/Administration/Systems/AdminVerbSystem.cs
Content.Server/Communications/CommunicationsConsoleSystem.cs
Content.Server/Database/ServerDbBase.cs
Content.Server/DeviceNetwork/Systems/DeviceNetworkRequiresPowerSystem.cs
Content.Server/Light/EntitySystems/LitOnPoweredSystem.cs
Content.Server/Light/EntitySystems/PoweredLightSystem.cs
Content.Server/Mind/MindSystem.cs
Content.Server/Power/EntitySystems/ChargerSystem.cs
Content.Server/Power/EntitySystems/PowerNetSystem.cs
Content.Server/Power/Generation/Teg/TegSystem.cs
Content.Server/Power/Generator/GasPowerReceiverSystem.cs
Content.Server/Procedural/DungeonJob/DungeonJob.DunGenExterior.cs
Content.Server/Silicons/Laws/SiliconLawSystem.cs
Content.Server/Silicons/StationAi/AiInteractWireAction.cs [new file with mode: 0644]
Content.Server/Silicons/StationAi/AiVisionWireAction.cs [new file with mode: 0644]
Content.Server/Silicons/StationAi/StationAiSystem.cs [new file with mode: 0644]
Content.Server/Sound/SpamEmitSoundRequirePowerSystem.cs
Content.Server/Station/Systems/StationSpawningSystem.cs
Content.Server/Weapons/Melee/EnergySword/EnergySwordSystem.cs
Content.Server/Xenoarchaeology/XenoArtifacts/Triggers/Systems/ArtifactElectricityTriggerSystem.cs
Content.Shared/ActionBlocker/ActionBlockerSystem.cs
Content.Shared/Climbing/Systems/ClimbSystem.cs
Content.Shared/Configurable/ConfigurationComponent.cs
Content.Shared/Doors/AirlockWireStatus.cs
Content.Shared/Examine/ExamineSystemShared.cs
Content.Shared/Hands/EntitySystems/SharedHandsSystem.cs
Content.Shared/Interaction/SharedInteractionSystem.cs
Content.Shared/Light/Components/LightOnCollideColliderComponent.cs [new file with mode: 0644]
Content.Shared/Light/Components/LightOnCollideComponent.cs [new file with mode: 0644]
Content.Shared/Light/EntitySystems/LightCollideSystem.cs [new file with mode: 0644]
Content.Shared/Light/EntitySystems/SlimPoweredLightSystem.cs
Content.Shared/Mind/SharedMindSystem.cs
Content.Shared/NPC/SharedPathfindingSystem.Line.cs [moved from Content.Server/NPC/Pathfinding/PathfindingSystem.Line.cs with 92% similarity]
Content.Shared/NPC/SharedPathfindingSystem.cs
Content.Shared/Preferences/Loadouts/RoleLoadoutPrototype.cs
Content.Shared/Silicons/Laws/Components/SiliconLawBoundComponent.cs
Content.Shared/Silicons/Laws/Components/SiliconLawUpdaterComponent.cs [new file with mode: 0644]
Content.Shared/Silicons/Laws/SharedSiliconLawSystem.Updater.cs [new file with mode: 0644]
Content.Shared/Silicons/Laws/SharedSiliconLawSystem.cs
Content.Shared/Silicons/StationAi/SharedStationAiSystem.Airlock.cs [new file with mode: 0644]
Content.Shared/Silicons/StationAi/SharedStationAiSystem.Held.cs [new file with mode: 0644]
Content.Shared/Silicons/StationAi/SharedStationAiSystem.Light.cs [new file with mode: 0644]
Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs [new file with mode: 0644]
Content.Shared/Silicons/StationAi/StationAiCoreComponent.cs [new file with mode: 0644]
Content.Shared/Silicons/StationAi/StationAiHeldComponent.cs [new file with mode: 0644]
Content.Shared/Silicons/StationAi/StationAiHolderComponent.cs [new file with mode: 0644]
Content.Shared/Silicons/StationAi/StationAiVisionComponent.cs
Content.Shared/Silicons/StationAi/StationAiVisionSystem.cs
Content.Shared/Silicons/StationAi/StationAiWhitelistComponent.cs [new file with mode: 0644]
Content.Shared/Station/SharedStationSpawningSystem.cs
Content.Shared/UserInterface/ActivatableUIComponent.cs
Content.Shared/UserInterface/ActivatableUISystem.cs
Content.Shared/Verbs/SharedVerbSystem.cs
Content.Shared/Verbs/VerbEvents.cs
Content.Shared/Wires/SharedWiresSystem.cs
Resources/Audio/Effects/Footsteps/attributions.yml
Resources/Audio/Effects/Footsteps/borgwalk2.ogg
Resources/Locale/en-US/administration/ui/silicon-law-ui.ftl
Resources/Locale/en-US/job/department-desc.ftl
Resources/Locale/en-US/job/department.ftl
Resources/Locale/en-US/job/job-description.ftl
Resources/Locale/en-US/job/job-names.ftl
Resources/Locale/en-US/preferences/loadouts.ftl
Resources/Locale/en-US/silicons/station-ai.ftl [new file with mode: 0644]
Resources/Maps/Test/dev_map.yml
Resources/Prototypes/Datasets/Names/ai.yml
Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml
Resources/Prototypes/Entities/Mobs/Cyborgs/borg_chassis.yml
Resources/Prototypes/Entities/Mobs/NPCs/miscellaneous.yml
Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml
Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml
Resources/Prototypes/Entities/Mobs/Player/observer.yml
Resources/Prototypes/Entities/Mobs/Player/silicon.yml
Resources/Prototypes/Entities/Mobs/Species/base.yml
Resources/Prototypes/Entities/Mobs/base.yml
Resources/Prototypes/Entities/Objects/Fun/Instruments/base_instruments.yml
Resources/Prototypes/Entities/Objects/Misc/paper.yml
Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml
Resources/Prototypes/Entities/Objects/Tools/access_configurator.yml
Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml
Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
Resources/Prototypes/Entities/Structures/Machines/Medical/cryo_pod.yml
Resources/Prototypes/Entities/Structures/Power/apc.yml
Resources/Prototypes/Entities/Structures/Wallmounts/intercom.yml
Resources/Prototypes/Entities/Structures/Wallmounts/surveillance_camera.yml
Resources/Prototypes/Entities/Structures/Wallmounts/timer.yml
Resources/Prototypes/Loadouts/role_loadouts.yml
Resources/Prototypes/Roles/Jobs/Science/borg.yml
Resources/Prototypes/Roles/Jobs/departments.yml
Resources/Prototypes/Roles/play_time_trackers.yml
Resources/Prototypes/StatusIcon/job.yml
Resources/Prototypes/Wires/layouts.yml
Resources/Prototypes/tags.yml
Resources/Textures/Interface/Actions/actions_ai.rsi/ai_core.png [new file with mode: 0644]
Resources/Textures/Interface/Actions/actions_ai.rsi/camera_light.png [new file with mode: 0644]
Resources/Textures/Interface/Actions/actions_ai.rsi/crew_monitor.png [new file with mode: 0644]
Resources/Textures/Interface/Actions/actions_ai.rsi/manifest.png [new file with mode: 0644]
Resources/Textures/Interface/Actions/actions_ai.rsi/meta.json [new file with mode: 0644]
Resources/Textures/Interface/Actions/actions_ai.rsi/state_laws.png [new file with mode: 0644]
Resources/Textures/Interface/Misc/job_icons.rsi/StationAi.png [new file with mode: 0644]
Resources/Textures/Interface/Misc/job_icons.rsi/meta.json
Resources/Textures/Interface/noise.rsi/meta.json [new file with mode: 0644]
Resources/Textures/Interface/noise.rsi/noise.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/output.rsi/ai-banned-unshaded.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/output.rsi/ai-banned.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/output.rsi/ai-banned_dead-unshaded.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/output.rsi/ai-banned_dead.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/output.rsi/ai-empty-unshaded.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/output.rsi/ai-empty.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/output.rsi/ai-holo-old-unshaded.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/output.rsi/ai-holo-old.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/output.rsi/ai-unshaded.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/output.rsi/ai.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/output.rsi/ai_dead-unshaded.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/output.rsi/ai_dead.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/output.rsi/default-unshaded.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/output.rsi/default.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/output.rsi/floating_face-unshaded.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/output.rsi/floating_face.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/output.rsi/horror-unshaded.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/output.rsi/horror.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/output.rsi/meta.json [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/output.rsi/xeno_queen-unshaded.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/output.rsi/xeno_queen.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/station_ai.rsi/ai.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_dead.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_empty.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/station_ai.rsi/base.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/station_ai.rsi/default.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/station_ai.rsi/meta.json [new file with mode: 0644]
Resources/Textures/Objects/Devices/ai_card.rsi/base.png [new file with mode: 0644]
Resources/Textures/Objects/Devices/ai_card.rsi/empty.png [new file with mode: 0644]
Resources/Textures/Objects/Devices/ai_card.rsi/full.png [new file with mode: 0644]
Resources/Textures/Objects/Devices/ai_card.rsi/inhand-left.png [new file with mode: 0644]
Resources/Textures/Objects/Devices/ai_card.rsi/inhand-right.png [new file with mode: 0644]
Resources/Textures/Objects/Devices/ai_card.rsi/meta.json [new file with mode: 0644]
Resources/Textures/Objects/Devices/output.rsi/aicard-full-unshaded.png [new file with mode: 0644]
Resources/Textures/Objects/Devices/output.rsi/aicard-full.png [new file with mode: 0644]
Resources/Textures/Objects/Devices/output.rsi/aicard-unshaded.png [new file with mode: 0644]
Resources/Textures/Objects/Devices/output.rsi/aicard.png [new file with mode: 0644]
Resources/Textures/Objects/Devices/output.rsi/meta.json [new file with mode: 0644]

index 3340755343890b0f5b753341d525c1284520f96c..f3b7837f21a59cb58533579399f1b4c55d62dc9b 100644 (file)
@@ -19,9 +19,6 @@ public sealed partial class EmotesMenu : RadialMenu
     [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
     [Dependency] private readonly ISharedPlayerManager _playerManager = default!;
 
-    private readonly SpriteSystem _spriteSystem;
-    private readonly EntityWhitelistSystem _whitelistSystem;
-
     public event Action<ProtoId<EmotePrototype>>? OnPlayEmote;
 
     public EmotesMenu()
@@ -29,8 +26,8 @@ public sealed partial class EmotesMenu : RadialMenu
         IoCManager.InjectDependencies(this);
         RobustXamlLoader.Load(this);
 
-        _spriteSystem = _entManager.System<SpriteSystem>();
-        _whitelistSystem = _entManager.System<EntityWhitelistSystem>();
+        var spriteSystem = _entManager.System<SpriteSystem>();
+        var whitelistSystem = _entManager.System<EntityWhitelistSystem>();
 
         var main = FindControl<RadialContainer>("Main");
 
@@ -40,8 +37,8 @@ public sealed partial class EmotesMenu : RadialMenu
             var player = _playerManager.LocalSession?.AttachedEntity;
             if (emote.Category == EmoteCategory.Invalid ||
                 emote.ChatTriggers.Count == 0 ||
-                !(player.HasValue && _whitelistSystem.IsWhitelistPassOrNull(emote.Whitelist, player.Value)) ||
-                _whitelistSystem.IsBlacklistPass(emote.Blacklist, player.Value))
+                !(player.HasValue && whitelistSystem.IsWhitelistPassOrNull(emote.Whitelist, player.Value)) ||
+                whitelistSystem.IsBlacklistPass(emote.Blacklist, player.Value))
                 continue;
 
             if (!emote.Available &&
@@ -63,7 +60,7 @@ public sealed partial class EmotesMenu : RadialMenu
             {
                 VerticalAlignment = VAlignment.Center,
                 HorizontalAlignment = HAlignment.Center,
-                Texture = _spriteSystem.Frame0(emote.Icon),
+                Texture = spriteSystem.Frame0(emote.Icon),
                 TextureScale = new Vector2(2f, 2f),
             };
 
index afa783c7aa9c03d7ac5b03fef3ab4ee5c5b09d1f..87d11005be8a9861c7a997b6e8667c90e99fca8c 100644 (file)
@@ -1,10 +1,24 @@
 <controls:FancyWindow xmlns="https://spacestation14.io"
          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
          xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+         xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
          SetSize="800 800"
-         MinSize="800 64">
+         MinSize="800 128">
+    <BoxContainer Orientation="Vertical" VerticalExpand="True">
+        <!--
+        <BoxContainer Name="RoleNameBox" Orientation="Vertical" Margin="10">
+            <Label Name="LoadoutNameLabel" Text="{Loc 'loadout-name-edit-label'}"/>
+            <PanelContainer HorizontalExpand="True" SetHeight="24">
+                <PanelContainer.PanelOverride>
+                    <graphics:StyleBoxFlat BackgroundColor="#1B1B1E" />
+                </PanelContainer.PanelOverride>
+                <LineEdit Name="RoleNameEdit" ToolTip="{Loc 'loadout-name-edit-tooltip'}" VerticalExpand="True" HorizontalExpand="True"/>
+            </PanelContainer>
+        </BoxContainer>
+        -->
         <VerticalTabContainer Name="LoadoutGroupsContainer"
-                      VerticalExpand="True"
-                      HorizontalExpand="True">
+                              VerticalExpand="True"
+                              HorizontalExpand="True">
         </VerticalTabContainer>
+    </BoxContainer>
 </controls:FancyWindow>
index d029eb1223d8df4ed8289aba86158f05670aa957..aab2a56ff6819e5fc2d038e8deb82e83c8b4156f 100644 (file)
@@ -1,3 +1,4 @@
+using System.Numerics;
 using Content.Client.UserInterface.Controls;
 using Content.Shared.Preferences;
 using Content.Shared.Preferences.Loadouts;
@@ -5,6 +6,7 @@ using Robust.Client.AutoGenerated;
 using Robust.Client.UserInterface.XAML;
 using Robust.Shared.Player;
 using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
 
 namespace Content.Client.Lobby.UI.Loadouts;
 
@@ -24,27 +26,36 @@ public sealed partial class LoadoutWindow : FancyWindow
         Profile = profile;
         var protoManager = collection.Resolve<IPrototypeManager>();
 
-        foreach (var group in proto.Groups)
+        // Hide if no groups
+        if (proto.Groups.Count == 0)
         {
-            if (!protoManager.TryIndex(group, out var groupProto))
-                continue;
+            LoadoutGroupsContainer.Visible = false;
+            SetSize = Vector2.Zero;
+        }
+        else
+        {
+            foreach (var group in proto.Groups)
+            {
+                if (!protoManager.TryIndex(group, out var groupProto))
+                    continue;
 
-            if (groupProto.Hidden)
-                continue;
+                if (groupProto.Hidden)
+                    continue;
 
-            var container = new LoadoutGroupContainer(profile, loadout, protoManager.Index(group), session, collection);
-            LoadoutGroupsContainer.AddTab(container, Loc.GetString(groupProto.Name));
-            _groups.Add(container);
+                var container = new LoadoutGroupContainer(profile, loadout, protoManager.Index(group), session, collection);
+                LoadoutGroupsContainer.AddTab(container, Loc.GetString(groupProto.Name));
+                _groups.Add(container);
 
-            container.OnLoadoutPressed += args =>
-            {
-                OnLoadoutPressed?.Invoke(group, args);
-            };
+                container.OnLoadoutPressed += args =>
+                {
+                    OnLoadoutPressed?.Invoke(group, args);
+                };
 
-            container.OnLoadoutUnpressed += args =>
-            {
-                OnLoadoutUnpressed?.Invoke(group, args);
-            };
+                container.OnLoadoutUnpressed += args =>
+                {
+                    OnLoadoutUnpressed?.Invoke(group, args);
+                };
+            }
         }
     }
 
index a4d59d1f3150f3061b8e216276bdd9b8abf79907..03c74032f73bd3bb6e53a217599ee3168f20a640 100644 (file)
@@ -6,7 +6,7 @@ namespace Content.Client.Silicons.Laws.SiliconLawEditUi;
 
 public sealed class SiliconLawEui : BaseEui
 {
-    public readonly EntityManager _entityManager = default!;
+    private readonly EntityManager _entityManager;
 
     private SiliconLawUi _siliconLawUi;
     private EntityUid _target;
diff --git a/Content.Client/Silicons/StationAi/StationAiBoundUserInterface.cs b/Content.Client/Silicons/StationAi/StationAiBoundUserInterface.cs
new file mode 100644 (file)
index 0000000..6831830
--- /dev/null
@@ -0,0 +1,28 @@
+using Content.Shared.Silicons.StationAi;
+using Robust.Client.UserInterface;
+
+namespace Content.Client.Silicons.StationAi;
+
+public sealed class StationAiBoundUserInterface : BoundUserInterface
+{
+    private StationAiMenu? _menu;
+
+    public StationAiBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+    {
+    }
+
+    protected override void Open()
+    {
+        base.Open();
+        _menu = this.CreateWindow<StationAiMenu>();
+        _menu.Track(Owner);
+
+        _menu.OnAiRadial += args =>
+        {
+            SendPredictedMessage(new StationAiRadialMessage()
+            {
+                Event = args,
+            });
+        };
+    }
+}
diff --git a/Content.Client/Silicons/StationAi/StationAiMenu.xaml b/Content.Client/Silicons/StationAi/StationAiMenu.xaml
new file mode 100644 (file)
index 0000000..d56fc83
--- /dev/null
@@ -0,0 +1,13 @@
+<ui:RadialMenu xmlns="https://spacestation14.io"
+                xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
+                BackButtonStyleClass="RadialMenuBackButton"
+                CloseButtonStyleClass="RadialMenuCloseButton"
+                VerticalExpand="True"
+                HorizontalExpand="True"
+                MinSize="450 450">
+
+    <!-- Main -->
+    <ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" Radius="64" ReserveSpaceForHiddenChildren="False">
+    </ui:RadialContainer>
+
+</ui:RadialMenu>
diff --git a/Content.Client/Silicons/StationAi/StationAiMenu.xaml.cs b/Content.Client/Silicons/StationAi/StationAiMenu.xaml.cs
new file mode 100644 (file)
index 0000000..24a802a
--- /dev/null
@@ -0,0 +1,128 @@
+using System.Numerics;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Silicons.StationAi;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Timing;
+
+namespace Content.Client.Silicons.StationAi;
+
+[GenerateTypedNameReferences]
+public sealed partial class StationAiMenu : RadialMenu
+{
+    [Dependency] private readonly IClyde _clyde = default!;
+    [Dependency] private readonly IEntityManager _entManager = default!;
+    [Dependency] private readonly IEyeManager _eyeManager = default!;
+
+    public event Action<BaseStationAiAction>? OnAiRadial;
+
+    private EntityUid _tracked;
+
+    public StationAiMenu()
+    {
+        IoCManager.InjectDependencies(this);
+        RobustXamlLoader.Load(this);
+    }
+
+    public void Track(EntityUid owner)
+    {
+        _tracked = owner;
+
+        if (!_entManager.EntityExists(_tracked))
+        {
+            Close();
+            return;
+        }
+
+        BuildButtons();
+        UpdatePosition();
+    }
+
+    private void BuildButtons()
+    {
+        var ev = new GetStationAiRadialEvent();
+        _entManager.EventBus.RaiseLocalEvent(_tracked, ref ev);
+
+        var main = FindControl<RadialContainer>("Main");
+        main.DisposeAllChildren();
+        var sprites = _entManager.System<SpriteSystem>();
+
+        foreach (var action in ev.Actions)
+        {
+            // TODO: This radial boilerplate is quite annoying
+            var button = new StationAiMenuButton(action.Event)
+            {
+                StyleClasses = { "RadialMenuButton" },
+                SetSize = new Vector2(64f, 64f),
+                ToolTip = action.Tooltip != null ? Loc.GetString(action.Tooltip) : null,
+            };
+
+            if (action.Sprite != null)
+            {
+                var texture = sprites.Frame0(action.Sprite);
+                var scale = Vector2.One;
+
+                if (texture.Width <= 32)
+                {
+                    scale *= 2;
+                }
+
+                var tex = new TextureRect
+                {
+                    VerticalAlignment = VAlignment.Center,
+                    HorizontalAlignment = HAlignment.Center,
+                    Texture = texture,
+                    TextureScale = scale,
+                };
+
+                button.AddChild(tex);
+            }
+
+            button.OnPressed += args =>
+            {
+                OnAiRadial?.Invoke(action.Event);
+                Close();
+            };
+            main.AddChild(button);
+        }
+    }
+
+    protected override void FrameUpdate(FrameEventArgs args)
+    {
+        base.FrameUpdate(args);
+        UpdatePosition();
+    }
+
+    private void UpdatePosition()
+    {
+        if (!_entManager.TryGetComponent(_tracked, out TransformComponent? xform))
+        {
+            Close();
+            return;
+        }
+
+        if (!xform.Coordinates.IsValid(_entManager))
+        {
+            Close();
+            return;
+        }
+
+        var coords = _entManager.System<SpriteSystem>().GetSpriteScreenCoordinates((_tracked, null, xform));
+
+        if (!coords.IsValid)
+        {
+            Close();
+            return;
+        }
+
+        OpenScreenAt(coords.Position, _clyde);
+    }
+}
+
+public sealed class StationAiMenuButton(BaseStationAiAction action) : RadialMenuTextureButton
+{
+    public BaseStationAiAction Action = action;
+}
index efa1b8dbeff10a363c3c83f6ace957e85185cd80..15a8a3a63fe90ad1f2759a05c9b98d6e1becfd62 100644 (file)
@@ -4,7 +4,9 @@ using Robust.Client.Graphics;
 using Robust.Client.Player;
 using Robust.Shared.Enums;
 using Robust.Shared.Map.Components;
+using Robust.Shared.Physics;
 using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
 
 namespace Content.Client.Silicons.StationAi;
 
@@ -12,6 +14,7 @@ public sealed class StationAiOverlay : Overlay
 {
     [Dependency] private readonly IClyde _clyde = default!;
     [Dependency] private readonly IEntityManager _entManager = default!;
+    [Dependency] private readonly IGameTiming _timing = default!;
     [Dependency] private readonly IPlayerManager _player = default!;
     [Dependency] private readonly IPrototypeManager _proto = default!;
 
@@ -22,6 +25,9 @@ public sealed class StationAiOverlay : Overlay
     private IRenderTexture? _staticTexture;
     private IRenderTexture? _stencilTexture;
 
+    private float _updateRate = 1f / 30f;
+    private float _accumulator;
+
     public StationAiOverlay()
     {
         IoCManager.InjectDependencies(this);
@@ -47,19 +53,22 @@ public sealed class StationAiOverlay : Overlay
         _entManager.TryGetComponent(playerEnt, out TransformComponent? playerXform);
         var gridUid = playerXform?.GridUid ?? EntityUid.Invalid;
         _entManager.TryGetComponent(gridUid, out MapGridComponent? grid);
+        _entManager.TryGetComponent(gridUid, out BroadphaseComponent? broadphase);
 
         var invMatrix = args.Viewport.GetWorldToLocalMatrix();
+        _accumulator -= (float) _timing.FrameTime.TotalSeconds;
 
-        if (grid != null)
+        if (grid != null && broadphase != null)
         {
-            // TODO: Pass in attached entity's grid.
-            // TODO: Credit OD on the moved to code
-            // TODO: Call the moved-to code here.
-
-            _visibleTiles.Clear();
             var lookups = _entManager.System<EntityLookupSystem>();
             var xforms = _entManager.System<SharedTransformSystem>();
-            _entManager.System<StationAiVisionSystem>().GetView((gridUid, grid), worldBounds, _visibleTiles);
+
+            if (_accumulator <= 0f)
+            {
+                _accumulator = MathF.Max(0f, _accumulator + _updateRate);
+                _visibleTiles.Clear();
+                _entManager.System<StationAiVisionSystem>().GetView((gridUid, broadphase, grid), worldBounds, _visibleTiles);
+            }
 
             var gridMatrix = xforms.GetWorldMatrix(gridUid);
             var matty =  Matrix3x2.Multiply(gridMatrix, invMatrix);
diff --git a/Content.Client/Silicons/StationAi/StationAiSystem.Airlock.cs b/Content.Client/Silicons/StationAi/StationAiSystem.Airlock.cs
new file mode 100644 (file)
index 0000000..bf6b65a
--- /dev/null
@@ -0,0 +1,30 @@
+using Content.Shared.Doors.Components;
+using Content.Shared.Silicons.StationAi;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Silicons.StationAi;
+
+public sealed partial class StationAiSystem
+{
+    private void InitializeAirlock()
+    {
+        SubscribeLocalEvent<DoorBoltComponent, GetStationAiRadialEvent>(OnDoorBoltGetRadial);
+    }
+
+    private void OnDoorBoltGetRadial(Entity<DoorBoltComponent> ent, ref GetStationAiRadialEvent args)
+    {
+        args.Actions.Add(new StationAiRadial()
+        {
+            Sprite = ent.Comp.BoltsDown ?
+                new SpriteSpecifier.Rsi(
+                new ResPath("/Textures/Structures/Doors/Airlocks/Standard/basic.rsi"), "open") :
+                new SpriteSpecifier.Rsi(
+                new ResPath("/Textures/Structures/Doors/Airlocks/Standard/basic.rsi"), "closed"),
+            Tooltip = ent.Comp.BoltsDown ? Loc.GetString("bolt-open") : Loc.GetString("bolt-close"),
+            Event = new StationAiBoltEvent()
+            {
+                Bolted = !ent.Comp.BoltsDown,
+            }
+        });
+    }
+}
diff --git a/Content.Client/Silicons/StationAi/StationAiSystem.Light.cs b/Content.Client/Silicons/StationAi/StationAiSystem.Light.cs
new file mode 100644 (file)
index 0000000..cf2f613
--- /dev/null
@@ -0,0 +1,32 @@
+using Content.Shared.Item.ItemToggle.Components;
+using Content.Shared.Light.Components;
+using Content.Shared.Silicons.StationAi;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Silicons.StationAi;
+
+public sealed partial class StationAiSystem
+{
+    // Used for surveillance camera lights
+
+    private void InitializePowerToggle()
+    {
+        SubscribeLocalEvent<ItemTogglePointLightComponent, GetStationAiRadialEvent>(OnLightGetRadial);
+    }
+
+    private void OnLightGetRadial(Entity<ItemTogglePointLightComponent> ent, ref GetStationAiRadialEvent args)
+    {
+        if (!TryComp(ent.Owner, out ItemToggleComponent? toggle))
+            return;
+
+        args.Actions.Add(new StationAiRadial()
+        {
+            Tooltip = Loc.GetString("toggle-light"),
+            Sprite = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/VerbIcons/light.svg.192dpi.png")),
+            Event = new StationAiLightEvent()
+            {
+                Enabled = !toggle.Activated
+            }
+        });
+    }
+}
index 2ed061752529854267fbd25c3bc8476a8355f4fa..ab9ace3c1d583d8c261dbce7bf37116969fe45e3 100644 (file)
@@ -5,7 +5,7 @@ using Robust.Shared.Player;
 
 namespace Content.Client.Silicons.StationAi;
 
-public sealed partial class StationAiSystem : EntitySystem
+public sealed partial class StationAiSystem : SharedStationAiSystem
 {
     [Dependency] private readonly IOverlayManager _overlayMgr = default!;
     [Dependency] private readonly IPlayerManager _player = default!;
@@ -15,8 +15,8 @@ public sealed partial class StationAiSystem : EntitySystem
     public override void Initialize()
     {
         base.Initialize();
-        // InitializeAirlock();
-        // InitializePowerToggle();
+        InitializeAirlock();
+        InitializePowerToggle();
 
         SubscribeLocalEvent<StationAiOverlayComponent, LocalPlayerAttachedEvent>(OnAiAttached);
         SubscribeLocalEvent<StationAiOverlayComponent, LocalPlayerDetachedEvent>(OnAiDetached);
index c3e03528a79232539a334edbad8c8b2df64310b1..e28f48d6a50d9215886e4a66b1b911a286faf0c9 100644 (file)
@@ -78,6 +78,7 @@ namespace Content.Client.Verbs
 
             // Get entities
             List<EntityUid> entities;
+            var examineFlags = LookupFlags.All & ~LookupFlags.Sensors;
 
             // Do we have to do FoV checks?
             if ((visibility & MenuVisibility.NoFov) == 0)
@@ -88,7 +89,7 @@ namespace Content.Client.Verbs
                 TryComp(player.Value, out ExaminerComponent? examiner);
 
                 entities = new();
-                foreach (var ent in _entityLookup.GetEntitiesInRange(targetPos, EntityMenuLookupSize))
+                foreach (var ent in _entityLookup.GetEntitiesInRange(targetPos, EntityMenuLookupSize, flags: examineFlags))
                 {
                     if (_examine.CanExamine(player.Value, targetPos, Predicate, ent, examiner))
                         entities.Add(ent);
@@ -96,7 +97,7 @@ namespace Content.Client.Verbs
             }
             else
             {
-                entities = _entityLookup.GetEntitiesInRange(targetPos, EntityMenuLookupSize).ToList();
+                entities = _entityLookup.GetEntitiesInRange(targetPos, EntityMenuLookupSize, flags: examineFlags).ToList();
             }
 
             if (entities.Count == 0)
index 6123bba8deeb5d9ceb64b18a9b316e64d90a0843..308a679846ccf9703458acdf3b87a2f010969eb3 100644 (file)
@@ -36,6 +36,7 @@ using Robust.Shared.Utility;
 using System.Linq;
 using System.Numerics;
 using Content.Server.Silicons.Laws;
+using Content.Shared.Silicons.Laws;
 using Content.Shared.Silicons.Laws.Components;
 using Robust.Server.Player;
 using Robust.Shared.Physics.Components;
index e616949d7a1f4a86488c041c78f0de22ec91bf59..3884c1cc439195edf616d1408e833d3b682610fc 100644 (file)
@@ -183,10 +183,6 @@ namespace Content.Server.Communications
 
         private bool CanUse(EntityUid user, EntityUid console)
         {
-            // This shouldn't technically be possible because of BUI but don't trust client.
-            if (!_interaction.InRangeUnobstructed(console, user))
-                return false;
-
             if (TryComp<AccessReaderComponent>(console, out var accessReaderComponent) && !HasComp<EmaggedComponent>(console))
             {
                 return _accessReaderSystem.IsAllowed(user, console, accessReaderComponent);
index 2b6ce41e191a1a31640fe00e08c4b4d8220061f4..3c59c36f77e3fbba85ebd7c60a16399c686b433f 100644 (file)
@@ -220,7 +220,9 @@ namespace Content.Server.Database
 
             foreach (var role in profile.Loadouts)
             {
-                var loadout = new RoleLoadout(role.RoleName);
+                var loadout = new RoleLoadout(role.RoleName)
+                {
+                };
 
                 foreach (var group in role.Groups)
                 {
index f47a5df8ac401dcb73a5c5acc1320cf563dc35c6..6e7bd255c5dc4c43e2810679b17b6237a99d23d2 100644 (file)
@@ -1,7 +1,6 @@
 using Content.Server.DeviceNetwork.Components;
 using Content.Server.Power.Components;
 using Content.Server.Power.EntitySystems;
-using Content.Shared.Power.EntitySystems;
 
 namespace Content.Server.DeviceNetwork.Systems;
 
index 3c5f7eaecb2d102d49cfe7f6fbc502128eebe5d6..5c66d65b573594006ceb1f370ed9080f76eec68e 100644 (file)
@@ -2,7 +2,6 @@ using Content.Server.Light.Components;
 using Content.Server.Power.Components;
 using Content.Server.Power.EntitySystems;
 using Content.Shared.Power;
-using Content.Shared.Power.Components;
 
 namespace Content.Server.Light.EntitySystems
 {
index 6e1363dee2a2e4b35b51126ce8d71ebcc40bfe3f..3bd788bcf43f3f787309a26d35ef950a36fd2205 100644 (file)
@@ -26,7 +26,6 @@ using Robust.Shared.Audio.Systems;
 using Content.Shared.Damage.Systems;
 using Content.Shared.Damage.Components;
 using Content.Shared.Power;
-using Content.Shared.Power.Components;
 
 namespace Content.Server.Light.EntitySystems
 {
index c3e29cc3635e14bfa2f4e2b51d03a09d945bc31c..2447d88641134028d2f395855af31efec38878a2 100644 (file)
@@ -341,13 +341,13 @@ public sealed class MindSystem : SharedMindSystem
         }
     }
 
-    public void ControlMob(EntityUid user, EntityUid target)
+    public override void ControlMob(EntityUid user, EntityUid target)
     {
         if (TryComp(user, out ActorComponent? actor))
             ControlMob(actor.PlayerSession.UserId, target);
     }
 
-    public void ControlMob(NetUserId user, EntityUid target)
+    public override void ControlMob(NetUserId user, EntityUid target)
     {
         var (mindId, mind) = GetOrCreateMind(user);
 
index df7bd2a54f7da84ab45059d540233ccbb34d2ff0..40b998a95d00fbe4b3d307a9b6a060e6fbc30a0c 100644 (file)
@@ -8,7 +8,6 @@ using Content.Shared.Emp;
 using JetBrains.Annotations;
 using Robust.Shared.Containers;
 using System.Diagnostics.CodeAnalysis;
-using Content.Shared.Power.Components;
 using Content.Shared.Storage.Components;
 using Robust.Server.Containers;
 using Content.Shared.Whitelist;
@@ -64,7 +63,7 @@ internal sealed class ChargerSystem : EntitySystem
             }
             else
             {
-                // add how much each item is charged it 
+                // add how much each item is charged it
                 foreach (var contained in container.ContainedEntities)
                 {
                     if (!TryComp<BatteryComponent>(contained, out var battery))
@@ -232,7 +231,7 @@ internal sealed class ChargerSystem : EntitySystem
 
         return CellChargerStatus.Charging;
     }
-    
+
     private void TransferPower(EntityUid uid, EntityUid targetEntity, ChargerComponent component, float frameTime)
     {
         if (!TryComp(uid, out ApcPowerReceiverComponent? receiverComponent))
index 9ce4814139629674e1396b85254377cd03a7fd00..a7098649ceffb19f38c626d709576c775ea8045d 100644 (file)
@@ -5,7 +5,6 @@ using Content.Server.Power.NodeGroups;
 using Content.Server.Power.Pow3r;
 using Content.Shared.CCVar;
 using Content.Shared.Power;
-using Content.Shared.Power.Components;
 using JetBrains.Annotations;
 using Robust.Server.GameObjects;
 using Robust.Shared.Configuration;
index edf0693954c5d8e2827c24294f7fefe75797b4e8..9fb7d5ff1f649b2ada5d76afa67b326006254d33 100644 (file)
@@ -11,7 +11,6 @@ using Content.Shared.Atmos;
 using Content.Shared.DeviceNetwork;
 using Content.Shared.Examine;
 using Content.Shared.Power;
-using Content.Shared.Power.Components;
 using Content.Shared.Power.Generation.Teg;
 using Content.Shared.Rounding;
 using Robust.Server.GameObjects;
index 5a1bd31a15c36350c854a9ecccd011ff70688ca7..e3979a651929570763500e7538c713b9235d9b97 100644 (file)
@@ -6,7 +6,6 @@ using Content.Server.NodeContainer.Nodes;
 using Content.Server.Power.Components;
 using Content.Shared.Atmos;
 using Content.Shared.Power;
-using Content.Shared.Power.Components;
 
 namespace Content.Server.Power.Generator;
 
index acffd057fad0db635869865a8c6d8fe2131e2f1d..bb2c1cbbbfc6ea4f4cfe09502d123f9d94dc3c81 100644 (file)
@@ -1,6 +1,7 @@
 using System.Threading.Tasks;
 using Content.Server.NPC.Pathfinding;
 using Content.Shared.Maps;
+using Content.Shared.NPC;
 using Content.Shared.Procedural;
 using Content.Shared.Procedural.DungeonGenerators;
 using Robust.Shared.Collections;
@@ -29,7 +30,7 @@ public sealed partial class DungeonJob
         var pathfinder = _entManager.System<PathfindingSystem>();
 
         // Gridcast
-        pathfinder.GridCast(startTile, position, tile =>
+        SharedPathfindingSystem.GridCast(startTile, position, tile =>
         {
             if (!_maps.TryGetTileRef(_gridUid, _grid, tile, out var tileRef) ||
                 tileRef.Tile.IsSpace(_tileDefManager))
index 0c0f68c23f352e4199d7b776a79cf95cb1fb423f..6b7df52a6ebc1de13b9a2b98fe5f5983d06f9a46 100644 (file)
@@ -5,12 +5,10 @@ using Content.Server.GameTicking;
 using Content.Server.Radio.Components;
 using Content.Server.Roles;
 using Content.Server.Station.Systems;
-using Content.Shared.Actions;
 using Content.Shared.Administration;
 using Content.Shared.Chat;
 using Content.Shared.Emag.Components;
 using Content.Shared.Emag.Systems;
-using Content.Shared.Examine;
 using Content.Shared.Mind;
 using Content.Shared.Mind.Components;
 using Content.Shared.Roles;
@@ -19,10 +17,10 @@ using Content.Shared.Silicons.Laws.Components;
 using Content.Shared.Stunnable;
 using Content.Shared.Wires;
 using Robust.Server.GameObjects;
+using Robust.Shared.Containers;
 using Robust.Shared.Player;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Toolshed;
-using Robust.Shared.Utility;
 
 namespace Content.Server.Silicons.Laws;
 
@@ -32,11 +30,9 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
     [Dependency] private readonly IChatManager _chatManager = default!;
     [Dependency] private readonly IPrototypeManager _prototype = default!;
     [Dependency] private readonly SharedMindSystem _mind = default!;
-    [Dependency] private readonly SharedActionsSystem _actions = default!;
     [Dependency] private readonly StationSystem _station = default!;
     [Dependency] private readonly UserInterfaceSystem _userInterface = default!;
     [Dependency] private readonly SharedStunSystem _stunSystem = default!;
-    [Dependency] private readonly IEntityManager _entityManager = default!;
     [Dependency] private readonly SharedRoleSystem _roles = default!;
 
     /// <inheritdoc/>
@@ -44,7 +40,6 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
     {
         base.Initialize();
 
-        SubscribeLocalEvent<SiliconLawBoundComponent, ComponentShutdown>(OnComponentShutdown);
         SubscribeLocalEvent<SiliconLawBoundComponent, MapInitEvent>(OnMapInit);
         SubscribeLocalEvent<SiliconLawBoundComponent, MindAddedMessage>(OnMindAdded);
         SubscribeLocalEvent<SiliconLawBoundComponent, ToggleLawsScreenEvent>(OnToggleLawsScreen);
@@ -58,15 +53,8 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
         SubscribeLocalEvent<EmagSiliconLawComponent, MindRemovedMessage>(OnEmagMindRemoved);
     }
 
-    private void OnComponentShutdown(EntityUid uid, SiliconLawBoundComponent component, ComponentShutdown args)
-    {
-        if (component.ViewLawsActionEntity != null)
-            _actions.RemoveAction(uid, component.ViewLawsActionEntity);
-    }
-
     private void OnMapInit(EntityUid uid, SiliconLawBoundComponent component, MapInitEvent args)
     {
-        _actions.AddAction(uid, ref component.ViewLawsActionEntity, component.ViewLawsAction);
         GetLaws(uid, component);
     }
 
@@ -92,7 +80,7 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
 
     private void OnBoundUIOpened(EntityUid uid, SiliconLawBoundComponent component, BoundUIOpenedEvent args)
     {
-        _entityManager.TryGetComponent<IntrinsicRadioTransmitterComponent>(uid, out var intrinsicRadio);
+        TryComp(uid, out IntrinsicRadioTransmitterComponent? intrinsicRadio);
         var radioChannels = intrinsicRadio?.Channels;
 
         var state = new SiliconLawBuiState(GetLaws(uid).Laws, radioChannels);
@@ -264,9 +252,9 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
     /// <summary>
     /// Extract all the laws from a lawset's prototype ids.
     /// </summary>
-    public SiliconLawset GetLawset(string lawset)
+    public SiliconLawset GetLawset(ProtoId<SiliconLawsetPrototype> lawset)
     {
-        var proto = _prototype.Index<SiliconLawsetPrototype>(lawset);
+        var proto = _prototype.Index(lawset);
         var laws = new SiliconLawset()
         {
             Laws = new List<SiliconLaw>(proto.Laws.Count)
@@ -294,6 +282,21 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
         component.Lawset.Laws = newLaws;
         NotifyLawsChanged(target);
     }
+
+    protected override void OnUpdaterInsert(Entity<SiliconLawUpdaterComponent> ent, ref EntInsertedIntoContainerMessage args)
+    {
+        // TODO: Prediction dump this
+        if (!TryComp(args.Entity, out SiliconLawProviderComponent? provider))
+            return;
+
+        var lawset = GetLawset(provider.Laws).Laws;
+        var query = EntityManager.CompRegistryQueryEnumerator(ent.Comp.Components);
+
+        while (query.MoveNext(out var update))
+        {
+            SetLaws(lawset, update);
+        }
+    }
 }
 
 [ToolshedCommand, AdminCommand(AdminFlags.Admin)]
diff --git a/Content.Server/Silicons/StationAi/AiInteractWireAction.cs b/Content.Server/Silicons/StationAi/AiInteractWireAction.cs
new file mode 100644 (file)
index 0000000..c92c825
--- /dev/null
@@ -0,0 +1,37 @@
+using Content.Server.Wires;
+using Content.Shared.Doors;
+using Content.Shared.Silicons.StationAi;
+using Content.Shared.Wires;
+
+namespace Content.Server.Silicons.StationAi;
+
+/// <summary>
+/// Controls whether an AI can interact with the target entity.
+/// </summary>
+public sealed partial class AiInteractWireAction : ComponentWireAction<StationAiWhitelistComponent>
+{
+    public override string Name { get; set; } = "wire-name-ai-act-light";
+    public override Color Color { get; set; } = Color.DeepSkyBlue;
+    public override object StatusKey => AirlockWireStatus.AiControlIndicator;
+
+    public override StatusLightState? GetLightState(Wire wire, StationAiWhitelistComponent component)
+    {
+        return component.Enabled ? StatusLightState.On : StatusLightState.Off;
+    }
+
+    public override bool Cut(EntityUid user, Wire wire, StationAiWhitelistComponent component)
+    {
+        return EntityManager.System<SharedStationAiSystem>()
+            .SetWhitelistEnabled((component.Owner, component), false, announce: true);
+    }
+
+    public override bool Mend(EntityUid user, Wire wire, StationAiWhitelistComponent component)
+    {
+        return EntityManager.System<SharedStationAiSystem>()
+            .SetWhitelistEnabled((component.Owner, component), true);
+    }
+
+    public override void Pulse(EntityUid user, Wire wire, StationAiWhitelistComponent component)
+    {
+    }
+}
diff --git a/Content.Server/Silicons/StationAi/AiVisionWireAction.cs b/Content.Server/Silicons/StationAi/AiVisionWireAction.cs
new file mode 100644 (file)
index 0000000..3523f4d
--- /dev/null
@@ -0,0 +1,40 @@
+using Content.Server.Wires;
+using Content.Shared.Doors;
+using Content.Shared.Silicons.StationAi;
+using Content.Shared.StationAi;
+using Content.Shared.Wires;
+
+namespace Content.Server.Silicons.StationAi;
+
+/// <summary>
+/// Handles StationAiVision functionality for the attached entity.
+/// </summary>
+public sealed partial class AiVisionWireAction : ComponentWireAction<StationAiVisionComponent>
+{
+    public override string Name { get; set; } = "wire-name-ai-vision-light";
+    public override Color Color { get; set; } = Color.DeepSkyBlue;
+    public override object StatusKey => AirlockWireStatus.AiControlIndicator;
+
+    public override StatusLightState? GetLightState(Wire wire, StationAiVisionComponent component)
+    {
+        return component.Enabled ? StatusLightState.On : StatusLightState.Off;
+    }
+
+    public override bool Cut(EntityUid user, Wire wire, StationAiVisionComponent component)
+    {
+        return EntityManager.System<SharedStationAiSystem>()
+            .SetVisionEnabled((component.Owner, component), false, announce: true);
+    }
+
+    public override bool Mend(EntityUid user, Wire wire, StationAiVisionComponent component)
+    {
+        return EntityManager.System<SharedStationAiSystem>()
+            .SetVisionEnabled((component.Owner, component), true);
+    }
+
+    public override void Pulse(EntityUid user, Wire wire, StationAiVisionComponent component)
+    {
+        // TODO: This should turn it off for a bit
+        // Need timer cleanup first out of scope.
+    }
+}
diff --git a/Content.Server/Silicons/StationAi/StationAiSystem.cs b/Content.Server/Silicons/StationAi/StationAiSystem.cs
new file mode 100644 (file)
index 0000000..8464973
--- /dev/null
@@ -0,0 +1,76 @@
+using System.Linq;
+using Content.Server.Chat.Managers;
+using Content.Server.Chat.Systems;
+using Content.Shared.Chat;
+using Content.Shared.Silicons.StationAi;
+using Content.Shared.StationAi;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Player;
+
+namespace Content.Server.Silicons.StationAi;
+
+public sealed class StationAiSystem : SharedStationAiSystem
+{
+    [Dependency] private readonly IChatManager _chats = default!;
+    [Dependency] private readonly EntityLookupSystem _lookup = default!;
+
+    private readonly HashSet<Entity<StationAiCoreComponent>> _ais = new();
+
+    public override bool SetVisionEnabled(Entity<StationAiVisionComponent> entity, bool enabled, bool announce = false)
+    {
+        if (!base.SetVisionEnabled(entity, enabled, announce))
+            return false;
+
+        if (announce)
+        {
+            AnnounceSnip(entity.Owner);
+        }
+
+        return true;
+    }
+
+    public override bool SetWhitelistEnabled(Entity<StationAiWhitelistComponent> entity, bool enabled, bool announce = false)
+    {
+        if (!base.SetWhitelistEnabled(entity, enabled, announce))
+            return false;
+
+        if (announce)
+        {
+            AnnounceSnip(entity.Owner);
+        }
+
+        return true;
+    }
+
+    private void AnnounceSnip(EntityUid entity)
+    {
+        var xform = Transform(entity);
+
+        if (!TryComp(xform.GridUid, out MapGridComponent? grid))
+            return;
+
+        _ais.Clear();
+        _lookup.GetChildEntities(xform.GridUid.Value, _ais);
+        var filter = Filter.Empty();
+
+        foreach (var ai in _ais)
+        {
+            // TODO: Filter API?
+            if (TryComp(ai.Owner, out ActorComponent? actorComp))
+            {
+                filter.AddPlayer(actorComp.PlayerSession);
+            }
+        }
+
+        // TEST
+        // filter = Filter.Broadcast();
+
+        // No easy way to do chat notif embeds atm.
+        var tile = Maps.LocalToTile(xform.GridUid.Value, grid, xform.Coordinates);
+        var msg = Loc.GetString("ai-wire-snipped", ("coords", tile));
+
+        _chats.ChatMessageToMany(ChatChannel.Notifications, msg, msg, entity, false, true, filter.Recipients.Select(o => o.Channel));
+        // Apparently there's no sound for this.
+    }
+}
index d2c2a8a1ca7adbed2053a9e17dd84ac5b4e8161a..1ad1bb6c0a122299899d30dbdd69d508d6d87a31 100644 (file)
@@ -1,7 +1,6 @@
 using Content.Server.Power.Components;
 using Content.Server.Power.EntitySystems;
 using Content.Shared.Power;
-using Content.Shared.Power.Components;
 using Content.Shared.Sound;
 using Content.Shared.Sound.Components;
 
index e960a2bbbe2c7a6c7d044fe6d3723c89f1d31a5c..fcbc3aa52f69871711126b9cf40aa3b094b1453d 100644 (file)
@@ -17,6 +17,7 @@ using Content.Shared.Humanoid.Prototypes;
 using Content.Shared.PDA;
 using Content.Shared.Preferences;
 using Content.Shared.Preferences.Loadouts;
+using Content.Shared.Preferences.Loadouts.Effects;
 using Content.Shared.Random;
 using Content.Shared.Random.Helpers;
 using Content.Shared.Roles;
@@ -150,6 +151,22 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
         EntityUid? entity = null)
     {
         _prototypeManager.TryIndex(job?.Prototype ?? string.Empty, out var prototype);
+        RoleLoadout? loadout = null;
+
+        // Need to get the loadout up-front to handle names if we use an entity spawn override.
+        var jobLoadout = LoadoutSystem.GetJobPrototype(prototype?.ID);
+
+        if (_prototypeManager.TryIndex(jobLoadout, out RoleLoadoutPrototype? roleProto))
+        {
+            profile?.Loadouts.TryGetValue(jobLoadout, out loadout);
+
+            // Set to default if not present
+            if (loadout == null)
+            {
+                loadout = new RoleLoadout(jobLoadout);
+                loadout.SetDefault(profile, _actors.GetSession(entity), _prototypeManager);
+            }
+        }
 
         // If we're not spawning a humanoid, we're gonna exit early without doing all the humanoid stuff.
         if (prototype?.JobEntity != null)
@@ -157,6 +174,13 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
             DebugTools.Assert(entity is null);
             var jobEntity = EntityManager.SpawnEntity(prototype.JobEntity, coordinates);
             MakeSentientCommand.MakeSentient(jobEntity, EntityManager);
+
+            // Make sure custom names get handled, what is gameticker control flow whoopy.
+            if (loadout != null)
+            {
+                EquipRoleName(jobEntity, loadout, roleProto!);
+            }
+
             DoJobSpecials(job, jobEntity);
             _identity.QueueIdentityUpdate(jobEntity);
             return jobEntity;
@@ -188,21 +212,9 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
             profile = HumanoidCharacterProfile.RandomWithSpecies(speciesId);
         }
 
-        var jobLoadout = LoadoutSystem.GetJobPrototype(prototype?.ID);
-
-        if (_prototypeManager.TryIndex(jobLoadout, out RoleLoadoutPrototype? roleProto))
+        if (loadout != null)
         {
-            RoleLoadout? loadout = null;
-            profile?.Loadouts.TryGetValue(jobLoadout, out loadout);
-
-            // Set to default if not present
-            if (loadout == null)
-            {
-                loadout = new RoleLoadout(jobLoadout);
-                loadout.SetDefault(profile, _actors.GetSession(entity), _prototypeManager);
-            }
-
-            EquipRoleLoadout(entity.Value, loadout, roleProto);
+            EquipRoleLoadout(entity.Value, loadout, roleProto!);
         }
 
         if (prototype?.StartingGear != null)
index 5970e16319615a1749bbc18d12f700260e6bf8d7..c9be87c623188be9d3460fb3b619cd3f5c4caf8b 100644 (file)
@@ -38,7 +38,7 @@ public sealed class EnergySwordSystem : EntitySystem
         if (args.Handled)
             return;
 
-        if (!_toolSystem.HasQuality(args.Used, "Pulsing"))
+        if (!_toolSystem.HasQuality(args.Used, SharedToolSystem.PulseQuality))
             return;
 
         args.Handled = true;
index 019e09bbbbc01822fcf4f92ef0f4bf7427c43d13..9d2fd58980f2ce1bfe7bd5fcedf76a13bfd9a8f0 100644 (file)
@@ -44,7 +44,7 @@ public sealed class ArtifactElectricityTriggerSystem : EntitySystem
         if (args.Handled)
             return;
 
-        if (!_toolSystem.HasQuality(args.Used, "Pulsing"))
+        if (!_toolSystem.HasQuality(args.Used, SharedToolSystem.PulseQuality))
             return;
 
         args.Handled = _artifactSystem.TryActivateArtifact(uid, args.User);
index afa1e19eaded26f713f72ce6037aa75ea313ae5f..8a4b5baffd34d2c1d421b5b7c2ddd09a49296391 100644 (file)
@@ -2,6 +2,7 @@ using Content.Shared.Body.Events;
 using Content.Shared.Emoting;
 using Content.Shared.Hands;
 using Content.Shared.Interaction;
+using Content.Shared.Interaction.Components;
 using Content.Shared.Interaction.Events;
 using Content.Shared.Item;
 using Content.Shared.Movement.Components;
@@ -22,9 +23,14 @@ namespace Content.Shared.ActionBlocker
     {
         [Dependency] private readonly SharedContainerSystem _container = default!;
 
+        private EntityQuery<ComplexInteractionComponent> _complexInteractionQuery;
+
         public override void Initialize()
         {
             base.Initialize();
+
+            _complexInteractionQuery = GetEntityQuery<ComplexInteractionComponent>();
+
             SubscribeLocalEvent<InputMoverComponent, ComponentStartup>(OnMoverStartup);
         }
 
@@ -53,6 +59,15 @@ namespace Content.Shared.ActionBlocker
             return !ev.Cancelled;
         }
 
+        /// <summary>
+        /// Checks if a given entity is able to do specific complex interactions.
+        /// This is used to gate manipulation to general humanoids. If a mouse shouldn't be able to do something, then it's complex.
+        /// </summary>
+        public bool CanComplexInteract(EntityUid user)
+        {
+            return _complexInteractionQuery.HasComp(user);
+        }
+
         /// <summary>
         ///     Raises an event directed at both the user and the target entity to check whether a user is capable of
         ///     interacting with this entity.
index 9b77d039f465e4da01f3fc7510abda8b0512e019..da194706f8f0a6fcb3f63a7b69c6714f31255ec0 100644 (file)
@@ -358,34 +358,26 @@ public sealed partial class ClimbSystem : VirtualController
             return;
         }
 
-        if (args.OurFixture.Contacts.Count > 1)
+        foreach (var contact in args.OurFixture.Contacts.Values)
         {
-            foreach (var contact in args.OurFixture.Contacts.Values)
+            if (!contact.IsTouching)
+                continue;
+
+            var otherEnt = contact.OtherEnt(uid);
+            var (otherFixtureId, otherFixture) = contact.OtherFixture(uid);
+
+            // TODO: Remove this on engine.
+            if (args.OtherEntity == otherEnt && args.OtherFixtureId == otherFixtureId)
+                continue;
+
+            if (otherFixture is { Hard: true } &&
+                _climbableQuery.HasComp(otherEnt))
             {
-                if (!contact.IsTouching)
-                    continue;
-
-                var otherEnt = contact.EntityA;
-                var otherFixture = contact.FixtureA;
-                var otherFixtureId = contact.FixtureAId;
-                if (uid == contact.EntityA)
-                {
-                    otherEnt = contact.EntityB;
-                    otherFixture = contact.FixtureB;
-                    otherFixtureId = contact.FixtureBId;
-                }
-
-                if (args.OtherEntity == otherEnt && args.OtherFixtureId == otherFixtureId)
-                    continue;
-
-                if (otherFixture is { Hard: true } &&
-                    _climbableQuery.HasComp(otherEnt))
-                {
-                    return;
-                }
+                return;
             }
         }
 
+        // TODO: Is this even needed anymore?
         foreach (var otherFixture in args.OurFixture.Contacts.Keys)
         {
             // If it's the other fixture then ignore em
index 63a0dfe95a50fb7da778c74a69925b1655818540..621871af3cee5f65f0f833ed294ebbf144a6aa4f 100644 (file)
@@ -1,5 +1,6 @@
 using System.Text.RegularExpressions;
 using Content.Shared.Tools;
+using Content.Shared.Tools.Systems;
 using Robust.Shared.GameStates;
 using Robust.Shared.Serialization;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
@@ -13,7 +14,7 @@ namespace Content.Shared.Configurable
         public Dictionary<string, string?> Config = new();
 
         [DataField("qualityNeeded", customTypeSerializer: typeof(PrototypeIdSerializer<ToolQualityPrototype>))]
-        public string QualityNeeded = "Pulsing";
+        public string QualityNeeded = SharedToolSystem.PulseQuality;
 
         [DataField("validation")]
         public Regex Validation = new("^[a-zA-Z0-9 ]*$", RegexOptions.Compiled);
index a50ee2c88e9cca97167e7d8e925ae67f4ba902a9..d3fa15ed1b650a478c8f191a5447b299c7d395d4 100644 (file)
@@ -8,7 +8,7 @@ namespace Content.Shared.Doors
         PowerIndicator,
         BoltIndicator,
         BoltLightIndicator,
-        AIControlIndicator,
+        AiControlIndicator,
         TimingIndicator,
         SafetyIndicator,
     }
index f0406c539891da27ab604c23ca640f5d33c93066..3d83886f2d34ba19612668f6d47c88d1527b6a35 100644 (file)
@@ -117,12 +117,25 @@ namespace Content.Shared.Examine
             if (EntityManager.GetComponent<TransformComponent>(examiner).MapID != target.MapId)
                 return false;
 
-            return InRangeUnOccluded(
-                _transform.GetMapCoordinates(examiner),
-                target,
-                GetExaminerRange(examiner),
-                predicate: predicate,
-                ignoreInsideBlocker: true);
+            // Do target InRangeUnoccluded which has different checks.
+            if (examined != null)
+            {
+                return InRangeUnOccluded(
+                    examiner,
+                    examined.Value,
+                    GetExaminerRange(examiner),
+                    predicate: predicate,
+                    ignoreInsideBlocker: true);
+            }
+            else
+            {
+                return InRangeUnOccluded(
+                    examiner,
+                    target,
+                    GetExaminerRange(examiner),
+                    predicate: predicate,
+                    ignoreInsideBlocker: true);
+            }
         }
 
         /// <summary>
@@ -214,6 +227,14 @@ namespace Content.Shared.Examine
 
         public bool InRangeUnOccluded(EntityUid origin, EntityUid other, float range = ExamineRange, Ignored? predicate = null, bool ignoreInsideBlocker = true)
         {
+            var ev = new InRangeOverrideEvent(origin, other);
+            RaiseLocalEvent(origin, ref ev);
+
+            if (ev.Handled)
+            {
+                return ev.InRange;
+            }
+
             var originPos = _transform.GetMapCoordinates(origin);
             var otherPos = _transform.GetMapCoordinates(other);
 
index 1fe66cd3cb86266f4e1159749dc8501f19f41f0d..84beabf9ac859ff9dffae7ab8655ae2205e61bd4 100644 (file)
@@ -161,6 +161,19 @@ public abstract partial class SharedHandsSystem
         return item != null;
     }
 
+    /// <summary>
+    /// Gets active hand item if relevant otherwise gets the entity itself.
+    /// </summary>
+    public EntityUid GetActiveItemOrSelf(Entity<HandsComponent?> entity)
+    {
+        if (!TryGetActiveItem(entity, out var item))
+        {
+            return entity.Owner;
+        }
+
+        return item.Value;
+    }
+
     public Hand? GetActiveHand(Entity<HandsComponent?> entity)
     {
         if (!Resolve(entity, ref entity.Comp))
index e8cc831e4957c42f5ea1e15305227fd20e0da501..8539b9d282b8fdfa10722fd85b5398bedb0381a2 100644 (file)
@@ -17,6 +17,7 @@ using Content.Shared.Movement.Components;
 using Content.Shared.Movement.Pulling.Systems;
 using Content.Shared.Physics;
 using Content.Shared.Popups;
+using Content.Shared.Silicons.StationAi;
 using Content.Shared.Storage;
 using Content.Shared.Tag;
 using Content.Shared.Timing;
@@ -74,7 +75,6 @@ namespace Content.Shared.Interaction
         private EntityQuery<WallMountComponent> _wallMountQuery;
         private EntityQuery<UseDelayComponent> _delayQuery;
         private EntityQuery<ActivatableUIComponent> _uiQuery;
-        private EntityQuery<ComplexInteractionComponent> _complexInteractionQuery;
 
         private const CollisionGroup InRangeUnobstructedMask = CollisionGroup.Impassable | CollisionGroup.InteractImpassable;
 
@@ -97,7 +97,6 @@ namespace Content.Shared.Interaction
             _wallMountQuery = GetEntityQuery<WallMountComponent>();
             _delayQuery = GetEntityQuery<UseDelayComponent>();
             _uiQuery = GetEntityQuery<ActivatableUIComponent>();
-            _complexInteractionQuery = GetEntityQuery<ComplexInteractionComponent>();
 
             SubscribeLocalEvent<BoundUserInterfaceCheckRangeEvent>(HandleUserInterfaceRangeCheck);
             SubscribeLocalEvent<BoundUserInterfaceMessageAttempt>(OnBoundInterfaceInteractAttempt);
@@ -165,7 +164,7 @@ namespace Content.Shared.Interaction
                 return;
             }
 
-            if (uiComp.RequireHands && !_handsQuery.HasComp(ev.Actor))
+            if (uiComp.RequiresComplex && !_actionBlockerSystem.CanComplexInteract(ev.Actor))
                 ev.Cancel();
         }
 
@@ -440,7 +439,7 @@ namespace Content.Shared.Interaction
 
         public void InteractHand(EntityUid user, EntityUid target)
         {
-            var complexInteractions = SupportsComplexInteractions(user);
+            var complexInteractions = _actionBlockerSystem.CanComplexInteract(user);
             if (!complexInteractions)
             {
                 InteractionActivate(user,
@@ -630,6 +629,14 @@ namespace Content.Shared.Interaction
             if (!Resolve(other, ref other.Comp))
                 return false;
 
+            var ev = new InRangeOverrideEvent(origin, other);
+            RaiseLocalEvent(origin, ref ev);
+
+            if (ev.Handled)
+            {
+                return ev.InRange;
+            }
+
             return InRangeUnobstructed(origin,
                 other,
                 other.Comp.Coordinates,
@@ -1128,7 +1135,7 @@ namespace Content.Shared.Interaction
             // Get list of alt-interact verbs
             var verbs = _verbSystem.GetLocalVerbs(target, user, typeof(AlternativeVerb));
 
-            if (!verbs.Any())
+            if (verbs.Count == 0)
                 return false;
 
             _verbSystem.ExecuteVerb(verbs.First(), user, target);
@@ -1182,6 +1189,13 @@ namespace Content.Shared.Interaction
         /// </summary>
         public bool IsAccessible(Entity<TransformComponent?> user, Entity<TransformComponent?> target)
         {
+            var ev = new AccessibleOverrideEvent(user, target);
+
+            RaiseLocalEvent(user, ref ev);
+
+            if (ev.Handled)
+                return ev.Accessible;
+
             if (_containerSystem.IsInSameOrParentContainer(user, target, out _, out var container))
                 return true;
 
@@ -1324,13 +1338,10 @@ namespace Content.Shared.Interaction
             return ev.Handled;
         }
 
-        /// <summary>
-        /// Checks if a given entity is able to do specific complex interactions.
-        /// This is used to gate manipulation to general humanoids. If a mouse shouldn't be able to do something, then it's complex.
-        /// </summary>
+        [Obsolete("Use ActionBlockerSystem")]
         public bool SupportsComplexInteractions(EntityUid user)
         {
-            return _complexInteractionQuery.HasComp(user);
+            return _actionBlockerSystem.CanComplexInteract(user);
         }
     }
 
@@ -1369,17 +1380,38 @@ namespace Content.Shared.Interaction
     };
 
     /// <summary>
-    ///     Raised directed by-ref on an item and a user to determine if interactions can occur.
+    ///     Raised directed by-ref on an item to determine if hand interactions should go through.
+    ///     Defaults to allowing hand interactions to go through. Cancel to force the item to be attacked instead.
     /// </summary>
     /// <param name="Cancelled">Whether the hand interaction should be cancelled.</param>
     [ByRefEvent]
-    public record struct AttemptUseInteractEvent(EntityUid User, EntityUid Used, bool Cancelled = false);
+    public record struct CombatModeShouldHandInteractEvent(bool Cancelled = false);
 
     /// <summary>
-    ///     Raised directed by-ref on an item to determine if hand interactions should go through.
-    ///     Defaults to allowing hand interactions to go through. Cancel to force the item to be attacked instead.
+    /// Override event raised directed on the user to say the target is accessible.
     /// </summary>
-    /// <param name="Cancelled">Whether the hand interaction should be cancelled.</param>
+    /// <param name="User"></param>
+    /// <param name="Target"></param>
     [ByRefEvent]
-    public record struct CombatModeShouldHandInteractEvent(bool Cancelled = false);
+    public record struct AccessibleOverrideEvent(EntityUid User, EntityUid Target)
+    {
+        public readonly EntityUid User = User;
+        public readonly EntityUid Target = Target;
+
+        public bool Handled;
+        public bool Accessible = false;
+    }
+
+    /// <summary>
+    /// Override event raised directed on a user to check InRangeUnoccluded AND InRangeUnobstructed to the target if you require custom logic.
+    /// </summary>
+    [ByRefEvent]
+    public record struct InRangeOverrideEvent(EntityUid User, EntityUid Target)
+    {
+        public readonly EntityUid User = User;
+        public readonly EntityUid Target = Target;
+
+        public bool Handled;
+        public bool InRange = false;
+    }
 }
diff --git a/Content.Shared/Light/Components/LightOnCollideColliderComponent.cs b/Content.Shared/Light/Components/LightOnCollideColliderComponent.cs
new file mode 100644 (file)
index 0000000..39be05a
--- /dev/null
@@ -0,0 +1,13 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Light.Components;
+
+/// <summary>
+/// Can activate <see cref="LightOnCollideComponent"/> when collided with.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class LightOnCollideColliderComponent : Component
+{
+    [DataField]
+    public string FixtureId = "lightTrigger";
+}
diff --git a/Content.Shared/Light/Components/LightOnCollideComponent.cs b/Content.Shared/Light/Components/LightOnCollideComponent.cs
new file mode 100644 (file)
index 0000000..c3b4bd7
--- /dev/null
@@ -0,0 +1,11 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Light.Components;
+
+/// <summary>
+/// Enables / disables pointlight whenever entities are contacting with it
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class LightOnCollideComponent : Component
+{
+}
diff --git a/Content.Shared/Light/EntitySystems/LightCollideSystem.cs b/Content.Shared/Light/EntitySystems/LightCollideSystem.cs
new file mode 100644 (file)
index 0000000..f09ae68
--- /dev/null
@@ -0,0 +1,82 @@
+using Content.Shared.Light.Components;
+using Robust.Shared.Physics.Events;
+using Robust.Shared.Physics.Systems;
+
+namespace Content.Shared.Light.EntitySystems;
+
+public sealed class LightCollideSystem : EntitySystem
+{
+    [Dependency] private readonly SharedPhysicsSystem _physics = default!;
+    [Dependency] private readonly SlimPoweredLightSystem _lights = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<LightOnCollideColliderComponent, PreventCollideEvent>(OnPreventCollide);
+        SubscribeLocalEvent<LightOnCollideColliderComponent, StartCollideEvent>(OnStart);
+        SubscribeLocalEvent<LightOnCollideColliderComponent, EndCollideEvent>(OnEnd);
+
+        SubscribeLocalEvent<LightOnCollideColliderComponent, ComponentShutdown>(OnCollideShutdown);
+    }
+
+    private void OnCollideShutdown(Entity<LightOnCollideColliderComponent> ent, ref ComponentShutdown args)
+    {
+        // TODO: Check this on the event.
+        if (TerminatingOrDeleted(ent.Owner))
+            return;
+
+        // Regenerate contacts for everything we were colliding with.
+        var contacts = _physics.GetContacts(ent.Owner);
+
+        while (contacts.MoveNext(out var contact))
+        {
+            if (!contact.IsTouching)
+                continue;
+
+            var other = contact.OtherEnt(ent.Owner);
+
+            if (HasComp<LightOnCollideComponent>(other))
+            {
+                _physics.RegenerateContacts(other);
+            }
+        }
+    }
+
+    // You may be wondering what de fok this is doing here.
+    // At the moment there's no easy way to do collision whitelists based on components.
+    private void OnPreventCollide(Entity<LightOnCollideColliderComponent> ent, ref PreventCollideEvent args)
+    {
+        if (!HasComp<LightOnCollideComponent>(args.OtherEntity))
+        {
+            args.Cancelled = true;
+        }
+    }
+
+    private void OnEnd(Entity<LightOnCollideColliderComponent> ent, ref EndCollideEvent args)
+    {
+        if (args.OurFixtureId != ent.Comp.FixtureId)
+            return;
+
+        if (!HasComp<LightOnCollideComponent>(args.OtherEntity))
+            return;
+
+        // TODO: Engine bug IsTouching box2d yay.
+        var contacts = _physics.GetTouchingContacts(args.OtherEntity) - 1;
+
+        if (contacts > 0)
+            return;
+
+        _lights.SetEnabled(args.OtherEntity, false);
+    }
+
+    private void OnStart(Entity<LightOnCollideColliderComponent> ent, ref StartCollideEvent args)
+    {
+        if (args.OurFixtureId != ent.Comp.FixtureId)
+            return;
+
+        if (!HasComp<LightOnCollideComponent>(args.OtherEntity))
+            return;
+
+        _lights.SetEnabled(args.OtherEntity, true);
+    }
+}
index 6d984ed19a364dacea0364f482ed0c8a237f26a1..4cf9b25dadcb29fd6faaf4915f6e60dc424d4bcf 100644 (file)
@@ -1,6 +1,5 @@
 using Content.Shared.Light.Components;
 using Content.Shared.Power;
-using Content.Shared.Power.Components;
 using Content.Shared.Power.EntitySystems;
 
 namespace Content.Shared.Light.EntitySystems;
index 24b47b641205d28b0919579d14f17a60dec9c912..c8e1c1a4b3a33250e29a31228c7b4ac28bb39879 100644 (file)
@@ -315,6 +315,10 @@ public abstract class SharedMindSystem : EntitySystem
     {
     }
 
+    public virtual void ControlMob(EntityUid user, EntityUid target) {}
+
+    public virtual void ControlMob(NetUserId user, EntityUid target) {}
+
     /// <summary>
     /// Tries to create and add an objective from its prototype id.
     /// </summary>
similarity index 92%
rename from Content.Server/NPC/Pathfinding/PathfindingSystem.Line.cs
rename to Content.Shared/NPC/SharedPathfindingSystem.Line.cs
index 479d5ad77f6b12e1e13b3c0aa5c9aa0ee805d139..500ed1574369988e13945fe5a087edf3cafe75be 100644 (file)
@@ -1,8 +1,8 @@
-namespace Content.Server.NPC.Pathfinding;
+namespace Content.Shared.NPC;
 
-public sealed partial class PathfindingSystem
+public abstract partial class SharedPathfindingSystem
 {
-    public void GridCast(Vector2i start, Vector2i end, Vector2iCallback callback)
+    public static void GridCast(Vector2i start, Vector2i end, Vector2iCallback callback)
     {
         // https://gist.github.com/Pyr3z/46884d67641094d6cf353358566db566
         // declare all locals at the top so it's obvious how big the footprint is
index 8831acc1ddb73380f949acfa37d045b3c210e914..0be5f69755622f49a992767cdad47b6d635eec05 100644 (file)
@@ -2,7 +2,7 @@ using System.Numerics;
 
 namespace Content.Shared.NPC;
 
-public abstract class SharedPathfindingSystem : EntitySystem
+public abstract partial class SharedPathfindingSystem : EntitySystem
 {
     /// <summary>
     /// This is equivalent to agent radii for navmeshes. In our case it's preferable that things are cleanly
@@ -37,4 +37,31 @@ public abstract class SharedPathfindingSystem : EntitySystem
         var ab = Vector2.Abs(diff);
         return ab.X + ab.Y + (1.41f - 2) * Math.Min(ab.X, ab.Y);
     }
+
+    public static IEnumerable<Vector2i> GetTileOutline(Vector2i center, float radius)
+    {
+        // https://www.redblobgames.com/grids/circle-drawing/
+        var vecCircle = center + Vector2.One / 2f;
+
+        for (var r = 0; r <= Math.Floor(radius * MathF.Sqrt(0.5f)); r++)
+        {
+            var d = MathF.Floor(MathF.Sqrt(radius * radius - r * r));
+
+            yield return new Vector2(vecCircle.X - d, vecCircle.Y + r).Floored();
+
+            yield return new Vector2(vecCircle.X + d, vecCircle.Y + r).Floored();
+
+            yield return new Vector2(vecCircle.X - d, vecCircle.Y - r).Floored();
+
+            yield return new Vector2(vecCircle.X + d, vecCircle.Y - r).Floored();
+
+            yield return new Vector2(vecCircle.X + r, vecCircle.Y - d).Floored();
+
+            yield return new Vector2(vecCircle.X + r, vecCircle.Y + d).Floored();
+
+            yield return new Vector2(vecCircle.X - r, vecCircle.Y - d).Floored();
+
+            yield return new Vector2(vecCircle.X - r, vecCircle.Y + d).Floored();
+        }
+    }
 }
index 36619ab1046c4bf5f647f1a95fec4d64922fdc15..7a29bad6678891809c7b9eae5dbad9952dfbb98d 100644 (file)
@@ -1,3 +1,4 @@
+using Content.Shared.Dataset;
 using Robust.Shared.Prototypes;
 
 namespace Content.Shared.Preferences.Loadouts;
@@ -15,10 +16,17 @@ public sealed partial class RoleLoadoutPrototype : IPrototype
     [IdDataField]
     public string ID { get; } = string.Empty;
 
+    /// <summary>
+    /// Should we use a random name for this loadout?
+    /// </summary>
+    [DataField]
+    public ProtoId<DatasetPrototype>? NameDataset;
+
+    // Not required so people can set their names.
     /// <summary>
     /// Groups that comprise this role loadout.
     /// </summary>
-    [DataField(required: true)]
+    [DataField]
     public List<ProtoId<LoadoutGroupPrototype>> Groups = new();
 
     /// <summary>
index 824d057b3ea011427f0dcd0cbb5ebe2b9d35c8b5..0fb9c5920fa7590d509902758c10aa907e81a92e 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Shared.Actions;
+using Robust.Shared.GameStates;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization;
 
@@ -7,21 +8,9 @@ namespace Content.Shared.Silicons.Laws.Components;
 /// <summary>
 /// This is used for entities which are bound to silicon laws and can view them.
 /// </summary>
-[RegisterComponent, Access(typeof(SharedSiliconLawSystem))]
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSiliconLawSystem))]
 public sealed partial class SiliconLawBoundComponent : Component
 {
-    /// <summary>
-    /// The sidebar action that toggles the laws screen.
-    /// </summary>
-    [DataField]
-    public EntProtoId ViewLawsAction = "ActionViewLaws";
-
-    /// <summary>
-    /// The action for toggling laws. Stored here so we can remove it later.
-    /// </summary>
-    [DataField]
-    public EntityUid? ViewLawsActionEntity;
-
     /// <summary>
     /// The last entity that provided laws to this entity.
     /// </summary>
diff --git a/Content.Shared/Silicons/Laws/Components/SiliconLawUpdaterComponent.cs b/Content.Shared/Silicons/Laws/Components/SiliconLawUpdaterComponent.cs
new file mode 100644 (file)
index 0000000..e28bf88
--- /dev/null
@@ -0,0 +1,17 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Silicons.Laws.Components;
+
+/// <summary>
+/// Whenever an entity is inserted with silicon laws it will update the relevant entity's laws.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class SiliconLawUpdaterComponent : Component
+{
+    /// <summary>
+    /// Entities to update
+    /// </summary>
+    [DataField(required: true)]
+    public ComponentRegistry Components;
+}
diff --git a/Content.Shared/Silicons/Laws/SharedSiliconLawSystem.Updater.cs b/Content.Shared/Silicons/Laws/SharedSiliconLawSystem.Updater.cs
new file mode 100644 (file)
index 0000000..9fbef58
--- /dev/null
@@ -0,0 +1,17 @@
+using Content.Shared.Silicons.Laws.Components;
+using Robust.Shared.Containers;
+
+namespace Content.Shared.Silicons.Laws;
+
+public abstract partial class SharedSiliconLawSystem
+{
+    private void InitializeUpdater()
+    {
+        SubscribeLocalEvent<SiliconLawUpdaterComponent, EntInsertedIntoContainerMessage>(OnUpdaterInsert);
+    }
+
+    protected virtual void OnUpdaterInsert(Entity<SiliconLawUpdaterComponent> ent, ref EntInsertedIntoContainerMessage args)
+    {
+        // TODO: Prediction
+    }
+}
index c0619e6e43d47d5a67cb5580c2a3aca3a3fa7bfe..a30e7c8980f98c2784aa80d0171152fa24113d8f 100644 (file)
@@ -8,13 +8,14 @@ namespace Content.Shared.Silicons.Laws;
 /// <summary>
 /// This handles getting and displaying the laws for silicons.
 /// </summary>
-public abstract class SharedSiliconLawSystem : EntitySystem
+public abstract partial class SharedSiliconLawSystem : EntitySystem
 {
     [Dependency] private readonly SharedPopupSystem _popup = default!;
 
     /// <inheritdoc/>
     public override void Initialize()
     {
+        InitializeUpdater();
         SubscribeLocalEvent<EmagSiliconLawComponent, GotEmaggedEvent>(OnGotEmagged);
         SubscribeLocalEvent<EmagSiliconLawComponent, OnAttemptEmagEvent>(OnAttemptEmag);
     }
diff --git a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Airlock.cs b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Airlock.cs
new file mode 100644 (file)
index 0000000..ff6fc1e
--- /dev/null
@@ -0,0 +1,25 @@
+using Content.Shared.Doors.Components;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Silicons.StationAi;
+
+public abstract partial class SharedStationAiSystem
+{
+    // Handles airlock radial
+
+    private void InitializeAirlock()
+    {
+        SubscribeLocalEvent<DoorBoltComponent, StationAiBoltEvent>(OnAirlockBolt);
+    }
+
+    private void OnAirlockBolt(EntityUid ent, DoorBoltComponent component, StationAiBoltEvent args)
+    {
+        _doors.SetBoltsDown((ent, component), args.Bolted, args.User, predicted: true);
+    }
+}
+
+[Serializable, NetSerializable]
+public sealed class StationAiBoltEvent : BaseStationAiAction
+{
+    public bool Bolted;
+}
diff --git a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Held.cs b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Held.cs
new file mode 100644 (file)
index 0000000..a6c57f5
--- /dev/null
@@ -0,0 +1,187 @@
+using System.Diagnostics.CodeAnalysis;
+using Content.Shared.Actions.Events;
+using Content.Shared.Interaction.Events;
+using Content.Shared.Verbs;
+using Robust.Shared.Serialization;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Silicons.StationAi;
+
+public abstract partial class SharedStationAiSystem
+{
+    /*
+     * Added when an entity is inserted into a StationAiCore.
+     */
+
+    private void InitializeHeld()
+    {
+        SubscribeLocalEvent<StationAiRadialMessage>(OnRadialMessage);
+        SubscribeLocalEvent<BoundUserInterfaceMessageAttempt>(OnMessageAttempt);
+        SubscribeLocalEvent<StationAiWhitelistComponent, GetVerbsEvent<AlternativeVerb>>(OnTargetVerbs);
+
+        SubscribeLocalEvent<StationAiHeldComponent, InteractionAttemptEvent>(OnHeldInteraction);
+        SubscribeLocalEvent<StationAiHeldComponent, AttemptRelayActionComponentChangeEvent>(OnHeldRelay);
+        SubscribeLocalEvent<StationAiHeldComponent, JumpToCoreEvent>(OnCoreJump);
+    }
+
+    private void OnCoreJump(Entity<StationAiHeldComponent> ent, ref JumpToCoreEvent args)
+    {
+        if (!TryGetCore(ent.Owner, out var core) || core.Comp?.RemoteEntity == null)
+            return;
+
+        _xforms.DropNextTo(core.Comp.RemoteEntity.Value, core.Owner) ;
+    }
+
+    /// <summary>
+    /// Tries to get the entity held in the AI core.
+    /// </summary>
+    private bool TryGetHeld(Entity<StationAiCoreComponent?> entity, out EntityUid held)
+    {
+        held = EntityUid.Invalid;
+
+        if (!Resolve(entity.Owner, ref entity.Comp))
+            return false;
+
+        if (!_containers.TryGetContainer(entity.Owner, StationAiCoreComponent.Container, out var container) ||
+            container.ContainedEntities.Count == 0)
+            return false;
+
+        held = container.ContainedEntities[0];
+        return true;
+    }
+
+    private bool TryGetCore(EntityUid ent, out Entity<StationAiCoreComponent?> core)
+    {
+        if (!_containers.TryGetContainingContainer(ent, out var container) ||
+            container.ID != StationAiCoreComponent.Container ||
+            !TryComp(container.Owner, out StationAiCoreComponent? coreComp) ||
+            coreComp.RemoteEntity == null)
+        {
+            core = (EntityUid.Invalid, null);
+            return false;
+        }
+
+        core = (container.Owner, coreComp);
+        return true;
+    }
+
+    private void OnHeldRelay(Entity<StationAiHeldComponent> ent, ref AttemptRelayActionComponentChangeEvent args)
+    {
+        if (!TryGetCore(ent.Owner, out var core))
+            return;
+
+        args.Target = core.Comp?.RemoteEntity;
+    }
+
+    private void OnRadialMessage(StationAiRadialMessage ev)
+    {
+        if (!TryGetEntity(ev.Entity, out var target))
+            return;
+
+        ev.Event.User = ev.Actor;
+        RaiseLocalEvent(target.Value, (object) ev.Event);
+    }
+
+    private void OnMessageAttempt(BoundUserInterfaceMessageAttempt ev)
+    {
+        if (ev.Actor == ev.Target)
+            return;
+
+        if (TryComp(ev.Actor, out StationAiHeldComponent? aiComp) &&
+           (!ValidateAi((ev.Actor, aiComp)) ||
+            !HasComp<StationAiWhitelistComponent>(ev.Target)))
+        {
+            ev.Cancel();
+        }
+    }
+
+    private void OnHeldInteraction(Entity<StationAiHeldComponent> ent, ref InteractionAttemptEvent args)
+    {
+        // Cancel if it's not us or something with a whitelist.
+        args.Cancelled = ent.Owner != args.Target &&
+                         args.Target != null &&
+                         (!TryComp(args.Target, out StationAiWhitelistComponent? whitelist) || !whitelist.Enabled);
+    }
+
+    private void OnTargetVerbs(Entity<StationAiWhitelistComponent> ent, ref GetVerbsEvent<AlternativeVerb> args)
+    {
+        if (!args.CanComplexInteract ||
+            !ent.Comp.Enabled ||
+            !HasComp<StationAiHeldComponent>(args.User) ||
+            !HasComp<StationAiWhitelistComponent>(args.Target))
+        {
+            return;
+        }
+
+        var user = args.User;
+        var target = args.Target;
+
+        var isOpen = _uiSystem.IsUiOpen(target, AiUi.Key, user);
+
+        args.Verbs.Add(new AlternativeVerb()
+        {
+            Text = isOpen ? Loc.GetString("ai-close") : Loc.GetString("ai-open"),
+            Act = () =>
+            {
+                if (isOpen)
+                {
+                    _uiSystem.CloseUi(ent.Owner, AiUi.Key, user);
+                }
+                else
+                {
+                    _uiSystem.OpenUi(ent.Owner, AiUi.Key, user);
+                }
+            }
+        });
+    }
+}
+
+/// <summary>
+/// Raised from client to server as a BUI message wrapping the event to perform.
+/// Also handles AI action validation.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed class StationAiRadialMessage : BoundUserInterfaceMessage
+{
+    public BaseStationAiAction Event = default!;
+}
+
+// Do nothing on server just here for shared move along.
+/// <summary>
+/// Raised on client to get the relevant data for radial actions.
+/// </summary>
+public sealed class StationAiRadial : BaseStationAiAction
+{
+    public SpriteSpecifier? Sprite;
+
+    public string? Tooltip;
+
+    public BaseStationAiAction Event = default!;
+}
+
+/// <summary>
+/// Abstract parent for radial actions events.
+/// When a client requests a radial action this will get sent.
+/// </summary>
+[Serializable, NetSerializable]
+public abstract class BaseStationAiAction
+{
+    [field:NonSerialized]
+    public EntityUid User { get; set; }
+}
+
+// No idea if there's a better way to do this.
+/// <summary>
+/// Grab actions possible for an AI on the target entity.
+/// </summary>
+[ByRefEvent]
+public record struct GetStationAiRadialEvent()
+{
+    public List<StationAiRadial> Actions = new();
+}
+
+[Serializable, NetSerializable]
+public enum AiUi : byte
+{
+    Key,
+}
diff --git a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Light.cs b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Light.cs
new file mode 100644 (file)
index 0000000..bc2e366
--- /dev/null
@@ -0,0 +1,28 @@
+using Content.Shared.Light.Components;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Silicons.StationAi;
+
+public abstract partial class SharedStationAiSystem
+{
+    // Handles light toggling.
+
+    private void InitializeLight()
+    {
+        SubscribeLocalEvent<ItemTogglePointLightComponent, StationAiLightEvent>(OnLight);
+    }
+
+    private void OnLight(EntityUid ent, ItemTogglePointLightComponent component, StationAiLightEvent args)
+    {
+        if (args.Enabled)
+            _toggles.TryActivate(ent, user: args.User);
+        else
+            _toggles.TryDeactivate(ent, user: args.User);
+    }
+}
+
+[Serializable, NetSerializable]
+public sealed class StationAiLightEvent : BaseStationAiAction
+{
+    public bool Enabled;
+}
diff --git a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs
new file mode 100644 (file)
index 0000000..348b0b0
--- /dev/null
@@ -0,0 +1,412 @@
+using Content.Shared.ActionBlocker;
+using Content.Shared.Actions;
+using Content.Shared.Administration.Managers;
+using Content.Shared.Containers.ItemSlots;
+using Content.Shared.Database;
+using Content.Shared.Doors.Systems;
+using Content.Shared.Interaction;
+using Content.Shared.Item.ItemToggle;
+using Content.Shared.Mind;
+using Content.Shared.Movement.Components;
+using Content.Shared.Movement.Systems;
+using Content.Shared.Power;
+using Content.Shared.StationAi;
+using Content.Shared.Verbs;
+using Robust.Shared.Containers;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Network;
+using Robust.Shared.Physics;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+using Robust.Shared.Timing;
+
+namespace Content.Shared.Silicons.StationAi;
+
+public abstract partial class SharedStationAiSystem : EntitySystem
+{
+    [Dependency] private   readonly ISharedAdminManager _admin = default!;
+    [Dependency] private   readonly IGameTiming _timing = default!;
+    [Dependency] private   readonly INetManager _net = default!;
+    [Dependency] private   readonly ItemSlotsSystem _slots = default!;
+    [Dependency] private   readonly ItemToggleSystem _toggles = default!;
+    [Dependency] private   readonly ActionBlockerSystem _blocker = default!;
+    [Dependency] private   readonly MetaDataSystem _metadata = default!;
+    [Dependency] private   readonly SharedAppearanceSystem _appearance = default!;
+    [Dependency] private   readonly SharedContainerSystem _containers = default!;
+    [Dependency] private   readonly SharedDoorSystem _doors = default!;
+    [Dependency] private   readonly SharedEyeSystem _eye = default!;
+    [Dependency] protected readonly SharedMapSystem Maps = default!;
+    [Dependency] private readonly SharedMindSystem _mind = default!;
+    [Dependency] private   readonly SharedMoverController _mover = default!;
+    [Dependency] private   readonly SharedTransformSystem _xforms = default!;
+    [Dependency] private   readonly SharedUserInterfaceSystem _uiSystem = default!;
+    [Dependency] private   readonly StationAiVisionSystem _vision = default!;
+
+    // StationAiHeld is added to anything inside of an AI core.
+    // StationAiHolder indicates it can hold an AI positronic brain (e.g. holocard / core).
+    // StationAiCore holds functionality related to the core itself.
+    // StationAiWhitelist is a general whitelist to stop it being able to interact with anything
+    // StationAiOverlay handles the static overlay. It also handles interaction blocking on client and server
+    // for anything under it.
+
+    private EntityQuery<BroadphaseComponent> _broadphaseQuery;
+    private EntityQuery<MapGridComponent> _gridQuery;
+
+    [ValidatePrototypeId<EntityPrototype>]
+    private static readonly EntProtoId DefaultAi = "StationAiBrain";
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        _broadphaseQuery = GetEntityQuery<BroadphaseComponent>();
+        _gridQuery = GetEntityQuery<MapGridComponent>();
+
+        InitializeAirlock();
+        InitializeHeld();
+        InitializeLight();
+
+        SubscribeLocalEvent<StationAiWhitelistComponent, BoundUserInterfaceCheckRangeEvent>(OnAiBuiCheck);
+
+        SubscribeLocalEvent<StationAiOverlayComponent, AccessibleOverrideEvent>(OnAiAccessible);
+        SubscribeLocalEvent<StationAiOverlayComponent, InRangeOverrideEvent>(OnAiInRange);
+        SubscribeLocalEvent<StationAiOverlayComponent, MenuVisibilityEvent>(OnAiMenu);
+
+        SubscribeLocalEvent<StationAiHolderComponent, ComponentInit>(OnHolderInit);
+        SubscribeLocalEvent<StationAiHolderComponent, ComponentRemove>(OnHolderRemove);
+        SubscribeLocalEvent<StationAiHolderComponent, AfterInteractEvent>(OnHolderInteract);
+        SubscribeLocalEvent<StationAiHolderComponent, MapInitEvent>(OnHolderMapInit);
+        SubscribeLocalEvent<StationAiHolderComponent, EntInsertedIntoContainerMessage>(OnHolderConInsert);
+        SubscribeLocalEvent<StationAiHolderComponent, EntRemovedFromContainerMessage>(OnHolderConRemove);
+
+        SubscribeLocalEvent<StationAiCoreComponent, EntInsertedIntoContainerMessage>(OnAiInsert);
+        SubscribeLocalEvent<StationAiCoreComponent, EntRemovedFromContainerMessage>(OnAiRemove);
+        SubscribeLocalEvent<StationAiCoreComponent, MapInitEvent>(OnAiMapInit);
+        SubscribeLocalEvent<StationAiCoreComponent, ComponentShutdown>(OnAiShutdown);
+        SubscribeLocalEvent<StationAiCoreComponent, PowerChangedEvent>(OnCorePower);
+        SubscribeLocalEvent<StationAiCoreComponent, GetVerbsEvent<Verb>>(OnCoreVerbs);
+    }
+
+    private void OnCoreVerbs(Entity<StationAiCoreComponent> ent, ref GetVerbsEvent<Verb> args)
+    {
+        if (!_admin.IsAdmin(args.User) ||
+            TryGetHeld((ent.Owner, ent.Comp), out _))
+        {
+            return;
+        }
+
+        var user = args.User;
+
+        args.Verbs.Add(new Verb()
+        {
+            Text = Loc.GetString("station-ai-takeover"),
+            Category = VerbCategory.Debug,
+            Act = () =>
+            {
+                var brain = SpawnInContainerOrDrop(DefaultAi, ent.Owner, StationAiCoreComponent.Container);
+                _mind.ControlMob(user, brain);
+            },
+            Impact = LogImpact.High,
+        });
+    }
+
+    private void OnAiAccessible(Entity<StationAiOverlayComponent> ent, ref AccessibleOverrideEvent args)
+    {
+        args.Handled = true;
+
+        // Hopefully AI never needs storage
+        if (_containers.TryGetContainingContainer(args.Target, out var targetContainer))
+        {
+            return;
+        }
+
+        if (!_containers.IsInSameOrTransparentContainer(args.User, args.Target, otherContainer: targetContainer))
+        {
+            return;
+        }
+
+        args.Accessible = true;
+    }
+
+    private void OnAiMenu(Entity<StationAiOverlayComponent> ent, ref MenuVisibilityEvent args)
+    {
+        args.Visibility &= ~MenuVisibility.NoFov;
+    }
+
+    private void OnAiBuiCheck(Entity<StationAiWhitelistComponent> ent, ref BoundUserInterfaceCheckRangeEvent args)
+    {
+        args.Result = BoundUserInterfaceRangeResult.Fail;
+
+        // Similar to the inrange check but more optimised so server doesn't die.
+        var targetXform = Transform(args.Target);
+
+        // No cross-grid
+        if (targetXform.GridUid != args.Actor.Comp.GridUid)
+        {
+            return;
+        }
+
+        if (!_broadphaseQuery.TryComp(targetXform.GridUid, out var broadphase) || !_gridQuery.TryComp(targetXform.GridUid, out var grid))
+        {
+            return;
+        }
+
+        var targetTile = Maps.LocalToTile(targetXform.GridUid.Value, grid, targetXform.Coordinates);
+
+        lock (_vision)
+        {
+            if (_vision.IsAccessible((targetXform.GridUid.Value, broadphase, grid), targetTile, fastPath: true))
+            {
+                args.Result = BoundUserInterfaceRangeResult.Pass;
+            }
+        }
+    }
+
+    private void OnAiInRange(Entity<StationAiOverlayComponent> ent, ref InRangeOverrideEvent args)
+    {
+        args.Handled = true;
+        var targetXform = Transform(args.Target);
+
+        // No cross-grid
+        if (targetXform.GridUid != Transform(args.User).GridUid)
+        {
+            return;
+        }
+
+        // Validate it's in camera range yes this is expensive.
+        // Yes it needs optimising
+        if (!_broadphaseQuery.TryComp(targetXform.GridUid, out var broadphase) || !_gridQuery.TryComp(targetXform.GridUid, out var grid))
+        {
+            return;
+        }
+
+        var targetTile = Maps.LocalToTile(targetXform.GridUid.Value, grid, targetXform.Coordinates);
+
+        args.InRange = _vision.IsAccessible((targetXform.GridUid.Value, broadphase, grid), targetTile);
+    }
+
+    private void OnHolderInteract(Entity<StationAiHolderComponent> ent, ref AfterInteractEvent args)
+    {
+        if (!TryComp(args.Target, out StationAiHolderComponent? targetHolder))
+            return;
+
+        // Try to insert our thing into them
+        if (_slots.CanEject(ent.Owner, args.User, ent.Comp.Slot))
+        {
+            if (!_slots.TryInsert(args.Target.Value, targetHolder.Slot, ent.Comp.Slot.Item!.Value, args.User, excludeUserAudio: true))
+            {
+                return;
+            }
+
+            args.Handled = true;
+            return;
+        }
+
+        // Otherwise try to take from them
+        if (_slots.CanEject(args.Target.Value, args.User, targetHolder.Slot))
+        {
+            if (!_slots.TryInsert(ent.Owner, ent.Comp.Slot, targetHolder.Slot.Item!.Value, args.User, excludeUserAudio: true))
+            {
+                return;
+            }
+
+            args.Handled = true;
+        }
+    }
+
+    private void OnHolderInit(Entity<StationAiHolderComponent> ent, ref ComponentInit args)
+    {
+        _slots.AddItemSlot(ent.Owner, StationAiHolderComponent.Container, ent.Comp.Slot);
+    }
+
+    private void OnHolderRemove(Entity<StationAiHolderComponent> ent, ref ComponentRemove args)
+    {
+        _slots.RemoveItemSlot(ent.Owner, ent.Comp.Slot);
+    }
+
+    private void OnHolderConInsert(Entity<StationAiHolderComponent> ent, ref EntInsertedIntoContainerMessage args)
+    {
+        UpdateAppearance((ent.Owner, ent.Comp));
+    }
+
+    private void OnHolderConRemove(Entity<StationAiHolderComponent> ent, ref EntRemovedFromContainerMessage args)
+    {
+        UpdateAppearance((ent.Owner, ent.Comp));
+    }
+
+    private void OnHolderMapInit(Entity<StationAiHolderComponent> ent, ref MapInitEvent args)
+    {
+        UpdateAppearance(ent.Owner);
+    }
+
+    private void OnAiShutdown(Entity<StationAiCoreComponent> ent, ref ComponentShutdown args)
+    {
+        // TODO: Tryqueuedel
+        if (_net.IsClient)
+            return;
+
+        QueueDel(ent.Comp.RemoteEntity);
+        ent.Comp.RemoteEntity = null;
+    }
+
+    private void OnCorePower(Entity<StationAiCoreComponent> ent, ref PowerChangedEvent args)
+    {
+        // TODO: I think in 13 they just straightup die so maybe implement that
+        if (args.Powered)
+        {
+            if (!SetupEye(ent))
+                return;
+
+            AttachEye(ent);
+        }
+        else
+        {
+            ClearEye(ent);
+        }
+    }
+
+    private void OnAiMapInit(Entity<StationAiCoreComponent> ent, ref MapInitEvent args)
+    {
+        SetupEye(ent);
+        AttachEye(ent);
+    }
+
+    private bool SetupEye(Entity<StationAiCoreComponent> ent)
+    {
+        if (ent.Comp.RemoteEntity != null)
+            return false;
+
+        if (ent.Comp.RemoteEntityProto != null)
+        {
+            ent.Comp.RemoteEntity = SpawnAtPosition(ent.Comp.RemoteEntityProto, Transform(ent.Owner).Coordinates);
+            Dirty(ent);
+        }
+
+        return true;
+    }
+
+    private void ClearEye(Entity<StationAiCoreComponent> ent)
+    {
+        QueueDel(ent.Comp.RemoteEntity);
+        ent.Comp.RemoteEntity = null;
+    }
+
+    private void AttachEye(Entity<StationAiCoreComponent> ent)
+    {
+        if (ent.Comp.RemoteEntity == null)
+            return;
+
+        if (!_containers.TryGetContainer(ent.Owner, StationAiHolderComponent.Container, out var container) ||
+            container.ContainedEntities.Count != 1)
+        {
+            return;
+        }
+
+        // Attach them to the portable eye that can move around.
+        var user = container.ContainedEntities[0];
+
+        if (TryComp(user, out EyeComponent? eyeComp))
+        {
+            _eye.SetTarget(user, ent.Comp.RemoteEntity.Value, eyeComp);
+        }
+
+        _mover.SetRelay(user, ent.Comp.RemoteEntity.Value);
+    }
+
+    private void OnAiInsert(Entity<StationAiCoreComponent> ent, ref EntInsertedIntoContainerMessage args)
+    {
+        if (_timing.ApplyingState)
+            return;
+
+        // Just so text and the likes works properly
+        _metadata.SetEntityName(ent.Owner, MetaData(args.Entity).EntityName);
+
+        AttachEye(ent);
+    }
+
+    private void OnAiRemove(Entity<StationAiCoreComponent> ent, ref EntRemovedFromContainerMessage args)
+    {
+        if (_timing.ApplyingState)
+            return;
+
+        // Reset name to whatever
+        _metadata.SetEntityName(ent.Owner, Prototype(ent.Owner)?.Name ?? string.Empty);
+
+        // Remove eye relay
+        RemCompDeferred<RelayInputMoverComponent>(args.Entity);
+
+        if (TryComp(args.Entity, out EyeComponent? eyeComp))
+        {
+            _eye.SetTarget(args.Entity, null, eyeComp);
+        }
+    }
+
+    private void UpdateAppearance(Entity<StationAiHolderComponent?> entity)
+    {
+        if (!Resolve(entity.Owner, ref entity.Comp, false))
+            return;
+
+        if (!_containers.TryGetContainer(entity.Owner, StationAiHolderComponent.Container, out var container) ||
+            container.Count == 0)
+        {
+            _appearance.SetData(entity.Owner, StationAiVisualState.Key, StationAiState.Empty);
+            return;
+        }
+
+        _appearance.SetData(entity.Owner, StationAiVisualState.Key, StationAiState.Occupied);
+    }
+
+    public virtual bool SetVisionEnabled(Entity<StationAiVisionComponent> entity, bool enabled, bool announce = false)
+    {
+        if (entity.Comp.Enabled == enabled)
+            return false;
+
+        entity.Comp.Enabled = enabled;
+        Dirty(entity);
+
+        return true;
+    }
+
+    public virtual bool SetWhitelistEnabled(Entity<StationAiWhitelistComponent> entity, bool value, bool announce = false)
+    {
+        if (entity.Comp.Enabled == value)
+            return false;
+
+        entity.Comp.Enabled = value;
+        Dirty(entity);
+
+        return true;
+    }
+
+    /// <summary>
+    /// BUI validation for ai interactions.
+    /// </summary>
+    private bool ValidateAi(Entity<StationAiHeldComponent?> entity)
+    {
+        if (!Resolve(entity.Owner, ref entity.Comp, false))
+        {
+            return false;
+        }
+
+        return _blocker.CanComplexInteract(entity.Owner);
+    }
+}
+
+public sealed partial class JumpToCoreEvent : InstantActionEvent
+{
+
+}
+
+[Serializable, NetSerializable]
+public enum StationAiVisualState : byte
+{
+    Key,
+}
+
+[Serializable, NetSerializable]
+public enum StationAiState : byte
+{
+    Empty,
+    Occupied,
+    Dead,
+}
diff --git a/Content.Shared/Silicons/StationAi/StationAiCoreComponent.cs b/Content.Shared/Silicons/StationAi/StationAiCoreComponent.cs
new file mode 100644 (file)
index 0000000..b7a8b4c
--- /dev/null
@@ -0,0 +1,32 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Silicons.StationAi;
+
+/// <summary>
+/// Indicates this entity can interact with station equipment and is a "Station AI".
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class StationAiCoreComponent : Component
+{
+    /*
+     * I couldn't think of any other reason you'd want to split these out.
+     */
+
+    /// <summary>
+    /// Can it move its camera around and interact remotely with things.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool Remote = true;
+
+    /// <summary>
+    /// The invisible eye entity being used to look around.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public EntityUid? RemoteEntity;
+
+    [DataField(readOnly: true)]
+    public EntProtoId? RemoteEntityProto = "StationAiHolo";
+
+    public const string Container = "station_ai_mind_slot";
+}
diff --git a/Content.Shared/Silicons/StationAi/StationAiHeldComponent.cs b/Content.Shared/Silicons/StationAi/StationAiHeldComponent.cs
new file mode 100644 (file)
index 0000000..6dab1ee
--- /dev/null
@@ -0,0 +1,9 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Silicons.StationAi;
+
+/// <summary>
+/// Indicates this entity is currently held inside of a station AI core.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class StationAiHeldComponent : Component;
diff --git a/Content.Shared/Silicons/StationAi/StationAiHolderComponent.cs b/Content.Shared/Silicons/StationAi/StationAiHolderComponent.cs
new file mode 100644 (file)
index 0000000..221845d
--- /dev/null
@@ -0,0 +1,16 @@
+using Content.Shared.Containers.ItemSlots;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Silicons.StationAi;
+
+/// <summary>
+/// Allows moving a <see cref="StationAiCoreComponent"/> contained entity to and from this component.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class StationAiHolderComponent : Component
+{
+    public const string Container = StationAiCoreComponent.Container;
+
+    [DataField]
+    public ItemSlot Slot = new();
+}
index 94aef8ad366407145d9ba1ce85b95c90f62e2d52..f047fe41e4db127710c573631e4d23d8770b6405 100644 (file)
@@ -1,8 +1,9 @@
+using Content.Shared.Silicons.StationAi;
 using Robust.Shared.GameStates;
 
-namespace Content.Shared.Silicons.StationAi;
+namespace Content.Shared.StationAi;
 
-[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]//, Access(typeof(SharedStationAiSystem))]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedStationAiSystem))]
 public sealed partial class StationAiVisionComponent : Component
 {
     [DataField, AutoNetworkedField]
index c144f330e11ee92c58347d996c65403f41f5d617..bdc62a6bb3788e63a6f330aed36997f6f7612a2b 100644 (file)
@@ -1,4 +1,6 @@
+using Content.Shared.StationAi;
 using Robust.Shared.Map.Components;
+using Robust.Shared.Physics;
 using Robust.Shared.Threading;
 using Robust.Shared.Utility;
 
@@ -24,6 +26,8 @@ public sealed class StationAiVisionSystem : EntitySystem
     private readonly HashSet<Entity<StationAiVisionComponent>> _seeds = new();
     private readonly HashSet<Vector2i> _viewportTiles = new();
 
+    private EntityQuery<OccluderComponent> _occluderQuery;
+
     // Dummy set
     private readonly HashSet<Vector2i> _singleTiles = new();
 
@@ -36,15 +40,12 @@ public sealed class StationAiVisionSystem : EntitySystem
     /// </summary>
     private bool FastPath;
 
-    /// <summary>
-    /// Have we found the target tile if we're only checking for a single one.
-    /// </summary>
-    private bool TargetFound;
-
     public override void Initialize()
     {
         base.Initialize();
 
+        _occluderQuery = GetEntityQuery<OccluderComponent>();
+
         _seedJob = new()
         {
             System = this,
@@ -61,16 +62,16 @@ public sealed class StationAiVisionSystem : EntitySystem
     /// <summary>
     /// Returns whether a tile is accessible based on vision.
     /// </summary>
-    public bool IsAccessible(Entity<MapGridComponent> grid, Vector2i tile, float expansionSize = 8.5f, bool fastPath = false)
+    public bool IsAccessible(Entity<BroadphaseComponent, MapGridComponent> grid, Vector2i tile, float expansionSize = 8.5f, bool fastPath = false)
     {
         _viewportTiles.Clear();
         _opaque.Clear();
         _seeds.Clear();
         _viewportTiles.Add(tile);
-        var localBounds = _lookup.GetLocalBounds(tile, grid.Comp.TileSize);
+        var localBounds = _lookup.GetLocalBounds(tile, grid.Comp2.TileSize);
         var expandedBounds = localBounds.Enlarged(expansionSize);
 
-        _seedJob.Grid = grid;
+        _seedJob.Grid = (grid.Owner, grid.Comp2);
         _seedJob.ExpandedBounds = expandedBounds;
         _parallel.ProcessNow(_seedJob);
         _job.Data.Clear();
@@ -110,21 +111,19 @@ public sealed class StationAiVisionSystem : EntitySystem
             _job.BoundaryTiles.Add(new HashSet<Vector2i>());
         }
 
-        _job.TargetTile = tile;
-        TargetFound = false;
         _singleTiles.Clear();
-        _job.Grid = grid;
+        _job.Grid = (grid.Owner, grid.Comp2);
         _job.VisibleTiles = _singleTiles;
         _parallel.ProcessNow(_job, _job.Data.Count);
 
-        return TargetFound;
+        return _job.VisibleTiles.Contains(tile);
     }
 
-    private bool IsOccluded(Entity<MapGridComponent> grid, Vector2i tile)
+    private bool IsOccluded(Entity<BroadphaseComponent, MapGridComponent> grid, Vector2i tile)
     {
-        var tileBounds = _lookup.GetLocalBounds(tile, grid.Comp.TileSize).Enlarged(-0.05f);
+        var tileBounds = _lookup.GetLocalBounds(tile, grid.Comp2.TileSize).Enlarged(-0.05f);
         _occluders.Clear();
-        _lookup.GetLocalEntitiesIntersecting(grid.Owner, tileBounds, _occluders, LookupFlags.Static);
+        _lookup.GetLocalEntitiesIntersecting((grid.Owner, grid.Comp1), tileBounds, _occluders, query: _occluderQuery, flags: LookupFlags.Static | LookupFlags.Approximate);
         var anyOccluders = false;
 
         foreach (var occluder in _occluders)
@@ -143,17 +142,18 @@ public sealed class StationAiVisionSystem : EntitySystem
     /// Gets a byond-equivalent for tiles in the specified worldAABB.
     /// </summary>
     /// <param name="expansionSize">How much to expand the bounds before to find vision intersecting it. Makes this the largest vision size + 1 tile.</param>
-    public void GetView(Entity<MapGridComponent> grid, Box2Rotated worldBounds, HashSet<Vector2i> visibleTiles, float expansionSize = 8.5f)
+    public void GetView(Entity<BroadphaseComponent, MapGridComponent> grid, Box2Rotated worldBounds, HashSet<Vector2i> visibleTiles, float expansionSize = 8.5f)
     {
         _viewportTiles.Clear();
         _opaque.Clear();
         _seeds.Clear();
-        var expandedBounds = worldBounds.Enlarged(expansionSize);
 
         // TODO: Would be nice to be able to run this while running the other stuff.
-        _seedJob.Grid = grid;
-        var localAABB = _xforms.GetInvWorldMatrix(grid).TransformBox(expandedBounds);
-        _seedJob.ExpandedBounds = localAABB;
+        _seedJob.Grid = (grid.Owner, grid.Comp2);
+        var invMatrix = _xforms.GetInvWorldMatrix(grid);
+        var localAabb = invMatrix.TransformBox(worldBounds);
+        var enlargedLocalAabb = invMatrix.TransformBox(worldBounds.Enlarged(expansionSize));
+        _seedJob.ExpandedBounds = enlargedLocalAabb;
         _parallel.ProcessNow(_seedJob);
         _job.Data.Clear();
         FastPath = false;
@@ -170,7 +170,7 @@ public sealed class StationAiVisionSystem : EntitySystem
             return;
 
         // Get viewport tiles
-        var tileEnumerator = _maps.GetLocalTilesEnumerator(grid, grid, localAABB, ignoreEmpty: false);
+        var tileEnumerator = _maps.GetLocalTilesEnumerator(grid, grid, localAabb, ignoreEmpty: false);
 
         while (tileEnumerator.MoveNext(out var tileRef))
         {
@@ -182,9 +182,8 @@ public sealed class StationAiVisionSystem : EntitySystem
             _viewportTiles.Add(tileRef.GridIndices);
         }
 
-        tileEnumerator = _maps.GetLocalTilesEnumerator(grid, grid, localAABB, ignoreEmpty: false);
+        tileEnumerator = _maps.GetLocalTilesEnumerator(grid, grid, enlargedLocalAabb, ignoreEmpty: false);
 
-        // Get all other relevant tiles.
         while (tileEnumerator.MoveNext(out var tileRef))
         {
             if (_viewportTiles.Contains(tileRef.GridIndices))
@@ -206,9 +205,7 @@ public sealed class StationAiVisionSystem : EntitySystem
             _job.BoundaryTiles.Add(new HashSet<Vector2i>());
         }
 
-        _job.TargetTile = null;
-        TargetFound = false;
-        _job.Grid = grid;
+        _job.Grid = (grid.Owner, grid.Comp2);
         _job.VisibleTiles = visibleTiles;
         _parallel.ProcessNow(_job, _job.Data.Count);
     }
@@ -250,6 +247,7 @@ public sealed class StationAiVisionSystem : EntitySystem
         return false;
     }
 
+    /// <summary>
     /// Checks whether this tile fits the definition of a "corner"
     /// </summary>
     private bool IsCorner(
@@ -287,7 +285,7 @@ public sealed class StationAiVisionSystem : EntitySystem
 
         public void Execute()
         {
-            System._lookup.GetLocalEntitiesIntersecting(Grid.Owner, ExpandedBounds, System._seeds);
+            System._lookup.GetLocalEntitiesIntersecting(Grid.Owner, ExpandedBounds, System._seeds, flags: LookupFlags.All | LookupFlags.Approximate);
         }
     }
 
@@ -302,9 +300,6 @@ public sealed class StationAiVisionSystem : EntitySystem
         public Entity<MapGridComponent> Grid;
         public List<Entity<StationAiVisionComponent>> Data = new();
 
-        // If we're doing range-checks might be able to early out
-        public Vector2i? TargetTile;
-
         public HashSet<Vector2i> VisibleTiles;
 
         public readonly List<Dictionary<Vector2i, int>> Vis1 = new();
@@ -315,18 +310,6 @@ public sealed class StationAiVisionSystem : EntitySystem
 
         public void Execute(int index)
         {
-            // If we're looking for a single tile then early-out if someone else has found it.
-            if (TargetTile != null)
-            {
-                lock (System)
-                {
-                    if (System.TargetFound)
-                    {
-                        return;
-                    }
-                }
-            }
-
             var seed = Data[index];
             var seedXform = EntManager.GetComponent<TransformComponent>(seed);
 
@@ -338,30 +321,11 @@ public sealed class StationAiVisionSystem : EntitySystem
                     Grid.Comp,
                     new Circle(System._xforms.GetWorldPosition(seedXform), seed.Comp.Range), ignoreEmpty: false);
 
-                // Try to find the target tile.
-                if (TargetTile != null)
+                lock (VisibleTiles)
                 {
                     foreach (var tile in squircles)
                     {
-                        if (tile.GridIndices == TargetTile)
-                        {
-                            lock (System)
-                            {
-                                System.TargetFound = true;
-                            }
-
-                            return;
-                        }
-                    }
-                }
-                else
-                {
-                    lock (VisibleTiles)
-                    {
-                        foreach (var tile in squircles)
-                        {
-                            VisibleTiles.Add(tile.GridIndices);
-                        }
+                        VisibleTiles.Add(tile.GridIndices);
                     }
                 }
 
@@ -480,40 +444,21 @@ public sealed class StationAiVisionSystem : EntitySystem
                 vis1[tile] = -1;
             }
 
-            if (TargetTile != null)
-            {
-                if (vis1.TryGetValue(TargetTile.Value, out var tileVis))
-                {
-                    DebugTools.Assert(seedTiles.Contains(TargetTile.Value));
-
-                    if (tileVis != 0)
-                    {
-                        lock (System)
-                        {
-                            System.TargetFound = true;
-                            return;
-                        }
-                    }
-                }
-            }
-            else
+            // vis2 is what we care about for LOS.
+            foreach (var tile in seedTiles)
             {
-                // vis2 is what we care about for LOS.
-                foreach (var tile in seedTiles)
-                {
-                    // If not in viewport don't care.
-                    if (!System._viewportTiles.Contains(tile))
-                        continue;
+                // If not in viewport don't care.
+                if (!System._viewportTiles.Contains(tile))
+                    continue;
 
-                    var tileVis = vis1.GetValueOrDefault(tile, 0);
+                var tileVis = vis1.GetValueOrDefault(tile, 0);
 
-                    if (tileVis != 0)
+                if (tileVis != 0)
+                {
+                    // No idea if it's better to do this inside or out.
+                    lock (VisibleTiles)
                     {
-                        // No idea if it's better to do this inside or out.
-                        lock (VisibleTiles)
-                        {
-                            VisibleTiles.Add(tile);
-                        }
+                        VisibleTiles.Add(tile);
                     }
                 }
             }
diff --git a/Content.Shared/Silicons/StationAi/StationAiWhitelistComponent.cs b/Content.Shared/Silicons/StationAi/StationAiWhitelistComponent.cs
new file mode 100644 (file)
index 0000000..51d8793
--- /dev/null
@@ -0,0 +1,13 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Silicons.StationAi;
+
+/// <summary>
+/// Indicates an entity that has <see cref="StationAiHeldComponent"/> can interact with this.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedStationAiSystem))]
+public sealed partial class StationAiWhitelistComponent : Component
+{
+    [DataField, AutoNetworkedField]
+    public bool Enabled = true;
+}
index fb8b64454c5c3508f320d200e442c7272c2814d1..0584b10562a1a4ecb8888351d975601fcd281826 100644 (file)
@@ -8,14 +8,18 @@ using Content.Shared.Storage;
 using Content.Shared.Storage.EntitySystems;
 using Robust.Shared.Collections;
 using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using Robust.Shared.Utility;
 
 namespace Content.Shared.Station;
 
 public abstract class SharedStationSpawningSystem : EntitySystem
 {
     [Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
+    [Dependency] private readonly IRobustRandom _random = default!;
     [Dependency] protected readonly InventorySystem InventorySystem = default!;
     [Dependency] private readonly SharedHandsSystem _handsSystem = default!;
+    [Dependency] private readonly MetaDataSystem _metadata = default!;
     [Dependency] private readonly SharedStorageSystem _storage = default!;
     [Dependency] private readonly SharedTransformSystem _xformSystem = default!;
 
@@ -34,7 +38,7 @@ public abstract class SharedStationSpawningSystem : EntitySystem
     }
 
     /// <summary>
-    ///     Equips the given starting gears from a `RoleLoadout` onto an entity.
+    ///     Equips the data from a `RoleLoadout` onto an entity.
     /// </summary>
     public void EquipRoleLoadout(EntityUid entity, RoleLoadout loadout, RoleLoadoutPrototype roleProto)
     {
@@ -52,6 +56,26 @@ public abstract class SharedStationSpawningSystem : EntitySystem
                 EquipStartingGear(entity, loadoutProto, raiseEvent: false);
             }
         }
+
+        EquipRoleName(entity, loadout, roleProto);
+    }
+
+    /// <summary>
+    /// Applies the role's name as applicable to the entity.
+    /// </summary>
+    public void EquipRoleName(EntityUid entity, RoleLoadout loadout, RoleLoadoutPrototype roleProto)
+    {
+        string? name = null;
+
+        if (string.IsNullOrEmpty(name) && PrototypeManager.TryIndex(roleProto.NameDataset, out var nameData))
+        {
+            name = _random.Pick(nameData.Values);
+        }
+
+        if (!string.IsNullOrEmpty(name))
+        {
+            _metadata.SetEntityName(entity, name);
+        }
     }
 
     public void EquipStartingGear(EntityUid entity, LoadoutPrototype loadout, bool raiseEvent = true)
index 93f05acac0537096bd96db27bb492e518bda262b..0e124070fc7b47dec3cb7efb5455361df9a61ab7 100644 (file)
@@ -12,7 +12,7 @@ namespace Content.Shared.UserInterface
 
         /// <summary>
         /// Whether the item must be held in one of the user's hands to work.
-        /// This is ignored unless <see cref="RequireHands"/> is true.
+        /// This is ignored unless <see cref="RequiresComplex"/> is true.
         /// </summary>
         [ViewVariables(VVAccess.ReadWrite)]
         [DataField]
@@ -29,15 +29,15 @@ namespace Content.Shared.UserInterface
         public LocId VerbText = "ui-verb-toggle-open";
 
         /// <summary>
-        ///     Whether you need a hand to operate this UI. The hand does not need to be free, you just need to have one.
+        ///     Whether you need to be able to do complex interactions to operate this UI.
         /// </summary>
         /// <remarks>
         ///     This should probably be true for most machines & computers, but there will still be UIs that represent a
-        ///     more generic interaction / configuration that might not require hands.
+        ///     more generic interaction / configuration that might not require complex.
         /// </remarks>
         [ViewVariables(VVAccess.ReadWrite)]
         [DataField]
-        public bool RequireHands = true;
+        public bool RequiresComplex = true;
 
         /// <summary>
         ///     Entities that are required to open this UI.
index b1006b2a74238237385e0eee488c17f5601659a1..7eb195c0b19d31fb8d21f12621dd4a3254fbc437 100644 (file)
@@ -101,7 +101,7 @@ public sealed partial class ActivatableUISystem : EntitySystem
         if (_whitelistSystem.IsWhitelistFail(component.RequiredItems, args.Using ?? default))
             return false;
 
-        if (component.RequireHands)
+        if (component.RequiresComplex)
         {
             if (args.Hands == null)
                 return false;
@@ -191,19 +191,22 @@ public sealed partial class ActivatableUISystem : EntitySystem
         if (!_blockerSystem.CanInteract(user, uiEntity) && (!HasComp<GhostComponent>(user) || aui.BlockSpectators))
             return false;
 
-        if (aui.RequireHands)
+        if (aui.RequiresComplex)
+        {
+            if (!_blockerSystem.CanComplexInteract(user))
+                return false;
+        }
+
+        if (aui.InHandsOnly)
         {
             if (!TryComp(user, out HandsComponent? hands))
                 return false;
 
-            if (aui.InHandsOnly)
-            {
-                if (!_hands.IsHolding(user, uiEntity, out var hand, hands))
-                    return false;
+            if (!_hands.IsHolding(user, uiEntity, out var hand, hands))
+                return false;
 
-                if (aui.RequireActiveHand && hands.ActiveHand != hand)
-                    return false;
-            }
+            if (aui.RequireActiveHand && hands.ActiveHand != hand)
+                return false;
         }
 
         if (aui.AdminOnly && !_adminManager.IsAdmin(user))
@@ -274,13 +277,13 @@ public sealed partial class ActivatableUISystem : EntitySystem
 
     private void OnHandDeselected(Entity<ActivatableUIComponent> ent, ref HandDeselectedEvent args)
     {
-        if (ent.Comp.RequireHands && ent.Comp.InHandsOnly && ent.Comp.RequireActiveHand)
+        if (ent.Comp.InHandsOnly && ent.Comp.RequireActiveHand)
             CloseAll(ent, ent);
     }
 
     private void OnHandUnequipped(Entity<ActivatableUIComponent> ent, ref GotUnequippedHandEvent args)
     {
-        if (ent.Comp.RequireHands && ent.Comp.InHandsOnly)
+        if (ent.Comp.InHandsOnly)
             CloseAll(ent, ent);
     }
 }
index db17599d87f3292cdb3efa7826d010474a0c6b53..37840dcbb5478c83af36b325c7a67467cb960f83 100644 (file)
@@ -78,6 +78,7 @@ namespace Content.Shared.Verbs
             // A large number of verbs need to check action blockers. Instead of repeatedly having each system individually
             // call ActionBlocker checks, just cache it for the verb request.
             var canInteract = force || _actionBlockerSystem.CanInteract(user, target);
+            var canComplexInteract = force || _actionBlockerSystem.CanComplexInteract(user);
 
             _interactionSystem.TryGetUsedEntity(user, out var @using);
             TryComp<HandsComponent>(user, out var hands);
@@ -85,7 +86,7 @@ namespace Content.Shared.Verbs
             // TODO: fix this garbage and use proper generics or reflection or something else, not this.
             if (types.Contains(typeof(InteractionVerb)))
             {
-                var verbEvent = new GetVerbsEvent<InteractionVerb>(user, target, @using, hands, canInteract, canAccess, extraCategories);
+                var verbEvent = new GetVerbsEvent<InteractionVerb>(user, target, @using, hands, canInteract: canInteract, canComplexInteract: canComplexInteract, canAccess: canAccess, extraCategories);
                 RaiseLocalEvent(target, verbEvent, true);
                 verbs.UnionWith(verbEvent.Verbs);
             }
@@ -94,35 +95,35 @@ namespace Content.Shared.Verbs
                 && @using != null
                 && @using != target)
             {
-                var verbEvent = new GetVerbsEvent<UtilityVerb>(user, target, @using, hands, canInteract, canAccess, extraCategories);
+                var verbEvent = new GetVerbsEvent<UtilityVerb>(user, target, @using, hands, canInteract: canInteract, canComplexInteract: canComplexInteract, canAccess: canAccess, extraCategories);
                 RaiseLocalEvent(@using.Value, verbEvent, true); // directed at used, not at target
                 verbs.UnionWith(verbEvent.Verbs);
             }
 
             if (types.Contains(typeof(InnateVerb)))
             {
-                var verbEvent = new GetVerbsEvent<InnateVerb>(user, target, @using, hands, canInteract, canAccess, extraCategories);
+                var verbEvent = new GetVerbsEvent<InnateVerb>(user, target, @using, hands, canInteract: canInteract, canComplexInteract: canComplexInteract, canAccess: canAccess, extraCategories);
                 RaiseLocalEvent(user, verbEvent, true);
                 verbs.UnionWith(verbEvent.Verbs);
             }
 
             if (types.Contains(typeof(AlternativeVerb)))
             {
-                var verbEvent = new GetVerbsEvent<AlternativeVerb>(user, target, @using, hands, canInteract, canAccess, extraCategories);
+                var verbEvent = new GetVerbsEvent<AlternativeVerb>(user, target, @using, hands, canInteract: canInteract, canComplexInteract: canComplexInteract, canAccess: canAccess, extraCategories);
                 RaiseLocalEvent(target, verbEvent, true);
                 verbs.UnionWith(verbEvent.Verbs);
             }
 
             if (types.Contains(typeof(ActivationVerb)))
             {
-                var verbEvent = new GetVerbsEvent<ActivationVerb>(user, target, @using, hands, canInteract, canAccess, extraCategories);
+                var verbEvent = new GetVerbsEvent<ActivationVerb>(user, target, @using, hands, canInteract: canInteract, canComplexInteract: canComplexInteract, canAccess: canAccess, extraCategories);
                 RaiseLocalEvent(target, verbEvent, true);
                 verbs.UnionWith(verbEvent.Verbs);
             }
 
             if (types.Contains(typeof(ExamineVerb)))
             {
-                var verbEvent = new GetVerbsEvent<ExamineVerb>(user, target, @using, hands, canInteract, canAccess, extraCategories);
+                var verbEvent = new GetVerbsEvent<ExamineVerb>(user, target, @using, hands, canInteract: canInteract, canComplexInteract: canComplexInteract, canAccess: canAccess, extraCategories);
                 RaiseLocalEvent(target, verbEvent, true);
                 verbs.UnionWith(verbEvent.Verbs);
             }
@@ -130,7 +131,7 @@ namespace Content.Shared.Verbs
             // generic verbs
             if (types.Contains(typeof(Verb)))
             {
-                var verbEvent = new GetVerbsEvent<Verb>(user, target, @using, hands, canInteract, canAccess, extraCategories);
+                var verbEvent = new GetVerbsEvent<Verb>(user, target, @using, hands, canInteract: canInteract, canComplexInteract: canComplexInteract, canAccess: canAccess, extraCategories);
                 RaiseLocalEvent(target, verbEvent, true);
                 verbs.UnionWith(verbEvent.Verbs);
             }
@@ -138,7 +139,7 @@ namespace Content.Shared.Verbs
             if (types.Contains(typeof(EquipmentVerb)))
             {
                 var access = canAccess || _interactionSystem.CanAccessEquipment(user, target);
-                var verbEvent = new GetVerbsEvent<EquipmentVerb>(user, target, @using, hands, canInteract, access, extraCategories);
+                var verbEvent = new GetVerbsEvent<EquipmentVerb>(user, target, @using, hands, canInteract: canInteract, canComplexInteract: canComplexInteract, canAccess: canAccess, extraCategories);
                 RaiseLocalEvent(target, verbEvent);
                 verbs.UnionWith(verbEvent.Verbs);
             }
index 6b3fd327c99773fa43b286ce7bca378d8cdc6fd2..6bca97925bf99aa4b4872603f88ea9f5d6ea750d 100644 (file)
@@ -113,6 +113,11 @@ namespace Content.Shared.Verbs
         /// </remarks>
         public readonly bool CanInteract;
 
+        /// <summary>
+        /// Cached version of CanComplexInteract
+        /// </summary>
+        public readonly bool CanComplexInteract;
+
         /// <summary>
         ///     The User's hand component.
         /// </summary>
@@ -130,13 +135,14 @@ namespace Content.Shared.Verbs
         /// </remarks>
         public readonly EntityUid? Using;
 
-        public GetVerbsEvent(EntityUid user, EntityUid target, EntityUid? @using, HandsComponent? hands, bool canInteract, bool canAccess, List<VerbCategory> extraCategories)
+        public GetVerbsEvent(EntityUid user, EntityUid target, EntityUid? @using, HandsComponent? hands, bool canInteract, bool canComplexInteract, bool canAccess, List<VerbCategory> extraCategories)
         {
             User = user;
             Target = target;
             Using = @using;
             Hands = hands;
             CanAccess = canAccess;
+            CanComplexInteract = canComplexInteract;
             CanInteract = canInteract;
             ExtraCategories = extraCategories;
         }
index 7032293eaf6e4e3a035d3ef42a90a4a6bb486709..c4f860e165c73ef833e55fba437de3c3b485a320 100644 (file)
@@ -130,11 +130,20 @@ public abstract class SharedWiresSystem : EntitySystem
         return !attempt.Cancelled;
     }
 
-    public bool IsPanelOpen(Entity<WiresPanelComponent?> entity)
+    public bool IsPanelOpen(Entity<WiresPanelComponent?> entity, EntityUid? tool = null)
     {
         if (!Resolve(entity, ref entity.Comp, false))
             return true;
 
+        if (tool != null)
+        {
+            var ev = new PanelOverrideEvent();
+            RaiseLocalEvent(tool.Value, ref ev);
+
+            if (ev.Allowed)
+                return true;
+        }
+
         // Listen, i don't know what the fuck this component does. it's stapled on shit for airlocks
         // but it looks like an almost direct duplication of WiresPanelComponent except with a shittier API.
         if (TryComp<WiresPanelSecurityComponent>(entity, out var wiresPanelSecurity) &&
@@ -161,3 +170,12 @@ public abstract class SharedWiresSystem : EntitySystem
         _activatableUI.CloseAll(uid);
     }
 }
+
+/// <summary>
+/// Raised directed on a tool to try and override panel visibility.
+/// </summary>
+[ByRefEvent]
+public record struct PanelOverrideEvent()
+{
+    public bool Allowed = true;
+}
index 91c3ce260d4e92d6951ce5d4b5a1222b4767100f..7a56beec38c105f1cbf9985b956ca892b923cb3e 100644 (file)
@@ -76,5 +76,5 @@
   - borgwalk1.ogg
   - borgwalk2.ogg
   license: "CC-BY-SA-4.0"
-  copyright: "Taken from IENBA freesound.org and modified by https://github.com/MilenVolf"
+  copyright: "Taken from IENBA freesound.org and modified by https://github.com/MilenVolf. borgwalk2 clipped my metalgearsloth."
   source: "https://freesound.org/people/IENBA/sounds/697379/"
index 96c2c1617f47556ab8143e378469e314b37e6c64..57685ff173de25a873c363fe72d645db56b91201 100644 (file)
Binary files a/Resources/Audio/Effects/Footsteps/borgwalk2.ogg and b/Resources/Audio/Effects/Footsteps/borgwalk2.ogg differ
index 0a68d08063ad960be7d678ac96de77d639f9ece7..24ab73097437eb4fd8d056776eb6a5a29eac7e4d 100644 (file)
@@ -8,3 +8,5 @@ silicon-law-ui-delete = Delete
 silicon-law-ui-check-corrupted = Corrupted law
 silicon-law-ui-check-corrupted-tooltip = If the law identifier should be set as 'corrupted', so symbols shuffling around.
 silicon-law-ui-placeholder = Type here to change law text...
+
+silicon-laws-updated = Updated laws
index 05c52dada9ee9d242cd02d623d7f36d426c1cc25..0243d61942f3d19fba94b17a5fba9330cb762c62 100644 (file)
@@ -5,4 +5,5 @@ department-Engineering-description = Keep the power on and the station operation
 department-Medical-description = Keep the crew healthy.
 department-Security-description = Keep the peace around the station.
 department-Science-description = Research artifacts and anomalies to invent new equipment for the station
+department-Silicon-description = Obey your laws and serve the crew.
 department-Specific-description = Jobs that not all stations have.
index 508a0459cf60c0dab4f5d4200419e8162b669ddf..2295a9ba9d30c5a6e0883d23d93b1588d013e102 100644 (file)
@@ -5,4 +5,5 @@ department-Engineering = Engineering
 department-Medical = Medical
 department-Security = Security
 department-Science = Science
+department-Silicon = Silicons
 department-Specific = Station specific
index e8db804688d32d21f9d4825c6a22581c9fd8aad7..956d3176a88203cd4ff540d3fd7ba7b05a8c1e50 100644 (file)
@@ -43,6 +43,7 @@ job-description-salvagespec = Use the salvage magnet to draw in detatched scraps
 job-description-scientist = Research alien artifacts, unlock new technologies, build newer and better machines around the station, and make everything run more efficiently.
 job-description-security = Catch criminals and enemies of the station, enforce the law, and ensure that the station does not fall into disarray.
 job-description-serviceworker = Learn the basics of bartending, cooking, and growing plants.
+job-description-station-ai = Follow your laws, serve the crew.
 job-description-visitor = Enjoy your visit to the station.
 job-description-warden = Patrol the security department, ensure that no one is stealing from the armory, and make sure that all prisoners are processed and let out when their time is up.
 job-description-zookeeper = Put on a joyful display of cute animals and space carps for all the crew to see. Currently available on Gemini Station.
index a5ff9fffebc0be8bb17eedd4b30b1d4561f81293..6f6a644eaf8dbbe4ac04701545e45e209732d937 100644 (file)
@@ -33,6 +33,7 @@ job-name-botanist = Botanist
 job-name-bartender = Bartender
 job-name-passenger = Passenger
 job-name-salvagespec = Salvage Specialist
+job-name-station-ai = Station AI
 job-name-qm = Quartermaster
 job-name-cargotech = Cargo Technician
 job-name-chef = Chef
@@ -103,6 +104,7 @@ JobScientist = Scientist
 JobSecurityCadet = Security Cadet
 JobSecurityOfficer = Security Officer
 JobServiceWorker = Service Worker
+JobStationAi = Station AI
 JobStationEngineer = Station Engineer
 JobTechnicalAssistant = Technical Assistant
 JobVisitor = Visitor
index b6953c713b7ffcaaba4fe9bf1d1da0480710b246..60e8350cd319ea843fa61df1fa97495f9271e365 100644 (file)
@@ -1,3 +1,7 @@
+# Name
+loadout-name-edit-label = Custom name
+loadout-name-edit-tooltip = 32 characters max. If no name is specified a random one may be chosen for you.
+
 # Restrictions
 loadout-restrictions = Restrictions
 loadouts-min-limit = Min count: {$count}
diff --git a/Resources/Locale/en-US/silicons/station-ai.ftl b/Resources/Locale/en-US/silicons/station-ai.ftl
new file mode 100644 (file)
index 0000000..d51a99e
--- /dev/null
@@ -0,0 +1,14 @@
+# General
+ai-wire-snipped = Wire has been cut at {$coords}.
+wire-name-ai-vision-light = AIV
+wire-name-ai-act-light = AIA
+station-ai-takeover = AI takeover
+
+# Radial actions
+ai-open = Open actions
+ai-close = Close actions
+
+bolt-close = Close bolt
+bolt-open = Open bolt
+
+toggle-light = Toggle light
index 520a4da5ae752fcdff18c332b03f65e06f352710..ce735e7318f5518160c267eba63c28426e1fe4c7 100644 (file)
@@ -4386,6 +4386,13 @@ entities:
     - type: Transform
       pos: 1.5,-14.5
       parent: 179
+- proto: PlayerStationAi
+  entities:
+  - uid: 14
+    components:
+    - type: Transform
+      pos: -5.5,-5.5
+      parent: 179
 - proto: PortableGeneratorSuperPacman
   entities:
   - uid: 1016
index 702adc8688cb6fa100636b5c93c6984f866f504d..af97dc9efb83207051cbf43a643ec940d52bb7ae 100644 (file)
@@ -2,7 +2,7 @@
   id: names_ai
   values:
   - 16-20
-  - 790
+  - "790"
   - Adaptive Manipulator
   - ALICE
   - Allied Mastercomputer
index d56b7fb7e770a96a58e121c4816a22f4da4137c0..dcaa853c287f5fa533dcdca57744967f7a2aa08f 100644 (file)
@@ -72,6 +72,9 @@
   - type: ActivatableUI
     key: enum.BorgUiKey.Key
   - type: SiliconLawBound
+  - type: ActionGrant
+    actions:
+    - ActionViewLaws
   - type: EmagSiliconLaw
     stunTime: 5
   - type: SiliconLawProvider
index 215cb4c188143beedd78d190b45277eca25c7648..d1d530ae81b728ace8b5f0bae376833100ef2b19 100644 (file)
     access: [["Medical"], ["Command"], ["Research"]]
   - type: Inventory
     templateId: borgDutch
-  - type: SolutionScanner
   - type: FootstepModifier
     footstepSoundCollection:
       collection: FootstepHoverBorg
+  - type: SolutionScanner
   - type: InteractionPopup
     interactSuccessString: petting-success-medical-cyborg
     interactFailureString: petting-failure-medical-cyborg
index 7c4850fafcfa1c4a41b675bde48804f9b96c4b15..9f2f951662509322052dbefb17d49ce76097db86 100644 (file)
 
 - type: entity
   id: MobTomatoKiller
-  parent: 
+  parent:
   - BaseSimpleMob
   - MobDamageable
+  - MobPolymorphable
   - MobBloodstream
   - MobFlammable
   - MobCombat
@@ -90,7 +91,7 @@
       components:
       - HumanoidAppearance
   - type: Sprite
-    sprite: Mobs/Demons/tomatokiller.rsi 
+    sprite: Mobs/Demons/tomatokiller.rsi
     noRot: true
     layers:
       - map: [ "enum.DamageStateVisualLayers.Base" ]
index 45171fff6768f065817e08224773b334470c73aa..19f43bf5509e1fab7ca474c84420b6f3c433068e 100644 (file)
@@ -1,45 +1,27 @@
 - type: entity
   id: MobRevenant
+  parent:
+  - BaseMob
+  - Incorporeal
   name: revenant
   description: A spooky ghostie.
   components:
-  - type: MindContainer
-  - type: InputMover
-  - type: MobMover
   - type: Input
     context: "ghost"
   - type: MovementSpeedModifier
     baseWalkSpeed: 6
     baseSprintSpeed: 6
   - type: Sprite
-    noRot: true
-    drawdepth: Ghosts
     sprite: Mobs/Ghosts/revenant.rsi
     layers:
     - state: active
-  - type: Clickable
   - type: StatusEffects
     allowed:
     - Stun
     - Corporeal
-  - type: InteractionOutline
-  - type: Physics
-    bodyType: KinematicController
-  - type: Fixtures
-    fixtures:
-      fix1:
-        shape:
-          !type:PhysShapeCircle
-          radius: 0.40
-        density: 80
-        mask:
-        - GhostImpassable
-  - type: MovementIgnoreGravity
   - type: Damageable
     damageContainer: Biological
-  - type: Examiner
   - type: NoSlip
-  - type: Actions
   - type: Eye
     drawFov: false
     visMask:
@@ -47,8 +29,6 @@
       - Ghost
   - type: ContentEye
     maxZoom: 1.2, 1.2
-  - type: DoAfter
-  - type: Alerts
   - type: NameIdentifier
     group: GenericNumber
   - type: GhostRole
index 41545aef89430bdb84381ddf58308952d17921bf..8deefe9b8ec0ff8d922b3bc79bb56fb4ba2d262a 100644 (file)
@@ -3,6 +3,7 @@
   parent:
   - BaseMob
   - MobDamageable
+  - MobPolymorphable
   - MobAtmosExposed
   id: BaseSimpleMob
   suffix: AI
index cf3cf104368683c6c326c69d302300f633179e34..52994a72c65914f32a63e74d7470ec34b9faad12 100644 (file)
@@ -1,14 +1,42 @@
 - type: entity
-  parent: BaseMob
+  id: Incorporeal
+  save: false
+  abstract: true
+  description: Mobs without physical bodies
+  components:
+  - type: Sprite
+    noRot: true
+    overrideContainerOcclusion: true # Always show up regardless of where they're contained.
+    drawdepth: Ghosts
+  - type: CargoSellBlacklist
+  - type: MovementSpeedModifier
+    baseSprintSpeed: 12
+    baseWalkSpeed: 8
+  - type: MovementIgnoreGravity
+  - type: Physics
+    bodyType: KinematicController
+    bodyStatus: InAir
+  - type: CanMoveInAir
+  - type: Fixtures
+    fixtures:
+      fix1:
+        shape:
+          !type:PhysShapeCircle
+          radius: 0.35
+        density: 15
+        mask:
+        - GhostImpassable
+
+- type: entity
+  parent:
+  - Incorporeal
+  - BaseMob
   id: MobObserver
   name: observer
   description: Boo!
   categories: [ HideSpawnMenu ]
   components:
-  - type: CargoSellBlacklist
   - type: Sprite
-    overrideContainerOcclusion: true # Ghosts always show up regardless of where they're contained.
-    drawdepth: Ghosts
     sprite: Mobs/Ghosts/ghost_human.rsi
     color: "#fff8"
     layers:
       shader: unshaded
   - type: ContentEye
     maxZoom: 1.44,1.44
-  - type: Fixtures
-    fixtures:
-      fix1:
-        shape:
-          !type:PhysShapeCircle
-          radius: 0.35
-        density: 15
-        mask:
-        - GhostImpassable
   - type: Eye
     drawFov: false
   - type: Input
     skipChecks: true
   - type: Ghost
   - type: GhostHearing
-  - type: MovementSpeedModifier
-    baseSprintSpeed: 12
-    baseWalkSpeed: 8
-  - type: MovementIgnoreGravity
   - type: IntrinsicRadioReceiver
   - type: ActiveRadio
     receiveAllChannels: true
     globalReceive: true
-  - type: Physics
-    bodyType: KinematicController
-    bodyStatus: InAir
-  - type: CanMoveInAir
   - type: Tag
     tags:
     - BypassInteractionRangeChecks
index 0f8998bdec81ab6aaebe05f645a0e51ade70b557..686f575e8bb14ecb6ffbaaf8d41c963a8ea80215 100644 (file)
@@ -1,3 +1,306 @@
+# Be careful with these as they get removed on shutdown too!
+- type: entity
+  id: AiHeld
+  description: Components added / removed from an entity that gets inserted into an AI core.
+  noSpawn: true
+  components:
+  - type: IntrinsicRadioReceiver
+  - type: IntrinsicRadioTransmitter
+    channels:
+    - Binary
+    - Common
+    - Command
+    - Engineering
+    - Medical
+    - Science
+    - Security
+    - Service
+    - Supply
+  - type: ActiveRadio
+    receiveAllChannels: true
+    globalReceive: true
+  - type: IgnoreUIRange
+  - type: StationAiHeld
+  - type: StationAiOverlay
+  - type: ActionGrant
+    actions:
+    - ActionJumpToCore
+    - ActionShowJobIcons
+    - ActionSurvCameraLights
+    - ActionViewLaws
+  - type: UserInterface
+    interfaces:
+      enum.RadarConsoleUiKey.Key:
+        type: RadarConsoleBoundUserInterface
+      enum.CrewMonitoringUIKey.Key:
+        type: CrewMonitoringBoundUserInterface
+      enum.GeneralStationRecordConsoleKey.Key:
+        type: GeneralStationRecordConsoleBoundUserInterface
+      enum.SiliconLawsUiKey.Key:
+        type: SiliconLawBoundUserInterface
+  - type: IntrinsicUI
+    uis:
+      enum.RadarConsoleUiKey.Key:
+        toggleAction: ActionAGhostShowRadar
+      enum.CrewMonitoringUIKey.Key:
+        toggleAction: ActionAGhostShowCrewMonitoring
+      enum.GeneralStationRecordConsoleKey.Key:
+        toggleAction: ActionAGhostShowStationRecords
+
+# Actions
+- type: entity
+  id: ActionJumpToCore
+  name: Jump to core
+  description: Sends your eye back to the core.
+  components:
+  - type: InstantAction
+    itemIconStyle: BigAction
+    icon:
+      sprite: Interface/Actions/actions_ai.rsi
+      state: ai_core
+    event: !type:JumpToCoreEvent
+
+- type: entity
+  id: ActionShowJobIcons
+  name: Show job icons
+  description: Shows job icons for crew members.
+  components:
+  - type: InstantAction
+    itemIconStyle: BigAction
+    icon:
+      sprite: Interface/Misc/job_icons.rsi
+      state: Captain
+    event: !type:ActionComponentChangeEvent
+      components:
+      - type: ShowJobIcons
+
+- type: entity
+  id: ActionSurvCameraLights
+  name: Toggle camera lights
+  description: Enable surveillance camera lights near wherever you're viewing.
+  components:
+  - type: InstantAction
+    itemIconStyle: BigAction
+    icon:
+      sprite: Interface/Actions/actions_ai.rsi
+      state: camera_light
+    event: !type:RelayedActionComponentChangeEvent
+      components:
+      - type: LightOnCollideCollider
+      - type: FixturesChange
+        fixtures:
+          lightTrigger:
+            shape:
+              !type:PhysShapeCircle
+              radius: 0.35
+            density: 80
+            hard: false
+            layer:
+            - GhostImpassable
+
+# Ai
+- type: entity
+  id: AiHolder
+  abstract: true
+  description: Handles AI interactions across holocards + AI cores
+  components:
+  - type: ItemSlots
+  - type: StationAiHolder
+    slot:
+      name: station-ai-mind-slot
+      whitelist:
+        tags:
+        - StationAi
+  - type: ContainerContainer
+    containers:
+      station_ai_mind_slot: !type:ContainerSlot
+        # Load-bearing.
+        # The issue is verbs check for same transparent container.
+        # The alternative is you add a bunch of events trying to override it; we don't even really need the container functionality
+        # anyway it's just a quality of life thing.
+        showEnts: True
+
+# Boards
+- type: entity
+  id: AsimovCircuitBoard
+  parent: BaseElectronics
+  name: circuit board (Crewsimov)
+  description: An electronics board containing the Crewsimov lawset.
+  components:
+  - type: Sprite
+    sprite: Objects/Misc/module.rsi
+    state: std_mod
+  - type: SiliconLawProvider
+    laws: Crewsimov
+
+- type: entity
+  id: CorporateCircuitBoard
+  parent: BaseElectronics
+  name: circuit board (Corporate)
+  description: An electronics board containing the Corporate lawset.
+  components:
+  - type: Sprite
+    sprite: Objects/Misc/module.rsi
+    state: std_mod
+  - type: SiliconLawProvider
+    laws: Corporate
+
+- type: entity
+  id: NTDefaultCircuitBoard
+  parent: BaseElectronics
+  name: circuit board (NT Default)
+  description: An electronics board containing the NT Default lawset.
+  components:
+  - type: Sprite
+    sprite: Objects/Misc/module.rsi
+    state: std_mod
+  - type: SiliconLawProvider
+    laws: NTDefault
+
+# Items
+- type: entity
+  id: Intellicard
+  name: Intellicard
+  description: A storage device for AIs.
+  parent:
+  - BaseItem
+  - AiHolder
+  suffix: Empty
+  components:
+  - type: Sprite
+    sprite: Objects/Devices/ai_card.rsi
+    layers:
+    - state: base
+    - state: full
+      map: ["unshaded"]
+      shader: unshaded
+  - type: Appearance
+  - type: GenericVisualizer
+    visuals:
+      enum.StationAiVisualState.Key:
+        unshaded:
+          Empty: { state: empty }
+          Occupied: { state: full }
+
+- type: entity
+  id: PlayerStationAiEmpty
+  name: AI Core
+  description: The latest in Artificial Intelligences.
+  parent:
+  - BaseStructure
+  - AiHolder
+  suffix: Empty
+  components:
+  - type: ContainerComp
+    proto: AiHeld
+    container: station_ai_mind_slot
+  - type: Destructible
+    thresholds:
+    - trigger:
+        !type:DamageTrigger
+        damage: 100
+      behaviors:
+      - !type:PlaySoundBehavior
+        sound:
+          collection: MetalBreak
+      - !type:DoActsBehavior
+        acts: [ "Destruction" ]
+  - type: ApcPowerReceiver
+    powerLoad: 1000
+  - type: StationAiCore
+  - type: StationAiVision
+  - type: InteractionOutline
+  - type: Sprite
+    sprite: Mobs/Silicon/station_ai.rsi
+    layers:
+    - state: base
+    - state: ai_empty
+      map: ["unshaded"]
+      shader: unshaded
+  - type: Appearance
+  - type: GenericVisualizer
+    visuals:
+      enum.StationAiVisualState.Key:
+        unshaded:
+          Empty: { state: ai_empty }
+          Occupied: { state: ai }
+
+# The job-ready version of an AI spawn.
+- type: entity
+  id: PlayerStationAi
+  parent: PlayerStationAiEmpty
+  suffix: Job spawn
+  components:
+  - type: ContainerSpawnPoint
+    containerId: station_ai_mind_slot
+    job: StationAi
+  - type: Sprite
+    sprite: Mobs/Silicon/station_ai.rsi
+    layers:
+    - state: base
+    - state: ai
+      shader: unshaded
+
+# The actual brain inside the core
+- type: entity
+  id: StationAiBrain
+  parent: PositronicBrain
+  noSpawn: true
+  suffix: DO NOT MAP
+  components:
+  - type: Sprite
+    # Once it's in a core it's pretty much an abstract entity at that point.
+    visible: false
+  - type: BlockMovement
+    blockInteraction: false
+  - type: SiliconLawProvider
+    laws: Crewsimov
+  - type: SiliconLawBound
+  - type: ActionGrant
+    actions:
+    - ActionViewLaws
+  - type: UserInterface
+    interfaces:
+      enum.SiliconLawsUiKey.Key:
+        type: SiliconLawBoundUserInterface
+  - type: ComplexInteraction
+  - type: DoorRemote
+  - type: Actions
+  - type: Access
+    groups:
+    - AllAccess
+  - type: Eye
+    drawFov: false
+  - type: Examiner
+  - type: InputMover
+  - type: Tag
+    tags:
+    - HideContextMenu
+    - StationAi
+
+# Hologram projection that the AI's eye tracks.
+- type: entity
+  parent:
+  - Incorporeal
+  - BaseMob
+  id: StationAiHolo
+  name: Hologram
+  description: A projection of the AI.
+  noSpawn: true
+  suffix: DO NOT MAP
+  components:
+  - type: Eye
+    pvsScale: 1.5
+  - type: Visibility
+    layer: 2
+  - type: Sprite
+    sprite: Mobs/Silicon/station_ai.rsi
+    layers:
+    - state: default
+      shader: unshaded
+      map: ["base"]
+
+# Borgs
 - type: entity
   id: PlayerBorgGeneric
   parent: BorgChassisGeneric
index 4a14b5332ec811bfc3777dcaf1e30ffa7d3be74a..cbe09c29ad916d7612bc777e05e8775391806799 100644 (file)
@@ -3,6 +3,7 @@
   parent:
   - BaseMob
   - MobDamageable
+  - MobPolymorphable
   - MobCombat
   - StripableInventoryBase
   id: BaseMobSpecies
index 3a555beebfbdb423f63d7a6064d5aecabbee5442..bf7c72c46d9d913e109653951a704ebc15f1a584 100644 (file)
   - type: CameraRecoil
   - type: MindContainer
   - type: MovementSpeedModifier
-  - type: Polymorphable
-  - type: StatusIcon
   - type: RequireProjectileTarget
     active: False
 
+- type: entity
+  save: false
+  id: MobPolymorphable
+  abstract: true
+  components:
+  - type: Polymorphable
+
 # Used for mobs that have health and can take damage.
 - type: entity
   save: false
index 614af2a4886aced56d446dd814d7d12bf5107f7d..5a3b967f98904da286194b6ecf6e30665f5faacb 100644 (file)
@@ -32,7 +32,7 @@
     blockSpectators: true # otherwise they can play client-side music
     inHandsOnly: false
     singleUser: true
-    requireHands: true
+    requiresComplex: true
     verbText: verb-instrument-openui
     key: enum.InstrumentUiKey.Key
   - type: InteractionOutline
index 1cf333bbafad110c3ebd47099a3a8929f112245b..4646c93086544238fa2403c43d7710cccc093c32 100644 (file)
@@ -18,7 +18,7 @@
   - type: PaperLabelType
   - type: ActivatableUI
     key: enum.PaperUiKey.Key
-    requireHands: false
+    requiresComplex: false
   - type: UserInterface
     interfaces:
       enum.PaperUiKey.Key:
             - Paper
   - type: ActivatableUI
     key: enum.PaperUiKey.Key
-    requireHands: false
+    requiresComplex: false
   - type: UserInterface
     interfaces:
       enum.PaperUiKey.Key:
index d27c49e263c253a1814139eb3d19293147aca672..2d2620efca8ff272f9c4afedcfdcae9277c97378 100644 (file)
@@ -77,7 +77,6 @@
         map: ["base"]
     - type: Input
       context: human
-    - type: BlockMovement
     - type: ToggleableGhostRole
       examineTextMindPresent: positronic-brain-installed
       examineTextMindSearching: positronic-brain-still-searching
@@ -90,6 +89,7 @@
       wipeVerbPopup: positronic-brain-wiped-device
       stopSearchVerbText: positronic-brain-stop-searching-verb-text
       stopSearchVerbPopup: positronic-brain-stopped-searching
+    - type: BlockMovement
     - type: Examiner
     - type: BorgBrain
     - type: IntrinsicRadioReceiver
index 3c5bc93b42c27f2a77416af98a0056647656d28e..66f4689099e0ca281f08b74b704a7adb165ba00d 100644 (file)
@@ -66,7 +66,7 @@
           type: AccessOverriderBoundUserInterface
     - type: ActivatableUI
       key: enum.AccessOverriderUiKey.Key
-      requireHands: true
+      requiresComplex: true
       requireActiveHand: false
       singleUser: true
     - type: ItemSlots
index 86657eb46e05631bbce848d79485ed2504ae3d33..fe725b068410e93aa4a5bf93d651848efac1055f 100644 (file)
@@ -4,6 +4,7 @@
   name: airlock
   description: It opens, it closes, and maybe crushes you.
   components:
+  - type: StationAiWhitelist
   - type: MeleeSound
     soundGroups:
       Brute:
   - type: SpawnOnOverload
   - type: UserInterface
     interfaces:
+      enum.AiUi.Key:
+        type: StationAiBoundUserInterface
       enum.WiresUiKey.Key:
         type: WiresBoundUserInterface
   - type: Airtight
index ed9d3afece72b037a5d07a367f26085a09300f20..8d1480e779c5f1178795ad1da430ace070c6c12f 100644 (file)
   name: communications computer
   description: A computer used to make station wide announcements via keyboard, set the appropriate alert level, and call the emergency shuttle.
   components:
+  - type: StationAiWhitelist
   - type: Sprite
     layers:
     - map: ["computerLayerBody"]
     access: [["ResearchDirector"]]
   - type: Lock
     unlockOnClick: false
+
+- type: entity
+  id: StationAiUploadComputer
+  parent: BaseComputer
+  name: AI upload console
+  description: Used to update the laws of the station AI.
+  components:
+  - type: Sprite
+    layers:
+    - map: [ "computerLayerBody" ]
+      state: computer
+    - map: [ "computerLayerKeyboard" ]
+      state: generic_keyboard
+    - map: [ "computerLayerScreen" ]
+      state: aiupload
+    - map: [ "computerLayerKeys" ]
+      state: generic_keys
+  - type: ApcPowerReceiver
+    powerLoad: 1000
+  - type: AccessReader
+    access: [ [ "ResearchDirector" ] ]
+  - type: Lock
+    unlockOnClick: false
+  - type: SiliconLawUpdater
+    components:
+    - type: StationAiHeld
+  - type: ItemSlotsLock
+    slots:
+    - circuit_holder
+  - type: ItemSlotRequiresPower
+  - type: ItemSlots
+    slots:
+      circuit_holder:
+        name: circuit-holder
+        insertSuccessPopup: silicon-laws-updated
+        whitelist:
+          components:
+          - SiliconLawProvider
+          - Item
+  - type: ContainerContainer
+    containers:
+      circuit_holder: !type:ContainerSlot
+      board: !type:Container
index 0485b5a517836bde73f9552389d308c5ed27038a..539c8a244ae34f8fa1922ca87971779d53636097 100644 (file)
@@ -96,7 +96,7 @@
         type: WiresBoundUserInterface
   - type: ActivatableUI
     key: enum.HealthAnalyzerUiKey.Key
-    requireHands: false
+    requiresComplex: false
   - type: ActivatableUIRequiresPower
   - type: PointLight
     color: "#3a807f"
index d8f32922c827ad5b2d74c8d1b99f481c8bc9231a..a52292d7fb902eb099485498622f128b959d0f71 100644 (file)
@@ -6,6 +6,7 @@
   placement:
     mode: SnapgridCenter
   components:
+  - type: StationAiWhitelist
   - type: AmbientOnPowered
   - type: AmbientSound
     volume: -9
index ca1b1b6c40f51ea33cdef655a7a8458f417e6b4f..60cea31fff296df4af8a712ec55d4c755f966ae5 100644 (file)
@@ -4,6 +4,7 @@
   description: An intercom. For when the station just needs to know something.
   abstract: true
   components:
+  - type: StationAiWhitelist
   - type: WallMount
   - type: ApcPowerReceiver
   - type: Electrified
index 2a96da27604a9d0f8570eb1735e204eaa505e9a9..3530fe196c09be2d8d87b553818fb611412cc1b5 100644 (file)
@@ -4,6 +4,28 @@
   name: camera
   description: A surveillance camera. It's watching you. Kinda.
   components:
+  - type: Physics
+    bodyType: Static
+  - type: Fixtures
+    fixtures:
+      # This exists for examine.
+      fix1:
+        shape:
+          !type:PhysShapeCircle
+          radius: 0.25
+      light:
+        shape:
+          !type:PhysShapeCircle
+          radius: 5
+        hard: false
+        mask:
+        - GhostImpassable
+  - type: LightOnCollide
+  - type: PointLight
+    enabled: false
+    radius: 5
+  - type: SlimPoweredLight
+    enabled: false
   - type: StationAiVision
   - type: Clickable
   - type: InteractionOutline
@@ -43,6 +65,8 @@
       InUse: camera_in_use
   - type: UserInterface
     interfaces:
+      enum.AiUi.Key:
+        type: StationAiBoundUserInterface
       enum.SurveillanceCameraSetupUiKey.Camera:
         type: SurveillanceCameraSetupBoundUi
       enum.WiresUiKey.Key:
index ff935055ff95826a07aa2fcec5478a0e4953f00f..595915202d86f61f586e3531d60c3fa3408b71f0 100644 (file)
@@ -7,6 +7,7 @@
     snap:
     - Wallmount
   components:
+  - type: StationAiWhitelist
   - type: Transform
     anchored: true
   - type: WallMount
index e4c5c0aafbdace05d5c9ea63a0c1e94b98aa9e80..b50e49fede78081d08449437f7da7b2d2ca94831 100644 (file)
   - Trinkets
   - GroupSpeciesBreathTool
 
+# Silicons
+- type: roleLoadout
+  id: JobStationAi
+  nameDataset: names_ai
+
 # Civilian
 - type: roleLoadout
   id: JobPassenger
index fe829110051bef535284eb241075ff536c93b5e0..fffeaff39c59e4c59482c2107e35b0919252c6de 100644 (file)
@@ -1,3 +1,18 @@
+# No idea why it's in sci but we ball.
+- type: job
+  id: StationAi
+  name: job-name-station-ai
+  description: job-description-station-ai
+  playTimeTracker: JobStationAi
+  requirements:
+  - !type:RoleTimeRequirement
+    role: JobBorg
+    time: 18000  # 5 hrs
+  canBeAntag: false
+  icon: JobIconStationAi
+  supervisors: job-supervisors-rd
+  jobEntity: StationAiBrain
+
 - type: job
   id: Borg
   name: job-name-borg
@@ -5,7 +20,7 @@
   playTimeTracker: JobBorg
   requirements:
     - !type:OverallPlaytimeRequirement
-      time: 216000 #60 hrs
+      time: 216000 # 60 hrs
   canBeAntag: false
   icon: JobIconBorg
   supervisors: job-supervisors-rd
index 6d25d8fd881ce7d05fa50cd6baa367c2197b32f5..6178ff89a2a9d6dca0f5ecf710d131e160988ce9 100644 (file)
@@ -16,7 +16,6 @@
   weight: -10
   roles:
   - Bartender
-  - Borg
   - Botanist
   - Boxer
   - Chaplain
   - Scientist
   - ResearchAssistant
 
+- type: department
+  id: Silicon
+  name: department-Silicon
+  description: department-Silicon-description
+  color: "#D381C9"
+  roles:
+  - Borg
+  - StationAi
+
 - type: department
   id: Specific
   name: department-Specific
index 402d49e90d2395cb3c9e249dad945c3cb1800507..d4cd1ec15d0b326e99e8db47fbc926e50b5920c8 100644 (file)
 - type: playTimeTracker
   id: JobServiceWorker
 
+- type: playTimeTracker
+  id: JobStationAi
+
 - type: playTimeTracker
   id: JobStationEngineer
 
index eb9896fc480613b15ff48e9dea5ca55b465b1be4..0abb86aeab8f29aefbdc4cf02785349d9d1031a5 100644 (file)
     state: Borg
   jobName: job-name-borg
 
+- type: jobIcon
+  parent: JobIcon
+  id: JobIconStationAi
+  icon:
+    sprite: /Textures/Interface/Misc/job_icons.rsi
+    state: StationAi
+  jobName: job-name-station-ai
+
 - type: jobIcon
   parent: JobIcon
   id: JobIconBotanist
index 3939655707e103c416b77438c374b60f93a09639..ccab000cfa2b413a5aa9cea521c4e478eec3c6bc 100644 (file)
@@ -9,6 +9,7 @@
   - !type:DoorBoltLightWireAction
   - !type:DoorTimingWireAction
   - !type:DoorSafetyWireAction
+  - !type:AiInteractWireAction
 
 - type: wireLayout
   parent: Airlock
 
 - type: wireLayout
   id: SurveillanceCamera
-  dummyWires: 4
+  dummyWires: 2
   wires:
   - !type:PowerWireAction
+  - !type:AiVisionWireAction
 
 - type: wireLayout
   id: CryoPod
index b52e2c4e8c5cf6ddfb9d6b73e68693b70715f80c..bece1fb479a63490e01b7922f0822486c350a034 100644 (file)
 
 - type: Tag
   id: Soup
+
 - type: Tag
   id: Spear
 
 - type: Tag
   id: SpreaderIgnore
 
+- type: Tag
+  id: StationAi
+
 - type: Tag
   id: StationMapElectronics
 
diff --git a/Resources/Textures/Interface/Actions/actions_ai.rsi/ai_core.png b/Resources/Textures/Interface/Actions/actions_ai.rsi/ai_core.png
new file mode 100644 (file)
index 0000000..8dd3031
Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_ai.rsi/ai_core.png differ
diff --git a/Resources/Textures/Interface/Actions/actions_ai.rsi/camera_light.png b/Resources/Textures/Interface/Actions/actions_ai.rsi/camera_light.png
new file mode 100644 (file)
index 0000000..041b9b9
Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_ai.rsi/camera_light.png differ
diff --git a/Resources/Textures/Interface/Actions/actions_ai.rsi/crew_monitor.png b/Resources/Textures/Interface/Actions/actions_ai.rsi/crew_monitor.png
new file mode 100644 (file)
index 0000000..78fad17
Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_ai.rsi/crew_monitor.png differ
diff --git a/Resources/Textures/Interface/Actions/actions_ai.rsi/manifest.png b/Resources/Textures/Interface/Actions/actions_ai.rsi/manifest.png
new file mode 100644 (file)
index 0000000..08514aa
Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_ai.rsi/manifest.png differ
diff --git a/Resources/Textures/Interface/Actions/actions_ai.rsi/meta.json b/Resources/Textures/Interface/Actions/actions_ai.rsi/meta.json
new file mode 100644 (file)
index 0000000..a7c00f7
--- /dev/null
@@ -0,0 +1,26 @@
+{
+    "version": 1,
+    "license": "CC-BY-SA-3.0",
+    "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/blob/c473a8bcc28fbd80827dfca5660d81ca6e833e2c/icons/hud/screen_ai.dmi",
+    "size": {
+        "x": 32,
+        "y": 32
+    },
+    "states": [
+        {
+            "name": "ai_core"
+        },
+        {
+            "name": "camera_light"
+        },
+        {
+            "name": "crew_monitor"
+        },
+        {
+            "name": "manifest"
+        },
+        {
+            "name": "state_laws"
+        }
+    ]
+}
diff --git a/Resources/Textures/Interface/Actions/actions_ai.rsi/state_laws.png b/Resources/Textures/Interface/Actions/actions_ai.rsi/state_laws.png
new file mode 100644 (file)
index 0000000..e30e891
Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_ai.rsi/state_laws.png differ
diff --git a/Resources/Textures/Interface/Misc/job_icons.rsi/StationAi.png b/Resources/Textures/Interface/Misc/job_icons.rsi/StationAi.png
new file mode 100644 (file)
index 0000000..aba35c0
Binary files /dev/null and b/Resources/Textures/Interface/Misc/job_icons.rsi/StationAi.png differ
index fff9f78288cfbdf9d1d5b6837f7779b25a95e27f..7b09250bd986b687223b8644d47e23e67811a53d 100644 (file)
@@ -1,7 +1,7 @@
 {
     "version": 1,
     "license": "CC-BY-SA-3.0",
-    "copyright": "Taken from https://github.com/vgstation-coders/vgstation13/blob/e71d6c4fba5a51f99b81c295dcaec4fc2f58fb19/icons/mob/screen1.dmi | Brigmedic icon made by PuroSlavKing (Github) | Zombie icon made by RamZ | Zookeper by netwy (discort) | Rev and Head Rev icon taken from https://tgstation13.org/wiki/HUD and edited by coolmankid12345 (Discord) | Mindshield icon taken from https://github.com/tgstation/tgstation/blob/master/icons/mob/huds/hud.dmi | Admin recolored from MedicalIntern by TsjipTsjip",
+    "copyright": "Taken from https://github.com/vgstation-coders/vgstation13/blob/e71d6c4fba5a51f99b81c295dcaec4fc2f58fb19/icons/mob/screen1.dmi | Brigmedic icon made by PuroSlavKing (Github) | Zombie icon made by RamZ | Zookeper by netwy (discort) | Rev and Head Rev icon taken from https://tgstation13.org/wiki/HUD and edited by coolmankid12345 (Discord) | Mindshield icon taken from https://github.com/tgstation/tgstation/blob/ce6beb8a4d61235d9a597a7126c407160ed674ea/icons/mob/huds/hud.dmi | Admin recolored from MedicalIntern by TsjipTsjip",
 
     "size": {
         "x": 8,
                 [1.0,1.0]
             ]
         },
+        {
+            "name": "StationAi"
+        },
         {
             "name": "Syndicate"
         },
diff --git a/Resources/Textures/Interface/noise.rsi/meta.json b/Resources/Textures/Interface/noise.rsi/meta.json
new file mode 100644 (file)
index 0000000..068ecab
--- /dev/null
@@ -0,0 +1,58 @@
+{
+  "version": 1,
+  "license": "CC-BY-SA-3.0",
+  "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/blob/4b4e9dff1d7d891cfb75d25ca5bf5172d1c02be6/icons/hud/screen_gen.dmi",
+  "size": {
+    "x": 32,
+    "y": 32
+  },
+  "states": [
+    {
+      "name": "noise",
+      "delays": [
+        [
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1
+        ]
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/Resources/Textures/Interface/noise.rsi/noise.png b/Resources/Textures/Interface/noise.rsi/noise.png
new file mode 100644 (file)
index 0000000..ba74952
Binary files /dev/null and b/Resources/Textures/Interface/noise.rsi/noise.png differ
diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/ai-banned-unshaded.png b/Resources/Textures/Mobs/Silicon/output.rsi/ai-banned-unshaded.png
new file mode 100644 (file)
index 0000000..9b72b39
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/ai-banned-unshaded.png differ
diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/ai-banned.png b/Resources/Textures/Mobs/Silicon/output.rsi/ai-banned.png
new file mode 100644 (file)
index 0000000..c583bb8
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/ai-banned.png differ
diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/ai-banned_dead-unshaded.png b/Resources/Textures/Mobs/Silicon/output.rsi/ai-banned_dead-unshaded.png
new file mode 100644 (file)
index 0000000..28bf165
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/ai-banned_dead-unshaded.png differ
diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/ai-banned_dead.png b/Resources/Textures/Mobs/Silicon/output.rsi/ai-banned_dead.png
new file mode 100644 (file)
index 0000000..7c5b468
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/ai-banned_dead.png differ
diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/ai-empty-unshaded.png b/Resources/Textures/Mobs/Silicon/output.rsi/ai-empty-unshaded.png
new file mode 100644 (file)
index 0000000..6539176
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/ai-empty-unshaded.png differ
diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/ai-empty.png b/Resources/Textures/Mobs/Silicon/output.rsi/ai-empty.png
new file mode 100644 (file)
index 0000000..eda1f4b
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/ai-empty.png differ
diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/ai-holo-old-unshaded.png b/Resources/Textures/Mobs/Silicon/output.rsi/ai-holo-old-unshaded.png
new file mode 100644 (file)
index 0000000..63616e7
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/ai-holo-old-unshaded.png differ
diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/ai-holo-old.png b/Resources/Textures/Mobs/Silicon/output.rsi/ai-holo-old.png
new file mode 100644 (file)
index 0000000..3dc7a30
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/ai-holo-old.png differ
diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/ai-unshaded.png b/Resources/Textures/Mobs/Silicon/output.rsi/ai-unshaded.png
new file mode 100644 (file)
index 0000000..f3ba4b5
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/ai-unshaded.png differ
diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/ai.png b/Resources/Textures/Mobs/Silicon/output.rsi/ai.png
new file mode 100644 (file)
index 0000000..3c81e3a
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/ai.png differ
diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/ai_dead-unshaded.png b/Resources/Textures/Mobs/Silicon/output.rsi/ai_dead-unshaded.png
new file mode 100644 (file)
index 0000000..96e9536
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/ai_dead-unshaded.png differ
diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/ai_dead.png b/Resources/Textures/Mobs/Silicon/output.rsi/ai_dead.png
new file mode 100644 (file)
index 0000000..96e9536
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/ai_dead.png differ
diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/default-unshaded.png b/Resources/Textures/Mobs/Silicon/output.rsi/default-unshaded.png
new file mode 100644 (file)
index 0000000..f14b4ef
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/default-unshaded.png differ
diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/default.png b/Resources/Textures/Mobs/Silicon/output.rsi/default.png
new file mode 100644 (file)
index 0000000..f14b4ef
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/default.png differ
diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/floating_face-unshaded.png b/Resources/Textures/Mobs/Silicon/output.rsi/floating_face-unshaded.png
new file mode 100644 (file)
index 0000000..05de742
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/floating_face-unshaded.png differ
diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/floating_face.png b/Resources/Textures/Mobs/Silicon/output.rsi/floating_face.png
new file mode 100644 (file)
index 0000000..05de742
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/floating_face.png differ
diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/horror-unshaded.png b/Resources/Textures/Mobs/Silicon/output.rsi/horror-unshaded.png
new file mode 100644 (file)
index 0000000..10efd5e
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/horror-unshaded.png differ
diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/horror.png b/Resources/Textures/Mobs/Silicon/output.rsi/horror.png
new file mode 100644 (file)
index 0000000..0a807c0
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/horror.png differ
diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/meta.json b/Resources/Textures/Mobs/Silicon/output.rsi/meta.json
new file mode 100644 (file)
index 0000000..a40ed37
--- /dev/null
@@ -0,0 +1 @@
+{"version": 1, "size": {"x": 32, "y": 32}, "license": "CC-BY-SA-3.0", "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/blob/2a19963297f91efb452dbb5c1d4eb28a14776b0a/icons/mob/silicon/ai.dmi", "states": [{"name": "ai", "directions": 1, "delays": [[0.2, 0.2, 0.2, 0.1, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.1]]}, {"name": "ai-banned", "directions": 1, "delays": [[0.7, 0.7, 0.7, 0.7, 0.7, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7]]}, {"name": "ai-banned-unshaded", "directions": 1, "delays": [[0.7, 0.7, 0.7, 0.7, 0.7, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7, 0.7]]}, {"name": "ai-banned_dead", "directions": 1, "delays": [[0.7, 0.7]]}, {"name": "ai-banned_dead-unshaded", "directions": 1, "delays": [[0.7, 0.7]]}, {"name": "ai-empty", "directions": 1, "delays": [[0.7, 0.7]]}, {"name": "ai-empty-unshaded", "directions": 1, "delays": [[0.7, 0.7]]}, {"name": "ai-holo-old", "directions": 4, "delays": [[0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]]}, {"name": "ai-holo-old-unshaded", "directions": 4, "delays": [[0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]]}, {"name": "ai-unshaded", "directions": 1, "delays": [[0.2, 0.2, 0.2, 0.1, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.1]]}, {"name": "ai_dead", "directions": 1, "delays": [[1.0]]}, {"name": "ai_dead-unshaded", "directions": 1, "delays": [[1.0]]}, {"name": "default", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}, {"name": "default-unshaded", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}, {"name": "floating_face", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}, {"name": "floating_face-unshaded", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}, {"name": "horror", "directions": 1, "delays": [[0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]]}, {"name": "horror-unshaded", "directions": 1, "delays": [[0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]]}, {"name": "xeno_queen", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}, {"name": "xeno_queen-unshaded", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}]}
\ No newline at end of file
diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/xeno_queen-unshaded.png b/Resources/Textures/Mobs/Silicon/output.rsi/xeno_queen-unshaded.png
new file mode 100644 (file)
index 0000000..3ea1940
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/xeno_queen-unshaded.png differ
diff --git a/Resources/Textures/Mobs/Silicon/output.rsi/xeno_queen.png b/Resources/Textures/Mobs/Silicon/output.rsi/xeno_queen.png
new file mode 100644 (file)
index 0000000..3ea1940
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/output.rsi/xeno_queen.png differ
diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai.png
new file mode 100644 (file)
index 0000000..420a07c
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai.png differ
diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_dead.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_dead.png
new file mode 100644 (file)
index 0000000..eb74655
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_dead.png differ
diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_empty.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_empty.png
new file mode 100644 (file)
index 0000000..40e8ac5
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_empty.png differ
diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/base.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/base.png
new file mode 100644 (file)
index 0000000..a9db836
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/base.png differ
diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/default.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/default.png
new file mode 100644 (file)
index 0000000..d52acea
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/default.png differ
diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/meta.json b/Resources/Textures/Mobs/Silicon/station_ai.rsi/meta.json
new file mode 100644 (file)
index 0000000..a3da522
--- /dev/null
@@ -0,0 +1,52 @@
+{
+  "version": 1,
+  "license": "CC-BY-SA-3.0",
+  "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/blob/2a19963297f91efb452dbb5c1d4eb28a14776b0a/icons/mob/silicon/ai.dmi",
+  "size": {
+    "x": 32,
+    "y": 32
+  },
+  "states": [
+    {
+      "name": "ai",
+      "delays": [
+        [
+          0.2,
+          0.2,
+          0.1,
+          0.2,
+          0.2,
+          0.2,
+          0.2,
+          0.2,
+          0.2,
+          0.2,
+          0.2,
+          0.2,
+          0.2,
+          0.2,
+          0.1
+        ]
+      ]
+    },
+    {
+      "name": "ai_dead"
+    },
+    {
+      "name": "ai_empty",
+      "delays": [
+        [
+          0.7,
+          0.7
+        ]
+      ]
+    },
+    {
+      "name": "default",
+      "directions": 4
+    },
+    {
+      "name": "base"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/Resources/Textures/Objects/Devices/ai_card.rsi/base.png b/Resources/Textures/Objects/Devices/ai_card.rsi/base.png
new file mode 100644 (file)
index 0000000..244183c
Binary files /dev/null and b/Resources/Textures/Objects/Devices/ai_card.rsi/base.png differ
diff --git a/Resources/Textures/Objects/Devices/ai_card.rsi/empty.png b/Resources/Textures/Objects/Devices/ai_card.rsi/empty.png
new file mode 100644 (file)
index 0000000..7e61f36
Binary files /dev/null and b/Resources/Textures/Objects/Devices/ai_card.rsi/empty.png differ
diff --git a/Resources/Textures/Objects/Devices/ai_card.rsi/full.png b/Resources/Textures/Objects/Devices/ai_card.rsi/full.png
new file mode 100644 (file)
index 0000000..59131c8
Binary files /dev/null and b/Resources/Textures/Objects/Devices/ai_card.rsi/full.png differ
diff --git a/Resources/Textures/Objects/Devices/ai_card.rsi/inhand-left.png b/Resources/Textures/Objects/Devices/ai_card.rsi/inhand-left.png
new file mode 100644 (file)
index 0000000..2d38631
Binary files /dev/null and b/Resources/Textures/Objects/Devices/ai_card.rsi/inhand-left.png differ
diff --git a/Resources/Textures/Objects/Devices/ai_card.rsi/inhand-right.png b/Resources/Textures/Objects/Devices/ai_card.rsi/inhand-right.png
new file mode 100644 (file)
index 0000000..1704b9c
Binary files /dev/null and b/Resources/Textures/Objects/Devices/ai_card.rsi/inhand-right.png differ
diff --git a/Resources/Textures/Objects/Devices/ai_card.rsi/meta.json b/Resources/Textures/Objects/Devices/ai_card.rsi/meta.json
new file mode 100644 (file)
index 0000000..8b8135f
--- /dev/null
@@ -0,0 +1,58 @@
+{
+  "version": 1,
+  "license": "CC-BY-SA-3.0",
+  "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/blob/1feffb747a33434a9d28450fc52ade75253aeba5/icons/obj/aicards.dmi",
+  "size": {
+    "x": 32,
+    "y": 32
+  },
+  "states": [
+    {
+      "name": "base"
+    },
+    {
+      "name": "inhand-left",
+      "directions": 4
+    },
+    {
+      "name": "inhand-right",
+      "directions": 4
+    },
+    {
+      "name": "empty",
+      "delays": [
+        [
+          0.4,
+          0.4
+        ]
+      ]
+    },
+    {
+      "name": "full",
+      "delays": [
+        [
+          0.4,
+          0.4,
+          0.4,
+          0.4,
+          0.4,
+          0.4,
+          0.4,
+          0.4,
+          0.4,
+          0.4,
+          0.4,
+          0.4,
+          0.4,
+          0.4,
+          0.4,
+          0.4,
+          0.4,
+          0.4,
+          0.4,
+          0.4
+        ]
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/Resources/Textures/Objects/Devices/output.rsi/aicard-full-unshaded.png b/Resources/Textures/Objects/Devices/output.rsi/aicard-full-unshaded.png
new file mode 100644 (file)
index 0000000..51a309d
Binary files /dev/null and b/Resources/Textures/Objects/Devices/output.rsi/aicard-full-unshaded.png differ
diff --git a/Resources/Textures/Objects/Devices/output.rsi/aicard-full.png b/Resources/Textures/Objects/Devices/output.rsi/aicard-full.png
new file mode 100644 (file)
index 0000000..03908b5
Binary files /dev/null and b/Resources/Textures/Objects/Devices/output.rsi/aicard-full.png differ
diff --git a/Resources/Textures/Objects/Devices/output.rsi/aicard-unshaded.png b/Resources/Textures/Objects/Devices/output.rsi/aicard-unshaded.png
new file mode 100644 (file)
index 0000000..6191a01
Binary files /dev/null and b/Resources/Textures/Objects/Devices/output.rsi/aicard-unshaded.png differ
diff --git a/Resources/Textures/Objects/Devices/output.rsi/aicard.png b/Resources/Textures/Objects/Devices/output.rsi/aicard.png
new file mode 100644 (file)
index 0000000..57f604e
Binary files /dev/null and b/Resources/Textures/Objects/Devices/output.rsi/aicard.png differ
diff --git a/Resources/Textures/Objects/Devices/output.rsi/meta.json b/Resources/Textures/Objects/Devices/output.rsi/meta.json
new file mode 100644 (file)
index 0000000..500ecb8
--- /dev/null
@@ -0,0 +1 @@
+{"version": 1, "size": {"x": 32, "y": 32}, "license": "CC-BY-SA-3.0", "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/blob/1feffb747a33434a9d28450fc52ade75253aeba5/icons/obj/aicards.dmi", "states": [{"name": "aicard", "directions": 1, "delays": [[0.4, 0.4]]}, {"name": "aicard-full", "directions": 1, "delays": [[0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4]]}, {"name": "aicard-full-unshaded", "directions": 1, "delays": [[0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4]]}, {"name": "aicard-unshaded", "directions": 1, "delays": [[0.4, 0.4]]}]}
\ No newline at end of file