malf killer 9000 (robotics console) (#24855)

* 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>
This commit is contained in:
deltanedas
2024-05-09 06:36:07 +00:00
committed by GitHub
parent 24ab5c0982
commit b33730db22
21 changed files with 891 additions and 4 deletions

View File

@@ -0,0 +1,7 @@
using Content.Shared.Robotics.Systems;
namespace Content.Client.Robotics.Systems;
public sealed class RoboticsConsoleSystem : SharedRoboticsConsoleSystem
{
}

View File

@@ -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();
}
}

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -1,6 +1,8 @@
using Content.Server.Actions; using Content.Server.Actions;
using Content.Server.Administration.Logs; using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers; using Content.Server.Administration.Managers;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Explosion.EntitySystems;
using Content.Server.Hands.Systems; using Content.Server.Hands.Systems;
using Content.Server.PowerCell; using Content.Server.PowerCell;
using Content.Shared.UserInterface; using Content.Shared.UserInterface;
@@ -26,6 +28,7 @@ using Robust.Server.GameObjects;
using Robust.Shared.Containers; using Robust.Shared.Containers;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.Silicons.Borgs; namespace Content.Server.Silicons.Borgs;
@@ -34,10 +37,13 @@ public sealed partial class BorgSystem : SharedBorgSystem
{ {
[Dependency] private readonly IAdminLogManager _adminLog = default!; [Dependency] private readonly IAdminLogManager _adminLog = default!;
[Dependency] private readonly IBanManager _banManager = default!; [Dependency] private readonly IBanManager _banManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedAccessSystem _access = default!; [Dependency] private readonly SharedAccessSystem _access = default!;
[Dependency] private readonly ActionsSystem _actions = default!; [Dependency] private readonly ActionsSystem _actions = default!;
[Dependency] private readonly AlertsSystem _alerts = 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 SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly HandsSystem _hands = default!; [Dependency] private readonly HandsSystem _hands = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!; [Dependency] private readonly MetaDataSystem _metaData = default!;
@@ -73,6 +79,7 @@ public sealed partial class BorgSystem : SharedBorgSystem
InitializeModules(); InitializeModules();
InitializeMMI(); InitializeMMI();
InitializeUI(); InitializeUI();
InitializeTransponder();
} }
private void OnMapInit(EntityUid uid, BorgChassisComponent component, MapInitEvent args) private void OnMapInit(EntityUid uid, BorgChassisComponent component, MapInitEvent args)

View File

@@ -21,12 +21,18 @@ public sealed partial class LockComponent : Component
public bool Locked = true; public bool Locked = true;
/// <summary> /// <summary>
/// Whether or not the lock is toggled by simply clicking. /// Whether or not the lock is locked by simply clicking.
/// </summary> /// </summary>
[DataField("lockOnClick"), ViewVariables(VVAccess.ReadWrite)] [DataField("lockOnClick"), ViewVariables(VVAccess.ReadWrite)]
[AutoNetworkedField] [AutoNetworkedField]
public bool LockOnClick; public bool LockOnClick;
/// <summary>
/// Whether or not the lock is unlocked by simply clicking.
/// </summary>
[DataField, AutoNetworkedField]
public bool UnlockOnClick = true;
/// <summary> /// <summary>
/// The sound played when unlocked. /// The sound played when unlocked.
/// </summary> /// </summary>

View File

@@ -58,12 +58,12 @@ public sealed class LockSystem : EntitySystem
return; return;
// Only attempt an unlock by default on Activate // Only attempt an unlock by default on Activate
if (lockComp.Locked) if (lockComp.Locked && lockComp.UnlockOnClick)
{ {
TryUnlock(uid, args.User, lockComp); TryUnlock(uid, args.User, lockComp);
args.Handled = true; args.Handled = true;
} }
else if (lockComp.LockOnClick) else if (!lockComp.Locked && lockComp.LockOnClick)
{ {
TryLock(uid, args.User, lockComp); TryLock(uid, args.User, lockComp);
args.Handled = true; args.Handled = true;
@@ -201,6 +201,18 @@ public sealed class LockSystem : EntitySystem
return true; 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> /// <summary>
/// Raises an event for other components to check whether or not /// Raises an event for other components to check whether or not
/// the entity can be locked in its current state. /// the entity can be locked in its current state.

View File

@@ -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;
}

View File

@@ -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";
}

View File

@@ -0,0 +1,8 @@
namespace Content.Shared.Robotics.Systems;
/// <summary>
/// Does nothing, only exists for access right now.
/// </summary>
public abstract class SharedRoboticsConsoleSystem : EntitySystem
{
}

View File

@@ -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;
}

View File

@@ -17,3 +17,8 @@ borg-ui-no-brain = No brain present
borg-ui-remove-battery = Remove borg-ui-remove-battery = Remove
borg-ui-modules-label = Modules: borg-ui-modules-label = Modules:
borg-ui-module-counter = {$actual}/{$max} 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!

View File

@@ -7,6 +7,8 @@ device-frequency-prototype-name-mailing-units = Mailing Units
device-frequency-prototype-name-pdas = PDAs device-frequency-prototype-name-pdas = PDAs
device-frequency-prototype-name-fax = Fax device-frequency-prototype-name-fax = Fax
device-frequency-prototype-name-basic-device = Basic Devices 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 ## camera frequencies
device-frequency-prototype-name-surveillance-camera-test = Subnet Test device-frequency-prototype-name-surveillance-camera-test = Subnet Test

View File

@@ -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.

View File

@@ -75,6 +75,17 @@
name: device-frequency-prototype-name-crew-monitor name: device-frequency-prototype-name-crew-monitor
frequency: 1261 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. # This frequency will likely have a LARGE number of listening entities. Please don't broadcast on this frequency.
- type: deviceFrequency - type: deviceFrequency

View File

@@ -217,9 +217,26 @@
- Cyborgs - Cyborgs
- type: StepTriggerImmune - 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 - type: entity
id: BaseBorgChassisNT id: BaseBorgChassisNT
parent: BaseBorgChassis parent: [BaseBorgChassis, BaseBorgTransponder]
abstract: true abstract: true
components: components:
- type: NpcFactionMember - type: NpcFactionMember

View File

@@ -20,6 +20,11 @@
- BorgModuleGeneric - BorgModuleGeneric
hasMindState: robot_e hasMindState: robot_e
noMindState: robot_e_r noMindState: robot_e_r
- type: BorgTransponder
sprite:
sprite: Mobs/Silicon/chassis.rsi
state: robot
name: cyborg
- type: Construction - type: Construction
node: cyborg node: cyborg
- type: Speech - type: Speech
@@ -57,6 +62,11 @@
- BorgModuleCargo - BorgModuleCargo
hasMindState: miner_e hasMindState: miner_e
noMindState: miner_e_r noMindState: miner_e_r
- type: BorgTransponder
sprite:
sprite: Mobs/Silicon/chassis.rsi
state: miner
name: salvage cyborg
- type: Construction - type: Construction
node: mining node: mining
- type: IntrinsicRadioTransmitter - type: IntrinsicRadioTransmitter
@@ -100,6 +110,11 @@
- BorgModuleEngineering - BorgModuleEngineering
hasMindState: engineer_e hasMindState: engineer_e
noMindState: engineer_e_r noMindState: engineer_e_r
- type: BorgTransponder
sprite:
sprite: Mobs/Silicon/chassis.rsi
state: engineer
name: engineer cyborg
- type: Construction - type: Construction
node: engineer node: engineer
- type: IntrinsicRadioTransmitter - type: IntrinsicRadioTransmitter
@@ -151,6 +166,11 @@
- BorgModuleJanitor - BorgModuleJanitor
hasMindState: janitor_e hasMindState: janitor_e
noMindState: janitor_e_r noMindState: janitor_e_r
- type: BorgTransponder
sprite:
sprite: Mobs/Silicon/chassis.rsi
state: janitor
name: janitor cyborg
- type: Construction - type: Construction
node: janitor node: janitor
- type: IntrinsicRadioTransmitter - type: IntrinsicRadioTransmitter
@@ -202,6 +222,11 @@
- BorgModuleMedical - BorgModuleMedical
hasMindState: medical_e hasMindState: medical_e
noMindState: medical_e_r noMindState: medical_e_r
- type: BorgTransponder
sprite:
sprite: Mobs/Silicon/chassis.rsi
state: medical
name: medical cyborg
- type: Construction - type: Construction
node: medical node: medical
- type: IntrinsicRadioTransmitter - type: IntrinsicRadioTransmitter
@@ -250,6 +275,11 @@
- BorgModuleService - BorgModuleService
hasMindState: service_e hasMindState: service_e
noMindState: service_e_r noMindState: service_e_r
- type: BorgTransponder
sprite:
sprite: Mobs/Silicon/chassis.rsi
state: service
name: service cyborg
- type: Construction - type: Construction
node: service node: service
- type: IntrinsicRadioTransmitter - type: IntrinsicRadioTransmitter

View File

@@ -384,3 +384,14 @@
state: cpu_engineering state: cpu_engineering
- type: ComputerBoard - type: ComputerBoard
prototype: ComputerSensorMonitoring 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

View File

@@ -1076,3 +1076,42 @@
- type: WiredNetworkConnection - type: WiredNetworkConnection
- type: DeviceList - type: DeviceList
- type: AtmosDevice - 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