]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
malf killer 9000 (robotics console) (#24855)
authordeltanedas <39013340+deltanedas@users.noreply.github.com>
Thu, 9 May 2024 06:36:07 +0000 (06:36 +0000)
committerGitHub <noreply@github.com>
Thu, 9 May 2024 06:36:07 +0000 (23:36 -0700)
* create devicenet frequencies

* create borg transponder and give it to all nt borgs

* add robotics console

* actually implement battery charge display + some fix

* tab

* real explosion

* little safer

* disable destroy button clientside too when on cooldown

* m

* how do i do this when i review things...

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
* webedit ops

Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
* ui updates

* oracle java

* do a thing

* update ui when a borg times out

* maybe fix test

* add IsLocked to LockSystem

* make destroying gib the chassis again, so emagging isnt sus

* use locking

* require using alt click to unlock so normal click is open ui

* the

* use LogType.Action

* take this L

* pocket lint?

* sharer

* pro ops

* robor pushmarkup

* m

* update and make it not use prototype anymore

* frame0

* update yaml

* untroll

* bad

* h

---------

Co-authored-by: deltanedas <@deltanedas:kde.org>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
21 files changed:
Content.Client/Robotics/Systems/RoboticsConsoleSystem.cs [new file with mode: 0644]
Content.Client/Robotics/UI/RoboticsConsoleBoundUserInterface.cs [new file with mode: 0644]
Content.Client/Robotics/UI/RoboticsConsoleWindow.xaml [new file with mode: 0644]
Content.Client/Robotics/UI/RoboticsConsoleWindow.xaml.cs [new file with mode: 0644]
Content.Server/Robotics/Systems/RoboticsConsoleSystem.cs [new file with mode: 0644]
Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs [new file with mode: 0644]
Content.Server/Silicons/Borgs/BorgSystem.cs
Content.Shared/Lock/LockComponent.cs
Content.Shared/Lock/LockSystem.cs
Content.Shared/Robotics/Components/RoboticsConsoleComponent.cs [new file with mode: 0644]
Content.Shared/Robotics/RoboticsConsoleUi.cs [new file with mode: 0644]
Content.Shared/Robotics/Systems/SharedRoboticsConsoleSystem.cs [new file with mode: 0644]
Content.Shared/Silicons/Borgs/Components/BorgTransponderComponent.cs [new file with mode: 0644]
Resources/Locale/en-US/borg/borg.ftl
Resources/Locale/en-US/devices/device-network.ftl
Resources/Locale/en-US/research/components/robotics-console.ftl [new file with mode: 0644]
Resources/Prototypes/Device/devicenet_frequencies.yml
Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml
Resources/Prototypes/Entities/Mobs/Cyborgs/borg_chassis.yml
Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml
Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml

diff --git a/Content.Client/Robotics/Systems/RoboticsConsoleSystem.cs b/Content.Client/Robotics/Systems/RoboticsConsoleSystem.cs
new file mode 100644 (file)
index 0000000..0219c96
--- /dev/null
@@ -0,0 +1,7 @@
+using Content.Shared.Robotics.Systems;
+
+namespace Content.Client.Robotics.Systems;
+
+public sealed class RoboticsConsoleSystem : SharedRoboticsConsoleSystem
+{
+}
diff --git a/Content.Client/Robotics/UI/RoboticsConsoleBoundUserInterface.cs b/Content.Client/Robotics/UI/RoboticsConsoleBoundUserInterface.cs
new file mode 100644 (file)
index 0000000..6185979
--- /dev/null
@@ -0,0 +1,50 @@
+using Content.Shared.Robotics;
+using Robust.Client.GameObjects;
+
+namespace Content.Client.Robotics.UI;
+
+public sealed class RoboticsConsoleBoundUserInterface : BoundUserInterface
+{
+    [ViewVariables]
+    public RoboticsConsoleWindow _window = default!;
+
+    public RoboticsConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+    {
+    }
+
+    protected override void Open()
+    {
+        base.Open();
+
+        _window = new RoboticsConsoleWindow(Owner);
+        _window.OnDisablePressed += address =>
+        {
+            SendMessage(new RoboticsConsoleDisableMessage(address));
+        };
+        _window.OnDestroyPressed += address =>
+        {
+            SendMessage(new RoboticsConsoleDestroyMessage(address));
+        };
+        _window.OnClose += Close;
+
+        _window.OpenCentered();
+    }
+
+    protected override void UpdateState(BoundUserInterfaceState state)
+    {
+        base.UpdateState(state);
+
+        if (state is not RoboticsConsoleState cast)
+            return;
+
+        _window?.UpdateState(cast);
+    }
+
+    protected override void Dispose(bool disposing)
+    {
+        base.Dispose(disposing);
+
+        if (disposing)
+            _window?.Dispose();
+    }
+}
diff --git a/Content.Client/Robotics/UI/RoboticsConsoleWindow.xaml b/Content.Client/Robotics/UI/RoboticsConsoleWindow.xaml
new file mode 100644 (file)
index 0000000..a3b3978
--- /dev/null
@@ -0,0 +1,40 @@
+<controls:FancyWindow xmlns="https://spacestation14.io"
+                     xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+                     Title="{Loc 'robotics-console-window-title'}"
+                     MinSize="600 450">
+    <BoxContainer Orientation="Vertical">
+         <!-- List of borgs -->
+        <BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True" Margin="10 10 10 10">
+            <Label Name="NoCyborgs" Text="{Loc 'robotics-console-no-cyborgs'}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
+            <ScrollContainer Name="CyborgsContainer" VerticalExpand="True" Visible="False">
+                <!-- Populated when loading state -->
+                <ItemList Name="Cyborgs"/>
+            </ScrollContainer>
+        </BoxContainer>
+
+        <PanelContainer StyleClasses="LowDivider" Margin="0 5 0 5"/>
+
+        <!-- Selected borg info -->
+        <Label Name="SelectCyborg" Text="{Loc 'robotics-console-select-cyborg'}" HorizontalAlignment="Center"/>
+        <BoxContainer Name="BorgContainer" Orientation="Vertical" MaxHeight="200" Visible="False">
+            <BoxContainer Margin="5 5 5 5" Orientation="Horizontal">
+                <PanelContainer VerticalExpand="True">
+                    <BoxContainer HorizontalAlignment="Center" VerticalAlignment="Center">
+                        <TextureRect Name="BorgSprite" TextureScale="4 4"/>
+                    </BoxContainer>
+                </PanelContainer>
+                <PanelContainer VerticalExpand="True" HorizontalExpand="True">
+                    <RichTextLabel Name="BorgInfo"/>
+                </PanelContainer>
+                <!-- TODO: button to open camera window for this borg -->
+            </BoxContainer>
+            <controls:StripeBack>
+                <BoxContainer Name="DangerZone" Margin="5" Orientation="Horizontal" HorizontalExpand="True" HorizontalAlignment="Center" Visible="False">
+                    <Button Name="DisableButton" Text="{Loc 'robotics-console-disable'}" StyleClasses="OpenRight"/>
+                    <Button Name="DestroyButton" Text="{Loc 'robotics-console-destroy'}" StyleClasses="OpenLeft"/>
+                </BoxContainer>
+                <Label Name="LockedMessage" Text="{Loc 'robotics-console-locked-message'}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
+            </controls:StripeBack>
+        </BoxContainer>
+    </BoxContainer>
+</controls:FancyWindow>
diff --git a/Content.Client/Robotics/UI/RoboticsConsoleWindow.xaml.cs b/Content.Client/Robotics/UI/RoboticsConsoleWindow.xaml.cs
new file mode 100644 (file)
index 0000000..3555099
--- /dev/null
@@ -0,0 +1,148 @@
+using Content.Client.Stylesheets;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Lock;
+using Content.Shared.Robotics;
+using Content.Shared.Robotics.Components;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Robotics.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class RoboticsConsoleWindow : FancyWindow
+{
+    [Dependency] private readonly IEntityManager _entMan = default!;
+    [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly IPrototypeManager _proto = default!;
+    private readonly LockSystem _lock;
+    private readonly SpriteSystem _sprite;
+
+    public Action<string>? OnDisablePressed;
+    public Action<string>? OnDestroyPressed;
+
+    private Entity<RoboticsConsoleComponent, LockComponent?> _console;
+    private string? _selected;
+    private Dictionary<string, CyborgControlData> _cyborgs = new();
+
+    public RoboticsConsoleWindow(EntityUid console)
+    {
+        RobustXamlLoader.Load(this);
+        IoCManager.InjectDependencies(this);
+
+        _lock = _entMan.System<LockSystem>();
+        _sprite = _entMan.System<SpriteSystem>();
+
+        _console = (console, _entMan.GetComponent<RoboticsConsoleComponent>(console), null);
+        _entMan.TryGetComponent(_console, out _console.Comp2);
+
+        Cyborgs.OnItemSelected += args =>
+        {
+            if (Cyborgs[args.ItemIndex].Metadata is not string address)
+                return;
+
+            _selected = address;
+            PopulateData();
+        };
+        Cyborgs.OnItemDeselected += _ =>
+        {
+            _selected = null;
+            PopulateData();
+        };
+
+        // these won't throw since buttons are only visible if a borg is selected
+        DisableButton.OnPressed += _ =>
+        {
+            OnDisablePressed?.Invoke(_selected!);
+        };
+        DestroyButton.OnPressed += _ =>
+        {
+            OnDestroyPressed?.Invoke(_selected!);
+        };
+
+        // cant put multiple styles in xaml for some reason
+        DestroyButton.StyleClasses.Add(StyleBase.ButtonCaution);
+    }
+
+    public void UpdateState(RoboticsConsoleState state)
+    {
+        _cyborgs = state.Cyborgs;
+
+        // clear invalid selection
+        if (_selected is {} selected && !_cyborgs.ContainsKey(selected))
+            _selected = null;
+
+        var hasCyborgs = _cyborgs.Count > 0;
+        NoCyborgs.Visible = !hasCyborgs;
+        CyborgsContainer.Visible = hasCyborgs;
+        PopulateCyborgs();
+
+        PopulateData();
+
+        var locked = _lock.IsLocked((_console, _console.Comp2));
+        DangerZone.Visible = !locked;
+        LockedMessage.Visible = locked;
+    }
+
+    private void PopulateCyborgs()
+    {
+        // _selected might get set to null when recreating so copy it first
+        var selected = _selected;
+        Cyborgs.Clear();
+        foreach (var (address, data) in _cyborgs)
+        {
+            var item = Cyborgs.AddItem(data.Name, _sprite.Frame0(data.ChassisSprite!), metadata: address);
+            item.Selected = address == selected;
+        }
+        _selected = selected;
+    }
+
+    private void PopulateData()
+    {
+        if (_selected is not {} selected)
+        {
+            SelectCyborg.Visible = true;
+            BorgContainer.Visible = false;
+            return;
+        }
+
+        SelectCyborg.Visible = false;
+        BorgContainer.Visible = true;
+
+        var data = _cyborgs[selected];
+        var model = data.ChassisName;
+
+        BorgSprite.Texture = _sprite.Frame0(data.ChassisSprite!);
+
+        var batteryColor = data.Charge switch {
+            < 0.2f => "red",
+            < 0.4f => "orange",
+            < 0.6f => "yellow",
+            < 0.8f => "green",
+            _ => "blue"
+        };
+
+        var text = new FormattedMessage();
+        text.PushMarkup(Loc.GetString("robotics-console-model", ("name", model)));
+        text.AddMarkup(Loc.GetString("robotics-console-designation"));
+        text.AddText($" {data.Name}\n"); // prevent players trolling by naming borg [color=red]satan[/color]
+        text.PushMarkup(Loc.GetString("robotics-console-battery", ("charge", (int) (data.Charge * 100f)), ("color", batteryColor)));
+        text.PushMarkup(Loc.GetString("robotics-console-brain", ("brain", data.HasBrain)));
+        text.AddMarkup(Loc.GetString("robotics-console-modules", ("count", data.ModuleCount)));
+        BorgInfo.SetMessage(text);
+
+        // how the turntables
+        DisableButton.Disabled = !data.HasBrain;
+        DestroyButton.Disabled = _timing.CurTime < _console.Comp1.NextDestroy;
+    }
+
+    protected override void FrameUpdate(FrameEventArgs args)
+    {
+        base.FrameUpdate(args);
+
+        DestroyButton.Disabled = _timing.CurTime < _console.Comp1.NextDestroy;
+    }
+}
diff --git a/Content.Server/Robotics/Systems/RoboticsConsoleSystem.cs b/Content.Server/Robotics/Systems/RoboticsConsoleSystem.cs
new file mode 100644 (file)
index 0000000..916694f
--- /dev/null
@@ -0,0 +1,146 @@
+using Content.Server.Administration.Logs;
+using Content.Server.DeviceNetwork;
+using Content.Server.DeviceNetwork.Systems;
+using Content.Server.Radio.EntitySystems;
+using Content.Shared.Lock;
+using Content.Shared.Database;
+using Content.Shared.DeviceNetwork;
+using Content.Shared.Robotics;
+using Content.Shared.Robotics.Components;
+using Content.Shared.Robotics.Systems;
+using Robust.Server.GameObjects;
+using Robust.Shared.Timing;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Content.Server.Research.Systems;
+
+/// <summary>
+/// Handles UI and state receiving for the robotics control console.
+/// <c>BorgTransponderComponent<c/> broadcasts state from the station's borgs to consoles.
+/// </summary>
+public sealed class RoboticsConsoleSystem : SharedRoboticsConsoleSystem
+{
+    [Dependency] private readonly DeviceNetworkSystem _deviceNetwork = default!;
+    [Dependency] private readonly IAdminLogManager _adminLogger = default!;
+    [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly LockSystem _lock = default!;
+    [Dependency] private readonly RadioSystem _radio = default!;
+    [Dependency] private readonly UserInterfaceSystem _ui = default!;
+
+    // almost never timing out more than 1 per tick so initialize with that capacity
+    private List<string> _removing = new(1);
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<RoboticsConsoleComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
+        Subs.BuiEvents<RoboticsConsoleComponent>(RoboticsConsoleUiKey.Key, subs =>
+        {
+            subs.Event<BoundUIOpenedEvent>(OnOpened);
+            subs.Event<RoboticsConsoleDisableMessage>(OnDisable);
+            subs.Event<RoboticsConsoleDestroyMessage>(OnDestroy);
+            // TODO: camera stuff
+        });
+    }
+
+    public override void Update(float frameTime)
+    {
+        base.Update(frameTime);
+
+        var now = _timing.CurTime;
+        var query = EntityQueryEnumerator<RoboticsConsoleComponent>();
+        while (query.MoveNext(out var uid, out var comp))
+        {
+            // remove cyborgs that havent pinged in a while
+            _removing.Clear();
+            foreach (var (address, data) in comp.Cyborgs)
+            {
+                if (now >= data.Timeout)
+                    _removing.Add(address);
+            }
+
+            // needed to prevent modifying while iterating it
+            foreach (var address in _removing)
+            {
+                comp.Cyborgs.Remove(address);
+            }
+
+            if (_removing.Count > 0)
+                UpdateUserInterface((uid, comp));
+        }
+    }
+
+    private void OnPacketReceived(Entity<RoboticsConsoleComponent> ent, ref DeviceNetworkPacketEvent args)
+    {
+        var payload = args.Data;
+        if (!payload.TryGetValue(DeviceNetworkConstants.Command, out string? command))
+            return;
+        if (command != DeviceNetworkConstants.CmdUpdatedState)
+            return;
+
+        if (!payload.TryGetValue(RoboticsConsoleConstants.NET_CYBORG_DATA, out CyborgControlData? data))
+            return;
+
+        var real = data.Value;
+        real.Timeout = _timing.CurTime + ent.Comp.Timeout;
+        ent.Comp.Cyborgs[args.SenderAddress] = real;
+
+        UpdateUserInterface(ent);
+    }
+
+    private void OnOpened(Entity<RoboticsConsoleComponent> ent, ref BoundUIOpenedEvent args)
+    {
+        UpdateUserInterface(ent);
+    }
+
+    private void OnDisable(Entity<RoboticsConsoleComponent> ent, ref RoboticsConsoleDisableMessage args)
+    {
+        if (_lock.IsLocked(ent.Owner))
+            return;
+
+        if (!ent.Comp.Cyborgs.TryGetValue(args.Address, out var data))
+            return;
+
+        var payload = new NetworkPayload()
+        {
+            [DeviceNetworkConstants.Command] = RoboticsConsoleConstants.NET_DISABLE_COMMAND
+        };
+
+        _deviceNetwork.QueuePacket(ent, args.Address, payload);
+        _adminLogger.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(args.Actor):user} disabled borg {data.Name} with address {args.Address}");
+    }
+
+    private void OnDestroy(Entity<RoboticsConsoleComponent> ent, ref RoboticsConsoleDestroyMessage args)
+    {
+        if (_lock.IsLocked(ent.Owner))
+            return;
+
+        var now = _timing.CurTime;
+        if (now < ent.Comp.NextDestroy)
+            return;
+
+        if (!ent.Comp.Cyborgs.Remove(args.Address, out var data))
+            return;
+
+        var payload = new NetworkPayload()
+        {
+            [DeviceNetworkConstants.Command] = RoboticsConsoleConstants.NET_DESTROY_COMMAND
+        };
+
+        _deviceNetwork.QueuePacket(ent, args.Address, payload);
+
+        var message = Loc.GetString(ent.Comp.DestroyMessage, ("name", data.Name));
+        _radio.SendRadioMessage(ent, message, ent.Comp.RadioChannel, ent);
+        _adminLogger.Add(LogType.Action, LogImpact.Extreme, $"{ToPrettyString(args.Actor):user} destroyed borg {data.Name} with address {args.Address}");
+
+        ent.Comp.NextDestroy = now + ent.Comp.DestroyCooldown;
+        Dirty(ent, ent.Comp);
+    }
+
+    private void UpdateUserInterface(Entity<RoboticsConsoleComponent> ent)
+    {
+        var state = new RoboticsConsoleState(ent.Comp.Cyborgs);
+        _ui.SetUiState(ent.Owner, RoboticsConsoleUiKey.Key, state);
+    }
+}
diff --git a/Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs b/Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs
new file mode 100644 (file)
index 0000000..1c10cbe
--- /dev/null
@@ -0,0 +1,107 @@
+using Content.Shared.DeviceNetwork;
+using Content.Shared.Emag.Components;
+using Content.Shared.Popups;
+using Content.Shared.Robotics;
+using Content.Shared.Silicons.Borgs.Components;
+using Content.Server.DeviceNetwork;
+using Content.Server.DeviceNetwork.Components;
+using Content.Server.DeviceNetwork.Systems;
+using Content.Server.Explosion.Components;
+
+namespace Content.Server.Silicons.Borgs;
+
+/// <inheritdoc/>
+public sealed partial class BorgSystem
+{
+    private void InitializeTransponder()
+    {
+        SubscribeLocalEvent<BorgTransponderComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
+    }
+
+    public override void Update(float frameTime)
+    {
+        base.Update(frameTime);
+
+        var now = _timing.CurTime;
+        var query = EntityQueryEnumerator<BorgTransponderComponent, BorgChassisComponent, DeviceNetworkComponent, MetaDataComponent>();
+        while (query.MoveNext(out var uid, out var comp, out var chassis, out var device, out var meta))
+        {
+            if (now < comp.NextBroadcast)
+                continue;
+
+            var charge = 0f;
+            if (_powerCell.TryGetBatteryFromSlot(uid, out var battery))
+                charge = battery.CurrentCharge / battery.MaxCharge;
+
+            var data = new CyborgControlData(
+                comp.Sprite,
+                comp.Name,
+                meta.EntityName,
+                charge,
+                chassis.ModuleCount,
+                chassis.BrainEntity != null);
+
+            var payload = new NetworkPayload()
+            {
+                [DeviceNetworkConstants.Command] = DeviceNetworkConstants.CmdUpdatedState,
+                [RoboticsConsoleConstants.NET_CYBORG_DATA] = data
+            };
+            _deviceNetwork.QueuePacket(uid, null, payload, device: device);
+
+            comp.NextBroadcast = now + comp.BroadcastDelay;
+        }
+    }
+
+    private void OnPacketReceived(Entity<BorgTransponderComponent> ent, ref DeviceNetworkPacketEvent args)
+    {
+        var payload = args.Data;
+        if (!payload.TryGetValue(DeviceNetworkConstants.Command, out string? command))
+            return;
+
+        if (command == RoboticsConsoleConstants.NET_DISABLE_COMMAND)
+            Disable(ent);
+        else if (command == RoboticsConsoleConstants.NET_DESTROY_COMMAND)
+            Destroy(ent.Owner);
+    }
+
+    private void Disable(Entity<BorgTransponderComponent, BorgChassisComponent?> ent)
+    {
+        if (!Resolve(ent, ref ent.Comp2) || ent.Comp2.BrainEntity is not {} brain)
+            return;
+
+        // this won't exactly be stealthy but if you are malf its better than actually disabling you
+        if (CheckEmagged(ent, "disabled"))
+            return;
+
+        var message = Loc.GetString(ent.Comp1.DisabledPopup, ("name", Name(ent)));
+        Popup.PopupEntity(message, ent);
+        _container.Remove(brain, ent.Comp2.BrainContainer);
+    }
+
+    private void Destroy(Entity<ExplosiveComponent?> ent)
+    {
+        if (!Resolve(ent, ref ent.Comp))
+            return;
+
+        // this is stealthy until someone realises you havent exploded
+        if (CheckEmagged(ent, "destroyed"))
+        {
+            // prevent reappearing on the console a few seconds later
+            RemComp<BorgTransponderComponent>(ent);
+            return;
+        }
+
+        _explosion.TriggerExplosive(ent, ent.Comp, delete: false);
+    }
+
+    private bool CheckEmagged(EntityUid uid, string name)
+    {
+        if (HasComp<EmaggedComponent>(uid))
+        {
+            Popup.PopupEntity(Loc.GetString($"borg-transponder-emagged-{name}-popup"), uid, uid, PopupType.LargeCaution);
+            return true;
+        }
+
+        return false;
+    }
+}
index 0f14fef0ed6ae4f28733b7a6aa2764827e21f500..ceab044d4c1c5aace3b1d7b8938dae9feeff6b3f 100644 (file)
@@ -1,6 +1,8 @@
 using Content.Server.Actions;
 using Content.Server.Administration.Logs;
 using Content.Server.Administration.Managers;
+using Content.Server.DeviceNetwork.Systems;
+using Content.Server.Explosion.EntitySystems;
 using Content.Server.Hands.Systems;
 using Content.Server.PowerCell;
 using Content.Shared.UserInterface;
@@ -26,6 +28,7 @@ using Robust.Server.GameObjects;
 using Robust.Shared.Containers;
 using Robust.Shared.Player;
 using Robust.Shared.Random;
+using Robust.Shared.Timing;
 
 namespace Content.Server.Silicons.Borgs;
 
@@ -34,10 +37,13 @@ public sealed partial class BorgSystem : SharedBorgSystem
 {
     [Dependency] private readonly IAdminLogManager _adminLog = default!;
     [Dependency] private readonly IBanManager _banManager = default!;
+    [Dependency] private readonly IGameTiming _timing = default!;
     [Dependency] private readonly IRobustRandom _random = default!;
     [Dependency] private readonly SharedAccessSystem _access = default!;
     [Dependency] private readonly ActionsSystem _actions = default!;
     [Dependency] private readonly AlertsSystem _alerts = default!;
+    [Dependency] private readonly DeviceNetworkSystem _deviceNetwork = default!;
+    [Dependency] private readonly ExplosionSystem _explosion = default!;
     [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
     [Dependency] private readonly HandsSystem _hands = default!;
     [Dependency] private readonly MetaDataSystem _metaData = default!;
@@ -73,6 +79,7 @@ public sealed partial class BorgSystem : SharedBorgSystem
         InitializeModules();
         InitializeMMI();
         InitializeUI();
+        InitializeTransponder();
     }
 
     private void OnMapInit(EntityUid uid, BorgChassisComponent component, MapInitEvent args)
index 5587fc2698badd2d3f96406cdebd308387db5e6b..e3e2bc6df1331573f31e66ea5ffe1e830160d41b 100644 (file)
@@ -21,12 +21,18 @@ public sealed partial class LockComponent : Component
     public bool Locked  = true;
 
     /// <summary>
-    /// Whether or not the lock is toggled by simply clicking.
+    /// Whether or not the lock is locked by simply clicking.
     /// </summary>
     [DataField("lockOnClick"), ViewVariables(VVAccess.ReadWrite)]
     [AutoNetworkedField]
     public bool LockOnClick;
 
+    /// <summary>
+    /// Whether or not the lock is unlocked by simply clicking.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool UnlockOnClick = true;
+
     /// <summary>
     /// The sound played when unlocked.
     /// </summary>
index 5644a6b02f6ea16c2fe874d22828c1931bdd6432..54f5d801ea0cb529a7a6f1b7730c5295f0d6ca4f 100644 (file)
@@ -58,12 +58,12 @@ public sealed class LockSystem : EntitySystem
             return;
 
         // Only attempt an unlock by default on Activate
-        if (lockComp.Locked)
+        if (lockComp.Locked && lockComp.UnlockOnClick)
         {
             TryUnlock(uid, args.User, lockComp);
             args.Handled = true;
         }
-        else if (lockComp.LockOnClick)
+        else if (!lockComp.Locked && lockComp.LockOnClick)
         {
             TryLock(uid, args.User, lockComp);
             args.Handled = true;
@@ -201,6 +201,18 @@ public sealed class LockSystem : EntitySystem
         return true;
     }
 
+    /// <summary>
+    /// Returns true if the entity is locked.
+    /// Entities with no lock component are considered unlocked.
+    /// </summary>
+    public bool IsLocked(Entity<LockComponent?> ent)
+    {
+        if (!Resolve(ent, ref ent.Comp, false))
+            return false;
+
+        return ent.Comp.Locked;
+    }
+
     /// <summary>
     /// Raises an event for other components to check whether or not
     /// the entity can be locked in its current state.
diff --git a/Content.Shared/Robotics/Components/RoboticsConsoleComponent.cs b/Content.Shared/Robotics/Components/RoboticsConsoleComponent.cs
new file mode 100644 (file)
index 0000000..4329e43
--- /dev/null
@@ -0,0 +1,53 @@
+using Content.Shared.Radio;
+using Content.Shared.Robotics;
+using Content.Shared.Robotics.Systems;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Shared.Robotics.Components;
+
+/// <summary>
+/// Robotics console for managing borgs.
+/// </summary>
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedRoboticsConsoleSystem))]
+[AutoGenerateComponentState, AutoGenerateComponentPause]
+public sealed partial class RoboticsConsoleComponent : Component
+{
+    /// <summary>
+    /// Address and data of each cyborg.
+    /// </summary>
+    [DataField]
+    public Dictionary<string, CyborgControlData> Cyborgs = new();
+
+    /// <summary>
+    /// After not responding for this length of time borgs are removed from the console.
+    /// </summary>
+    [DataField]
+    public TimeSpan Timeout = TimeSpan.FromSeconds(10);
+
+    /// <summary>
+    /// Radio channel to send messages on.
+    /// </summary>
+    [DataField]
+    public ProtoId<RadioChannelPrototype> RadioChannel = "Science";
+
+    /// <summary>
+    /// Radio message sent when destroying a borg.
+    /// </summary>
+    [DataField]
+    public LocId DestroyMessage = "robotics-console-cyborg-destroyed";
+
+    /// <summary>
+    /// Cooldown on destroying borgs to prevent complete abuse.
+    /// </summary>
+    [DataField]
+    public TimeSpan DestroyCooldown = TimeSpan.FromSeconds(30);
+
+    /// <summary>
+    /// When a borg can next be destroyed.
+    /// </summary>
+    [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
+    [AutoNetworkedField, AutoPausedField]
+    public TimeSpan NextDestroy = TimeSpan.Zero;
+}
diff --git a/Content.Shared/Robotics/RoboticsConsoleUi.cs b/Content.Shared/Robotics/RoboticsConsoleUi.cs
new file mode 100644 (file)
index 0000000..1be89be
--- /dev/null
@@ -0,0 +1,126 @@
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Robotics;
+
+[Serializable, NetSerializable]
+public enum RoboticsConsoleUiKey : byte
+{
+    Key
+}
+
+[Serializable, NetSerializable]
+public sealed class RoboticsConsoleState : BoundUserInterfaceState
+{
+    /// <summary>
+    /// Map of device network addresses to cyborg data.
+    /// </summary>
+    public Dictionary<string, CyborgControlData> Cyborgs;
+
+    public RoboticsConsoleState(Dictionary<string, CyborgControlData> cyborgs)
+    {
+        Cyborgs = cyborgs;
+    }
+}
+
+/// <summary>
+/// Message to disable the selected cyborg.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed class RoboticsConsoleDisableMessage : BoundUserInterfaceMessage
+{
+    public readonly string Address;
+
+    public RoboticsConsoleDisableMessage(string address)
+    {
+        Address = address;
+    }
+}
+
+/// <summary>
+/// Message to destroy the selected cyborg.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed class RoboticsConsoleDestroyMessage : BoundUserInterfaceMessage
+{
+    public readonly string Address;
+
+    public RoboticsConsoleDestroyMessage(string address)
+    {
+        Address = address;
+    }
+}
+
+/// <summary>
+/// All data a client needs to render the console UI for a single cyborg.
+/// Created by <c>BorgTransponderComponent</c> and sent to clients by <c>RoboticsConsoleComponent</c>.
+/// </summary>
+[DataRecord, Serializable, NetSerializable]
+public record struct CyborgControlData
+{
+    /// <summary>
+    /// Texture of the borg chassis.
+    /// </summary>
+    [DataField(required: true)]
+    public SpriteSpecifier? ChassisSprite;
+
+    /// <summary>
+    /// Name of the borg chassis.
+    /// </summary>
+    [DataField(required: true)]
+    public string ChassisName = string.Empty;
+
+    /// <summary>
+    /// Name of the borg's entity, including its silicon id.
+    /// </summary>
+    [DataField(required: true)]
+    public string Name = string.Empty;
+
+    /// <summary>
+    /// Battery charge from 0 to 1.
+    /// </summary>
+    [DataField]
+    public float Charge;
+
+    /// <summary>
+    /// How many modules this borg has, just useful information for roboticists.
+    /// Lets them keep track of the latejoin borgs that need new modules and stuff.
+    /// </summary>
+    [DataField]
+    public int ModuleCount;
+
+    /// <summary>
+    /// Whether the borg has a brain installed or not.
+    /// </summary>
+    [DataField]
+    public bool HasBrain;
+
+    /// <summary>
+    /// When this cyborg's data will be deleted.
+    /// Set by the console when receiving the packet.
+    /// </summary>
+    [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
+    public TimeSpan Timeout = TimeSpan.Zero;
+
+    public CyborgControlData(SpriteSpecifier? chassisSprite, string chassisName, string name, float charge, int moduleCount, bool hasBrain)
+    {
+        ChassisSprite = chassisSprite;
+        ChassisName = chassisName;
+        Name = name;
+        Charge = charge;
+        ModuleCount = moduleCount;
+        HasBrain = hasBrain;
+    }
+}
+
+public static class RoboticsConsoleConstants
+{
+    // broadcast by cyborgs on Robotics Console frequency
+    public const string NET_CYBORG_DATA = "cyborg-data";
+
+    // sent by robotics console to cyborgs on Cyborg Control frequency
+    public const string NET_DISABLE_COMMAND = "cyborg-disable";
+    public const string NET_DESTROY_COMMAND = "cyborg-destroy";
+}
diff --git a/Content.Shared/Robotics/Systems/SharedRoboticsConsoleSystem.cs b/Content.Shared/Robotics/Systems/SharedRoboticsConsoleSystem.cs
new file mode 100644 (file)
index 0000000..25b3c5d
--- /dev/null
@@ -0,0 +1,8 @@
+namespace Content.Shared.Robotics.Systems;
+
+/// <summary>
+/// Does nothing, only exists for access right now.
+/// </summary>
+public abstract class SharedRoboticsConsoleSystem : EntitySystem
+{
+}
diff --git a/Content.Shared/Silicons/Borgs/Components/BorgTransponderComponent.cs b/Content.Shared/Silicons/Borgs/Components/BorgTransponderComponent.cs
new file mode 100644 (file)
index 0000000..8c15e20
--- /dev/null
@@ -0,0 +1,43 @@
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Silicons.Borgs.Components;
+
+/// <summary>
+/// Periodically broadcasts borg data to robotics consoles.
+/// When not emagged, handles disabling and destroying commands as expected.
+/// </summary>
+[RegisterComponent, Access(typeof(SharedBorgSystem))]
+public sealed partial class BorgTransponderComponent : Component
+{
+    /// <summary>
+    /// Sprite of the chassis to send.
+    /// </summary>
+    [DataField(required: true)]
+    public SpriteSpecifier? Sprite;
+
+    /// <summary>
+    /// Name of the chassis to send.
+    /// </summary>
+    [DataField(required: true)]
+    public string Name = string.Empty;
+
+    /// <summary>
+    /// Popup shown to everyone when a borg is disabled.
+    /// Gets passed a string "name".
+    /// </summary>
+    [DataField]
+    public LocId DisabledPopup = "borg-transponder-disabled-popup";
+
+    /// <summary>
+    /// How long to wait between each broadcast.
+    /// </summary>
+    [DataField]
+    public TimeSpan BroadcastDelay = TimeSpan.FromSeconds(5);
+
+    /// <summary>
+    /// When to next broadcast data.
+    /// </summary>
+    [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
+    public TimeSpan NextBroadcast = TimeSpan.Zero;
+}
index 2f51331a83e95b1f09798edc8e1d76aa81eaa9e9..c9005eb7961792ac83d67f5cac821a56277d0f71 100644 (file)
@@ -17,3 +17,8 @@ borg-ui-no-brain = No brain present
 borg-ui-remove-battery = Remove
 borg-ui-modules-label = Modules:
 borg-ui-module-counter = {$actual}/{$max}
+
+# Transponder
+borg-transponder-disabled-popup = A brain shoots out the top of {$name}!
+borg-transponder-emagged-disabled-popup = Your transponder's lights go out!
+borg-transponder-emagged-destroyed-popup = Your transponder's fuse blows!
index 8ce90bb2374e04949abe4f3db5264b74d063bd23..9ae101a1e8985170445d29c9df6cfa3806f1b91a 100644 (file)
@@ -7,6 +7,8 @@ device-frequency-prototype-name-mailing-units = Mailing Units
 device-frequency-prototype-name-pdas = PDAs
 device-frequency-prototype-name-fax = Fax
 device-frequency-prototype-name-basic-device = Basic Devices
+device-frequency-prototype-name-cyborg-control = Cyborg Control
+device-frequency-prototype-name-robotics-console = Robotics Console
 
 ## camera frequencies
 device-frequency-prototype-name-surveillance-camera-test = Subnet Test
diff --git a/Resources/Locale/en-US/research/components/robotics-console.ftl b/Resources/Locale/en-US/research/components/robotics-console.ftl
new file mode 100644 (file)
index 0000000..978fa9a
--- /dev/null
@@ -0,0 +1,19 @@
+robotics-console-window-title = Robotics Console
+robotics-console-no-cyborgs = No Cyborgs!
+
+robotics-console-select-cyborg = Select a cyborg above.
+robotics-console-model = [color=gray]Model:[/color] {$name}
+# name is not formatted to prevent players trolling
+robotics-console-designation = [color=gray]Designation:[/color]
+robotics-console-battery = [color=gray]Battery charge:[/color] [color={$color}]{$charge}[/color]%
+robotics-console-modules = [color=gray]Modules installed:[/color] {$count}
+robotics-console-brain = [color=gray]Brain installed:[/color] [color={$brain ->
+    [true] green]Yes
+    *[false] red]No
+}[/color]
+
+robotics-console-locked-message = Controls locked, swipe ID.
+robotics-console-disable = Disable
+robotics-console-destroy = Destroy
+
+robotics-console-cyborg-destroyed = The cyborg {$name} has been remotely destroyed.
index db48d117e0b867e8a32a318ff83a48115ee57175..ecdbb3bb4c249952009e2d298a98a4dc7eb22cb2 100644 (file)
   name: device-frequency-prototype-name-crew-monitor
   frequency: 1261
 
+# Cyborgs broadcast to consoles on this frequency
+- type: deviceFrequency
+  id: RoboticsConsole
+  name: device-frequency-prototype-name-robotics-console
+  frequency: 1291
+
+# Console sends commands to cyborgs on this frequency
+- type: deviceFrequency
+  id: CyborgControl
+  name: device-frequency-prototype-name-cyborg-control
+  frequency: 1292
 
 # This frequency will likely have a LARGE number of listening entities. Please don't broadcast on this frequency.
 - type: deviceFrequency
index 2df281971af3b6f55ceb47c006903c82de003a6e..187aeae2650aab5afe36f3b92c51e896aa562faa 100644 (file)
       - Cyborgs
   - type: StepTriggerImmune
 
+- type: entity
+  abstract: true
+  id: BaseBorgTransponder
+  components:
+  - type: BorgTransponder
+  - type: DeviceNetwork
+    deviceNetId: Wireless
+    receiveFrequencyId: CyborgControl
+    transmitFrequencyId: RoboticsConsole
+  # explosion does most of its damage in the center and less at the edges
+  - type: Explosive
+    explosionType: Minibomb
+    totalIntensity: 30
+    intensitySlope: 20
+    maxIntensity: 20
+    canCreateVacuum: false # its for killing the borg not the station
+
 - type: entity
   id: BaseBorgChassisNT
-  parent: BaseBorgChassis
+  parent: [BaseBorgChassis, BaseBorgTransponder]
   abstract: true
   components:
   - type: NpcFactionMember
index 9365c6da56902f1107f6cbd9f48c3405e144762f..b7db886255019261d3ade2e097fd0dcdedc0756c 100644 (file)
       - BorgModuleGeneric
     hasMindState: robot_e
     noMindState: robot_e_r
+  - type: BorgTransponder
+    sprite:
+      sprite: Mobs/Silicon/chassis.rsi
+      state: robot
+    name: cyborg
   - type: Construction
     node: cyborg
   - type: Speech
       - BorgModuleCargo
     hasMindState: miner_e
     noMindState: miner_e_r
+  - type: BorgTransponder
+    sprite:
+      sprite: Mobs/Silicon/chassis.rsi
+      state: miner
+    name: salvage cyborg
   - type: Construction
     node: mining
   - type: IntrinsicRadioTransmitter
       - BorgModuleEngineering
     hasMindState: engineer_e
     noMindState: engineer_e_r
+  - type: BorgTransponder
+    sprite:
+      sprite: Mobs/Silicon/chassis.rsi
+      state: engineer
+    name: engineer cyborg
   - type: Construction
     node: engineer
   - type: IntrinsicRadioTransmitter
       - BorgModuleJanitor
     hasMindState: janitor_e
     noMindState: janitor_e_r
+  - type: BorgTransponder
+    sprite:
+      sprite: Mobs/Silicon/chassis.rsi
+      state: janitor
+    name: janitor cyborg
   - type: Construction
     node: janitor
   - type: IntrinsicRadioTransmitter
       - BorgModuleMedical
     hasMindState: medical_e
     noMindState: medical_e_r
+  - type: BorgTransponder
+    sprite:
+      sprite: Mobs/Silicon/chassis.rsi
+      state: medical
+    name: medical cyborg
   - type: Construction
     node: medical
   - type: IntrinsicRadioTransmitter
       - BorgModuleService
     hasMindState: service_e
     noMindState: service_e_r
+  - type: BorgTransponder
+    sprite:
+      sprite: Mobs/Silicon/chassis.rsi
+      state: service
+    name: service cyborg
   - type: Construction
     node: service
   - type: IntrinsicRadioTransmitter
index ba91f6bc53568f1baba5773d6851dfcfe89901d1..93aff069f0017254546955cff909dec98e6f0cb9 100644 (file)
       state: cpu_engineering
     - type: ComputerBoard
       prototype: ComputerSensorMonitoring
+
+- type: entity
+  parent: BaseComputerCircuitboard
+  id: RoboticsConsoleCircuitboard
+  name: robotics control console board
+  description: A computer printed circuit board for a robotics control console.
+  components:
+    - type: Sprite
+      state: cpu_science
+    - type: ComputerBoard
+      prototype: ComputerRoboticsControl
index 56570697dffbddef0a354bb72674464d795bed18..b5c7a1a19c351841d5c149a279a0a5e3717c0e9f 100644 (file)
     - type: WiredNetworkConnection
     - type: DeviceList
     - type: AtmosDevice
+
+- type: entity
+  parent: BaseComputer
+  id: ComputerRoboticsControl
+  name: robotics control console
+  description: Used to remotely monitor, disable and destroy the station's cyborgs.
+  components:
+  - type: Sprite
+    layers:
+    - map: ["computerLayerBody"]
+      state: computer
+    - map: ["computerLayerKeyboard"]
+      state: generic_keyboard
+    - map: ["computerLayerScreen"]
+      state: robot
+    - map: ["computerLayerKeys"]
+      state: rd_key
+  - type: RoboticsConsole
+  - type: ActiveRadio
+    channels:
+    - Science
+  - type: ActivatableUI
+    key: enum.RoboticsConsoleUiKey.Key
+  - type: UserInterface
+    interfaces:
+      enum.RoboticsConsoleUiKey.Key:
+        type: RoboticsConsoleBoundUserInterface
+  - type: ApcPowerReceiver
+    powerLoad: 1000
+  - type: DeviceNetwork
+    deviceNetId: Wireless
+    receiveFrequencyId: RoboticsConsole
+    transmitFrequencyId: CyborgControl
+  - type: Computer
+    board: RoboticsConsoleCircuitboard
+  - type: AccessReader # only used for dangerous things
+    access: [["ResearchDirector"]]
+  - type: Lock
+    unlockOnClick: false