]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Predict borgs (#41600)
authorslarticodefast <161409025+slarticodefast@users.noreply.github.com>
Sun, 30 Nov 2025 10:25:22 +0000 (11:25 +0100)
committerGitHub <noreply@github.com>
Sun, 30 Nov 2025 10:25:22 +0000 (10:25 +0000)
* predict borgs

* small fix

* fix MMI item slot serialization

* fix movement speed for mothership core

* review and minor improvement

* fix resolve

* review

29 files changed:
Content.Client/Silicons/Borgs/BorgBoundUserInterface.cs
Content.Client/Silicons/Borgs/BorgMenu.xaml.cs
Content.Client/Silicons/Borgs/BorgSystem.Battery.cs [new file with mode: 0644]
Content.Client/Silicons/Borgs/BorgSystem.cs
Content.Server/Light/EntitySystems/HandheldLightSystem.cs
Content.Server/Silicons/Borgs/BorgSystem.MMI.cs [deleted file]
Content.Server/Silicons/Borgs/BorgSystem.Modules.cs [deleted file]
Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs
Content.Server/Silicons/Borgs/BorgSystem.Ui.cs [deleted file]
Content.Server/Silicons/Borgs/BorgSystem.cs
Content.Shared/Interaction/SharedInteractionSystem.cs
Content.Shared/Light/Components/HandheldLightComponent.cs
Content.Shared/Light/SharedHandheldLightSystem.cs
Content.Shared/PowerCell/PowerCellSystem.API.cs
Content.Shared/Silicons/Borgs/BorgUI.cs
Content.Shared/Silicons/Borgs/Components/BorgChassisComponent.cs
Content.Shared/Silicons/Borgs/Components/BorgModuleComponent.cs
Content.Shared/Silicons/Borgs/Components/ItemBorgModuleComponent.cs
Content.Shared/Silicons/Borgs/Components/MMIComponent.cs
Content.Shared/Silicons/Borgs/Components/MMILinkedComponent.cs
Content.Shared/Silicons/Borgs/Components/SelectableBorgModuleComponent.cs
Content.Shared/Silicons/Borgs/SharedBorgSystem.API.cs [new file with mode: 0644]
Content.Shared/Silicons/Borgs/SharedBorgSystem.MMI.cs [new file with mode: 0644]
Content.Shared/Silicons/Borgs/SharedBorgSystem.Module.cs [new file with mode: 0644]
Content.Shared/Silicons/Borgs/SharedBorgSystem.Ui.cs [new file with mode: 0644]
Content.Shared/Silicons/Borgs/SharedBorgSystem.cs
Resources/Prototypes/Alerts/alerts.yml
Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml
Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml

index ed9bf40a481af40fb23cecf03840a3905861ed13..381bfacefbfb5e1e573d38de21a446ece448d90d 100644 (file)
@@ -1,6 +1,5 @@
 using Content.Shared.Silicons.Borgs;
 using JetBrains.Annotations;
-using Robust.Client.GameObjects;
 using Robust.Client.UserInterface;
 
 namespace Content.Client.Silicons.Borgs;
@@ -24,31 +23,29 @@ public sealed class BorgBoundUserInterface : BoundUserInterface
 
         _menu.BrainButtonPressed += () =>
         {
-            SendMessage(new BorgEjectBrainBuiMessage());
+            SendPredictedMessage(new BorgEjectBrainBuiMessage());
         };
 
         _menu.EjectBatteryButtonPressed += () =>
         {
-            SendMessage(new BorgEjectBatteryBuiMessage());
+            SendPredictedMessage(new BorgEjectBatteryBuiMessage());
         };
 
         _menu.NameChanged += name =>
         {
-            SendMessage(new BorgSetNameBuiMessage(name));
+            SendPredictedMessage(new BorgSetNameBuiMessage(name));
         };
 
         _menu.RemoveModuleButtonPressed += module =>
         {
-            SendMessage(new BorgRemoveModuleBuiMessage(EntMan.GetNetEntity(module)));
+            SendPredictedMessage(new BorgRemoveModuleBuiMessage(EntMan.GetNetEntity(module)));
         };
     }
 
-    protected override void UpdateState(BoundUserInterfaceState state)
+    public override void Update()
     {
-        base.UpdateState(state);
-
-        if (state is not BorgBuiState msg)
-            return;
-        _menu?.UpdateState(msg);
+        _menu?.UpdateBatteryButton();
+        _menu?.UpdateBrainButton();
+        _menu?.UpdateModulePanel();
     }
 }
index aea590e334f03bc66fd470c3bed2f898e85ec07b..0acc3a26468a21b75bfadbaa695cef28eaf94cd0 100644 (file)
@@ -3,8 +3,8 @@ using Content.Client.UserInterface.Controls;
 using Content.Shared.CCVar;
 using Content.Shared.NameIdentifier;
 using Content.Shared.NameModifier.EntitySystems;
-using Content.Shared.Preferences;
-using Content.Shared.Silicons.Borgs;
+using Content.Shared.Power.EntitySystems;
+using Content.Shared.PowerCell;
 using Content.Shared.Silicons.Borgs.Components;
 using Robust.Client.AutoGenerated;
 using Robust.Client.UserInterface.Controls;
@@ -20,6 +20,8 @@ public sealed partial class BorgMenu : FancyWindow
     [Dependency] private readonly IConfigurationManager _cfgManager = default!;
     [Dependency] private readonly IEntityManager _entity = default!;
     private readonly NameModifierSystem _nameModifier;
+    private readonly PowerCellSystem _powerCell;
+    private readonly PredictedBatterySystem _battery;
 
     public Action? BrainButtonPressed;
     public Action? EjectBatteryButtonPressed;
@@ -41,6 +43,8 @@ public sealed partial class BorgMenu : FancyWindow
         IoCManager.InjectDependencies(this);
 
         _nameModifier = _entity.System<NameModifierSystem>();
+        _powerCell = _entity.System<PowerCellSystem>();
+        _battery = _entity.System<PredictedBatterySystem>();
 
         _maxNameLength = _cfgManager.GetCVar(CCVars.MaxNameLength);
 
@@ -52,8 +56,6 @@ public sealed partial class BorgMenu : FancyWindow
         NameLineEdit.OnTextChanged += OnNameChanged;
         NameLineEdit.OnTextEntered += OnNameEntered;
         NameLineEdit.OnFocusExit += OnNameFocusExit;
-
-        UpdateBrainButton();
     }
 
     public void SetEntity(EntityUid entity)
@@ -73,6 +75,10 @@ public sealed partial class BorgMenu : FancyWindow
             NameIdentifierLabel.Visible = false;
             NameLineEdit.Text = _entity.GetComponent<MetaDataComponent>(Entity).EntityName;
         }
+
+        UpdateBatteryButton();
+        UpdateBrainButton();
+        UpdateModulePanel();
     }
 
     protected override void FrameUpdate(FrameEventArgs args)
@@ -80,21 +86,24 @@ public sealed partial class BorgMenu : FancyWindow
         base.FrameUpdate(args);
 
         AccumulatedTime += args.DeltaSeconds;
-        BorgSprite.OverrideDirection = (Direction) ((int) AccumulatedTime % 4 * 2);
-    }
+        BorgSprite.OverrideDirection = (Direction)((int)AccumulatedTime % 4 * 2);
 
-    public void UpdateState(BorgBuiState state)
-    {
-        EjectBatteryButton.Disabled = !state.HasBattery;
-        ChargeBar.Value = state.ChargePercent;
+        var chargeFraction = 0f;
+
+        if (_powerCell.TryGetBatteryFromSlot(Entity, out var battery))
+            chargeFraction = _battery.GetCharge(battery.Value.AsNullable()) / battery.Value.Comp.MaxCharge;
+
+        ChargeBar.Value = chargeFraction;
         ChargeLabel.Text = Loc.GetString("borg-ui-charge-label",
-            ("charge", (int) MathF.Round(state.ChargePercent * 100)));
+            ("charge", (int)MathF.Round(chargeFraction * 100)));
+    }
 
-        UpdateBrainButton();
-        UpdateModulePanel();
+    public void UpdateBatteryButton()
+    {
+        EjectBatteryButton.Disabled = !_powerCell.HasBattery(Entity);
     }
 
-    private void UpdateBrainButton()
+    public void UpdateBrainButton()
     {
         if (_entity.TryGetComponent(Entity, out BorgChassisComponent? chassis) && chassis.BrainEntity is { } brain)
         {
@@ -113,7 +122,7 @@ public sealed partial class BorgMenu : FancyWindow
         }
     }
 
-    private void UpdateModulePanel()
+    public void UpdateModulePanel()
     {
         if (!_entity.TryGetComponent(Entity, out BorgChassisComponent? chassis))
             return;
diff --git a/Content.Client/Silicons/Borgs/BorgSystem.Battery.cs b/Content.Client/Silicons/Borgs/BorgSystem.Battery.cs
new file mode 100644 (file)
index 0000000..5239892
--- /dev/null
@@ -0,0 +1,83 @@
+using Content.Shared.PowerCell.Components;
+using Content.Shared.Silicons.Borgs.Components;
+using Robust.Shared.Player;
+
+namespace Content.Client.Silicons.Borgs;
+
+public sealed partial class BorgSystem
+{
+    // How often to update the battery alert.
+    // Also gets updated instantly when switching bodies or a battery is inserted or removed.
+    private static readonly TimeSpan AlertUpdateDelay = TimeSpan.FromSeconds(0.5f);
+
+    // Don't put this on the component because we only need to track the time for a single entity
+    // and we don't want to TryComp it every single tick.
+    private TimeSpan _nextAlertUpdate = TimeSpan.Zero;
+    private EntityQuery<BorgChassisComponent> _chassisQuery;
+    private EntityQuery<PowerCellSlotComponent> _slotQuery;
+
+    public void InitializeBattery()
+    {
+        SubscribeLocalEvent<BorgChassisComponent, LocalPlayerAttachedEvent>(OnPlayerAttached);
+        SubscribeLocalEvent<BorgChassisComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
+
+        _chassisQuery = GetEntityQuery<BorgChassisComponent>();
+        _slotQuery = GetEntityQuery<PowerCellSlotComponent>();
+    }
+
+    private void OnPlayerAttached(Entity<BorgChassisComponent> ent, ref LocalPlayerAttachedEvent args)
+    {
+        UpdateBatteryAlert((ent.Owner, ent.Comp, null));
+    }
+
+    private void OnPlayerDetached(Entity<BorgChassisComponent> ent, ref LocalPlayerDetachedEvent args)
+    {
+        // Remove all borg related alerts.
+        _alerts.ClearAlert(ent.Owner, ent.Comp.BatteryAlert);
+        _alerts.ClearAlert(ent.Owner, ent.Comp.NoBatteryAlert);
+    }
+
+    private void UpdateBatteryAlert(Entity<BorgChassisComponent, PowerCellSlotComponent?> ent)
+    {
+        if (!Resolve(ent, ref ent.Comp2, false))
+            return;
+
+        if (!_powerCell.TryGetBatteryFromSlot((ent.Owner, ent.Comp2), out var battery))
+        {
+            _alerts.ShowAlert(ent.Owner, ent.Comp1.NoBatteryAlert);
+            return;
+        }
+
+        // Alert levels from 0 to 10.
+        var chargeLevel = (short)MathF.Round(_battery.GetChargeLevel(battery.Value.AsNullable()) * 10f);
+
+        // we make sure 0 only shows if they have absolutely no battery.
+        // also account for floating point imprecision
+        if (chargeLevel == 0 && _powerCell.HasDrawCharge((ent.Owner, null, ent.Comp2)))
+        {
+            chargeLevel = 1;
+        }
+
+        _alerts.ShowAlert(ent.Owner, ent.Comp1.BatteryAlert, chargeLevel);
+    }
+
+    // Periodically update the charge indicator.
+    // We do this with a client-side alert so that we don't have to network the charge level.
+    public void UpdateBattery(float frameTime)
+    {
+        if (_player.LocalEntity is not { } localPlayer)
+            return;
+
+        var curTime = _timing.CurTime;
+
+        if (curTime < _nextAlertUpdate)
+            return;
+
+        _nextAlertUpdate = curTime + AlertUpdateDelay;
+
+        if (!_chassisQuery.TryComp(localPlayer, out var chassis) || !_slotQuery.TryComp(localPlayer, out var slot))
+            return;
+
+        UpdateBatteryAlert((localPlayer, chassis, slot));
+    }
+}
index 81689dbd606f97f82c2ead72a8535bb843ded758..a4ca3725dc641684d5aa108141815f95d0273568 100644 (file)
@@ -1,72 +1,93 @@
-using Content.Shared.Mobs;
+using Content.Shared.Alert;
+using Content.Shared.Mobs;
+using Content.Shared.Power.EntitySystems;
+using Content.Shared.PowerCell;
 using Content.Shared.Silicons.Borgs;
 using Content.Shared.Silicons.Borgs.Components;
 using Robust.Client.GameObjects;
+using Robust.Client.Player;
 using Robust.Shared.Containers;
+using Robust.Shared.Timing;
 
 namespace Content.Client.Silicons.Borgs;
 
 /// <inheritdoc/>
-public sealed class BorgSystem : SharedBorgSystem
+public sealed partial class BorgSystem : SharedBorgSystem
 {
     [Dependency] private readonly AppearanceSystem _appearance = default!;
     [Dependency] private readonly SpriteSystem _sprite = default!;
+    [Dependency] private readonly UserInterfaceSystem _ui = default!;
+    [Dependency] private readonly PowerCellSystem _powerCell = default!;
+    [Dependency] private readonly PredictedBatterySystem _battery = default!;
+    [Dependency] private readonly AlertsSystem _alerts = default!;
+    [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly IPlayerManager _player = default!;
 
     public override void Initialize()
     {
         base.Initialize();
 
+        InitializeBattery();
+
         SubscribeLocalEvent<BorgChassisComponent, AppearanceChangeEvent>(OnBorgAppearanceChanged);
         SubscribeLocalEvent<MMIComponent, AppearanceChangeEvent>(OnMMIAppearanceChanged);
     }
 
-    private void OnBorgAppearanceChanged(EntityUid uid, BorgChassisComponent component, ref AppearanceChangeEvent args)
+    public override void UpdateUI(Entity<BorgChassisComponent?> chassis)
+    {
+        if (_ui.TryGetOpenUi(chassis.Owner, BorgUiKey.Key, out var bui))
+            bui.Update();
+    }
+
+    private void OnBorgAppearanceChanged(Entity<BorgChassisComponent> chassis, ref AppearanceChangeEvent args)
     {
         if (args.Sprite == null)
             return;
-        UpdateBorgAppearance(uid, component, args.Component, args.Sprite);
+
+        UpdateBorgAppearance((chassis.Owner, chassis.Comp, args.Component, args.Sprite));
     }
 
-    protected override void OnInserted(EntityUid uid, BorgChassisComponent component, EntInsertedIntoContainerMessage args)
+    protected override void OnInserted(Entity<BorgChassisComponent> chassis, ref EntInsertedIntoContainerMessage args)
     {
-        if (!component.Initialized)
+        if (!chassis.Comp.Initialized)
             return;
 
-        base.OnInserted(uid, component, args);
-        UpdateBorgAppearance(uid, component);
+        base.OnInserted(chassis, ref args);
+        UpdateUI(chassis.AsNullable());
+        UpdateBorgAppearance((chassis, chassis.Comp));
+        UpdateBatteryAlert((chassis.Owner, chassis.Comp, null));
     }
 
-    protected override void OnRemoved(EntityUid uid, BorgChassisComponent component, EntRemovedFromContainerMessage args)
+    protected override void OnRemoved(Entity<BorgChassisComponent> chassis, ref EntRemovedFromContainerMessage args)
     {
-        if (!component.Initialized)
+        if (!chassis.Comp.Initialized)
             return;
 
-        base.OnRemoved(uid, component, args);
-        UpdateBorgAppearance(uid, component);
+        base.OnRemoved(chassis, ref args);
+        UpdateUI(chassis.AsNullable());
+        UpdateBorgAppearance((chassis, chassis.Comp));
+        UpdateBatteryAlert((chassis.Owner, chassis.Comp, null));
     }
 
-    private void UpdateBorgAppearance(EntityUid uid,
-        BorgChassisComponent? component = null,
-        AppearanceComponent? appearance = null,
-        SpriteComponent? sprite = null)
+    private void UpdateBorgAppearance(Entity<BorgChassisComponent?, AppearanceComponent?, SpriteComponent?> ent)
     {
-        if (!Resolve(uid, ref component, ref appearance, ref sprite))
+        if (!Resolve(ent, ref ent.Comp1, ref ent.Comp2, ref ent.Comp3))
             return;
 
-        if (_appearance.TryGetData<MobState>(uid, MobStateVisuals.State, out var state, appearance))
+        if (_appearance.TryGetData<MobState>(ent.Owner, MobStateVisuals.State, out var state, ent.Comp2))
         {
             if (state != MobState.Alive)
             {
-                _sprite.LayerSetVisible((uid, sprite), BorgVisualLayers.Light, false);
+                _sprite.LayerSetVisible((ent.Owner, ent.Comp3), BorgVisualLayers.Light, false);
                 return;
             }
         }
 
-        if (!_appearance.TryGetData<bool>(uid, BorgVisuals.HasPlayer, out var hasPlayer, appearance))
+        if (!_appearance.TryGetData<bool>(ent.Owner, BorgVisuals.HasPlayer, out var hasPlayer, ent.Comp2))
             hasPlayer = false;
 
-        _sprite.LayerSetVisible((uid, sprite), BorgVisualLayers.Light, component.BrainEntity != null || hasPlayer);
-        _sprite.LayerSetRsiState((uid, sprite), BorgVisualLayers.Light, hasPlayer ? component.HasMindState : component.NoMindState);
+        _sprite.LayerSetVisible((ent.Owner, ent.Comp3), BorgVisualLayers.Light, ent.Comp1.BrainEntity != null || hasPlayer);
+        _sprite.LayerSetRsiState((ent.Owner, ent.Comp3), BorgVisualLayers.Light, hasPlayer ? ent.Comp1.HasMindState : ent.Comp1.NoMindState);
     }
 
     private void OnMMIAppearanceChanged(EntityUid uid, MMIComponent component, ref AppearanceChangeEvent args)
@@ -107,4 +128,10 @@ public sealed class BorgSystem : SharedBorgSystem
         borg.Comp.HasMindState = hasMindState;
         borg.Comp.NoMindState = noMindState;
     }
+
+    public override void Update(float frameTime)
+    {
+        base.Update(frameTime);
+        UpdateBattery(frameTime);
+    }
 }
index 4102e2dca0c848a5a7c0f36b81e7b8ed056f569b..da024ee07564bedad0ca437449a7b8ba4be2e967 100644 (file)
@@ -1,7 +1,6 @@
 using Content.Server.Actions;
 using Content.Server.Popups;
 using Content.Shared.Actions;
-using Content.Shared.Examine;
 using Content.Shared.Interaction;
 using Content.Shared.Light;
 using Content.Shared.Light.Components;
@@ -44,7 +43,6 @@ namespace Content.Server.Light.EntitySystems
             SubscribeLocalEvent<HandheldLightComponent, MapInitEvent>(OnMapInit);
             SubscribeLocalEvent<HandheldLightComponent, ComponentShutdown>(OnShutdown);
 
-            SubscribeLocalEvent<HandheldLightComponent, ExaminedEvent>(OnExamine);
 
             SubscribeLocalEvent<HandheldLightComponent, ActivateInWorldEvent>(OnActivate);
 
@@ -142,13 +140,6 @@ namespace Content.Server.Light.EntitySystems
             return ent.Comp.Activated ? TurnOff(ent) : TurnOn(user, ent);
         }
 
-        private void OnExamine(EntityUid uid, HandheldLightComponent component, ExaminedEvent args)
-        {
-            args.PushMarkup(component.Activated
-                ? Loc.GetString("handheld-light-component-on-examine-is-on-message")
-                : Loc.GetString("handheld-light-component-on-examine-is-off-message"));
-        }
-
         public override void Shutdown()
         {
             base.Shutdown();
diff --git a/Content.Server/Silicons/Borgs/BorgSystem.MMI.cs b/Content.Server/Silicons/Borgs/BorgSystem.MMI.cs
deleted file mode 100644 (file)
index b41f139..0000000
+++ /dev/null
@@ -1,97 +0,0 @@
-using Content.Shared.Containers.ItemSlots;
-using Content.Shared.Mind.Components;
-using Content.Shared.Roles;
-using Content.Shared.Roles.Components;
-using Content.Shared.Silicons.Borgs.Components;
-using Robust.Shared.Containers;
-
-namespace Content.Server.Silicons.Borgs;
-
-/// <inheritdoc/>
-public sealed partial class BorgSystem
-{
-
-    [Dependency] private readonly SharedRoleSystem _roles = default!;
-
-    public void InitializeMMI()
-    {
-        SubscribeLocalEvent<MMIComponent, ComponentInit>(OnMMIInit);
-        SubscribeLocalEvent<MMIComponent, EntInsertedIntoContainerMessage>(OnMMIEntityInserted);
-        SubscribeLocalEvent<MMIComponent, MindAddedMessage>(OnMMIMindAdded);
-        SubscribeLocalEvent<MMIComponent, MindRemovedMessage>(OnMMIMindRemoved);
-
-        SubscribeLocalEvent<MMILinkedComponent, MindAddedMessage>(OnMMILinkedMindAdded);
-        SubscribeLocalEvent<MMILinkedComponent, EntGotRemovedFromContainerMessage>(OnMMILinkedRemoved);
-    }
-
-    private void OnMMIInit(EntityUid uid, MMIComponent component, ComponentInit args)
-    {
-        if (!TryComp<ItemSlotsComponent>(uid, out var itemSlots))
-            return;
-
-        if (ItemSlots.TryGetSlot(uid, component.BrainSlotId, out var slot, itemSlots))
-            component.BrainSlot = slot;
-        else
-            ItemSlots.AddItemSlot(uid, component.BrainSlotId, component.BrainSlot, itemSlots);
-    }
-
-    private void OnMMIEntityInserted(EntityUid uid, MMIComponent component, EntInsertedIntoContainerMessage args)
-    {
-        if (args.Container.ID != component.BrainSlotId)
-            return;
-
-        var ent = args.Entity;
-        var linked = EnsureComp<MMILinkedComponent>(ent);
-        linked.LinkedMMI = uid;
-        Dirty(uid, component);
-
-        if (_mind.TryGetMind(ent, out var mindId, out var mind))
-        {
-            _mind.TransferTo(mindId, uid, true, mind: mind);
-
-            if (!_roles.MindHasRole<SiliconBrainRoleComponent>(mindId))
-                _roles.MindAddRole(mindId, "MindRoleSiliconBrain", silent: true);
-        }
-
-        _appearance.SetData(uid, MMIVisuals.BrainPresent, true);
-    }
-
-    private void OnMMIMindAdded(EntityUid uid, MMIComponent component, MindAddedMessage args)
-    {
-        _appearance.SetData(uid, MMIVisuals.HasMind, true);
-    }
-
-    private void OnMMIMindRemoved(EntityUid uid, MMIComponent component, MindRemovedMessage args)
-    {
-        _appearance.SetData(uid, MMIVisuals.HasMind, false);
-    }
-
-    private void OnMMILinkedMindAdded(EntityUid uid, MMILinkedComponent component, MindAddedMessage args)
-    {
-        if (!_mind.TryGetMind(uid, out var mindId, out var mind) ||
-            component.LinkedMMI == null)
-            return;
-
-        _mind.TransferTo(mindId, component.LinkedMMI, true, mind: mind);
-    }
-
-    private void OnMMILinkedRemoved(EntityUid uid, MMILinkedComponent component, EntGotRemovedFromContainerMessage args)
-    {
-        if (Terminating(uid))
-            return;
-
-        if (component.LinkedMMI is not { } linked)
-            return;
-        RemComp(uid, component);
-
-        if (_mind.TryGetMind(linked, out var mindId, out var mind))
-        {
-            if (_roles.MindHasRole<SiliconBrainRoleComponent>(mindId))
-                _roles.MindRemoveRole<SiliconBrainRoleComponent>(mindId);
-
-            _mind.TransferTo(mindId, uid, true, mind: mind);
-        }
-
-        _appearance.SetData(linked, MMIVisuals.BrainPresent, false);
-    }
-}
diff --git a/Content.Server/Silicons/Borgs/BorgSystem.Modules.cs b/Content.Server/Silicons/Borgs/BorgSystem.Modules.cs
deleted file mode 100644 (file)
index 67408d1..0000000
+++ /dev/null
@@ -1,425 +0,0 @@
-using System.Linq;
-using Content.Shared.Hands.Components;
-using Content.Shared.Interaction.Components;
-using Content.Shared.Silicons.Borgs.Components;
-using Content.Shared.Whitelist;
-using Robust.Shared.Containers;
-
-namespace Content.Server.Silicons.Borgs;
-
-/// <inheritdoc/>
-public sealed partial class BorgSystem
-{
-    public void InitializeModules()
-    {
-        SubscribeLocalEvent<BorgModuleComponent, EntGotInsertedIntoContainerMessage>(OnModuleGotInserted);
-        SubscribeLocalEvent<BorgModuleComponent, EntGotRemovedFromContainerMessage>(OnModuleGotRemoved);
-
-        SubscribeLocalEvent<SelectableBorgModuleComponent, BorgModuleInstalledEvent>(OnSelectableInstalled);
-        SubscribeLocalEvent<SelectableBorgModuleComponent, BorgModuleUninstalledEvent>(OnSelectableUninstalled);
-        SubscribeLocalEvent<SelectableBorgModuleComponent, BorgModuleActionSelectedEvent>(OnSelectableAction);
-
-        SubscribeLocalEvent<ItemBorgModuleComponent, ComponentStartup>(OnProvideItemStartup);
-        SubscribeLocalEvent<ItemBorgModuleComponent, BorgModuleSelectedEvent>(OnItemModuleSelected);
-        SubscribeLocalEvent<ItemBorgModuleComponent, BorgModuleUnselectedEvent>(OnItemModuleUnselected);
-    }
-
-    private void OnModuleGotInserted(EntityUid uid, BorgModuleComponent component, EntGotInsertedIntoContainerMessage args)
-    {
-        var chassis = args.Container.Owner;
-
-        if (!TryComp<BorgChassisComponent>(chassis, out var chassisComp) ||
-            args.Container != chassisComp.ModuleContainer ||
-            !Toggle.IsActivated(chassis))
-            return;
-
-        if (!_powerCell.HasDrawCharge(uid))
-            return;
-
-        InstallModule(chassis, uid, chassisComp, component);
-    }
-
-    private void OnModuleGotRemoved(EntityUid uid, BorgModuleComponent component, EntGotRemovedFromContainerMessage args)
-    {
-        var chassis = args.Container.Owner;
-
-        if (!TryComp<BorgChassisComponent>(chassis, out var chassisComp) ||
-            args.Container != chassisComp.ModuleContainer)
-            return;
-
-        UninstallModule(chassis, uid, chassisComp, component);
-    }
-
-    private void OnProvideItemStartup(EntityUid uid, ItemBorgModuleComponent component, ComponentStartup args)
-    {
-        Container.EnsureContainer<Container>(uid, component.HoldingContainer);
-    }
-
-    private void OnSelectableInstalled(EntityUid uid, SelectableBorgModuleComponent component, ref BorgModuleInstalledEvent args)
-    {
-        var chassis = args.ChassisEnt;
-
-        if (_actions.AddAction(chassis, ref component.ModuleSwapActionEntity, out var action, component.ModuleSwapActionId, uid))
-        {
-            var actEnt = (component.ModuleSwapActionEntity.Value, action);
-            _actions.SetEntityIcon(actEnt, uid);
-            if (TryComp<BorgModuleIconComponent>(uid, out var moduleIconComp))
-                _actions.SetIcon(actEnt, moduleIconComp.Icon);
-
-            /// Set a custom name and description on the action. The borg module action prototypes are shared across
-            /// all modules. Extract localized names, then populate variables with the info from the module itself.
-            var moduleName = Name(uid);
-            var actionMetaData = MetaData(component.ModuleSwapActionEntity.Value);
-
-            var instanceName = Loc.GetString("borg-module-action-name", ("moduleName", moduleName));
-            _metaData.SetEntityName(component.ModuleSwapActionEntity.Value, instanceName, actionMetaData);
-            var instanceDesc = Loc.GetString("borg-module-action-description", ("moduleName", moduleName));
-            _metaData.SetEntityDescription(component.ModuleSwapActionEntity.Value, instanceDesc, actionMetaData);
-        }
-
-        if (!TryComp(chassis, out BorgChassisComponent? chassisComp))
-            return;
-
-        if (chassisComp.SelectedModule == null)
-            SelectModule(chassis, uid, chassisComp, component);
-    }
-
-    private void OnSelectableUninstalled(EntityUid uid, SelectableBorgModuleComponent component, ref BorgModuleUninstalledEvent args)
-    {
-        var chassis = args.ChassisEnt;
-        _actions.RemoveProvidedActions(chassis, uid);
-        if (!TryComp(chassis, out BorgChassisComponent? chassisComp))
-            return;
-
-        if (chassisComp.SelectedModule == uid)
-            UnselectModule(chassis, chassisComp);
-    }
-
-    private void OnSelectableAction(EntityUid uid, SelectableBorgModuleComponent component, BorgModuleActionSelectedEvent args)
-    {
-        var chassis = args.Performer;
-        if (!TryComp<BorgChassisComponent>(chassis, out var chassisComp))
-            return;
-
-        var selected = chassisComp.SelectedModule;
-
-        args.Handled = true;
-        UnselectModule(chassis, chassisComp);
-
-        if (selected != uid)
-        {
-            SelectModule(chassis, uid, chassisComp, component);
-        }
-    }
-
-    /// <summary>
-    /// Selects a module, enabling the borg to use its provided abilities.
-    /// </summary>
-    public void SelectModule(EntityUid chassis,
-        EntityUid moduleUid,
-        BorgChassisComponent? chassisComp = null,
-        SelectableBorgModuleComponent? selectable = null,
-        BorgModuleComponent? moduleComp = null)
-    {
-        if (LifeStage(chassis) >= EntityLifeStage.Terminating)
-            return;
-
-        if (!Resolve(chassis, ref chassisComp))
-            return;
-
-        if (!Resolve(moduleUid, ref moduleComp) || !moduleComp.Installed || moduleComp.InstalledEntity != chassis)
-        {
-            Log.Error($"{ToPrettyString(chassis)} attempted to select uninstalled module {ToPrettyString(moduleUid)}");
-            return;
-        }
-
-        if (selectable == null && !HasComp<SelectableBorgModuleComponent>(moduleUid))
-        {
-            Log.Error($"{ToPrettyString(chassis)} attempted to select invalid module {ToPrettyString(moduleUid)}");
-            return;
-        }
-
-        if (!chassisComp.ModuleContainer.Contains(moduleUid))
-        {
-            Log.Error($"{ToPrettyString(chassis)} does not contain the installed module {ToPrettyString(moduleUid)}");
-            return;
-        }
-
-        if (chassisComp.SelectedModule != null)
-            return;
-
-        if (chassisComp.SelectedModule == moduleUid)
-            return;
-
-        UnselectModule(chassis, chassisComp);
-
-        var ev = new BorgModuleSelectedEvent(chassis);
-        RaiseLocalEvent(moduleUid, ref ev);
-        chassisComp.SelectedModule = moduleUid;
-        Dirty(chassis, chassisComp);
-    }
-
-    /// <summary>
-    /// Unselects a module, removing its provided abilities
-    /// </summary>
-    public void UnselectModule(EntityUid chassis, BorgChassisComponent? chassisComp = null)
-    {
-        if (LifeStage(chassis) >= EntityLifeStage.Terminating)
-            return;
-
-        if (!Resolve(chassis, ref chassisComp))
-            return;
-
-        if (chassisComp.SelectedModule == null)
-            return;
-
-        var ev = new BorgModuleUnselectedEvent(chassis);
-        RaiseLocalEvent(chassisComp.SelectedModule.Value, ref ev);
-        chassisComp.SelectedModule = null;
-        Dirty(chassis, chassisComp);
-    }
-
-    private void OnItemModuleSelected(EntityUid uid, ItemBorgModuleComponent component, ref BorgModuleSelectedEvent args)
-    {
-        ProvideItems(args.Chassis, uid, component: component);
-    }
-
-    private void OnItemModuleUnselected(EntityUid uid, ItemBorgModuleComponent component, ref BorgModuleUnselectedEvent args)
-    {
-        RemoveProvidedItems(args.Chassis, uid, component: component);
-    }
-
-    private void ProvideItems(EntityUid chassis, EntityUid uid, BorgChassisComponent? chassisComponent = null, ItemBorgModuleComponent? component = null)
-    {
-        if (!Resolve(chassis, ref chassisComponent) || !Resolve(uid, ref component))
-            return;
-
-        if (!TryComp<HandsComponent>(chassis, out var hands))
-            return;
-
-        if (!_container.TryGetContainer(uid, component.HoldingContainer, out var container))
-            return;
-
-        var xform = Transform(chassis);
-
-        for (var i = 0; i < component.Hands.Count; i++)
-        {
-            var hand = component.Hands[i];
-            var handId = $"{uid}-hand-{i}";
-
-            _hands.AddHand((chassis, hands), handId, hand.Hand);
-            EntityUid? item = null;
-
-            if (component.StoredItems is not null)
-            {
-                if (component.StoredItems.TryGetValue(handId, out var storedItem))
-                {
-                    item = storedItem;
-                    _container.Remove(storedItem, container, force: true);
-                }
-            }
-            else if (hand.Item is { } itemProto)
-            {
-                item = Spawn(itemProto, xform.Coordinates);
-            }
-
-            if (item is { } pickUp)
-            {
-                _hands.DoPickup(chassis, handId, pickUp, hands);
-                if (!hand.ForceRemovable && hand.Hand.Whitelist == null && hand.Hand.Blacklist == null)
-                {
-                    EnsureComp<UnremoveableComponent>(pickUp);
-                }
-            }
-        }
-
-        Dirty(uid, component);
-    }
-
-    private void RemoveProvidedItems(EntityUid chassis, EntityUid uid, BorgChassisComponent? chassisComponent = null, ItemBorgModuleComponent? component = null)
-    {
-        if (!Resolve(chassis, ref chassisComponent) || !Resolve(uid, ref component))
-            return;
-
-        if (!TryComp<HandsComponent>(chassis, out var hands))
-            return;
-
-        if (!_container.TryGetContainer(uid, component.HoldingContainer, out var container))
-            return;
-
-        if (TerminatingOrDeleted(uid))
-            return;
-
-        component.StoredItems ??= new();
-
-        for (var i = 0; i < component.Hands.Count; i++)
-        {
-            var handId = $"{uid}-hand-{i}";
-
-            if (_hands.TryGetHeldItem(chassis, handId, out var held))
-            {
-                RemComp<UnremoveableComponent>(held.Value);
-                _container.Insert(held.Value, container);
-                component.StoredItems[handId] = held.Value;
-            }
-            else
-            {
-                component.StoredItems.Remove(handId);
-            }
-
-            _hands.RemoveHand(chassis, handId);
-        }
-
-        Dirty(uid, component);
-    }
-
-    /// <summary>
-    /// Checks if a given module can be inserted into a borg
-    /// </summary>
-    public bool CanInsertModule(EntityUid uid, EntityUid module, BorgChassisComponent? component = null, BorgModuleComponent? moduleComponent = null, EntityUid? user = null)
-    {
-        if (!Resolve(uid, ref component) || !Resolve(module, ref moduleComponent))
-            return false;
-
-        if (component.ModuleContainer.ContainedEntities.Count >= component.MaxModules)
-        {
-            if (user != null)
-                Popup.PopupEntity(Loc.GetString("borg-module-too-many"), uid, user.Value);
-            return false;
-        }
-
-        if (_whitelistSystem.IsWhitelistFail(component.ModuleWhitelist, module))
-        {
-            if (user != null)
-                Popup.PopupEntity(Loc.GetString("borg-module-whitelist-deny"), uid, user.Value);
-            return false;
-        }
-
-        if (TryComp<ItemBorgModuleComponent>(module, out var itemModuleComp))
-        {
-            foreach (var containedModuleUid in component.ModuleContainer.ContainedEntities)
-            {
-                if (!TryComp<ItemBorgModuleComponent>(containedModuleUid, out var containedItemModuleComp))
-                    continue;
-
-                if (containedItemModuleComp.Hands.Count == itemModuleComp.Hands.Count &&
-                    containedItemModuleComp.Hands.All(itemModuleComp.Hands.Contains))
-                {
-                    if (user != null)
-                        Popup.PopupEntity(Loc.GetString("borg-module-duplicate"), uid, user.Value);
-                    return false;
-                }
-            }
-        }
-
-        return true;
-    }
-
-    /// <summary>
-    /// Check if a module can be removed from a borg.
-    /// </summary>
-    /// <param name="borg">The borg that the module is being removed from.</param>
-    /// <param name="module">The module to remove from the borg.</param>
-    /// <param name="user">The user attempting to remove the module.</param>
-    /// <returns>True if the module can be removed.</returns>
-    public bool CanRemoveModule(
-        Entity<BorgChassisComponent> borg,
-        Entity<BorgModuleComponent> module,
-        EntityUid? user = null)
-    {
-        if (module.Comp.DefaultModule)
-            return false;
-
-        return true;
-    }
-
-    /// <summary>
-    /// Installs and activates all modules currently inside the borg's module container
-    /// </summary>
-    public void InstallAllModules(EntityUid uid, BorgChassisComponent? component = null)
-    {
-        if (!Resolve(uid, ref component))
-            return;
-
-        var query = GetEntityQuery<BorgModuleComponent>();
-        foreach (var moduleEnt in new List<EntityUid>(component.ModuleContainer.ContainedEntities))
-        {
-            if (!query.TryGetComponent(moduleEnt, out var moduleComp))
-                continue;
-
-            InstallModule(uid, moduleEnt, component, moduleComp);
-        }
-    }
-
-    /// <summary>
-    /// Deactivates all modules currently inside the borg's module container
-    /// </summary>
-    /// <param name="uid"></param>
-    /// <param name="component"></param>
-    public void DisableAllModules(EntityUid uid, BorgChassisComponent? component = null)
-    {
-        if (!Resolve(uid, ref component))
-            return;
-
-        var query = GetEntityQuery<BorgModuleComponent>();
-        foreach (var moduleEnt in new List<EntityUid>(component.ModuleContainer.ContainedEntities))
-        {
-            if (!query.TryGetComponent(moduleEnt, out var moduleComp))
-                continue;
-
-            UninstallModule(uid, moduleEnt, component, moduleComp);
-        }
-    }
-
-    /// <summary>
-    /// Installs a single module into a borg.
-    /// </summary>
-    public void InstallModule(EntityUid uid, EntityUid module, BorgChassisComponent? component, BorgModuleComponent? moduleComponent = null)
-    {
-        if (!Resolve(uid, ref component) || !Resolve(module, ref moduleComponent))
-            return;
-
-        if (moduleComponent.Installed)
-            return;
-
-        moduleComponent.InstalledEntity = uid;
-        var ev = new BorgModuleInstalledEvent(uid);
-        RaiseLocalEvent(module, ref ev);
-    }
-
-    /// <summary>
-    /// Uninstalls a single module from a borg.
-    /// </summary>
-    public void UninstallModule(EntityUid uid, EntityUid module, BorgChassisComponent? component, BorgModuleComponent? moduleComponent = null)
-    {
-        if (!Resolve(uid, ref component) || !Resolve(module, ref moduleComponent))
-            return;
-
-        if (!moduleComponent.Installed)
-            return;
-
-        moduleComponent.InstalledEntity = null;
-        var ev = new BorgModuleUninstalledEvent(uid);
-        RaiseLocalEvent(module, ref ev);
-    }
-
-    /// <summary>
-    /// Sets <see cref="BorgChassisComponent.MaxModules"/>.
-    /// </summary>
-    /// <param name="ent">The borg to modify.</param>
-    /// <param name="maxModules">The new max module count.</param>
-    public void SetMaxModules(Entity<BorgChassisComponent> ent, int maxModules)
-    {
-        ent.Comp.MaxModules = maxModules;
-    }
-
-    /// <summary>
-    /// Sets <see cref="BorgChassisComponent.ModuleWhitelist"/>.
-    /// </summary>
-    /// <param name="ent">The borg to modify.</param>
-    /// <param name="whitelist">The new module whitelist.</param>
-    public void SetModuleWhitelist(Entity<BorgChassisComponent> ent, EntityWhitelist? whitelist)
-    {
-        ent.Comp.ModuleWhitelist = whitelist;
-    }
-}
index cc665c9e62691473e8e9815886354959d0443e80..a6d3cf1dffb572a778e143abba7b491c4fb3b84d 100644 (file)
@@ -1,9 +1,7 @@
-using Content.Shared.Containers.ItemSlots;
 using Content.Shared.DeviceNetwork;
 using Content.Shared.Damage.Components;
 using Content.Shared.FixedPoint;
 using Content.Shared.Mobs;
-using Content.Shared.Mobs.Systems;
 using Content.Shared.Movement.Components;
 using Content.Shared.Popups;
 using Content.Shared.Robotics;
@@ -18,10 +16,6 @@ namespace Content.Server.Silicons.Borgs;
 /// <inheritdoc/>
 public sealed partial class BorgSystem
 {
-    [Dependency] private readonly EmagSystem _emag = default!;
-    [Dependency] private readonly MobThresholdSystem _mobThresholdSystem = default!;
-    [Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!;
-
     private void InitializeTransponder()
     {
         SubscribeLocalEvent<BorgTransponderComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
@@ -83,7 +77,7 @@ public sealed partial class BorgSystem
             return;
 
         var message = Loc.GetString(ent.Comp1.DisabledPopup, ("name", Name(ent, ent.Comp3)));
-        Popup.PopupEntity(message, ent);
+        _popup.PopupEntity(message, ent);
         _container.Remove(brain, ent.Comp2.BrainContainer);
     }
 
@@ -111,7 +105,7 @@ public sealed partial class BorgSystem
         if (CheckEmagged(ent, "disabled"))
             ent.Comp1.FakeDisabling = true;
         else
-            Popup.PopupEntity(Loc.GetString(ent.Comp1.DisablingPopup), ent);
+            _popup.PopupEntity(Loc.GetString(ent.Comp1.DisablingPopup), ent);
 
         ent.Comp1.NextDisable = _timing.CurTime + ent.Comp1.DisableDelay;
     }
@@ -134,7 +128,7 @@ public sealed partial class BorgSystem
         }
 
         var message = Loc.GetString(ent.Comp.DestroyingPopup, ("name", Name(ent)));
-        Popup.PopupEntity(message, ent);
+        _popup.PopupEntity(message, ent);
         _trigger.ActivateTimerTrigger(ent.Owner);
 
         // prevent a shitter borg running into people
@@ -145,7 +139,7 @@ public sealed partial class BorgSystem
     {
         if (_emag.CheckFlag(uid, EmagType.Interaction))
         {
-            Popup.PopupEntity(Loc.GetString($"borg-transponder-emagged-{name}-popup"), uid, uid, PopupType.LargeCaution);
+            _popup.PopupEntity(Loc.GetString($"borg-transponder-emagged-{name}-popup"), uid, uid, PopupType.LargeCaution);
             return true;
         }
 
diff --git a/Content.Server/Silicons/Borgs/BorgSystem.Ui.cs b/Content.Server/Silicons/Borgs/BorgSystem.Ui.cs
deleted file mode 100644 (file)
index 1306e54..0000000
+++ /dev/null
@@ -1,163 +0,0 @@
-using System.Linq;
-using Content.Shared.UserInterface;
-using Content.Shared.CCVar;
-using Content.Shared.Database;
-using Content.Shared.NameIdentifier;
-using Content.Shared.PowerCell.Components;
-using Content.Shared.Preferences;
-using Content.Shared.Silicons.Borgs;
-using Content.Shared.Silicons.Borgs.Components;
-using Robust.Shared.Configuration;
-
-namespace Content.Server.Silicons.Borgs;
-
-/// <inheritdoc/>
-public sealed partial class BorgSystem
-{
-    // CCvar.
-    private int _maxNameLength;
-
-    public void InitializeUI()
-    {
-        SubscribeLocalEvent<BorgChassisComponent, BeforeActivatableUIOpenEvent>(OnBeforeBorgUiOpen);
-        SubscribeLocalEvent<BorgChassisComponent, BorgEjectBrainBuiMessage>(OnEjectBrainBuiMessage);
-        SubscribeLocalEvent<BorgChassisComponent, BorgEjectBatteryBuiMessage>(OnEjectBatteryBuiMessage);
-        SubscribeLocalEvent<BorgChassisComponent, BorgSetNameBuiMessage>(OnSetNameBuiMessage);
-        SubscribeLocalEvent<BorgChassisComponent, BorgRemoveModuleBuiMessage>(OnRemoveModuleBuiMessage);
-
-        Subs.CVar(_cfgManager, CCVars.MaxNameLength, value => _maxNameLength = value, true);
-    }
-
-    private void OnBeforeBorgUiOpen(EntityUid uid, BorgChassisComponent component, BeforeActivatableUIOpenEvent args)
-    {
-        UpdateUI(uid, component);
-    }
-
-    private void OnEjectBrainBuiMessage(EntityUid uid, BorgChassisComponent component, BorgEjectBrainBuiMessage args)
-    {
-        if (component.BrainEntity is not { } brain)
-            return;
-
-        _adminLog.Add(LogType.Action, LogImpact.Medium,
-            $"{ToPrettyString(args.Actor):player} removed brain {ToPrettyString(brain)} from borg {ToPrettyString(uid)}");
-        _container.Remove(brain, component.BrainContainer);
-        _hands.TryPickupAnyHand(args.Actor, brain);
-        UpdateUI(uid, component);
-    }
-
-    private void OnEjectBatteryBuiMessage(EntityUid uid, BorgChassisComponent component, BorgEjectBatteryBuiMessage args)
-    {
-        if (TryEjectPowerCell(uid, component, out var ents))
-            _hands.TryPickupAnyHand(args.Actor, ents.First());
-    }
-
-    private void OnSetNameBuiMessage(EntityUid uid, BorgChassisComponent component, BorgSetNameBuiMessage args)
-    {
-        if (args.Name.Length > _maxNameLength ||
-            args.Name.Length == 0 ||
-            string.IsNullOrWhiteSpace(args.Name) ||
-            string.IsNullOrEmpty(args.Name))
-        {
-            return;
-        }
-
-        var name = args.Name.Trim();
-
-        var metaData = MetaData(uid);
-
-        // don't change the name if the value doesn't actually change
-        if (metaData.EntityName.Equals(name, StringComparison.InvariantCulture))
-            return;
-
-        _adminLog.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(args.Actor):player} set borg \"{ToPrettyString(uid)}\"'s name to: {name}");
-        _metaData.SetEntityName(uid, name, metaData);
-    }
-
-    private void OnRemoveModuleBuiMessage(EntityUid uid, BorgChassisComponent component, BorgRemoveModuleBuiMessage args)
-    {
-        var module = GetEntity(args.Module);
-
-        if (!component.ModuleContainer.Contains(module))
-            return;
-
-        if (!CanRemoveModule((uid, component), (module, Comp<BorgModuleComponent>(module)), args.Actor))
-            return;
-
-        _adminLog.Add(LogType.Action, LogImpact.Medium,
-            $"{ToPrettyString(args.Actor):player} removed module {ToPrettyString(module)} from borg {ToPrettyString(uid)}");
-        _container.Remove(module, component.ModuleContainer);
-        _hands.TryPickupAnyHand(args.Actor, module);
-
-        UpdateUI(uid, component);
-    }
-
-    public void UpdateBattery(Entity<BorgChassisComponent> ent)
-    {
-        UpdateBatteryAlert(ent);
-
-        // if we aren't drawing and suddenly get enough power to draw again, reeanble.
-        if (_powerCell.HasDrawCharge(ent.Owner))
-        {
-            Toggle.TryActivate(ent.Owner);
-        }
-
-        UpdateUI(ent, ent);
-    }
-
-    // TODO: Move to client so we don't have to network this periodically.
-    private void UpdateBatteryAlert(Entity<BorgChassisComponent> ent, PowerCellSlotComponent? slotComponent = null)
-    {
-        if (!_powerCell.TryGetBatteryFromSlot((ent.Owner, slotComponent), out var battery))
-        {
-            _alerts.ClearAlert(ent.Owner, ent.Comp.BatteryAlert);
-            _alerts.ShowAlert(ent.Owner, ent.Comp.NoBatteryAlert);
-            return;
-        }
-
-        var chargePercent = (short)MathF.Round(_battery.GetChargeLevel(battery.Value.AsNullable()) * 10f);
-
-        // we make sure 0 only shows if they have absolutely no battery.
-        // also account for floating point imprecision
-        if (chargePercent == 0 && _powerCell.HasDrawCharge((ent.Owner, null, slotComponent)))
-        {
-            chargePercent = 1;
-        }
-
-        _alerts.ClearAlert(ent.Owner, ent.Comp.NoBatteryAlert);
-        _alerts.ShowAlert(ent.Owner, ent.Comp.BatteryAlert, chargePercent);
-    }
-
-    // TODO: Component states and update this on the client
-    public void UpdateUI(EntityUid uid, BorgChassisComponent? component = null)
-    {
-        if (!Resolve(uid, ref component))
-            return;
-
-        var chargePercent = 0f;
-        var hasBattery = false;
-        if (_powerCell.TryGetBatteryFromSlot(uid, out var battery))
-        {
-            hasBattery = true;
-            chargePercent = _battery.GetCharge(battery.Value.AsNullable()) / battery.Value.Comp.MaxCharge;
-        }
-
-        var state = new BorgBuiState(chargePercent, hasBattery);
-        _ui.SetUiState(uid, BorgUiKey.Key, state);
-    }
-
-    // periodically update the charge indicator
-    // TODO: Move this to the client.
-    public void UpdateBattery(float frameTime)
-    {
-        var curTime = _timing.CurTime;
-        var query = EntityQueryEnumerator<BorgChassisComponent>();
-        while (query.MoveNext(out var uid, out var borgChassis))
-        {
-            if (curTime < borgChassis.NextBatteryUpdate)
-                continue;
-
-            UpdateBattery((uid, borgChassis));
-            borgChassis.NextBatteryUpdate = curTime + TimeSpan.FromSeconds(1);
-        }
-    }
-}
index 24fde24f33e357b9ba69f8757d941c7ca142b024..6f37d55013b84ccbdd56fef1b899f315555ffb93 100644 (file)
@@ -1,39 +1,17 @@
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using Content.Server.Actions;
-using Content.Server.Administration.Logs;
 using Content.Server.Administration.Managers;
 using Content.Server.DeviceNetwork.Systems;
-using Content.Server.Hands.Systems;
-using Content.Shared.Alert;
-using Content.Shared.Body.Events;
-using Content.Shared.Database;
-using Content.Shared.IdentityManagement;
-using Content.Shared.Interaction;
-using Content.Shared.Item.ItemToggle.Components;
-using Content.Shared.Mind;
-using Content.Shared.Mind.Components;
-using Content.Shared.Mobs;
+using Content.Shared.Containers.ItemSlots;
+using Content.Shared.Emag.Systems;
 using Content.Shared.Mobs.Systems;
-using Content.Shared.Movement.Systems;
-using Content.Shared.Pointing;
-using Content.Shared.Power;
+using Content.Shared.Popups;
 using Content.Shared.Power.EntitySystems;
 using Content.Shared.PowerCell;
-using Content.Shared.PowerCell.Components;
 using Content.Shared.Roles;
 using Content.Shared.Silicons.Borgs;
-using Content.Shared.Silicons.Borgs.Components;
-using Content.Shared.Throwing;
 using Content.Shared.Trigger.Systems;
-using Content.Shared.Whitelist;
-using Content.Shared.Wires;
-using Robust.Server.GameObjects;
-using Robust.Shared.Configuration;
 using Robust.Shared.Containers;
 using Robust.Shared.Player;
 using Robust.Shared.Prototypes;
-using Robust.Shared.Random;
 using Robust.Shared.Timing;
 
 namespace Content.Server.Silicons.Borgs;
@@ -41,28 +19,18 @@ namespace Content.Server.Silicons.Borgs;
 /// <inheritdoc/>
 public sealed partial class BorgSystem : SharedBorgSystem
 {
-    [Dependency] private readonly IAdminLogManager _adminLog = default!;
     [Dependency] private readonly IBanManager _banManager = default!;
-    [Dependency] private readonly IConfigurationManager _cfgManager = default!;
     [Dependency] private readonly IGameTiming _timing = default!;
-    [Dependency] private readonly IRobustRandom _random = default!;
-    [Dependency] private readonly ActionsSystem _actions = default!;
-    [Dependency] private readonly AlertsSystem _alerts = default!;
     [Dependency] private readonly DeviceNetworkSystem _deviceNetwork = default!;
-    [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
     [Dependency] private readonly TriggerSystem _trigger = default!;
-    [Dependency] private readonly HandsSystem _hands = default!;
-    [Dependency] private readonly MetaDataSystem _metaData = default!;
-    [Dependency] private readonly SharedMindSystem _mind = default!;
     [Dependency] private readonly MobStateSystem _mobState = default!;
-    [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
-    [Dependency] private readonly PowerCellSystem _powerCell = default!;
-    [Dependency] private readonly ThrowingSystem _throwing = default!;
-    [Dependency] private readonly UserInterfaceSystem _ui = default!;
     [Dependency] private readonly SharedContainerSystem _container = default!;
-    [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
-    [Dependency] private readonly ISharedPlayerManager _player = default!;
     [Dependency] private readonly PredictedBatterySystem _battery = default!;
+    [Dependency] private readonly EmagSystem _emag = default!;
+    [Dependency] private readonly MobThresholdSystem _mobThresholdSystem = default!;
+    [Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!;
+    [Dependency] private readonly SharedPopupSystem _popup = default!;
+    [Dependency] private readonly PowerCellSystem _powerCell = default!;
 
     public static readonly ProtoId<JobPrototype> BorgJobId = "Borg";
 
@@ -71,264 +39,10 @@ public sealed partial class BorgSystem : SharedBorgSystem
     {
         base.Initialize();
 
-        SubscribeLocalEvent<BorgChassisComponent, MapInitEvent>(OnMapInit);
-        SubscribeLocalEvent<BorgChassisComponent, AfterInteractUsingEvent>(OnChassisInteractUsing);
-        SubscribeLocalEvent<BorgChassisComponent, MindAddedMessage>(OnMindAdded);
-        SubscribeLocalEvent<BorgChassisComponent, MindRemovedMessage>(OnMindRemoved);
-        SubscribeLocalEvent<BorgChassisComponent, MobStateChangedEvent>(OnMobStateChanged);
-        SubscribeLocalEvent<BorgChassisComponent, BeingGibbedEvent>(OnBeingGibbed);
-        SubscribeLocalEvent<BorgChassisComponent, PowerCellChangedEvent>(OnPowerCellChanged);
-        SubscribeLocalEvent<BorgChassisComponent, PredictedBatteryChargeChangedEvent>(OnBatteryChargeChanged);
-        SubscribeLocalEvent<BorgChassisComponent, PowerCellSlotEmptyEvent>(OnPowerCellSlotEmpty);
-        SubscribeLocalEvent<BorgChassisComponent, GetCharactedDeadIcEvent>(OnGetDeadIC);
-        SubscribeLocalEvent<BorgChassisComponent, GetCharacterUnrevivableIcEvent>(OnGetUnrevivableIC);
-        SubscribeLocalEvent<BorgChassisComponent, ItemToggledEvent>(OnToggled);
-
-        SubscribeLocalEvent<BorgBrainComponent, MindAddedMessage>(OnBrainMindAdded);
-        SubscribeLocalEvent<BorgBrainComponent, PointAttemptEvent>(OnBrainPointAttempt);
-
-        InitializeModules();
-        InitializeMMI();
-        InitializeUI();
         InitializeTransponder();
     }
 
-    private void OnMapInit(EntityUid uid, BorgChassisComponent component, MapInitEvent args)
-    {
-        UpdateBatteryAlert((uid, component));
-        _movementSpeedModifier.RefreshMovementSpeedModifiers(uid);
-    }
-
-    private void OnChassisInteractUsing(EntityUid uid, BorgChassisComponent component, AfterInteractUsingEvent args)
-    {
-        if (!args.CanReach || args.Handled || uid == args.User)
-            return;
-
-        var used = args.Used;
-        TryComp<BorgBrainComponent>(used, out var brain);
-        TryComp<BorgModuleComponent>(used, out var module);
-
-        if (TryComp<WiresPanelComponent>(uid, out var panel) && !panel.Open)
-        {
-            if (brain != null || module != null)
-            {
-                Popup.PopupEntity(Loc.GetString("borg-panel-not-open"), uid, args.User);
-            }
-            return;
-        }
-
-        if (component.BrainEntity == null && brain != null &&
-            _whitelistSystem.IsWhitelistPassOrNull(component.BrainWhitelist, used))
-        {
-            if (_mind.TryGetMind(used, out _, out var mind) &&
-                _player.TryGetSessionById(mind.UserId, out var session))
-            {
-                if (!CanPlayerBeBorged(session))
-                {
-                    Popup.PopupEntity(Loc.GetString("borg-player-not-allowed"), used, args.User);
-                    return;
-                }
-            }
-
-            _container.Insert(used, component.BrainContainer);
-            _adminLog.Add(LogType.Action, LogImpact.Medium,
-                $"{ToPrettyString(args.User):player} installed brain {ToPrettyString(used)} into borg {ToPrettyString(uid)}");
-            args.Handled = true;
-            UpdateUI(uid, component);
-        }
-
-        if (module != null && CanInsertModule(uid, used, component, module, args.User))
-        {
-            InsertModule((uid, component), used);
-            _adminLog.Add(LogType.Action, LogImpact.Low,
-                $"{ToPrettyString(args.User):player} installed module {ToPrettyString(used)} into borg {ToPrettyString(uid)}");
-            args.Handled = true;
-            UpdateUI(uid, component);
-        }
-    }
-
-    /// <summary>
-    /// Inserts a new module into a borg, the same as if a player inserted it manually.
-    /// </summary>
-    /// <para>
-    /// This does not run checks to see if the borg is actually allowed to be inserted, such as whitelists.
-    /// </para>
-    /// <param name="ent">The borg to insert into.</param>
-    /// <param name="module">The module to insert.</param>
-    public void InsertModule(Entity<BorgChassisComponent> ent, EntityUid module)
-    {
-        _container.Insert(module, ent.Comp.ModuleContainer);
-    }
-
-    // todo: consider transferring over the ghost role? managing that might suck.
-    protected override void OnInserted(EntityUid uid, BorgChassisComponent component, EntInsertedIntoContainerMessage args)
-    {
-        base.OnInserted(uid, component, args);
-
-        if (HasComp<BorgBrainComponent>(args.Entity) && _mind.TryGetMind(args.Entity, out var mindId, out var mind) && args.Container == component.BrainContainer)
-        {
-            _mind.TransferTo(mindId, uid, mind: mind);
-        }
-    }
-
-    protected override void OnRemoved(EntityUid uid, BorgChassisComponent component, EntRemovedFromContainerMessage args)
-    {
-        base.OnRemoved(uid, component, args);
-
-        if (HasComp<BorgBrainComponent>(args.Entity) && _mind.TryGetMind(uid, out var mindId, out var mind) && args.Container == component.BrainContainer)
-        {
-            _mind.TransferTo(mindId, args.Entity, mind: mind);
-        }
-    }
-
-    private void OnMindAdded(EntityUid uid, BorgChassisComponent component, MindAddedMessage args)
-    {
-        BorgActivate(uid, component);
-    }
-
-    private void OnMindRemoved(EntityUid uid, BorgChassisComponent component, MindRemovedMessage args)
-    {
-        BorgDeactivate(uid, component);
-    }
-
-    private void OnMobStateChanged(EntityUid uid, BorgChassisComponent component, MobStateChangedEvent args)
-    {
-        if (args.NewMobState == MobState.Alive)
-        {
-            if (_mind.TryGetMind(uid, out _, out _))
-                _powerCell.SetDrawEnabled(uid, true);
-        }
-        else
-        {
-            _powerCell.SetDrawEnabled(uid, false);
-        }
-    }
-
-    private void OnBeingGibbed(EntityUid uid, BorgChassisComponent component, ref BeingGibbedEvent args)
-    {
-        TryEjectPowerCell(uid, component, out var _);
-
-        _container.EmptyContainer(component.BrainContainer);
-        _container.EmptyContainer(component.ModuleContainer);
-    }
-
-    private void OnPowerCellChanged(Entity<BorgChassisComponent> ent, ref PowerCellChangedEvent args)
-    {
-        UpdateBattery(ent);
-    }
-
-    private void OnBatteryChargeChanged(Entity<BorgChassisComponent> ent, ref PredictedBatteryChargeChangedEvent args)
-    {
-        UpdateBattery(ent);
-    }
-
-    private void OnPowerCellSlotEmpty(EntityUid uid, BorgChassisComponent component, ref PowerCellSlotEmptyEvent args)
-    {
-        Toggle.TryDeactivate(uid);
-        UpdateUI(uid, component);
-    }
-
-    private void OnGetDeadIC(EntityUid uid, BorgChassisComponent component, ref GetCharactedDeadIcEvent args)
-    {
-        args.Dead = true;
-    }
-
-    private void OnGetUnrevivableIC(EntityUid uid, BorgChassisComponent component, ref GetCharacterUnrevivableIcEvent args)
-    {
-        args.Unrevivable = true;
-    }
-
-    private void OnToggled(Entity<BorgChassisComponent> ent, ref ItemToggledEvent args)
-    {
-        var (uid, comp) = ent;
-        if (args.Activated)
-            InstallAllModules(uid, comp);
-        else
-            DisableAllModules(uid, comp);
-
-        // only enable the powerdraw if there is a player in the chassis
-        var drawing = _mind.TryGetMind(uid, out _, out _) && _mobState.IsAlive(ent);
-        _powerCell.SetDrawEnabled(uid, drawing);
-
-        UpdateUI(uid, comp);
-
-        _movementSpeedModifier.RefreshMovementSpeedModifiers(uid);
-    }
-
-    private void OnBrainMindAdded(EntityUid uid, BorgBrainComponent component, MindAddedMessage args)
-    {
-        if (!Container.TryGetOuterContainer(uid, Transform(uid), out var container))
-            return;
-
-        var containerEnt = container.Owner;
-
-        if (!TryComp<BorgChassisComponent>(containerEnt, out var chassisComponent) ||
-            container.ID != chassisComponent.BrainContainerId)
-            return;
-
-        if (!_mind.TryGetMind(uid, out var mindId, out var mind) ||
-            !_player.TryGetSessionById(mind.UserId, out var session))
-            return;
-
-        if (!CanPlayerBeBorged(session))
-        {
-            Popup.PopupEntity(Loc.GetString("borg-player-not-allowed-eject"), uid);
-            Container.RemoveEntity(containerEnt, uid);
-            _throwing.TryThrow(uid, _random.NextVector2() * 5, 5f);
-            return;
-        }
-
-        _mind.TransferTo(mindId, containerEnt, mind: mind);
-    }
-
-    private void OnBrainPointAttempt(EntityUid uid, BorgBrainComponent component, PointAttemptEvent args)
-    {
-        args.Cancel();
-    }
-
-    public bool TryEjectPowerCell(EntityUid uid, BorgChassisComponent component, [NotNullWhen(true)] out List<EntityUid>? ents)
-    {
-        ents = null;
-
-        if (!TryComp<PowerCellSlotComponent>(uid, out var slotComp) ||
-            !Container.TryGetContainer(uid, slotComp.CellSlotId, out var container) ||
-            !container.ContainedEntities.Any())
-            return false;
-
-        ents = Container.EmptyContainer(container);
-
-        return true;
-    }
-
-    /// <summary>
-    /// Activates a borg when a player occupies it
-    /// </summary>
-    public void BorgActivate(EntityUid uid, BorgChassisComponent component)
-    {
-        Popup.PopupEntity(Loc.GetString("borg-mind-added", ("name", Identity.Name(uid, EntityManager))), uid);
-        if (_powerCell.HasDrawCharge(uid))
-        {
-            Toggle.TryActivate(uid);
-            _powerCell.SetDrawEnabled(uid, _mobState.IsAlive(uid));
-        }
-        _appearance.SetData(uid, BorgVisuals.HasPlayer, true);
-    }
-
-    /// <summary>
-    /// Deactivates a borg when a player leaves it.
-    /// </summary>
-    public void BorgDeactivate(EntityUid uid, BorgChassisComponent component)
-    {
-        Popup.PopupEntity(Loc.GetString("borg-mind-removed", ("name", Identity.Name(uid, EntityManager))), uid);
-        Toggle.TryDeactivate(uid);
-        _powerCell.SetDrawEnabled(uid, false);
-        _appearance.SetData(uid, BorgVisuals.HasPlayer, false);
-    }
-
-    /// <summary>
-    /// Checks that a player has fulfilled the requirements for the borg job.
-    /// </summary>
-    public bool CanPlayerBeBorged(ICommonSession session)
+    public override bool CanPlayerBeBorged(ICommonSession session)
     {
         if (_banManager.GetJobBans(session.UserId)?.Contains(BorgJobId) == true)
             return false;
@@ -341,6 +55,5 @@ public sealed partial class BorgSystem : SharedBorgSystem
         base.Update(frameTime);
 
         UpdateTransponder(frameTime);
-        UpdateBattery(frameTime);
     }
 }
index b4ee94c3aabd5b611ef6c2ab3f0881a40292025e..d34e86ad5cc29b4de4376b3f8faa00c6fbd4f4aa 100644 (file)
@@ -233,6 +233,9 @@ namespace Content.Shared.Interaction
         /// </summary>
         private void OnUnequip(EntityUid uid, UnremoveableComponent item, GotUnequippedEvent args)
         {
+            if (_gameTiming.ApplyingState)
+                return; // The changes are already networked with the same gamestate as the container event.
+
             if (!item.DeleteOnDrop)
                 RemCompDeferred<UnremoveableComponent>(uid);
             else
@@ -241,6 +244,9 @@ namespace Content.Shared.Interaction
 
         private void OnUnequipHand(EntityUid uid, UnremoveableComponent item, GotUnequippedHandEvent args)
         {
+            if (_gameTiming.ApplyingState)
+                return; // The changes are already networked with the same gamestate as the container event.
+
             if (!item.DeleteOnDrop)
                 RemCompDeferred<UnremoveableComponent>(uid);
             else
@@ -249,6 +255,11 @@ namespace Content.Shared.Interaction
 
         private void OnDropped(EntityUid uid, UnremoveableComponent item, DroppedEvent args)
         {
+            if (_gameTiming.ApplyingState)
+                return; // The changes are already networked with the same gamestate as the container event.
+            // Other than the two cases above this is not a container event, but adding and removing hands is networked similarly
+            // and removing hands causes items to be dropped.
+
             if (!item.DeleteOnDrop)
                 RemCompDeferred<UnremoveableComponent>(uid);
             else
index 6c70a82f93a039eee8ee531d5fc5c148bf7e911b..9f6dddb2301060530ecc7ee14683d7635368e377 100644 (file)
@@ -10,6 +10,8 @@ namespace Content.Shared.Light.Components;
 public sealed partial class HandheldLightComponent : Component
 {
     public byte? Level;
+
+    [DataField]
     public bool Activated;
 
     [ViewVariables(VVAccess.ReadWrite)]
index 0f507e1365f254a0251dc924df8df015741d26ce..cbff7f95db1341c41c863107fbf5cec2ad50bf6b 100644 (file)
@@ -1,7 +1,7 @@
 using Content.Shared.Actions;
 using Content.Shared.Clothing.EntitySystems;
+using Content.Shared.Examine;
 using Content.Shared.Item;
-using Content.Shared.Light;
 using Content.Shared.Light.Components;
 using Content.Shared.Toggleable;
 using Content.Shared.Verbs;
@@ -24,7 +24,7 @@ public abstract class SharedHandheldLightSystem : EntitySystem
         base.Initialize();
         SubscribeLocalEvent<HandheldLightComponent, ComponentInit>(OnInit);
         SubscribeLocalEvent<HandheldLightComponent, ComponentHandleState>(OnHandleState);
-
+        SubscribeLocalEvent<HandheldLightComponent, ExaminedEvent>(OnExamine);
         SubscribeLocalEvent<HandheldLightComponent, GetVerbsEvent<ActivationVerb>>(AddToggleLightVerb);
     }
 
@@ -45,6 +45,13 @@ public abstract class SharedHandheldLightSystem : EntitySystem
         SetActivated(uid, state.Activated, component, false);
     }
 
+    private void OnExamine(EntityUid uid, HandheldLightComponent component, ExaminedEvent args)
+    {
+        args.PushMarkup(component.Activated
+            ? Loc.GetString("handheld-light-component-on-examine-is-on-message")
+            : Loc.GetString("handheld-light-component-on-examine-is-off-message"));
+    }
+
     public void SetActivated(EntityUid uid, bool activated, HandheldLightComponent? component = null, bool makeNoise = true)
     {
         if (!Resolve(uid, ref component))
index 30c5228236cf22953e53f799a45ca7750bf045c6..15376bda8951880f60bcc5b3cfb0f7d669cdf363 100644 (file)
@@ -8,6 +8,23 @@ namespace Content.Shared.PowerCell;
 
 public sealed partial class PowerCellSystem
 {
+    /// <summary>
+    /// Checks if a power cell slot has a battery inside.
+    /// </summary>
+    [PublicAPI]
+    public bool HasBattery(Entity<PowerCellSlotComponent?> ent)
+    {
+        if (!Resolve(ent, ref ent.Comp, false))
+            return false;
+
+        if (!_itemSlots.TryGetSlot(ent.Owner, ent.Comp.CellSlotId, out var slot))
+        {
+            return false;
+        }
+
+        return slot.Item != null;
+    }
+
     /// <summary>
     /// Gets the power cell battery inside a power cell slot.
     /// </summary>
@@ -22,7 +39,7 @@ public sealed partial class PowerCellSystem
             return false;
         }
 
-        if (!_itemSlots.TryGetSlot(ent.Owner, ent.Comp.CellSlotId, out ItemSlot? slot))
+        if (!_itemSlots.TryGetSlot(ent.Owner, ent.Comp.CellSlotId, out var slot))
         {
             battery = null;
             return false;
@@ -77,10 +94,39 @@ public sealed partial class PowerCellSystem
         return false;
     }
 
+    /// <summary>
+    /// Tries to eject the power cell battery inside a power cell slot.
+    /// This checks if the user has a free hand to do the ejection and if the slot is locked.
+    /// </summary>
+    /// <param name="ent">The entity with the power cell slot.</param>
+    /// <param name="battery">The power cell that was ejected.</param>
+    /// <param name="user">The player trying to eject the power cell from the slot.</param>
+    /// <returns>If a power cell was sucessfully ejected.</returns>
+    [PublicAPI]
+    public bool TryEjectBatteryFromSlot(
+        Entity<PowerCellSlotComponent?> ent,
+        [NotNullWhen(true)] out EntityUid? battery,
+        EntityUid? user = null)
+    {
+        if (!Resolve(ent, ref ent.Comp, false))
+        {
+            battery = null;
+            return false;
+        }
+
+        if (!_itemSlots.TryEject(ent.Owner, ent.Comp.CellSlotId, user, out battery, excludeUserAudio: true))
+        {
+            battery = null;
+            return false;
+        }
+
+        return true;
+    }
+
     /// <summary>
     /// Returns whether the entity has a slotted battery and charge for the requested action.
     /// </summary>
-    /// <param name="ent">The power cell.</param>
+    /// <param name="ent">The entity with the power cell slot.</param>
     /// <param name="charge">The charge that is needed.</param>
     /// <param name="user">Show a popup to this user with the relevant details if specified.</param>
     /// <param name="predicted">Whether to predict the popup or not.</param>
@@ -119,7 +165,7 @@ public sealed partial class PowerCellSystem
     /// <summary>
     /// Tries to use charge from a slotted battery.
     /// </summary>
-    /// <param name="ent">The power cell.</param>
+    /// <param name="ent">The entity with the power cell slot.</param>
     /// <param name="charge">The charge that is needed.</param>
     /// <param name="user">Show a popup to this user with the relevant details if specified.</param>
     /// <param name="predicted">Whether to predict the popup or not.</param>
@@ -157,7 +203,7 @@ public sealed partial class PowerCellSystem
     /// <summary>
     /// Gets number of remaining uses for the given charge cost.
     /// </summary>
-    /// <param name="ent">The power cell.</param>
+    /// <param name="ent">The entity with the power cell slot.</param>
     /// <param name="cost">The cost per use.</param>
     [PublicAPI]
     public int GetRemainingUses(Entity<PowerCellSlotComponent?> ent, float cost)
@@ -171,7 +217,7 @@ public sealed partial class PowerCellSystem
     /// <summary>
     /// Gets number of maximum uses at full charge for the given charge cost.
     /// </summary>
-    /// <param name="ent">The power cell.</param>
+    /// <param name="ent">The entity with the power cell slot.</param>
     /// <param name="cost">The cost per use.</param>
     [PublicAPI]
     public int GetMaxUses(Entity<PowerCellSlotComponent?> ent, float cost)
index fd6abc89926e1ff9d58b0f417f4fbfa9ebe0c6f1..9d843d4046bb0c33694d8288471581ae5ab1aff7 100644 (file)
@@ -8,50 +8,38 @@ public enum BorgUiKey : byte
     Key
 }
 
+/// <summary>
+/// Send when a player uses the borg BUI to eject a brain.
+/// </summary>
 [Serializable, NetSerializable]
-public sealed class BorgBuiState : BoundUserInterfaceState
-{
-    public float ChargePercent;
-
-    public bool HasBattery;
-
-    public BorgBuiState(float chargePercent, bool hasBattery)
-    {
-        ChargePercent = chargePercent;
-        HasBattery = hasBattery;
-    }
-}
-
-[Serializable, NetSerializable]
-public sealed class BorgEjectBrainBuiMessage : BoundUserInterfaceMessage
-{
-
-}
+public sealed class BorgEjectBrainBuiMessage : BoundUserInterfaceMessage;
 
+/// <summary>
+/// Send when a player uses the borg BUI to eject a power cell.
+/// </summary>
 [Serializable, NetSerializable]
-public sealed class BorgEjectBatteryBuiMessage : BoundUserInterfaceMessage
-{
-
-}
+public sealed class BorgEjectBatteryBuiMessage : BoundUserInterfaceMessage;
 
+/// <summary>
+/// Send when a player uses the borg BUI to change a borg's name.
+/// </summary>
 [Serializable, NetSerializable]
-public sealed class BorgSetNameBuiMessage : BoundUserInterfaceMessage
+public sealed class BorgSetNameBuiMessage(string name) : BoundUserInterfaceMessage
 {
-    public string Name;
-
-    public BorgSetNameBuiMessage(string name)
-    {
-        Name = name;
-    }
+    /// <summary>
+    /// The new name.
+    /// </summary>
+    public string Name = name;
 }
 
+/// <summary>
+/// Send when a player uses the borg BUI to remove a borg module.
+/// </summary>
 [Serializable, NetSerializable]
-public sealed class BorgRemoveModuleBuiMessage : BoundUserInterfaceMessage
+public sealed class BorgRemoveModuleBuiMessage(NetEntity module) : BoundUserInterfaceMessage
 {
-    public NetEntity Module;
-
-    public BorgRemoveModuleBuiMessage(NetEntity module)
-    {
-        Module = module;
-    }
+    /// <summary>
+    /// The module to eject.
+    /// </summary>
+    public NetEntity Module = module;
 }
index 98a5b205e9252a415ee4ef41801576984c1c8494..a6c63bc602827b916fbe85253810d0e514dd63ed 100644 (file)
@@ -1,5 +1,6 @@
 using Content.Shared.Alert;
 using Content.Shared.Whitelist;
+using Robust.Shared.Audio;
 using Robust.Shared.Containers;
 using Robust.Shared.GameStates;
 using Robust.Shared.Prototypes;
@@ -13,80 +14,132 @@ namespace Content.Shared.Silicons.Borgs.Components;
 /// "brain", legs, modules, and battery. Essentially the master component
 /// for borg logic.
 /// </summary>
-[RegisterComponent, NetworkedComponent, Access(typeof(SharedBorgSystem))]
-[AutoGenerateComponentState, AutoGenerateComponentPause]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
+[Access(typeof(SharedBorgSystem))]
 public sealed partial class BorgChassisComponent : Component
 {
+    /// <summary>
+    /// Is this borg currently activated?
+    /// If activated the borg
+    /// - has door access,
+    /// - can use modules and
+    /// - has full movement speed.
+    /// To be activated the borg
+    /// - needs to have a player controlling it (a mind),
+    /// - needs enough charge in its power cell and
+    /// - needs to be alive (not crit or dead).
+    /// </summary>
+    /// <remarks>
+    /// Don't try to use ItemToggle for this.
+    /// It used that before and it had a ton of conflicts with other item toggling behavior from the billion components that use it somehow.
+    /// </remarks>
+    [DataField, AutoNetworkedField]
+    public bool Active;
+
+    /// <summary>
+    /// The sound to play when the borg activates.
+    /// </summary>
+    /// <remarks>
+    /// The same as the flashlight. This playing used to be a bug, but the sound is nostalgic at this point, so I'm keeping it.
+    /// </remarks>
+    [DataField]
+    public SoundSpecifier ActivateSound = new SoundPathSpecifier("/Audio/Items/flashlight_on.ogg");
+
+    /// <summary>
+    /// The sound to play when the borg deactivates.
+    /// </summary>
+    [DataField]
+    public SoundSpecifier DeactivateSound = new SoundPathSpecifier("/Audio/Items/flashlight_off.ogg");
+
     #region Brain
     /// <summary>
-    /// A whitelist for which entities count as valid brains
+    /// A whitelist for which entities count as valid brains.
     /// </summary>
-    [DataField("brainWhitelist")]
+    [DataField]
     public EntityWhitelist? BrainWhitelist;
 
     /// <summary>
-    /// The container ID for the brain
+    /// The container ID for the posibrain or MMI.
     /// </summary>
-    [DataField("brainContainerId")]
+    [DataField]
     public string BrainContainerId = "borg_brain";
 
-    [ViewVariables(VVAccess.ReadWrite)]
+    /// <summary>
+    /// The container for the posibrain or MMI.
+    /// </summary>
+    [ViewVariables]
     public ContainerSlot BrainContainer = default!;
 
-    public EntityUid? BrainEntity => BrainContainer.ContainedEntity;
+    /// <summary>
+    /// The posibrain or MMI inserted into this borg, if any.
+    /// </summary>
+    [ViewVariables]
+    public EntityUid? BrainEntity => BrainContainer?.ContainedEntity;
     #endregion
 
     #region Modules
     /// <summary>
-    /// A whitelist for what types of modules can be installed into this borg
+    /// A whitelist for what types of modules can be installed into this borg.
     /// </summary>
-    [DataField("moduleWhitelist")]
+    [DataField, AutoNetworkedField]
     public EntityWhitelist? ModuleWhitelist;
 
     /// <summary>
-    /// How many modules can be installed in this borg
+    /// How many modules can be installed in this borg?
     /// </summary>
-    [DataField("maxModules"), ViewVariables(VVAccess.ReadWrite)]
+    [DataField, AutoNetworkedField]
     public int MaxModules = 3;
 
     /// <summary>
-    /// The ID for the module container
+    /// The ID for the module container.
     /// </summary>
-    [DataField("moduleContainerId")]
+    [DataField]
     public string ModuleContainerId = "borg_module";
 
-    [ViewVariables(VVAccess.ReadWrite)]
+    /// <summary>
+    /// The module container.
+    /// </summary>
+    [ViewVariables]
     public Container ModuleContainer = default!;
 
+    /// <summary>
+    /// How many modules are currently installed?
+    /// </summary>
+    [ViewVariables]
     public int ModuleCount => ModuleContainer.ContainedEntities.Count;
     #endregion
 
     /// <summary>
-    /// The currently selected module
+    /// The currently selected module.
     /// </summary>
-    [DataField("selectedModule"), AutoNetworkedField]
+    [DataField, AutoNetworkedField]
     public EntityUid? SelectedModule;
 
     #region Visuals
-    [DataField("hasMindState")]
+    [DataField]
     public string HasMindState = string.Empty;
 
-    [DataField("noMindState")]
+    [DataField]
     public string NoMindState = string.Empty;
     #endregion
 
+    /// <summary>
+    /// The battery charge alert.
+    /// </summary>
     [DataField]
     public ProtoId<AlertPrototype> BatteryAlert = "BorgBattery";
 
+    /// <summary>
+    /// The alert for a missing battery.
+    /// </summary>
     [DataField]
     public ProtoId<AlertPrototype> NoBatteryAlert = "BorgBatteryNone";
 
     /// <summary>
-    /// The next update time for the battery charge level.
-    /// Used for the alert and borg UI.
+    /// The next update time the battery is checked for automatic reactivation.
     /// </summary>
     [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
-    [AutoPausedField]
+    [AutoNetworkedField, AutoPausedField]
     public TimeSpan NextBatteryUpdate = TimeSpan.Zero;
 
     /// <summary>
@@ -99,7 +152,8 @@ public sealed partial class BorgChassisComponent : Component
 [Serializable, NetSerializable]
 public enum BorgVisuals : byte
 {
-    HasPlayer
+    HasPlayer,
+    Powered,
 }
 
 [Serializable, NetSerializable]
index f50b08893266d677bb225c01efc08d4b3569244d..5c7f6cfddbac096ab2ac3dbe32fbc0aec181422a 100644 (file)
@@ -1,5 +1,4 @@
 using Robust.Shared.GameStates;
-using Robust.Shared.Prototypes;
 
 namespace Content.Shared.Silicons.Borgs.Components;
 
@@ -7,23 +6,26 @@ namespace Content.Shared.Silicons.Borgs.Components;
 /// This is used for modules that can be inserted into borgs
 /// to give them unique abilities and attributes.
 /// </summary>
-[RegisterComponent, NetworkedComponent, Access(typeof(SharedBorgSystem))]
-[AutoGenerateComponentState]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(SharedBorgSystem))]
 public sealed partial class BorgModuleComponent : Component
 {
     /// <summary>
-    /// The entity this module is installed into
+    /// The entity this module is installed into.
     /// </summary>
-    [DataField("installedEntity")]
+    [DataField, AutoNetworkedField]
     public EntityUid? InstalledEntity;
 
+    /// <summary>
+    /// Is this module currently installed?
+    /// </summary>
+    [ViewVariables]
     public bool Installed => InstalledEntity != null;
 
     /// <summary>
     /// If true, this is a "default" module that cannot be removed from a borg.
     /// </summary>
-    [DataField]
-    [AutoNetworkedField]
+    [DataField, AutoNetworkedField]
     public bool DefaultModule;
 
     /// <summary>
@@ -37,13 +39,13 @@ public sealed partial class BorgModuleComponent : Component
 /// <summary>
 /// Raised on a module when it is installed in order to add specific behavior to an entity.
 /// </summary>
-/// <param name="ChassisEnt"></param>
+/// <param name="ChassisEnt">The borg the module is being installed in.</param>
 [ByRefEvent]
 public readonly record struct BorgModuleInstalledEvent(EntityUid ChassisEnt);
 
 /// <summary>
 /// Raised on a module when it's uninstalled in order to
 /// </summary>
-/// <param name="ChassisEnt"></param>
+/// <param name="ChassisEnt">The borg the module is being uninstalled from.</param>
 [ByRefEvent]
 public readonly record struct BorgModuleUninstalledEvent(EntityUid ChassisEnt);
index 3680a4cbde83c27c44f124b6f96e34cd882b7383..db91e9d8b5666ea88194d9044561839c0b918651 100644 (file)
@@ -1,5 +1,4 @@
 using Content.Shared.Hands.Components;
-using Robust.Shared.Containers;
 using Robust.Shared.GameStates;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization;
@@ -9,7 +8,8 @@ namespace Content.Shared.Silicons.Borgs.Components;
 /// <summary>
 /// This is used for a <see cref="BorgModuleComponent"/> that provides items to the entity it's installed into.
 /// </summary>
-[RegisterComponent, NetworkedComponent, Access(typeof(SharedBorgSystem))]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(SharedBorgSystem))]
 public sealed partial class ItemBorgModuleComponent : Component
 {
     /// <summary>
@@ -19,10 +19,17 @@ public sealed partial class ItemBorgModuleComponent : Component
     public List<BorgHand> Hands = new();
 
     /// <summary>
-    /// The items stored within the hands. Null until the first time items are stored.
+    /// The items stored within the hands.
     /// </summary>
-    [DataField]
-    public Dictionary<string, EntityUid>? StoredItems;
+    [DataField, AutoNetworkedField]
+    public Dictionary<string, EntityUid> StoredItems = new();
+
+    /// <summary>
+    /// Whether the provided items have been spawned.
+    /// This happens the first time the module is used.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool Spawned;
 
     /// <summary>
     /// An ID for the container where items are stored when not in use.
@@ -31,12 +38,21 @@ public sealed partial class ItemBorgModuleComponent : Component
     public string HoldingContainer = "holding_container";
 }
 
+/// <summary>
+/// A single hand provided by the module.
+/// </summary>
 [DataDefinition, Serializable, NetSerializable]
 public partial record struct BorgHand
 {
+    /// <summary>
+    /// The item to spawn in the hand, if any.
+    /// </summary>
     [DataField]
     public EntProtoId? Item;
 
+    /// <summary>
+    /// The settings for the hand, including a whitelist.
+    /// </summary>
     [DataField]
     public Hand Hand = new();
 
index 3ded725efc72bddd1a7c0bcecd789fa822f3f77b..51031c930e333dc458679a63be16443bba9c9914 100644 (file)
@@ -9,37 +9,38 @@ namespace Content.Shared.Silicons.Borgs.Components;
 /// in an item slot before transferring consciousness.
 /// Used for borg stuff.
 /// </summary>
-[RegisterComponent, NetworkedComponent, Access(typeof(SharedBorgSystem))]
+[RegisterComponent, NetworkedComponent]
+[Access(typeof(SharedBorgSystem))]
 public sealed partial class MMIComponent : Component
 {
     /// <summary>
     /// The ID of the itemslot that holds the brain.
     /// </summary>
-    [DataField("brainSlotId")]
+    [DataField]
     public string BrainSlotId = "brain_slot";
 
     /// <summary>
-    /// The <see cref="ItemSlot"/> for this implanter
+    /// The <see cref="ItemSlot"/> for this MMI. Holds the brain.
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
-    public ItemSlot BrainSlot = default!;
+    [DataField(required: true)]
+    public ItemSlot BrainSlot = new();
 
     /// <summary>
     /// The sprite state when the brain inserted has a mind.
     /// </summary>
-    [DataField("hasMindState")]
+    [DataField]
     public string HasMindState = "mmi_alive";
 
     /// <summary>
     /// The sprite state when the brain inserted doesn't have a mind.
     /// </summary>
-    [DataField("noMindState")]
+    [DataField]
     public string NoMindState = "mmi_dead";
 
     /// <summary>
     /// The sprite state when there is no brain inserted.
     /// </summary>
-    [DataField("noBrainState")]
+    [DataField]
     public string NoBrainState = "mmi_off";
 }
 
index 639c6a42692814e00a78434ee91f2076924a92b8..cebbc9b746fa61bfc0dd8792a49c591a27497cf6 100644 (file)
@@ -3,7 +3,7 @@
 namespace Content.Shared.Silicons.Borgs.Components;
 
 /// <summary>
-/// This is used for an entity that is linked to an MMI.
+/// This is used for an entity that is linked to an MMI, usually a brain.
 /// Mostly for receiving events.
 /// </summary>
 [RegisterComponent, NetworkedComponent, Access(typeof(SharedBorgSystem))]
@@ -13,6 +13,6 @@ public sealed partial class MMILinkedComponent : Component
     /// <summary>
     /// The MMI this entity is linked to.
     /// </summary>
-    [DataField("linkedMMI"), AutoNetworkedField]
+    [DataField, AutoNetworkedField]
     public EntityUid? LinkedMMI;
 }
index ee6dee3689f9b81c8cf826c8780728827812a099..69e58caa28494897e3eb7fb9d1df151aa7cf2344 100644 (file)
@@ -1,28 +1,33 @@
 using Content.Shared.Actions;
 using Robust.Shared.GameStates;
 using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
 
 namespace Content.Shared.Silicons.Borgs.Components;
 
 /// <summary>
 /// This is used for <see cref="BorgModuleComponent"/>s that can be "swapped" to, as opposed to having passive effects.
 /// </summary>
-[RegisterComponent, NetworkedComponent, Access(typeof(SharedBorgSystem))]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(SharedBorgSystem))]
 public sealed partial class SelectableBorgModuleComponent : Component
 {
-    [DataField("moduleSwapAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
-    public string? ModuleSwapActionId = "ActionBorgSwapModule";
+    /// <summary>
+    /// The sidebar action prototype for swapping to this module.
+    /// </summary>
+    [DataField]
+    public EntProtoId ModuleSwapAction = "ActionBorgSwapModule";
 
     /// <summary>
-    /// The sidebar action for swapping to this module.
+    /// The sidebar action entity for swapping to this module.
     /// </summary>
-    [DataField("moduleSwapActionEntity")] public EntityUid? ModuleSwapActionEntity;
+    [DataField, AutoNetworkedField]
+    public EntityUid? ModuleSwapActionEntity;
 }
 
-public sealed partial class BorgModuleActionSelectedEvent : InstantActionEvent
-{
-}
+/// <summary>
+/// Raised on a borg module entity with <see cref="SelectableBorgModuleComponent"/> when a player uses the action provided by it.
+/// </summary>
+public sealed partial class BorgModuleActionSelectedEvent : InstantActionEvent;
 
 /// <summary>
 /// Event raised by-ref on a module when it is selected
diff --git a/Content.Shared/Silicons/Borgs/SharedBorgSystem.API.cs b/Content.Shared/Silicons/Borgs/SharedBorgSystem.API.cs
new file mode 100644 (file)
index 0000000..821ef35
--- /dev/null
@@ -0,0 +1,318 @@
+using System.Linq;
+using Content.Shared.Silicons.Borgs.Components;
+using Content.Shared.Whitelist;
+using Robust.Shared.Player;
+
+namespace Content.Shared.Silicons.Borgs;
+
+public abstract partial class SharedBorgSystem
+{
+    /// <summary>
+    /// Can this borg currently activate it's <see cref="ItemToggleComponent"/>?
+    /// The requirements for this are
+    /// - Having enough power in its power cell
+    /// - Having a player mind attached
+    /// - The borg is alive (not crit or dead).
+    /// </summary>
+    public bool CanActivate(Entity<BorgChassisComponent> chassis)
+    {
+        if (!_powerCell.HasDrawCharge(chassis.Owner))
+            return false;
+
+        // TODO: Replace this with something else, only the client's own mind is networked to them,
+        // so this will always be false for the minds of other clients.
+        if (!_mind.TryGetMind(chassis.Owner, out _, out _))
+            return false;
+
+        if (!_mobState.IsAlive(chassis.Owner))
+            return false;
+
+        return true;
+    }
+
+    /// <summary>
+    /// Activates the borg if the conditions are met.
+    /// Returns true if the borg was activated.
+    /// </summary>
+    public bool TryActivate(Entity<BorgChassisComponent> chassis, EntityUid? user = null)
+    {
+        if (chassis.Comp.Active)
+            return false; // Already active.
+
+        if (CanActivate(chassis))
+        {
+            SetActive(chassis, true, user);
+            return true;
+        }
+
+        return false;
+    }
+
+    /// <summary>
+    /// Activates or deactivates a borg.
+    /// If active the borg
+    /// - has door access,
+    /// - can use modules and
+    /// - has full movement speed.
+    /// </summary>
+    public void SetActive(Entity<BorgChassisComponent> chassis, bool active, EntityUid? user = null)
+    {
+        if (chassis.Comp.Active == active)
+            return;
+
+        chassis.Comp.Active = active;
+        Dirty(chassis);
+
+        if (active)
+            InstallAllModules(chassis.AsNullable());
+        else
+            DisableAllModules(chassis.AsNullable());
+
+        _access.SetAccessEnabled(chassis.Owner, active); // Needs a player so that scientists can't drag around an empty borg for free AA.
+        _powerCell.SetDrawEnabled(chassis.Owner, active);
+        _movementSpeedModifier.RefreshMovementSpeedModifiers(chassis);
+
+        var sound = active ? chassis.Comp.ActivateSound : chassis.Comp.DeactivateSound;
+        // If a user is given predict the audio for them, if not keep it unpredicted.
+        if (user != null)
+            _audio.PlayPredicted(sound, chassis.Owner, user);
+        else if (_net.IsServer)
+            _audio.PlayPvs(sound, chassis.Owner);
+    }
+
+    /// <summary>
+    /// Inserts a new module into a borg, the same as if a player inserted it manually.
+    /// This does not run checks to see if the borg is actually allowed to be inserted, such as whitelists.
+    /// </summary>
+    /// <param name="ent">The borg to insert into.</param>
+    /// <param name="module">The module to insert.</param>
+    public void InsertModule(Entity<BorgChassisComponent> ent, EntityUid module)
+    {
+        _container.Insert(module, ent.Comp.ModuleContainer);
+    }
+
+    /// <summary>
+    /// Sets <see cref="BorgChassisComponent.ModuleWhitelist"/>.
+    /// </summary>
+    /// <param name="ent">The borg to modify.</param>
+    /// <param name="whitelist">The new module whitelist.</param>
+    public void SetModuleWhitelist(Entity<BorgChassisComponent> ent, EntityWhitelist? whitelist)
+    {
+        ent.Comp.ModuleWhitelist = whitelist;
+        Dirty(ent);
+    }
+
+    /// <summary>
+    /// Sets <see cref="BorgChassisComponent.MaxModules"/>.
+    /// </summary>
+    /// <param name="ent">The borg to modify.</param>
+    /// <param name="maxModules">The new max module count.</param>
+    public void SetMaxModules(Entity<BorgChassisComponent> ent, int maxModules)
+    {
+        ent.Comp.MaxModules = maxModules;
+        Dirty(ent);
+    }
+
+    /// <summary>
+    /// Checks that a player has fulfilled the requirements for the borg job, i.e. they are not banned from that role.
+    /// Always true on the client.
+    /// </summary>
+    /// <remarks>
+    /// TODO: This currently causes mispredicts, but we have no way of knowing on the client if a player is banned.
+    /// Maybe solve this by giving banned players an unborgable trait instead?
+    /// </remarks>
+    public virtual bool CanPlayerBeBorged(ICommonSession session)
+    {
+        return true;
+    }
+
+    /// <summary>
+    /// Installs a single module into a borg.
+    /// </summary>
+    public void InstallModule(Entity<BorgChassisComponent?> borg, Entity<BorgModuleComponent?> module)
+    {
+        if (!Resolve(borg, ref borg.Comp) || !Resolve(module, ref module.Comp))
+            return;
+
+        if (module.Comp.Installed)
+            return;
+
+        module.Comp.InstalledEntity = borg.Owner;
+        Dirty(module);
+        var ev = new BorgModuleInstalledEvent(borg.Owner);
+        RaiseLocalEvent(module, ref ev);
+    }
+
+    /// <summary>
+    /// Uninstalls a single module from a borg.
+    /// </summary>
+    public void UninstallModule(Entity<BorgChassisComponent?> borg, Entity<BorgModuleComponent?> module)
+    {
+        if (!Resolve(borg, ref borg.Comp) || !Resolve(module, ref module.Comp))
+            return;
+
+        if (!module.Comp.Installed)
+            return;
+
+        module.Comp.InstalledEntity = null;
+        Dirty(module);
+        var ev = new BorgModuleUninstalledEvent(borg.Owner);
+        RaiseLocalEvent(module, ref ev);
+    }
+
+    /// <summary>
+    /// Installs and activates all modules currently inside the borg's module container.
+    /// </summary>
+    public void InstallAllModules(Entity<BorgChassisComponent?> borg)
+    {
+        if (!Resolve(borg, ref borg.Comp))
+            return;
+
+        foreach (var moduleEnt in new List<EntityUid>(borg.Comp.ModuleContainer.ContainedEntities))
+        {
+            if (!_moduleQuery.TryGetComponent(moduleEnt, out var moduleComp))
+                continue;
+
+            InstallModule(borg, (moduleEnt, moduleComp));
+        }
+    }
+
+    /// <summary>
+    /// Deactivates all modules currently inside the borg's module container.
+    /// </summary>
+    public void DisableAllModules(Entity<BorgChassisComponent?> borg)
+    {
+        if (!Resolve(borg, ref borg.Comp))
+            return;
+
+        foreach (var moduleEnt in new List<EntityUid>(borg.Comp.ModuleContainer.ContainedEntities))
+        {
+            if (!_moduleQuery.TryGetComponent(moduleEnt, out var moduleComp))
+                continue;
+
+            UninstallModule(borg, (moduleEnt, moduleComp));
+        }
+    }
+
+    /// <summary>
+    /// Sets <see cref="BorgModuleComponent.DefaultModule"/>.
+    /// </summary>
+    public void SetBorgModuleDefault(Entity<BorgModuleComponent> ent, bool newDefault)
+    {
+        ent.Comp.DefaultModule = newDefault;
+        Dirty(ent);
+    }
+
+    /// <summary>
+    /// Checks if a given module can be inserted into a borg.
+    /// </summary>
+    public bool CanInsertModule(Entity<BorgChassisComponent?> chassis, Entity<BorgModuleComponent?> module, EntityUid? user = null)
+    {
+        if (!Resolve(chassis, ref chassis.Comp) || !Resolve(module, ref module.Comp))
+            return false;
+
+        if (chassis.Comp.ModuleContainer.ContainedEntities.Count >= chassis.Comp.MaxModules)
+        {
+            _popup.PopupClient(Loc.GetString("borg-module-too-many"), chassis.Owner, user);
+            return false;
+        }
+
+        if (_whitelist.IsWhitelistFail(chassis.Comp.ModuleWhitelist, module))
+        {
+            _popup.PopupClient(Loc.GetString("borg-module-whitelist-deny"), chassis.Owner, user);
+            return false;
+        }
+
+        if (TryComp<ItemBorgModuleComponent>(module, out var itemModuleComp))
+        {
+            foreach (var containedModuleUid in chassis.Comp.ModuleContainer.ContainedEntities)
+            {
+                if (!TryComp<ItemBorgModuleComponent>(containedModuleUid, out var containedItemModuleComp))
+                    continue;
+
+                if (containedItemModuleComp.Hands.Count == itemModuleComp.Hands.Count &&
+                    containedItemModuleComp.Hands.All(itemModuleComp.Hands.Contains))
+                {
+                    _popup.PopupClient(Loc.GetString("borg-module-duplicate"), chassis.Owner, user);
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /// <summary>
+    /// Check if a module can be removed from a borg.
+    /// </summary>
+    /// <param name="module">The module to remove from the borg.</param>
+    /// <returns>True if the module can be removed.</returns>
+    public bool CanRemoveModule(Entity<BorgModuleComponent> module)
+    {
+        if (module.Comp.DefaultModule)
+            return false;
+
+        return true;
+    }
+
+    /// <summary>
+    /// Selects a module, enabling the borg to use its provided abilities.
+    /// </summary>
+    public void SelectModule(Entity<BorgChassisComponent?> chassis,
+        Entity<BorgModuleComponent?> module)
+    {
+        if (LifeStage(chassis) >= EntityLifeStage.Terminating)
+            return;
+
+        if (!Resolve(chassis, ref chassis.Comp))
+            return;
+
+        if (!Resolve(module, ref module.Comp) || !module.Comp.Installed || module.Comp.InstalledEntity != chassis.Owner)
+        {
+            Log.Error($"{ToPrettyString(chassis)} attempted to select uninstalled module {ToPrettyString(module)}");
+            return;
+        }
+
+        if (!HasComp<SelectableBorgModuleComponent>(module))
+        {
+            Log.Error($"{ToPrettyString(chassis)} attempted to select invalid module {ToPrettyString(module)}");
+            return;
+        }
+
+        if (!chassis.Comp.ModuleContainer.Contains(module))
+        {
+            Log.Error($"{ToPrettyString(chassis)} does not contain the installed module {ToPrettyString(module)}");
+            return;
+        }
+
+        if (chassis.Comp.SelectedModule == module.Owner)
+            return;
+
+        UnselectModule(chassis);
+
+        var ev = new BorgModuleSelectedEvent(chassis);
+        RaiseLocalEvent(module, ref ev);
+        chassis.Comp.SelectedModule = module.Owner;
+        Dirty(chassis);
+    }
+
+    /// <summary>
+    /// Unselects a module, removing its provided abilities.
+    /// </summary>
+    public void UnselectModule(Entity<BorgChassisComponent?> chassis)
+    {
+        if (LifeStage(chassis) >= EntityLifeStage.Terminating)
+            return;
+
+        if (!Resolve(chassis, ref chassis.Comp))
+            return;
+
+        if (chassis.Comp.SelectedModule == null)
+            return;
+
+        var ev = new BorgModuleUnselectedEvent(chassis);
+        RaiseLocalEvent(chassis.Comp.SelectedModule.Value, ref ev);
+        chassis.Comp.SelectedModule = null;
+        Dirty(chassis);
+    }
+}
diff --git a/Content.Shared/Silicons/Borgs/SharedBorgSystem.MMI.cs b/Content.Shared/Silicons/Borgs/SharedBorgSystem.MMI.cs
new file mode 100644 (file)
index 0000000..efcd5a3
--- /dev/null
@@ -0,0 +1,94 @@
+using Content.Shared.Mind.Components;
+using Content.Shared.Roles.Components;
+using Content.Shared.Silicons.Borgs.Components;
+using Robust.Shared.Containers;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Silicons.Borgs;
+
+public abstract partial class SharedBorgSystem
+{
+    private static readonly EntProtoId SiliconBrainRole = "MindRoleSiliconBrain";
+
+    public void InitializeMMI()
+    {
+        SubscribeLocalEvent<MMIComponent, ComponentInit>(OnMMIInit);
+        SubscribeLocalEvent<MMIComponent, EntInsertedIntoContainerMessage>(OnMMIEntityInserted);
+        SubscribeLocalEvent<MMIComponent, MindAddedMessage>(OnMMIMindAdded);
+        SubscribeLocalEvent<MMIComponent, MindRemovedMessage>(OnMMIMindRemoved);
+
+        SubscribeLocalEvent<MMILinkedComponent, MindAddedMessage>(OnMMILinkedMindAdded);
+        SubscribeLocalEvent<MMILinkedComponent, EntGotRemovedFromContainerMessage>(OnMMILinkedRemoved);
+    }
+
+    private void OnMMIInit(Entity<MMIComponent> ent, ref ComponentInit args)
+    {
+        _itemSlots.AddItemSlot(ent.Owner, ent.Comp.BrainSlotId, ent.Comp.BrainSlot);
+    }
+
+    private void OnMMIEntityInserted(Entity<MMIComponent> ent, ref EntInsertedIntoContainerMessage args)
+    {
+        if (_timing.ApplyingState)
+            return; // The changes are already networked with the same game state
+
+        if (args.Container.ID != ent.Comp.BrainSlotId)
+            return;
+
+        var brain = args.Entity;
+        var linked = EnsureComp<MMILinkedComponent>(brain);
+        linked.LinkedMMI = ent.Owner;
+        Dirty(brain, linked);
+
+        if (_mind.TryGetMind(brain, out var mindId, out var mindComp))
+        {
+            _mind.TransferTo(mindId, ent.Owner, true, mind: mindComp);
+
+            if (!_roles.MindHasRole<SiliconBrainRoleComponent>(mindId))
+                _roles.MindAddRole(mindId, SiliconBrainRole, silent: true);
+        }
+
+        _appearance.SetData(ent.Owner, MMIVisuals.BrainPresent, true);
+    }
+
+    private void OnMMIMindAdded(Entity<MMIComponent> ent, ref MindAddedMessage args)
+    {
+        _appearance.SetData(ent.Owner, MMIVisuals.HasMind, true);
+    }
+
+    private void OnMMIMindRemoved(Entity<MMIComponent> ent, ref MindRemovedMessage args)
+    {
+        _appearance.SetData(ent.Owner, MMIVisuals.HasMind, false);
+    }
+
+    private void OnMMILinkedMindAdded(Entity<MMILinkedComponent> ent, ref MindAddedMessage args)
+    {
+        if (ent.Comp.LinkedMMI == null || !_mind.TryGetMind(ent.Owner, out var mindId, out var mindComp))
+            return;
+
+        _mind.TransferTo(mindId, ent.Comp.LinkedMMI, true, mind: mindComp);
+    }
+
+    private void OnMMILinkedRemoved(Entity<MMILinkedComponent> ent, ref EntGotRemovedFromContainerMessage args)
+    {
+        if (_timing.ApplyingState)
+            return; // The changes are already networked with the same game state
+
+        if (Terminating(ent.Owner))
+            return;
+
+        if (ent.Comp.LinkedMMI is not { } linked)
+            return;
+
+        RemCompDeferred<MMILinkedComponent>(ent.Owner);
+
+        if (_mind.TryGetMind(linked, out var mindId, out var mindComp))
+        {
+            if (_roles.MindHasRole<SiliconBrainRoleComponent>(mindId))
+                _roles.MindRemoveRole<SiliconBrainRoleComponent>(mindId);
+
+            _mind.TransferTo(mindId, ent.Owner, true, mind: mindComp);
+        }
+
+        _appearance.SetData(linked, MMIVisuals.BrainPresent, false);
+    }
+}
diff --git a/Content.Shared/Silicons/Borgs/SharedBorgSystem.Module.cs b/Content.Shared/Silicons/Borgs/SharedBorgSystem.Module.cs
new file mode 100644 (file)
index 0000000..3f00695
--- /dev/null
@@ -0,0 +1,234 @@
+using Content.Shared.Examine;
+using Content.Shared.Hands.Components;
+using Content.Shared.Interaction.Components;
+using Content.Shared.Localizations;
+using Content.Shared.Silicons.Borgs.Components;
+using Robust.Shared.Containers;
+
+namespace Content.Shared.Silicons.Borgs;
+
+public abstract partial class SharedBorgSystem
+{
+    private EntityQuery<BorgModuleComponent> _moduleQuery;
+
+    public void InitializeModule()
+    {
+        SubscribeLocalEvent<BorgModuleComponent, ExaminedEvent>(OnModuleExamine);
+        SubscribeLocalEvent<BorgModuleComponent, EntGotInsertedIntoContainerMessage>(OnModuleGotInserted);
+        SubscribeLocalEvent<BorgModuleComponent, EntGotRemovedFromContainerMessage>(OnModuleGotRemoved);
+
+        SubscribeLocalEvent<SelectableBorgModuleComponent, BorgModuleInstalledEvent>(OnSelectableInstalled);
+        SubscribeLocalEvent<SelectableBorgModuleComponent, BorgModuleUninstalledEvent>(OnSelectableUninstalled);
+        SubscribeLocalEvent<SelectableBorgModuleComponent, BorgModuleActionSelectedEvent>(OnSelectableAction);
+
+        SubscribeLocalEvent<ItemBorgModuleComponent, ComponentStartup>(OnProvideItemStartup);
+        SubscribeLocalEvent<ItemBorgModuleComponent, BorgModuleSelectedEvent>(OnItemModuleSelected);
+        SubscribeLocalEvent<ItemBorgModuleComponent, BorgModuleUnselectedEvent>(OnItemModuleUnselected);
+
+        _moduleQuery = GetEntityQuery<BorgModuleComponent>();
+    }
+
+    private void OnModuleExamine(Entity<BorgModuleComponent> ent, ref ExaminedEvent args)
+    {
+        if (ent.Comp.BorgFitTypes == null)
+            return;
+
+        if (ent.Comp.BorgFitTypes.Count == 0)
+            return;
+
+        var typeList = new List<string>();
+
+        foreach (var type in ent.Comp.BorgFitTypes)
+        {
+            typeList.Add(Loc.GetString(type));
+        }
+
+        var types = ContentLocalizationManager.FormatList(typeList);
+        args.PushMarkup(Loc.GetString("borg-module-fit", ("types", types)));
+    }
+
+    private void OnModuleGotInserted(Entity<BorgModuleComponent> module, ref EntGotInsertedIntoContainerMessage args)
+    {
+        if (_timing.ApplyingState)
+            return; // The changes are already networked with the same game state
+
+        var chassis = args.Container.Owner;
+
+        if (!TryComp<BorgChassisComponent>(chassis, out var chassisComp) ||
+            args.Container != chassisComp.ModuleContainer ||
+            !chassisComp.Active)
+            return;
+
+        InstallModule((chassis, chassisComp), module.AsNullable());
+    }
+
+    private void OnModuleGotRemoved(Entity<BorgModuleComponent> module, ref EntGotRemovedFromContainerMessage args)
+    {
+        if (_timing.ApplyingState)
+            return; // The changes are already networked with the same game state
+
+        var chassis = args.Container.Owner;
+
+        if (!TryComp<BorgChassisComponent>(chassis, out var chassisComp) ||
+            args.Container != chassisComp.ModuleContainer)
+            return;
+
+        UninstallModule((chassis, chassisComp), module.AsNullable());
+    }
+
+    private void OnSelectableInstalled(Entity<SelectableBorgModuleComponent> module, ref BorgModuleInstalledEvent args)
+    {
+        var chassis = args.ChassisEnt;
+
+        if (_actions.AddAction(chassis, ref module.Comp.ModuleSwapActionEntity, out var action, module.Comp.ModuleSwapAction, module.Owner))
+        {
+            Dirty(module); // for ModuleSwapActionEntity after the action has been spawned
+            var actEnt = (module.Comp.ModuleSwapActionEntity.Value, action);
+            _actions.SetEntityIcon(actEnt, module.Owner);
+            if (TryComp<BorgModuleIconComponent>(module, out var moduleIconComp))
+                _actions.SetIcon(actEnt, moduleIconComp.Icon);
+
+            /// Set a custom name and description on the action. The borg module action prototypes are shared across
+            /// all modules. Extract localized names, then populate variables with the info from the module itself.
+            var moduleName = Name(module);
+            var actionMetaData = MetaData(module.Comp.ModuleSwapActionEntity.Value);
+
+            var instanceName = Loc.GetString("borg-module-action-name", ("moduleName", moduleName));
+            _metaData.SetEntityName(module.Comp.ModuleSwapActionEntity.Value, instanceName, actionMetaData);
+            var instanceDesc = Loc.GetString("borg-module-action-description", ("moduleName", moduleName));
+            _metaData.SetEntityDescription(module.Comp.ModuleSwapActionEntity.Value, instanceDesc, actionMetaData);
+        }
+
+        if (!TryComp<BorgChassisComponent>(chassis, out var chassisComp))
+            return;
+
+        if (chassisComp.SelectedModule == null)
+            SelectModule((chassis, chassisComp), module.Owner);
+    }
+
+    private void OnSelectableUninstalled(Entity<SelectableBorgModuleComponent> module, ref BorgModuleUninstalledEvent args)
+    {
+        var chassis = args.ChassisEnt;
+        _actions.RemoveProvidedActions(chassis, module.Owner);
+        if (!TryComp<BorgChassisComponent>(chassis, out var chassisComp))
+            return;
+
+        if (chassisComp.SelectedModule == module.Owner)
+            UnselectModule((chassis, chassisComp));
+    }
+
+    private void OnSelectableAction(Entity<SelectableBorgModuleComponent> module, ref BorgModuleActionSelectedEvent args)
+    {
+        var chassis = args.Performer;
+        if (!TryComp<BorgChassisComponent>(chassis, out var chassisComp))
+            return;
+
+        var selected = chassisComp.SelectedModule;
+
+        args.Handled = true;
+        UnselectModule((chassis, chassisComp));
+
+        if (selected != module.Owner)
+        {
+            SelectModule((chassis, chassisComp), module.Owner);
+        }
+    }
+
+    private void OnProvideItemStartup(Entity<ItemBorgModuleComponent> module, ref ComponentStartup args)
+    {
+        _container.EnsureContainer<Container>(module.Owner, module.Comp.HoldingContainer);
+    }
+
+    private void OnItemModuleSelected(Entity<ItemBorgModuleComponent> module, ref BorgModuleSelectedEvent args)
+    {
+        ProvideItems(args.Chassis, module.AsNullable());
+    }
+
+    private void OnItemModuleUnselected(Entity<ItemBorgModuleComponent> module, ref BorgModuleUnselectedEvent args)
+    {
+        RemoveProvidedItems(args.Chassis, module.AsNullable());
+    }
+
+    private void ProvideItems(Entity<BorgChassisComponent?> chassis, Entity<ItemBorgModuleComponent?> module)
+    {
+        if (!Resolve(chassis, ref chassis.Comp) || !Resolve(module, ref module.Comp))
+            return;
+
+        if (!TryComp<HandsComponent>(chassis, out var hands))
+            return;
+
+        if (!_container.TryGetContainer(module, module.Comp.HoldingContainer, out var container))
+            return;
+
+        var xform = Transform(chassis);
+
+        for (var i = 0; i < module.Comp.Hands.Count; i++)
+        {
+            var hand = module.Comp.Hands[i];
+            var handId = $"{GetNetEntity(module.Owner)}-hand-{i}";
+
+            _hands.AddHand((chassis.Owner, hands), handId, hand.Hand);
+            EntityUid? item = null;
+
+            if (module.Comp.Spawned)
+            {
+                if (module.Comp.StoredItems.TryGetValue(handId, out var storedItem))
+                {
+                    item = storedItem;
+                    // DoPickup handles removing the item from the container.
+                }
+            }
+            else if (hand.Item is { } itemProto)
+            {
+                item = PredictedSpawnAtPosition(itemProto, xform.Coordinates);
+            }
+
+            if (item is { } pickUp)
+            {
+                _hands.DoPickup(chassis, handId, pickUp, hands);
+                if (!hand.ForceRemovable && hand.Hand.Whitelist == null && hand.Hand.Blacklist == null)
+                {
+                    EnsureComp<UnremoveableComponent>(pickUp);
+                }
+            }
+        }
+
+        module.Comp.Spawned = true;
+        Dirty(module);
+    }
+
+    private void RemoveProvidedItems(Entity<BorgChassisComponent?> chassis, Entity<ItemBorgModuleComponent?> module)
+    {
+        if (!Resolve(chassis, ref chassis.Comp) || !Resolve(module, ref module.Comp))
+            return;
+
+        if (!TryComp<HandsComponent>(chassis, out var hands))
+            return;
+
+        if (!_container.TryGetContainer(module, module.Comp.HoldingContainer, out var container))
+            return;
+
+        if (TerminatingOrDeleted(module))
+            return;
+
+        for (var i = 0; i < module.Comp.Hands.Count; i++)
+        {
+            var handId = $"{GetNetEntity(module.Owner)}-hand-{i}";
+
+            if (_hands.TryGetHeldItem((chassis.Owner, hands), handId, out var held))
+            {
+                RemComp<UnremoveableComponent>(held.Value);
+                _container.Insert(held.Value, container);
+                module.Comp.StoredItems[handId] = held.Value;
+            }
+            else
+            {
+                module.Comp.StoredItems.Remove(handId);
+            }
+
+            _hands.RemoveHand((chassis.Owner, hands), handId);
+        }
+
+        Dirty(module);
+    }
+}
diff --git a/Content.Shared/Silicons/Borgs/SharedBorgSystem.Ui.cs b/Content.Shared/Silicons/Borgs/SharedBorgSystem.Ui.cs
new file mode 100644 (file)
index 0000000..e5e8fa8
--- /dev/null
@@ -0,0 +1,79 @@
+using Content.Shared.CCVar;
+using Content.Shared.Database;
+using Content.Shared.PowerCell.Components;
+using Content.Shared.Silicons.Borgs.Components;
+
+namespace Content.Shared.Silicons.Borgs;
+
+public abstract partial class SharedBorgSystem
+{
+    // CCvar
+    private int _maxNameLength;
+
+    public void InitializeUI()
+    {
+        SubscribeLocalEvent<BorgChassisComponent, BorgEjectBrainBuiMessage>(OnEjectBrainBuiMessage);
+        SubscribeLocalEvent<BorgChassisComponent, BorgEjectBatteryBuiMessage>(OnEjectBatteryBuiMessage);
+        SubscribeLocalEvent<BorgChassisComponent, BorgSetNameBuiMessage>(OnSetNameBuiMessage);
+        SubscribeLocalEvent<BorgChassisComponent, BorgRemoveModuleBuiMessage>(OnRemoveModuleBuiMessage);
+
+        Subs.CVar(_configuration, CCVars.MaxNameLength, value => _maxNameLength = value, true);
+    }
+
+    public virtual void UpdateUI(Entity<BorgChassisComponent?> chassis) { }
+
+    private void OnEjectBrainBuiMessage(Entity<BorgChassisComponent> chassis, ref BorgEjectBrainBuiMessage args)
+    {
+        if (chassis.Comp.BrainEntity is not { } brain)
+            return;
+
+        _adminLog.Add(LogType.Action, LogImpact.Medium,
+            $"{args.Actor} removed brain {brain} from borg {chassis.Owner}");
+        _container.Remove(brain, chassis.Comp.BrainContainer);
+        _hands.TryPickupAnyHand(args.Actor, brain);
+    }
+
+    private void OnEjectBatteryBuiMessage(Entity<BorgChassisComponent> chassis, ref BorgEjectBatteryBuiMessage args)
+    {
+        if (_powerCell.TryEjectBatteryFromSlot(chassis.Owner, out var powerCell, args.Actor))
+            _hands.TryPickupAnyHand(args.Actor, powerCell.Value);
+    }
+
+    private void OnSetNameBuiMessage(Entity<BorgChassisComponent> chassis, ref BorgSetNameBuiMessage args)
+    {
+        if (args.Name.Length > _maxNameLength ||
+            args.Name.Length == 0 ||
+            string.IsNullOrWhiteSpace(args.Name) ||
+            string.IsNullOrEmpty(args.Name))
+        {
+            return;
+        }
+
+        var name = args.Name.Trim();
+
+        var metaData = MetaData(chassis);
+
+        // don't change the name if the value doesn't actually change
+        if (metaData.EntityName.Equals(name, StringComparison.InvariantCulture))
+            return;
+
+        _adminLog.Add(LogType.Action, LogImpact.High, $"{args.Actor} set borg \"{chassis.Owner}\"'s name to: {name}");
+        _metaData.SetEntityName(chassis, name, metaData);
+    }
+
+    private void OnRemoveModuleBuiMessage(Entity<BorgChassisComponent> chassis, ref BorgRemoveModuleBuiMessage args)
+    {
+        var module = GetEntity(args.Module);
+
+        if (!chassis.Comp.ModuleContainer.Contains(module))
+            return;
+
+        if (!CanRemoveModule((module, Comp<BorgModuleComponent>(module))))
+            return;
+
+        _adminLog.Add(LogType.Action, LogImpact.Medium,
+            $"{args.Actor} removed module {module} from borg {chassis.Owner}");
+        _container.Remove(module, chassis.Comp.ModuleContainer);
+        _hands.TryPickupAnyHand(args.Actor, module);
+    }
+}
index f9b4ec7cb81d40ab9d42d02baa85e4405e50eff9..d965a362ff6a24286b661367a6a484ffe47b5b80 100644 (file)
@@ -1,16 +1,37 @@
+using Content.Shared.Access.Systems;
+using Content.Shared.Actions;
+using Content.Shared.Administration.Logs;
+using Content.Shared.Body.Events;
 using Content.Shared.Containers.ItemSlots;
-using Content.Shared.Examine;
+using Content.Shared.Database;
+using Content.Shared.Hands.EntitySystems;
 using Content.Shared.IdentityManagement;
-using Content.Shared.Item.ItemToggle;
-using Content.Shared.Localizations;
+using Content.Shared.Interaction;
+using Content.Shared.Light;
+using Content.Shared.Light.Components;
+using Content.Shared.Mind;
+using Content.Shared.Mind.Components;
+using Content.Shared.Mobs;
+using Content.Shared.Mobs.Systems;
 using Content.Shared.Movement.Components;
 using Content.Shared.Movement.Systems;
+using Content.Shared.Pointing;
 using Content.Shared.Popups;
+using Content.Shared.PowerCell;
 using Content.Shared.PowerCell.Components;
+using Content.Shared.Roles;
 using Content.Shared.Silicons.Borgs.Components;
+using Content.Shared.Throwing;
 using Content.Shared.UserInterface;
 using Content.Shared.Wires;
+using Content.Shared.Whitelist;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Configuration;
 using Robust.Shared.Containers;
+using Robust.Shared.Network;
+using Robust.Shared.Player;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
 
 namespace Content.Shared.Silicons.Borgs;
 
@@ -19,46 +40,63 @@ namespace Content.Shared.Silicons.Borgs;
 /// </summary>
 public abstract partial class SharedBorgSystem : EntitySystem
 {
-    [Dependency] protected readonly SharedContainerSystem Container = default!;
-    [Dependency] protected readonly ItemSlotsSystem ItemSlots = default!;
-    [Dependency] protected readonly ItemToggleSystem Toggle = default!;
-    [Dependency] protected readonly SharedPopupSystem Popup = default!;
+    [Dependency] private readonly SharedContainerSystem _container = default!;
+    [Dependency] private readonly ItemSlotsSystem _itemSlots = default!;
+    [Dependency] private readonly SharedPopupSystem _popup = default!;
+    [Dependency] private readonly SharedRoleSystem _roles = default!;
+    [Dependency] private readonly SharedMindSystem _mind = default!;
+    [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+    [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
+    [Dependency] private readonly PowerCellSystem _powerCell = default!;
+    [Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
+    [Dependency] private readonly SharedHandsSystem _hands = default!;
+    [Dependency] private readonly SharedActionsSystem _actions = default!;
+    [Dependency] private readonly MetaDataSystem _metaData = default!;
+    [Dependency] private readonly MobStateSystem _mobState = default!;
+    [Dependency] private readonly ThrowingSystem _throwing = default!;
+    [Dependency] private readonly ISharedPlayerManager _player = default!;
+    [Dependency] private readonly IRobustRandom _random = default!;
+    [Dependency] private readonly IConfigurationManager _configuration = default!;
+    [Dependency] private readonly ISharedAdminLogManager _adminLog = default!;
+    [Dependency] private readonly INetManager _net = default!;
+    [Dependency] private readonly SharedHandheldLightSystem _handheldLight = default!;
+    [Dependency] private readonly SharedAccessSystem _access = default!;
+    [Dependency] private readonly SharedAudioSystem _audio = default!;
 
     /// <inheritdoc/>
     public override void Initialize()
     {
         base.Initialize();
 
+        InitializeMMI();
+        InitializeModule();
+        InitializeRelay();
+        InitializeUI();
+
+        SubscribeLocalEvent<TryGetIdentityShortInfoEvent>(OnTryGetIdentityShortInfo);
+
         SubscribeLocalEvent<BorgChassisComponent, ComponentStartup>(OnStartup);
+        SubscribeLocalEvent<BorgChassisComponent, MapInitEvent>(OnMapInit);
         SubscribeLocalEvent<BorgChassisComponent, ItemSlotInsertAttemptEvent>(OnItemSlotInsertAttempt);
         SubscribeLocalEvent<BorgChassisComponent, ItemSlotEjectAttemptEvent>(OnItemSlotEjectAttempt);
         SubscribeLocalEvent<BorgChassisComponent, EntInsertedIntoContainerMessage>(OnInserted);
         SubscribeLocalEvent<BorgChassisComponent, EntRemovedFromContainerMessage>(OnRemoved);
+        SubscribeLocalEvent<BorgChassisComponent, MindAddedMessage>(OnMindAdded);
+        SubscribeLocalEvent<BorgChassisComponent, MindRemovedMessage>(OnMindRemoved);
+        SubscribeLocalEvent<BorgChassisComponent, AfterInteractUsingEvent>(OnChassisInteractUsing);
         SubscribeLocalEvent<BorgChassisComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovementSpeedModifiers);
         SubscribeLocalEvent<BorgChassisComponent, ActivatableUIOpenAttemptEvent>(OnUIOpenAttempt);
-        SubscribeLocalEvent<TryGetIdentityShortInfoEvent>(OnTryGetIdentityShortInfo);
-        SubscribeLocalEvent<BorgModuleComponent, ExaminedEvent>(OnModuleExamine);
-
-        InitializeRelay();
-    }
-
-    private void OnModuleExamine(Entity<BorgModuleComponent> ent, ref ExaminedEvent args)
-    {
-        if (ent.Comp.BorgFitTypes == null)
-            return;
-
-        if (ent.Comp.BorgFitTypes.Count == 0)
-            return;
+        SubscribeLocalEvent<BorgChassisComponent, MobStateChangedEvent>(OnMobStateChanged);
+        SubscribeLocalEvent<BorgChassisComponent, BeingGibbedEvent>(OnBeingGibbed);
+        SubscribeLocalEvent<BorgChassisComponent, GetCharactedDeadIcEvent>(OnGetDeadIC);
+        SubscribeLocalEvent<BorgChassisComponent, GetCharacterUnrevivableIcEvent>(OnGetUnrevivableIC);
+        SubscribeLocalEvent<BorgChassisComponent, PowerCellSlotEmptyEvent>(OnPowerCellSlotEmpty);
+        SubscribeLocalEvent<BorgChassisComponent, PowerCellChangedEvent>(OnPowerCellChanged);
 
-        var typeList = new List<string>();
+        SubscribeLocalEvent<BorgBrainComponent, MindAddedMessage>(OnBrainMindAdded);
+        SubscribeLocalEvent<BorgBrainComponent, PointAttemptEvent>(OnBrainPointAttempt);
 
-        foreach (var type in ent.Comp.BorgFitTypes)
-        {
-            typeList.Add(Loc.GetString(type));
-        }
-
-        var types = ContentLocalizationManager.FormatList(typeList);
-        args.PushMarkup(Loc.GetString("borg-module-fit", ("types", types)));
     }
 
     private void OnTryGetIdentityShortInfo(TryGetIdentityShortInfoEvent args)
@@ -68,6 +106,8 @@ public abstract partial class SharedBorgSystem : EntitySystem
             return;
         }
 
+        // TODO: Why the hell is this only broadcasted and not raised directed on the entity?
+        // This is doing a ton of HasComps/TryComps.
         if (!HasComp<BorgChassisComponent>(args.ForActor))
         {
             return;
@@ -77,82 +117,265 @@ public abstract partial class SharedBorgSystem : EntitySystem
         args.Handled = true;
     }
 
-    private void OnItemSlotInsertAttempt(EntityUid uid, BorgChassisComponent component, ref ItemSlotInsertAttemptEvent args)
+    private void OnStartup(Entity<BorgChassisComponent> chassis, ref ComponentStartup args)
+    {
+        if (!TryComp<ContainerManagerComponent>(chassis, out var containerManager))
+            return;
+
+        chassis.Comp.BrainContainer = _container.EnsureContainer<ContainerSlot>(chassis.Owner, chassis.Comp.BrainContainerId, containerManager);
+        chassis.Comp.ModuleContainer = _container.EnsureContainer<Container>(chassis.Owner, chassis.Comp.ModuleContainerId, containerManager);
+    }
+
+    private void OnMapInit(Entity<BorgChassisComponent> chassis, ref MapInitEvent args)
+    {
+        _movementSpeedModifier.RefreshMovementSpeedModifiers(chassis.Owner);
+    }
+
+    private void OnItemSlotInsertAttempt(Entity<BorgChassisComponent> chassis, ref ItemSlotInsertAttemptEvent args)
     {
         if (args.Cancelled)
             return;
 
-        if (!TryComp<PowerCellSlotComponent>(uid, out var cellSlotComp) ||
-            !TryComp<WiresPanelComponent>(uid, out var panel))
+        if (!TryComp<PowerCellSlotComponent>(chassis, out var cellSlotComp) ||
+            !TryComp<WiresPanelComponent>(chassis, out var panelComp))
             return;
 
-        if (!ItemSlots.TryGetSlot(uid, cellSlotComp.CellSlotId, out var cellSlot) || cellSlot != args.Slot)
+        if (!_itemSlots.TryGetSlot(chassis.Owner, cellSlotComp.CellSlotId, out var cellSlot) || cellSlot != args.Slot)
             return;
 
-        if (!panel.Open || args.User == uid)
+        if (!panelComp.Open || args.User == chassis.Owner)
             args.Cancelled = true;
     }
 
-    private void OnItemSlotEjectAttempt(EntityUid uid, BorgChassisComponent component, ref ItemSlotEjectAttemptEvent args)
+    private void OnItemSlotEjectAttempt(Entity<BorgChassisComponent> chassis, ref ItemSlotEjectAttemptEvent args)
     {
         if (args.Cancelled)
             return;
 
-        if (!TryComp<PowerCellSlotComponent>(uid, out var cellSlotComp) ||
-            !TryComp<WiresPanelComponent>(uid, out var panel))
+        if (!TryComp<PowerCellSlotComponent>(chassis, out var cellSlotComp) ||
+            !TryComp<WiresPanelComponent>(chassis, out var panel))
             return;
 
-        if (!ItemSlots.TryGetSlot(uid, cellSlotComp.CellSlotId, out var cellSlot) || cellSlot != args.Slot)
+        if (!_itemSlots.TryGetSlot(chassis.Owner, cellSlotComp.CellSlotId, out var cellSlot) || cellSlot != args.Slot)
             return;
 
-        if (!panel.Open || args.User == uid)
+        if (!panel.Open || args.User == chassis.Owner)
             args.Cancelled = true;
     }
 
-    private void OnStartup(EntityUid uid, BorgChassisComponent component, ComponentStartup args)
+    // TODO: consider transferring over the ghost role? managing that might suck.
+    protected virtual void OnInserted(Entity<BorgChassisComponent> chassis, ref EntInsertedIntoContainerMessage args)
     {
-        if (!TryComp<ContainerManagerComponent>(uid, out var containerManager))
+        if (_timing.ApplyingState)
+            return; // The changes are already networked with the same game state
+
+        if (args.Container != chassis.Comp.BrainContainer)
             return;
 
-        component.BrainContainer = Container.EnsureContainer<ContainerSlot>(uid, component.BrainContainerId, containerManager);
-        component.ModuleContainer = Container.EnsureContainer<Container>(uid, component.ModuleContainerId, containerManager);
+        if (HasComp<BorgBrainComponent>(args.Entity) && _mind.TryGetMind(args.Entity, out var mindId, out var mind))
+        {
+            _mind.TransferTo(mindId, chassis.Owner, mind: mind);
+        }
     }
 
-    private void OnUIOpenAttempt(EntityUid uid, BorgChassisComponent component, ActivatableUIOpenAttemptEvent args)
+    protected virtual void OnRemoved(Entity<BorgChassisComponent> chassis, ref EntRemovedFromContainerMessage args)
     {
-        // borgs generaly can't view their own ui
-        if (args.User == uid && !component.CanOpenSelfUi)
-            args.Cancel();
+        if (_timing.ApplyingState)
+            return; // The changes are already networked with the same game state
+
+        if (args.Container != chassis.Comp.BrainContainer)
+            return;
+
+        if (HasComp<BorgBrainComponent>(args.Entity) && _mind.TryGetMind(chassis.Owner, out var mindId, out var mind))
+        {
+            _mind.TransferTo(mindId, args.Entity, mind: mind);
+        }
     }
 
-    protected virtual void OnInserted(EntityUid uid, BorgChassisComponent component, EntInsertedIntoContainerMessage args)
+    private void OnMindAdded(Entity<BorgChassisComponent> chassis, ref MindAddedMessage args)
     {
+        // Unpredicted because the event is raised on the server.
+        _popup.PopupEntity(Loc.GetString("borg-mind-added", ("name", Identity.Name(chassis.Owner, EntityManager))), chassis.Owner);
+
+        if (CanActivate(chassis))
+            SetActive(chassis, true);
+        _appearance.SetData(chassis.Owner, BorgVisuals.HasPlayer, true);
+    }
 
+    private void OnMindRemoved(Entity<BorgChassisComponent> chassis, ref MindRemovedMessage args)
+    {
+        // Unpredicted because the event is raised on the server.
+        _popup.PopupEntity(Loc.GetString("borg-mind-removed", ("name", Identity.Name(chassis.Owner, EntityManager))), chassis.Owner);
+
+        SetActive(chassis, false);
+        // Turn off the light so that the no-player visuals can be seen.
+        if (TryComp<HandheldLightComponent>(chassis.Owner, out var light))
+            _handheldLight.TurnOff((chassis.Owner, light), makeNoise: false); // Already plays a sound when toggling the borg off.
+        _appearance.SetData(chassis.Owner, BorgVisuals.HasPlayer, false);
     }
 
-    protected virtual void OnRemoved(EntityUid uid, BorgChassisComponent component, EntRemovedFromContainerMessage args)
+    private void OnChassisInteractUsing(Entity<BorgChassisComponent> chassis, ref AfterInteractUsingEvent args)
     {
+        if (!args.CanReach || args.Handled || chassis.Owner == args.User)
+            return;
 
+        var used = args.Used;
+        TryComp<BorgBrainComponent>(used, out var brain);
+        TryComp<BorgModuleComponent>(used, out var module);
+
+        if (TryComp<WiresPanelComponent>(chassis, out var panel) && !panel.Open)
+        {
+            if (brain != null || module != null)
+            {
+                _popup.PopupClient(Loc.GetString("borg-panel-not-open"), chassis, args.User);
+            }
+            return;
+        }
+
+        if (chassis.Comp.BrainEntity == null && brain != null &&
+            _whitelist.IsWhitelistPassOrNull(chassis.Comp.BrainWhitelist, used))
+        {
+            if (TryComp<ActorComponent>(used, out var actor) && !CanPlayerBeBorged(actor.PlayerSession))
+            {
+                // Don't use PopupClient because CanPlayerBeBorged is not predicted.
+                _popup.PopupEntity(Loc.GetString("borg-player-not-allowed"), used, args.User);
+                return;
+            }
+
+            _container.Insert(used, chassis.Comp.BrainContainer);
+            _adminLog.Add(LogType.Action, LogImpact.Medium,
+                $"{args.User} installed brain {used} into borg {chassis.Owner}");
+            args.Handled = true;
+            return;
+        }
+
+        if (module != null && CanInsertModule(chassis.AsNullable(), (used, module), args.User))
+        {
+            InsertModule(chassis, used);
+            _adminLog.Add(LogType.Action, LogImpact.Low,
+                $"{args.User} installed module {used} into borg {chassis.Owner}");
+            args.Handled = true;
+        }
     }
 
-    private void OnRefreshMovementSpeedModifiers(EntityUid uid, BorgChassisComponent component, RefreshMovementSpeedModifiersEvent args)
+    // Make the borg slower without power.
+    private void OnRefreshMovementSpeedModifiers(Entity<BorgChassisComponent> chassis, ref RefreshMovementSpeedModifiersEvent args)
     {
-        if (Toggle.IsActivated(uid))
+        if (chassis.Comp.Active)
             return;
 
-        if (!TryComp<MovementSpeedModifierComponent>(uid, out var movement))
+        if (!TryComp<MovementSpeedModifierComponent>(chassis, out var movement))
             return;
 
+        if (movement.BaseSprintSpeed == 0f)
+            return; // We already cannot move.
+
+        // Slow down to walk speed.
         var sprintDif = movement.BaseWalkSpeed / movement.BaseSprintSpeed;
         args.ModifySpeed(1f, sprintDif);
     }
 
-    /// <summary>
-    /// Sets <see cref="BorgModuleComponent.DefaultModule"/>.
-    /// </summary>
-    public void SetBorgModuleDefault(Entity<BorgModuleComponent> ent, bool newDefault)
+    private void OnUIOpenAttempt(Entity<BorgChassisComponent> chassis, ref ActivatableUIOpenAttemptEvent args)
     {
-        ent.Comp.DefaultModule = newDefault;
-        Dirty(ent);
+        // Borgs generally can't view their own UI.
+        if (args.User == chassis.Owner && !chassis.Comp.CanOpenSelfUi)
+            args.Cancel();
+    }
+
+    private void OnMobStateChanged(Entity<BorgChassisComponent> chassis, ref MobStateChangedEvent args)
+    {
+        if (args.NewMobState == MobState.Alive)
+        {
+            if (CanActivate(chassis))
+                SetActive(chassis, true, user: args.Origin);
+        }
+        else
+        {
+            SetActive(chassis, false, user: args.Origin);
+        }
+    }
+
+    private void OnBeingGibbed(Entity<BorgChassisComponent> chassis, ref BeingGibbedEvent args)
+    {
+        // Don't use the ItemSlotsSystem eject method since we don't want to play a sound and want we to eject the battery even if the slot is locked.
+        if (TryComp<PowerCellSlotComponent>(chassis, out var slotComp) &&
+            _container.TryGetContainer(chassis, slotComp.CellSlotId, out var slotContainer))
+            _container.EmptyContainer(slotContainer);
+
+        _container.EmptyContainer(chassis.Comp.BrainContainer);
+        _container.EmptyContainer(chassis.Comp.ModuleContainer);
+    }
+
+    private void OnGetDeadIC(Entity<BorgChassisComponent> chassis, ref GetCharactedDeadIcEvent args)
+    {
+        args.Dead = true;
+    }
+
+    private void OnGetUnrevivableIC(Entity<BorgChassisComponent> chassis, ref GetCharacterUnrevivableIcEvent args)
+    {
+        args.Unrevivable = true;
+    }
+
+    private void OnBrainMindAdded(Entity<BorgBrainComponent> brain, ref MindAddedMessage args)
+    {
+        if (!_container.TryGetContainingContainer(brain.Owner, out var container))
+            return;
+
+        var borg = container.Owner;
+
+        if (!TryComp<BorgChassisComponent>(borg, out var chassisComponent) ||
+            container.ID != chassisComponent.BrainContainerId)
+            return;
+
+        if (!_mind.TryGetMind(brain.Owner, out var mindId, out var mind) ||
+            !_player.TryGetSessionById(mind.UserId, out var session))
+            return;
+
+        if (!CanPlayerBeBorged(session))
+        {
+            // Don't use PopupClient because MindAddedMessage and CanPlayerBeBorged are not predicted.
+            _popup.PopupEntity(Loc.GetString("borg-player-not-allowed-eject"), brain);
+            _container.RemoveEntity(borg, brain);
+            _throwing.TryThrow(brain, _random.NextVector2() * 5, 5f);
+            return;
+        }
+
+        _mind.TransferTo(mindId, borg, mind: mind);
+    }
+
+    private void OnBrainPointAttempt(Entity<BorgBrainComponent> brain, ref PointAttemptEvent args)
+    {
+        args.Cancel();
+    }
+
+    // Raised when the power cell is empty or removed from the borg.
+    private void OnPowerCellSlotEmpty(Entity<BorgChassisComponent> chassis, ref PowerCellSlotEmptyEvent args)
+    {
+        SetActive(chassis, false);
+    }
+
+    // Raised when a power cell is inserted.
+    private void OnPowerCellChanged(Entity<BorgChassisComponent> chassis, ref PowerCellChangedEvent args)
+    {
+        if (CanActivate(chassis))
+            SetActive(chassis, true);
+    }
+
+    public override void Update(float frameTime)
+    {
+        var curTime = _timing.CurTime;
+        var query = EntityQueryEnumerator<BorgChassisComponent>();
+        while (query.MoveNext(out var uid, out var borgChassis))
+        {
+            if (curTime < borgChassis.NextBatteryUpdate)
+                continue;
+
+            borgChassis.NextBatteryUpdate = curTime + TimeSpan.FromSeconds(1);
+            Dirty(uid, borgChassis);
+
+            // If we aren't drawing and suddenly get enough power to draw again, reenable.
+            if (CanActivate((uid, borgChassis)))
+                SetActive((uid, borgChassis), true);
+        }
     }
 }
index 719fa9379a6577ad769d348b2097feb32fb17bfd..5c70b7e5f146e2ff2c9656d2cf0743bea4722463 100644 (file)
   description: alerts-battery-desc
   minSeverity: 0
   maxSeverity: 10
+  clientHandled: true # the power cell is read on the client so that we don't have to periodically network the charge
 
 - type: alert
   id: BorgBatteryNone
     state: battery-none
   name: alerts-no-battery-name
   description: alerts-no-battery-desc
+  clientHandled: true # the power cell battery is read on the client so that we don't have to periodically network the charge
 
 - type: alert
   id: Internals
index 8d8c3671ee619031f37aa6dfa810ef8bb28590b2..fb0509bedf3c25b07f383cfb7b295b9caaf37418 100644 (file)
   - type: PowerCellSlot
     cellSlotId: cell_slot
     fitsInCharger: true
-  - type: ItemToggle
-    onActivate: false # You should not be able to turn off a borg temporarily.
-    activated: false # gets activated when a mind is added
-    onUse: false # no item-borg toggling sorry
-  - type: ItemTogglePointLight
-  - type: AccessToggle
-  # TODO: refactor movement to just be based on toggle like speedboots but for the boots themselves
-  # TODO: or just have sentient speedboots be fast idk
+  - type: Access
+    enabled: false # needs a player so that scientists can't drag around an empty borg for free AA
   - type: PowerCellDraw
     drawRate: 0.6
-  # no ToggleCellDraw since dont want to lose access when power is gone
+    enabled: false # the borg is only activated when a player takes over, otherwise the battery is likely empty
   - type: ItemSlots
     slots:
       cell_slot:
     wattage: 0.2
     blinkingBehaviourId: blinking
     radiatingBehaviourId: radiating
+  # These two components are required to make HandheldLight work, even though we don't even have ItemToggle.
+  # The code is a total mess and needs a complete rewrite.
+  - type: ItemTogglePointLight
+  - type: ToggleableVisuals
+    spriteLayer: light
   - type: LightBehaviour
     behaviours:
     - !type:FadeBehaviour
       startValue: 0.1
       endValue: 2.0
       isLooped: true
-  - type: ToggleableVisuals
-    spriteLayer: light
   - type: PointLight
     enabled: false
     mask: /Textures/Effects/LightMasks/cone.png
     factions:
     - NanoTrasen
   - type: Access
-    enabled: false
     groups:
     - AllAccess
     tags:
     factions:
     - NanoTrasen #The seemingly best fit. It was a regular NT cyborg once, after all.
   - type: Access
-    enabled: false
     groups:
     - AllAccess #Randomized access would be fun. AllAccess is the best i can think of right now that does make it too hard for it to enter the station or navigate it..
   - type: AccessReader
     - Robotics
     - Xenoborgs
   - type: Access
-    enabled: false
     tags:
     - Xenoborg
   - type: AccessReader
index 4d27a0f07abbdaec61e8ca41263215794a768a06..69d84810c6b2d925637d36f148083730bd4772b5 100644 (file)
   - type: Input
     context: human
   - type: MMI
+    brainSlot:
+      name: positronic-brain-slot-component-slot-name-brain
+      whitelist:
+        components:
+        - Brain
   - type: BorgBrain
   - type: BlockMovement
   - type: Examiner
   - type: Speech
     speechSounds: Pai
   - type: ItemSlots
-    slots:
-      brain_slot:
-        name: positronic-brain-slot-component-slot-name-brain
-        whitelist:
-          components:
-          - Brain
   - type: ContainerContainer
     containers:
       brain_slot: !type:ContainerSlot
   id: MMIFilled
   suffix: Filled
   components:
-  - type: ItemSlots
-    slots:
-      brain_slot:
-        name: "Brain"
-        startingItem: OrganHumanBrain
-        whitelist:
-          components:
-            - Brain
+  - type: MMI
+    brainSlot:
+      name: positronic-brain-slot-component-slot-name-brain
+      startingItem: OrganHumanBrain
+      whitelist:
+        components:
+        - Brain
 
 - type: entity
   parent: BaseItem