Ai Control Port (#3658)

This commit is contained in:
EckoAurum
2026-04-05 04:53:42 +09:00
committed by GitHub
parent ef2ce6565d
commit f097148d99
26 changed files with 750 additions and 11 deletions
@@ -77,6 +77,16 @@ public sealed partial class RoboticsConsoleWindow : FancyWindow
if (_selected is {} selected && !_cyborgs.ContainsKey(selected))
_selected = null;
// Corvax-Next-AiRemoteControl-Start
var isAiControllable = false;
if (_selected != null)
{
_cyborgs.TryGetValue(_selected, out var data);
isAiControllable = data.IsAiControllable;
}
// Corvax-Next-AiRemoteControl-End
var hasCyborgs = _cyborgs.Count > 0;
NoCyborgs.Visible = !hasCyborgs;
CyborgsContainer.Visible = hasCyborgs;
@@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 ImHoks <imhokzzzz@gmail.com>
// SPDX-FileCopyrightText: 2025 KillanGenifer <killangenifer@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._CorvaxNext.Silicons.Borgs;
namespace Content.Client._CorvaxNext.Silicons.Borgs;
public sealed partial class AiRemoteControlSystem : SharedAiRemoteControlSystem
{
}
@@ -0,0 +1,18 @@
<Control xmlns="https://spacestation14.io"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
Margin="0 0 0 10">
<PanelContainer VerticalExpand="True" StyleClasses="BackgroundDark">
<BoxContainer Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True"
Margin="5 5 5 5">
<customControls:HSeparator Margin="0 5 0 5"/>
<RichTextLabel Name="DeviceName"/>
<BoxContainer Name="RemoteControlButtons" Orientation="Horizontal" HorizontalExpand="True" Margin="0 5 0 0">
<Button Name="MoveButton" StyleClasses="chatSelectorOptionButton" Text="{Loc ai-remote-ui-menu-moveto}"/>
<Control Margin="2 0 0 0"/>
<Button Name="TakeControlButton" StyleClasses="chatSelectorOptionButton" Text="{Loc ai-remote-control}"/>
</BoxContainer>
</BoxContainer>
</PanelContainer>
</Control>
@@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 ImHoks <imhokzzzz@gmail.com>
// SPDX-FileCopyrightText: 2025 KillanGenifer <killangenifer@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Robust.Client.UserInterface;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Content.Shared._CorvaxNext.Silicons.Borgs.Components;
namespace Content.Client._CorvaxNext.Silicons.Laws.Ui;
[GenerateTypedNameReferences]
public sealed partial class RemoteDeviceDisplay : Control
{
public event Action<RemoteDeviceActionEvent>? OnRemoteDeviceAction;
public RemoteDeviceDisplay(NetEntity netEntityUid, string displayName)
{
RobustXamlLoader.Load(this);
DeviceName.SetMessage(displayName);
MoveButton.OnPressed += _ =>
{
OnRemoteDeviceAction?.Invoke(new RemoteDeviceActionEvent(
RemoteDeviceActionEvent.RemoteDeviceActionType.MoveToDevice,
netEntityUid));
};
TakeControlButton.OnPressed += _ =>
{
OnRemoteDeviceAction?.Invoke(new RemoteDeviceActionEvent(
RemoteDeviceActionEvent.RemoteDeviceActionType.TakeControl,
netEntityUid));
};
}
}
@@ -0,0 +1,45 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 ImHoks <imhokzzzz@gmail.com>
// SPDX-FileCopyrightText: 2025 KillanGenifer <killangenifer@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._CorvaxNext.Silicons.Borgs.Components;
using Robust.Client.UserInterface;
using static Content.Shared._CorvaxNext.Silicons.Borgs.Components.AiRemoteControllerComponent;
namespace Content.Client._CorvaxNext.Silicons.Laws.Ui;
public sealed class RemoteDevicesBoundUserInterface : BoundUserInterface
{
private RemoteDevicesMenu? _menu;
private EntityUid _owner;
public RemoteDevicesBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
_owner = owner;
}
protected override void Open()
{
base.Open();
_menu = this.CreateWindow<RemoteDevicesMenu>();
_menu.OnRemoteDeviceAction += SendAction;
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (state is not RemoteDevicesBuiState msg)
return;
_menu?.Update(_owner, msg);
}
public void SendAction(RemoteDeviceActionEvent action)
{
SendMessage(new RemoteDeviceActionMessage(action));
}
}
@@ -0,0 +1,27 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
Title="{Loc ai-remote-ui-menu-title}"
MinSize="355 100"
SetSize="355 415">
<BoxContainer Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True">
<PanelContainer VerticalExpand="True" Margin="10 10 10 10">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#1B1B1E"/>
</PanelContainer.PanelOverride>
<ScrollContainer
HScrollEnabled="False"
HorizontalExpand="True"
VerticalExpand="True">
<BoxContainer
Name="RemoteDevicesDisplayContainer"
Orientation="Vertical"
VerticalExpand="True"
Margin="10 10 10 0">
</BoxContainer>
</ScrollContainer>
</PanelContainer>
</BoxContainer>
</controls:FancyWindow>
@@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 ImHoks <imhokzzzz@gmail.com>
// SPDX-FileCopyrightText: 2025 KillanGenifer <killangenifer@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Robust.Client.UserInterface.XAML;
using FancyWindow = Content.Client.UserInterface.Controls.FancyWindow;
using Robust.Client.AutoGenerated;
using Content.Shared._CorvaxNext.Silicons.Borgs.Components;
namespace Content.Client._CorvaxNext.Silicons.Laws.Ui;
[GenerateTypedNameReferences]
public sealed partial class RemoteDevicesMenu : FancyWindow
{
public event Action<RemoteDeviceActionEvent>? OnRemoteDeviceAction;
public RemoteDevicesMenu()
{
RobustXamlLoader.Load(this);
}
public void Update(EntityUid uid, RemoteDevicesBuiState state)
{
RemoteDevicesDisplayContainer.Children.Clear();
if (state.DeviceList == null)
return;
foreach (var device in state.DeviceList)
{
var control = new RemoteDeviceDisplay(device.NetEntityUid, device.DisplayName);
control.OnRemoteDeviceAction += action =>
{
OnRemoteDeviceAction?.Invoke(action);
};
RemoteDevicesDisplayContainer.AddChild(control);
}
}
}
@@ -1,5 +1,6 @@
using Content.Server.Inventory;
using Content.Server.Radio.Components;
using Content.Shared._CorvaxNext.Silicons.Borgs.Components;
using Content.Shared.Inventory;
using Content.Shared.Silicons.Borgs;
using Content.Shared.Silicons.Borgs.Components;
@@ -28,6 +29,23 @@ public sealed class BorgSwitchableTypeSystem : SharedBorgSwitchableTypeSystem
if (TryComp(ent, out ActiveRadioComponent? activeRadio))
activeRadio.Channels = [.. radioChannels];
// Corvax-Next-AiRemoteControl-Start
if (TryComp(ent, out AiRemoteControllerComponent? aiRemoteComp))
{
if (TryComp(aiRemoteComp.AiHolder, out IntrinsicRadioTransmitterComponent? stationAiTransmitter) && transmitter != null)
{
aiRemoteComp.PreviouslyTransmitterChannels = [.. radioChannels];
transmitter.Channels = [.. stationAiTransmitter.Channels];
}
if (TryComp(aiRemoteComp.AiHolder, out ActiveRadioComponent? stationAiActiveRadio) && activeRadio != null)
{
aiRemoteComp.PreviouslyActiveRadioChannels = [.. radioChannels];
activeRadio.Channels = [.. stationAiActiveRadio.Channels];
}
}
// Corvax-Next-AiRemoteControl-End
// Borg transponder for the robotics console
if (TryComp(ent, out BorgTransponderComponent? transponder))
{
@@ -6,7 +6,9 @@ using Content.Shared.Silicons.Borgs.Components;
using Content.Shared.DeviceNetwork.Components;
using Content.Shared.DeviceNetwork.Events;
using Content.Shared.Emag.Systems;
using Robust.Shared.Player;
using Robust.Shared.Utility;
using Content.Shared._CorvaxNext.Silicons.Borgs.Components; // Corvax-Next-AiRemoteControl
namespace Content.Server.Silicons.Borgs;
@@ -47,7 +49,8 @@ public sealed partial class BorgSystem
charge,
chassis.ModuleCount,
hasBrain,
canDisable);
canDisable,
HasComp<AiRemoteControllerComponent>(uid)); // Corvax-Next-AiRemoteControl
var payload = new NetworkPayload()
{
@@ -5,6 +5,7 @@ using Content.Server.DeviceNetwork.Systems;
using Content.Server.Explosion.EntitySystems;
using Content.Server.Hands.Systems;
using Content.Server.PowerCell;
using Content.Shared._CorvaxNext.Silicons.Borgs.Components;
using Content.Shared.UserInterface;
using Content.Shared.Access;
using Content.Shared.Access.Components;
@@ -25,6 +26,8 @@ using Content.Shared.PowerCell.Components;
using Content.Shared.Roles;
using Content.Shared.Silicons.Borgs;
using Content.Shared.Silicons.Borgs.Components;
using Content.Shared.Silicons.StationAi;
using Content.Shared.StationAi;
using Content.Shared.Throwing;
using Content.Shared.Whitelist;
using Content.Shared.Wires;
@@ -144,6 +147,21 @@ public sealed partial class BorgSystem : SharedBorgSystem
args.Handled = true;
UpdateUI(uid, component);
}
// Corvax-Next-AiRemoteControl-Start
if (component.BrainEntity == null && HasComp<AiRemoteBrainComponent>(used) &&
_whitelistSystem.IsWhitelistPassOrNull(component.BrainWhitelist, used))
{
EnsureComp<AiRemoteControllerComponent>(uid);
_container.Insert(used, component.BrainContainer);
_adminLog.Add(LogType.Action, LogImpact.Medium,
$"{ToPrettyString(args.User):player} installed ai remote brain {ToPrettyString(used)} into borg {ToPrettyString(uid)}");
args.Handled = true;
BorgActivate(uid, component);
UpdateUI(uid, component);
}
// Corvax-Next-AiRemoteControl-End
}
/// <summary>
@@ -178,6 +196,15 @@ public sealed partial class BorgSystem : SharedBorgSystem
{
_mind.TransferTo(mindId, args.Entity, mind: mind);
}
// Corvax-Next-AiRemoteControl-Start
if (HasComp<AiRemoteBrainComponent>(args.Entity))
{
BorgDeactivate(uid, component);
RemComp<AiRemoteControllerComponent>(uid);
RemComp<StationAiVisionComponent>(uid);
}
// Corvax-Next-AiRemoteControl-End
}
private void OnMindAdded(EntityUid uid, BorgChassisComponent component, MindAddedMessage args)
@@ -4,6 +4,7 @@ using Content.Server.Chat.Managers;
using Content.Server.Radio.Components;
using Content.Server.Roles;
using Content.Server.Station.Systems;
using Content.Shared._CorvaxNext.Silicons.Borgs.Components; // Corvax-Next-AiRemoteControl
using Content.Shared.Administration;
using Content.Shared.Chat;
using Content.Shared.Emag.Systems;
@@ -13,6 +14,9 @@ using Content.Shared.Mind.Components;
using Content.Shared.Roles;
using Content.Shared.Silicons.Laws;
using Content.Shared.Silicons.Laws.Components;
using Content.Shared.Silicons.StationAi; // Corvax-Next-AiRemoteControl
using Content.Shared.Tag; // Corvax-Next-AiRemoteControl
using Content.Shared.Wires;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
@@ -33,6 +37,7 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly UserInterfaceSystem _userInterface = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly TagSystem _tagSystem = default!; // Corvax-Next-AiRemoteControl
[Dependency] private readonly IMapManager _map = default!; // Mono - Law update is grid-only now.
/// <inheritdoc/>
@@ -63,6 +68,12 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
if (!TryComp<ActorComponent>(uid, out var actor))
return;
// Corvax-Next-AiRemoteControl-Start
if (HasComp<AiRemoteControllerComponent>(uid)
|| _tagSystem.HasTag(uid, "StationAi")) // skip a law's notification for remotable and AI
return;
// Corvax-Next-AiRemoteControl-End
var msg = Loc.GetString("laws-notify");
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", msg));
_chatManager.ChatMessageToOne(ChatChannel.Server, msg, wrappedMessage, default, false, actor.PlayerSession.Channel, colorOverride: Color.FromHex("#5ed7aa"));
@@ -155,6 +166,11 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
if (component.Lawset == null)
component.Lawset = GetLawset(component.Laws);
// Corvax-Next-AiRemoteControl-Start
if (HasComp<AiRemoteControllerComponent>(uid)) // You can't emag controllable entities
return;
// Corvax-Next-AiRemoteControl-End
// Show the silicon has been subverted.
component.Subverted = true;
@@ -281,7 +297,7 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
/// <summary>
/// Set the laws of a silicon entity while notifying the player.
/// </summary>
public void SetLaws(List<SiliconLaw> newLaws, EntityUid target, SoundSpecifier? cue = null)
public void SetLaws(List<SiliconLaw> newLaws, EntityUid target, SoundSpecifier? cue = null, bool silent = false) // AiRemoteControl - silent
{
if (!TryComp<SiliconLawProviderComponent>(target, out var component))
return;
@@ -290,7 +306,8 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
component.Lawset = new SiliconLawset();
component.Lawset.Laws = newLaws;
NotifyLawsChanged(target, cue);
if (!silent) // AiRemoteControl - silent
NotifyLawsChanged(target, cue);
}
protected override void OnUpdaterInsert(Entity<SiliconLawUpdaterComponent> ent, ref EntInsertedIntoContainerMessage args)
@@ -312,8 +329,16 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
continue;
SetLaws(lawset, update, provider.LawUploadSound);
// Mono edit end
// Corvax-Next-AiRemoteControl-Start
if (TryComp<StationAiHeldComponent>(update, out var stationAiHeldComp)
&& stationAiHeldComp.CurrentConnectedEntity != null
&& HasComp<SiliconLawProviderComponent>(stationAiHeldComp.CurrentConnectedEntity))
{
SetLaws(lawset, stationAiHeldComp.CurrentConnectedEntity.Value, provider.LawUploadSound);
}
// Corvax-Next-AiRemoteControl-End
}
// Mono edit end
}
}
@@ -0,0 +1,208 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 ImHoks <142083149+ImHoks@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 ImHoks <imhokzzzz@gmail.com>
// SPDX-FileCopyrightText: 2025 KillanGenifer <killangenifer@gmail.com>
// SPDX-FileCopyrightText: 2025 gluesniffler <159397573+gluesniffler@users.noreply.github.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Server.Radio.Components;
using Content.Server.Silicons.Laws;
using Content.Shared._CorvaxNext.Silicons.Borgs;
using Content.Shared._CorvaxNext.Silicons.Borgs.Components;
using Content.Shared.Actions;
using Content.Shared.Mind;
using Content.Shared.Silicons.Laws.Components;
using Content.Shared.Silicons.StationAi;
using Content.Shared.StationAi;
using Content.Shared.Verbs;
using Robust.Shared.Map; // Mono
using Robust.Server.GameObjects;
using Robust.Shared.Player;
using System.Threading.Tasks.Dataflow;
namespace Content.Server._CorvaxNext.Silicons.Borgs;
public sealed class AiRemoteControlSystem : SharedAiRemoteControlSystem
{
[Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly SiliconLawSystem _lawSystem = default!;
[Dependency] private readonly SharedStationAiSystem _stationAiSystem = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly UserInterfaceSystem _userInterface = default!;
[Dependency] private readonly SharedTransformSystem _xformSystem = default!;
[Dependency] private readonly IMapManager _map = default!; // Mono
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AiRemoteControllerComponent, ReturnMindIntoAiEvent>(OnReturnMindIntoAi);
SubscribeLocalEvent<AiRemoteControllerComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<AiRemoteControllerComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<AiRemoteControllerComponent, GetVerbsEvent<AlternativeVerb>>(OnGetVerbs);
SubscribeLocalEvent<StationAiHeldComponent, AiRemoteControllerComponent.RemoteDeviceActionMessage>(OnUiRemoteAction);
SubscribeLocalEvent<StationAiHeldComponent, ToggleRemoteDevicesScreenEvent>(OnToggleRemoteDevicesScreen);
}
private void OnMapInit(Entity<AiRemoteControllerComponent> entity, ref MapInitEvent args)
{
var visionComp = EnsureComp<StationAiVisionComponent>(entity.Owner);
EntityUid? actionEnt = null;
_actions.AddAction(entity.Owner, ref actionEnt, entity.Comp.BackToAiAction);
if (actionEnt != null)
entity.Comp.BackToAiActionEntity = actionEnt.Value;
}
private void OnShutdown(Entity<AiRemoteControllerComponent> entity, ref ComponentShutdown args)
{
_actions.RemoveAction(entity.Owner, entity.Comp.BackToAiActionEntity);
var backArgs = new ReturnMindIntoAiEvent();
backArgs.Performer = entity;
if (TryComp(entity, out IntrinsicRadioTransmitterComponent? transmitter)
&& entity.Comp.PreviouslyTransmitterChannels != null)
transmitter.Channels = [.. entity.Comp.PreviouslyTransmitterChannels];
if (TryComp(entity, out ActiveRadioComponent? activeRadio)
&& entity.Comp.PreviouslyActiveRadioChannels != null)
activeRadio.Channels = [.. entity.Comp.PreviouslyActiveRadioChannels];
ReturnMindIntoAi(entity);
}
private void OnGetVerbs(Entity<AiRemoteControllerComponent> entity, ref GetVerbsEvent<AlternativeVerb> args)
{
var user = args.User;
if (!TryComp<StationAiHeldComponent>(user, out var stationAiHeldComp))
return;
var verb = new AlternativeVerb
{
Text = Loc.GetString("ai-remote-control"),
Act = () => AiTakeControl(user, entity)
};
args.Verbs.Add(verb);
}
private void OnReturnMindIntoAi(Entity<AiRemoteControllerComponent> entity, ref ReturnMindIntoAiEvent args) =>
ReturnMindIntoAi(entity);
public void AiTakeControl(EntityUid ai, EntityUid entity)
{
if (!_mind.TryGetMind(ai, out var mindId, out var mind))
return;
if (!TryComp<StationAiHeldComponent>(ai, out var stationAiHeldComp))
return;
if (!TryComp<AiRemoteControllerComponent>(entity, out var aiRemoteComp))
return;
if (!_map.TryFindGridAt(Transform(ai).MapPosition, out var grid, out var _) || Transform(entity).GridUid != grid)
return; // Mono no controlling borgs outside the ai's grid.
if (TryComp(entity, out IntrinsicRadioTransmitterComponent? transmitter))
{
aiRemoteComp.PreviouslyTransmitterChannels = [.. transmitter.Channels];
if (TryComp(ai, out IntrinsicRadioTransmitterComponent? stationAiTransmitter))
transmitter.Channels = [.. stationAiTransmitter.Channels];
}
if (TryComp(entity, out ActiveRadioComponent? activeRadio))
{
aiRemoteComp.PreviouslyActiveRadioChannels = [.. activeRadio.Channels];
if (TryComp(ai, out ActiveRadioComponent? stationAiActiveRadio))
activeRadio.Channels = [.. stationAiActiveRadio.Channels];
}
_mind.ControlMob(ai, entity);
aiRemoteComp.AiHolder = ai;
aiRemoteComp.LinkedMind = mindId;
stationAiHeldComp.CurrentConnectedEntity = entity;
if (!_stationAiSystem.TryGetCore(ai, out var stationAiCore))
return;
_stationAiSystem.SwitchRemoteEntityMode(stationAiCore, false);
RewriteLaws(ai, entity);
}
private void OnToggleRemoteDevicesScreen(EntityUid uid, StationAiHeldComponent component, ToggleRemoteDevicesScreenEvent args)
{
if (args.Handled || !TryComp<ActorComponent>(uid, out var actor))
return;
args.Handled = true;
_userInterface.TryToggleUi(uid, RemoteDeviceUiKey.Key, actor.PlayerSession);
var aiGrid = Transform(uid).GridUid; // Mono
var query = EntityManager.EntityQueryEnumerator<AiRemoteControllerComponent>();
var remoteDevices = new List<RemoteDevicesData>();
while (query.MoveNext(out var queryUid, out var comp))
{
if (Transform(queryUid).GridUid != aiGrid)
continue;
var data = new RemoteDevicesData
{
NetEntityUid = GetNetEntity(queryUid),
DisplayName = Comp<MetaDataComponent>(queryUid).EntityName
};
remoteDevices.Add(data);
}
var state = new RemoteDevicesBuiState(remoteDevices);
_userInterface.SetUiState(uid, RemoteDeviceUiKey.Key, state);
}
private void OnUiRemoteAction(EntityUid uid, StationAiHeldComponent component, AiRemoteControllerComponent.RemoteDeviceActionMessage msg)
{
if (msg.RemoteAction == null)
return;
var target = GetEntity(msg.RemoteAction?.Target);
if (!HasComp<AiRemoteControllerComponent>(target))
return;
switch (msg.RemoteAction?.ActionType)
{
case RemoteDeviceActionEvent.RemoteDeviceActionType.MoveToDevice:
if (!_stationAiSystem.TryGetCore(uid, out var stationAiCore)
|| stationAiCore.Comp?.RemoteEntity == null)
return;
_xformSystem.SetCoordinates(stationAiCore.Comp.RemoteEntity.Value, Transform(target.Value).Coordinates);
break;
case RemoteDeviceActionEvent.RemoteDeviceActionType.TakeControl:
AiTakeControl(uid, target.Value);
break;
}
}
private void RewriteLaws(EntityUid from, EntityUid to)
{
if (!TryComp<SiliconLawProviderComponent>(from, out var fromLawsComp))
return;
if (!TryComp<SiliconLawProviderComponent>(to, out var toLawsComp))
return;
if (fromLawsComp.Lawset == null)
return;
var fromLaws = _lawSystem.GetLaws(from);
_lawSystem.SetLaws(fromLaws.Laws, to, silent: true);
}
}
+8 -2
View File
@@ -1,4 +1,4 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Utility;
@@ -111,7 +111,12 @@ public partial record struct CyborgControlData
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan Timeout = TimeSpan.Zero;
public CyborgControlData(SpriteSpecifier? chassisSprite, string chassisName, string name, float charge, int moduleCount, bool hasBrain, bool canDisable)
// Corvax-Next-AiRemoteControl-Start
[DataField]
public bool IsAiControllable;
// Corvax-Next-AiRemoteControl-End
public CyborgControlData(SpriteSpecifier? chassisSprite, string chassisName, string name, float charge, int moduleCount, bool hasBrain, bool canDisable, bool isAiControllable) // Corvax-Next-AiRemoteControl
{
ChassisSprite = chassisSprite;
ChassisName = chassisName;
@@ -120,6 +125,7 @@ public partial record struct CyborgControlData
ModuleCount = moduleCount;
HasBrain = hasBrain;
CanDisable = canDisable;
IsAiControllable = isAiControllable; // Corvax-Next-AiRemoteControl
}
}
@@ -1,10 +1,11 @@
using Content.Shared.Emag.Systems;
using Content.Shared.Emag.Systems;
using Content.Shared.Mind;
using Content.Shared.Popups;
using Content.Shared.Silicons.Laws.Components;
using Content.Shared.Stunnable;
using Content.Shared.Wires;
using Robust.Shared.Audio;
using Content.Shared._CorvaxNext.Silicons.Borgs.Components;
namespace Content.Shared.Silicons.Laws;
@@ -35,6 +36,11 @@ public abstract partial class SharedSiliconLawSystem : EntitySystem
if (_emag.CheckFlag(uid, EmagType.Interaction))
return;
// Corvax-Next-AiRemoteControl-Start
if (HasComp<AiRemoteControllerComponent>(uid))
return;
// Corvax-Next-AiRemoteControl-End
// prevent self-emagging
if (uid == args.UserUid)
{
@@ -28,6 +28,7 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
using System.Diagnostics.CodeAnalysis;
using Content.Shared._CorvaxNext.Silicons.Borgs;
namespace Content.Shared.Silicons.StationAi;
@@ -57,6 +58,8 @@ public abstract partial class SharedStationAiSystem : EntitySystem
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly StationAiVisionSystem _vision = default!;
[Dependency] private readonly SharedAiRemoteControlSystem _remoteSystem = default!; // Corvax-Next-AiRemoteControl
// 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.
@@ -231,6 +234,13 @@ public abstract partial class SharedStationAiSystem : EntitySystem
// Try to insert our thing into them
if (_slots.CanEject(ent.Owner, args.User, ent.Comp.Slot))
{
// Corvax-Next-AiRemoteControl-Start
if (ent.Comp.Slot.Item != null
&& TryComp<StationAiHeldComponent>(ent.Comp.Slot.Item, out var stationAiHeldComp))
if (stationAiHeldComp.CurrentConnectedEntity != null)
_remoteSystem.ReturnMindIntoAi(stationAiHeldComp.CurrentConnectedEntity.Value);
// Corvax-Next-AiRemoteControl-End
if (!_slots.TryInsert(args.Args.Target.Value, targetHolder.Slot, ent.Comp.Slot.Item!.Value, args.User, excludeUserAudio: true))
{
return;
@@ -289,6 +299,12 @@ public abstract partial class SharedStationAiSystem : EntitySystem
AnnounceIntellicardUsage(held, intelliComp.WarningSound);
}
// Corvax-Next-AiRemoteControl-Start
if (TryComp<StationAiHeldComponent>(held, out var heldComp))
if (heldComp.CurrentConnectedEntity != null)
AnnounceIntellicardUsage(heldComp.CurrentConnectedEntity.Value, intelliComp.WarningSound);
// Corvax-Next-AiRemoteControl-End
var doAfterArgs = new DoAfterArgs(EntityManager, args.User, cardHasAi ? intelliComp.UploadTime : intelliComp.DownloadTime, new IntellicardDoAfterEvent(), args.Target, ent.Owner)
{
BreakOnDamage = true,
@@ -6,4 +6,10 @@ namespace Content.Shared.Silicons.StationAi;
/// Indicates this entity is currently held inside of a station AI core.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class StationAiHeldComponent : Component;
// Corvax-Next-AiRemoteControl-Start
public sealed partial class StationAiHeldComponent : Component
{
[DataField]
public EntityUid? CurrentConnectedEntity;
}
// Corvax-Next-AiRemoteControl-End
@@ -0,0 +1,12 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 ImHoks <imhokzzzz@gmail.com>
// SPDX-FileCopyrightText: 2025 KillanGenifer <killangenifer@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Content.Shared._CorvaxNext.Silicons.Borgs.Components;
[RegisterComponent]
public sealed partial class AiRemoteBrainComponent : Component
{
}
@@ -0,0 +1,69 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 ImHoks <imhokzzzz@gmail.com>
// SPDX-FileCopyrightText: 2025 KillanGenifer <killangenifer@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared._CorvaxNext.Silicons.Borgs.Components;
[RegisterComponent]
public sealed partial class AiRemoteControllerComponent : Component
{
[DataField] public EntityUid? AiHolder;
[DataField] public EntityUid? LinkedMind;
[DataField] public string[]? PreviouslyTransmitterChannels;
[DataField] public string[]? PreviouslyActiveRadioChannels;
[DataField] public EntProtoId BackToAiAction = "ActionBackToAi";
[DataField] public EntityUid? BackToAiActionEntity;
[Serializable, NetSerializable]
public sealed class RemoteDeviceActionMessage : BoundUserInterfaceMessage
{
public readonly RemoteDeviceActionEvent? RemoteAction;
public RemoteDeviceActionMessage(RemoteDeviceActionEvent remoteDeviceAction)
{
RemoteAction = remoteDeviceAction;
}
}
}
[Serializable, NetSerializable]
public sealed class RemoteDeviceActionEvent : EntityEventArgs
{
public enum RemoteDeviceActionType
{
MoveToDevice,
TakeControl
}
public RemoteDeviceActionType ActionType;
public NetEntity Target;
public RemoteDeviceActionEvent(RemoteDeviceActionType actionType, NetEntity target)
{
ActionType = actionType;
Target = target;
}
}
[Serializable, NetSerializable]
public record struct RemoteDevicesData()
{
public string DisplayName = string.Empty;
public NetEntity NetEntityUid = NetEntity.Invalid;
}
[Serializable, NetSerializable]
public sealed class RemoteDevicesBuiState : BoundUserInterfaceState
{
public List<RemoteDevicesData> DeviceList;
public RemoteDevicesBuiState(List<RemoteDevicesData> deviceList)
{
DeviceList = deviceList;
}
}
@@ -0,0 +1,68 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 ImHoks <142083149+ImHoks@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 ImHoks <imhokzzzz@gmail.com>
// SPDX-FileCopyrightText: 2025 KillanGenifer <killangenifer@gmail.com>
// SPDX-FileCopyrightText: 2025 gluesniffler <159397573+gluesniffler@users.noreply.github.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._CorvaxNext.Silicons.Borgs.Components;
using Content.Shared.Actions;
using Content.Shared.Mind;
using Content.Shared.Silicons.StationAi;
using Robust.Shared.Serialization;
namespace Content.Shared._CorvaxNext.Silicons.Borgs;
public abstract class SharedAiRemoteControlSystem : EntitySystem
{
[Dependency] private readonly SharedStationAiSystem _stationAiSystem = default!;
[Dependency] private readonly SharedTransformSystem _xformSystem = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
public override void Initialize()
{
base.Initialize();
}
public void ReturnMindIntoAi(EntityUid entity)
{
if (!TryComp<AiRemoteControllerComponent>(entity, out var remoteComp))
return;
if (remoteComp?.AiHolder == null
|| !_stationAiSystem.TryGetCore(remoteComp.AiHolder.Value, out var stationAiCore)
|| stationAiCore.Comp?.RemoteEntity == null)
return;
if (remoteComp.LinkedMind == null)
return;
if (!TryComp<StationAiHeldComponent>(remoteComp.AiHolder.Value, out var stationAiHeldComp))
return;
stationAiHeldComp.CurrentConnectedEntity = null;
_mind.TransferTo(remoteComp.LinkedMind.Value, remoteComp.AiHolder);
_stationAiSystem.SwitchRemoteEntityMode(stationAiCore, true);
remoteComp.AiHolder = null;
remoteComp.LinkedMind = null;
_xformSystem.SetCoordinates(stationAiCore.Comp.RemoteEntity.Value, Transform(entity).Coordinates);
}
}
public sealed partial class ReturnMindIntoAiEvent : InstantActionEvent
{
}
public sealed partial class ToggleRemoteDevicesScreenEvent : InstantActionEvent
{
}
[Serializable, NetSerializable]
public enum RemoteDeviceUiKey : byte
{
Key
}
@@ -0,0 +1,4 @@
# UI
ai-remote-ui-menu-title = Available devices
ai-remote-ui-menu-moveto = Move to
ai-remote-control = Take control
@@ -38,6 +38,7 @@
- ActionJumpToCore
- ActionSurvCameraLights
- ActionAIViewLaws
- ActionOpenRemoteDevicesMenu # Corvax-Next-AiRemoteControl
- type: UserInterface
interfaces:
enum.RadarConsoleUiKey.Key:
@@ -50,6 +51,8 @@
type: SiliconLawBoundUserInterface
enum.CommunicationsConsoleUiKey.Key:
type: CommunicationsConsoleBoundUserInterface
enum.RemoteDeviceUiKey.Key: # Corvax-Next-AiRemoteControl
type: RemoteDevicesBoundUserInterface
- type: IntrinsicUI
uis:
enum.RadarConsoleUiKey.Key:
@@ -116,6 +119,7 @@
whitelist:
tags:
- StationAi
- CanPilot # Mono
- type: ContainerContainer
containers:
station_ai_mind_slot: !type:ContainerSlot
@@ -508,6 +512,7 @@
- HideContextMenu
- StationAi
- NoConsoleSound
- CanPilot #Mono
- type: StartingMindRole
mindRole: "MindRoleSiliconBrain"
silent: true
@@ -7,6 +7,7 @@
- PositronicBrain
- SciFlash
- CyborgEndoskeleton
- AiRemoteBrain
- type: latheRecipePack
id: BorgModulesStatic
@@ -0,0 +1,34 @@
# SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
# SPDX-FileCopyrightText: 2025 KillanGenifer <killangenifer@gmail.com>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
- type: entity
id: ActionBackToAi
name: Back to Ai
description: Sends your mind back to the core.
components:
- type: InstantAction
checkConsciousness: false
checkCanInteract: false
priority: -9
itemIconStyle: BigAction
icon:
sprite: Interface/Actions/actions_ai.rsi
state: ai_core
event: !type:ReturnMindIntoAiEvent
- type: entity
id: ActionOpenRemoteDevicesMenu
name: Open list of remote devices
description: Opens a menu with available remote devices.
components:
- type: InstantAction
checkConsciousness: false
checkCanInteract: false
priority: -9
itemIconStyle: BigAction
event: !type:ToggleRemoteDevicesScreenEvent
icon:
sprite: Mobs/Silicon/chassis.rsi
state: robot
@@ -0,0 +1,20 @@
# SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
# SPDX-FileCopyrightText: 2025 KillanGenifer <killangenifer@gmail.com>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
- type: entity
parent: BaseItem
id: AiRemoteBrain
name: "AI-Cyborg Network Device"
description: Artificial Intelligence cyborg control board.
components:
- type: Sprite
sprite: Objects/Misc/module.rsi
state: boris
- type: Examiner
- type: AiRemoteBrain
- type: Actions
- type: Tag
tags:
- CannotSuicide
@@ -1,3 +1,8 @@
# SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
# SPDX-FileCopyrightText: 2025 ImHoks <imhokzzzz@gmail.com>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
- type: latheRecipe
id: ModsuitChestplate
result: ModsuitChestplate
@@ -50,3 +55,14 @@
materials:
Glass: 550
Plasma: 250
# AI
- type: latheRecipe
id: AiRemoteBrain
result: AiRemoteBrain
completetime: 4
materials:
Steel: 300
Glass: 400
Gold: 400
Silver: 200
@@ -94,12 +94,15 @@
- ActionJumpToCore
- ActionSurvCameraLights
- ActionAIViewLaws
- ActionOpenRemoteDevicesMenu # Corvax-Next-AiRemoteControl
- type: UserInterface
interfaces:
enum.RadarConsoleUiKey.Key:
type: RadarConsoleBoundUserInterface
enum.SiliconLawsUiKey.Key:
type: SiliconLawBoundUserInterface
enum.RemoteDeviceUiKey.Key: # Corvax-Next-AiRemoteControl
type: RemoteDevicesBoundUserInterface
- type: IntrinsicUI
uis:
enum.RadarConsoleUiKey.Key:
@@ -115,9 +118,6 @@
- sprite: Mobs/Silicon/station_ai.rsi
state: default
- type: ShowJobIcons
- type: Tag
tags:
- CanPilot
- type: AdvancedPilot # can use both gunnery and shuttle console even on capitals
- type: entity