]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Station Anchor (#26098)
authorJulian Giebel <juliangiebel@live.de>
Sat, 31 Aug 2024 14:40:28 +0000 (16:40 +0200)
committerGitHub <noreply@github.com>
Sat, 31 Aug 2024 14:40:28 +0000 (10:40 -0400)
* Work on abstracting out chargeup functionality/ui from grav gen

* Work on station anchor

* Finish implementing station anchors

* uhh yeah

* ok.

* fix tests

* whoops

* Get the last extraneous yaml fail

* PJB review

* beast mode... ACTIVATE!

---------

Co-authored-by: Ed <96445749+TheShuEd@users.noreply.github.com>
Co-authored-by: EmoGarbage404 <retron404@gmail.com>
30 files changed:
Content.Client/Anomaly/Ui/AnomalyGeneratorBoundUserInterface.cs
Content.Client/Gravity/GravitySystem.cs
Content.Client/Gravity/UI/GravityGeneratorBoundUserInterface.cs [deleted file]
Content.Client/Gravity/UI/GravityGeneratorWindow.xaml.cs [deleted file]
Content.Client/Power/PowerCharge/PowerChargeBoundUserInterface.cs [new file with mode: 0644]
Content.Client/Power/PowerCharge/PowerChargeComponent.cs [new file with mode: 0644]
Content.Client/Power/PowerCharge/PowerChargeWindow.xaml [moved from Content.Client/Gravity/UI/GravityGeneratorWindow.xaml with 60% similarity]
Content.Client/Power/PowerCharge/PowerChargeWindow.xaml.cs [new file with mode: 0644]
Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs
Content.IntegrationTests/Tests/GravityGridTest.cs
Content.Server/Gravity/GravityGeneratorComponent.cs
Content.Server/Gravity/GravityGeneratorSystem.cs
Content.Server/Power/Components/PowerChargeComponent.cs [new file with mode: 0644]
Content.Server/Power/EntitySystems/PowerChargeSystem.cs [new file with mode: 0644]
Content.Server/Shuttles/Components/StationAnchorComponent.cs [new file with mode: 0644]
Content.Server/Shuttles/Systems/StationAnchorSystem.cs [new file with mode: 0644]
Content.Shared/Gravity/SharedGravityGeneratorComponent.cs
Content.Shared/Power/SharedPowerCharge.cs [new file with mode: 0644]
Content.Shared/Power/SharedPowerChargeComponent.cs [new file with mode: 0644]
Content.Shared/Shuttles/Systems/SharedShuttleSystem.cs
Resources/Locale/en-US/components/station-anchor-component.ftl [new file with mode: 0644]
Resources/Locale/en-US/power/components/power-charging-component.ftl [new file with mode: 0644]
Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml
Resources/Prototypes/Entities/Structures/Machines/gravity_generator.yml
Resources/Prototypes/Entities/Structures/Machines/lathe.yml
Resources/Prototypes/Entities/Structures/Shuttles/station_anchor.yml [new file with mode: 0644]
Resources/Prototypes/Recipes/Lathes/electronics.yml
Resources/Textures/Structures/Machines/station_anchor.rsi/meta.json [new file with mode: 0644]
Resources/Textures/Structures/Machines/station_anchor.rsi/station_anchor.png [new file with mode: 0644]
Resources/Textures/Structures/Machines/station_anchor.rsi/station_anchor_unlit.png [new file with mode: 0644]

index 5d1985485c4b930180c8aba9507512cebc3dc9da..f088ac1976bceb43565c872529b498ff6878eaf6 100644 (file)
@@ -1,7 +1,5 @@
 using Content.Shared.Anomaly;
-using Content.Shared.Gravity;
 using JetBrains.Annotations;
-using Robust.Client.GameObjects;
 using Robust.Client.UserInterface;
 
 namespace Content.Client.Anomaly.Ui;
index 3e87f76ba2bf3641cac2bf4369890dc0b6af7a80..dd51436a1f5e1c671db734b7143fd3b63c340c8f 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Shared.Gravity;
+using Content.Shared.Power;
 using Robust.Client.GameObjects;
 
 namespace Content.Client.Gravity;
@@ -21,7 +22,7 @@ public sealed partial class GravitySystem : SharedGravitySystem
         if (args.Sprite == null)
             return;
 
-        if (_appearanceSystem.TryGetData<GravityGeneratorStatus>(uid, GravityGeneratorVisuals.State, out var state, args.Component))
+        if (_appearanceSystem.TryGetData<PowerChargeStatus>(uid, PowerChargeVisuals.State, out var state, args.Component))
         {
             if (comp.SpriteMap.TryGetValue(state, out var spriteState))
             {
@@ -30,7 +31,7 @@ public sealed partial class GravitySystem : SharedGravitySystem
             }
         }
 
-        if (_appearanceSystem.TryGetData<float>(uid, GravityGeneratorVisuals.Charge, out var charge, args.Component))
+        if (_appearanceSystem.TryGetData<float>(uid, PowerChargeVisuals.Charge, out var charge, args.Component))
         {
             var layer = args.Sprite.LayerMapGet(GravityGeneratorVisualLayers.Core);
             switch (charge)
diff --git a/Content.Client/Gravity/UI/GravityGeneratorBoundUserInterface.cs b/Content.Client/Gravity/UI/GravityGeneratorBoundUserInterface.cs
deleted file mode 100644 (file)
index 32b4074..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-using Content.Shared.Gravity;
-using JetBrains.Annotations;
-using Robust.Client.UserInterface;
-
-namespace Content.Client.Gravity.UI
-{
-    [UsedImplicitly]
-    public sealed class GravityGeneratorBoundUserInterface : BoundUserInterface
-    {
-        [ViewVariables]
-        private GravityGeneratorWindow? _window;
-
-        public GravityGeneratorBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
-        {
-        }
-
-        protected override void Open()
-        {
-            base.Open();
-
-            _window = this.CreateWindow<GravityGeneratorWindow>();
-            _window.SetEntity(Owner);
-        }
-
-        protected override void UpdateState(BoundUserInterfaceState state)
-        {
-            base.UpdateState(state);
-
-            var castState = (SharedGravityGeneratorComponent.GeneratorState) state;
-            _window?.UpdateState(castState);
-        }
-
-        public void SetPowerSwitch(bool on)
-        {
-            SendMessage(new SharedGravityGeneratorComponent.SwitchGeneratorMessage(on));
-        }
-    }
-}
diff --git a/Content.Client/Gravity/UI/GravityGeneratorWindow.xaml.cs b/Content.Client/Gravity/UI/GravityGeneratorWindow.xaml.cs
deleted file mode 100644 (file)
index 6f04133..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-using Content.Shared.Gravity;
-using Robust.Client.AutoGenerated;
-using Robust.Client.GameObjects;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.XAML;
-using FancyWindow = Content.Client.UserInterface.Controls.FancyWindow;
-
-namespace Content.Client.Gravity.UI
-{
-    [GenerateTypedNameReferences]
-    public sealed partial class GravityGeneratorWindow : FancyWindow
-    {
-        private readonly ButtonGroup _buttonGroup = new();
-
-        public event Action<bool>? OnPowerSwitch;
-
-        public GravityGeneratorWindow()
-        {
-            RobustXamlLoader.Load(this);
-            IoCManager.InjectDependencies(this);
-
-            OnButton.Group = _buttonGroup;
-            OffButton.Group = _buttonGroup;
-
-            OnButton.OnPressed += _ => OnPowerSwitch?.Invoke(true);
-            OffButton.OnPressed += _ => OnPowerSwitch?.Invoke(false);
-        }
-
-        public void SetEntity(EntityUid uid)
-        {
-            EntityView.SetEntity(uid);
-        }
-
-        public void UpdateState(SharedGravityGeneratorComponent.GeneratorState state)
-        {
-            if (state.On)
-                OnButton.Pressed = true;
-            else
-                OffButton.Pressed = true;
-
-            PowerLabel.Text = Loc.GetString(
-                "gravity-generator-window-power-label",
-                ("draw", state.PowerDraw),
-                ("max", state.PowerDrawMax));
-
-            PowerLabel.SetOnlyStyleClass(MathHelper.CloseTo(state.PowerDraw, state.PowerDrawMax) ? "Good" : "Caution");
-
-            ChargeBar.Value = state.Charge;
-            ChargeText.Text = (state.Charge / 255f).ToString("P0");
-            StatusLabel.Text = Loc.GetString(state.PowerStatus switch
-            {
-                GravityGeneratorPowerStatus.Off => "gravity-generator-window-status-off",
-                GravityGeneratorPowerStatus.Discharging => "gravity-generator-window-status-discharging",
-                GravityGeneratorPowerStatus.Charging => "gravity-generator-window-status-charging",
-                GravityGeneratorPowerStatus.FullyCharged => "gravity-generator-window-status-fully-charged",
-                _ => throw new ArgumentOutOfRangeException()
-            });
-
-            StatusLabel.SetOnlyStyleClass(state.PowerStatus switch
-            {
-                GravityGeneratorPowerStatus.Off => "Danger",
-                GravityGeneratorPowerStatus.Discharging => "Caution",
-                GravityGeneratorPowerStatus.Charging => "Caution",
-                GravityGeneratorPowerStatus.FullyCharged => "Good",
-                _ => throw new ArgumentOutOfRangeException()
-            });
-
-            EtaLabel.Text = state.EtaSeconds >= 0
-                ? Loc.GetString("gravity-generator-window-eta-value", ("left", TimeSpan.FromSeconds(state.EtaSeconds)))
-                : Loc.GetString("gravity-generator-window-eta-none");
-
-            EtaLabel.SetOnlyStyleClass(state.EtaSeconds >= 0 ? "Caution" : "Disabled");
-        }
-    }
-}
diff --git a/Content.Client/Power/PowerCharge/PowerChargeBoundUserInterface.cs b/Content.Client/Power/PowerCharge/PowerChargeBoundUserInterface.cs
new file mode 100644 (file)
index 0000000..7a36b8d
--- /dev/null
@@ -0,0 +1,38 @@
+using Content.Shared.Power;
+using Robust.Client.UserInterface;
+
+namespace Content.Client.Power.PowerCharge;
+
+public sealed class PowerChargeBoundUserInterface : BoundUserInterface
+{
+    [ViewVariables]
+    private PowerChargeWindow? _window;
+
+    public PowerChargeBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+    {
+    }
+
+    public void SetPowerSwitch(bool on)
+    {
+        SendMessage(new SwitchChargingMachineMessage(on));
+    }
+
+    protected override void Open()
+    {
+        base.Open();
+        if (!EntMan.TryGetComponent(Owner, out PowerChargeComponent? component))
+            return;
+
+        _window = this.CreateWindow<PowerChargeWindow>();
+        _window.UpdateWindow(this, Loc.GetString(component.WindowTitle));
+    }
+
+    protected override void UpdateState(BoundUserInterfaceState state)
+    {
+        base.UpdateState(state);
+        if (state is not PowerChargeState chargeState)
+            return;
+
+        _window?.UpdateState(chargeState);
+    }
+}
diff --git a/Content.Client/Power/PowerCharge/PowerChargeComponent.cs b/Content.Client/Power/PowerCharge/PowerChargeComponent.cs
new file mode 100644 (file)
index 0000000..ab5baa4
--- /dev/null
@@ -0,0 +1,10 @@
+using Content.Shared.Power;
+
+namespace Content.Client.Power.PowerCharge;
+
+/// <inheritdoc cref="Content.Shared.Power.SharedPowerChargeComponent" />
+[RegisterComponent]
+public sealed partial class PowerChargeComponent : SharedPowerChargeComponent
+{
+
+}
similarity index 60%
rename from Content.Client/Gravity/UI/GravityGeneratorWindow.xaml
rename to Content.Client/Power/PowerCharge/PowerChargeWindow.xaml
index 853f437a2bf539d5efb06bb14abf9501d8013a48..4e61255326ef3621aa1471532e5bbc85d57cdbb3 100644 (file)
@@ -1,27 +1,26 @@
 <controls:FancyWindow xmlns="https://spacestation14.io"
                 xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
-                Title="{Loc 'gravity-generator-window-title'}"
                 MinSize="270 130"
                 SetSize="360 180">
     <BoxContainer Margin="4 0" Orientation="Horizontal">
         <BoxContainer Orientation="Vertical" HorizontalExpand="True">
             <GridContainer Margin="2 0 0 0" Columns="2">
                 <!-- Power -->
-                <Label Text="{Loc 'gravity-generator-window-power'}" HorizontalExpand="True" StyleClasses="StatusFieldTitle" />
+                <Label Text="{Loc 'power-charge-window-power'}" HorizontalExpand="True" StyleClasses="StatusFieldTitle" />
                 <BoxContainer Orientation="Horizontal" MinWidth="120">
-                    <Button Name="OnButton" Text="{Loc 'gravity-generator-window-power-on'}" StyleClasses="OpenRight" />
-                    <Button Name="OffButton" Text="{Loc 'gravity-generator-window-power-off'}" StyleClasses="OpenLeft" />
+                    <Button Name="OnButton" Text="{Loc 'power-charge-window-power-on'}" StyleClasses="OpenRight" />
+                    <Button Name="OffButton" Text="{Loc 'power-charge-window-power-off'}" StyleClasses="OpenLeft" />
                 </BoxContainer>
                 <Control /> <!-- Empty control to act as a spacer in the grid. -->
                 <Label Name="PowerLabel" Text="0 / 0 W" />
                 <!-- Status -->
-                <Label Text="{Loc 'gravity-generator-window-status'}"  StyleClasses="StatusFieldTitle" />
-                <Label Name="StatusLabel" Text="{Loc 'gravity-generator-window-status-fully-charged'}" />
+                <Label Text="{Loc 'power-charge-window-status'}"  StyleClasses="StatusFieldTitle" />
+                <Label Name="StatusLabel" Text="{Loc 'power-charge-window-status-fully-charged'}" />
                 <!-- ETA -->
-                <Label Text="{Loc 'gravity-generator-window-eta'}" StyleClasses="StatusFieldTitle" />
+                <Label Text="{Loc 'power-charge-window-eta'}" StyleClasses="StatusFieldTitle" />
                 <Label Name="EtaLabel" Text="N/A" />
                 <!-- Charge -->
-                <Label Text="{Loc 'gravity-generator-window-charge'}" StyleClasses="StatusFieldTitle" />
+                <Label Text="{Loc 'power-charge-window-charge'}" StyleClasses="StatusFieldTitle" />
                 <ProgressBar Name="ChargeBar" MaxValue="255">
                     <Label Name="ChargeText" Margin="4 0" Text="0 %" />
                 </ProgressBar>
@@ -31,5 +30,4 @@
             <SpriteView Name="EntityView" SetSize="96 96" OverrideDirection="South" />
         </PanelContainer>
     </BoxContainer>
-
 </controls:FancyWindow>
diff --git a/Content.Client/Power/PowerCharge/PowerChargeWindow.xaml.cs b/Content.Client/Power/PowerCharge/PowerChargeWindow.xaml.cs
new file mode 100644 (file)
index 0000000..6739e24
--- /dev/null
@@ -0,0 +1,72 @@
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Power;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Power.PowerCharge;
+
+[GenerateTypedNameReferences]
+public sealed partial class PowerChargeWindow : FancyWindow
+{
+    private readonly ButtonGroup _buttonGroup = new();
+
+    public PowerChargeWindow()
+    {
+        RobustXamlLoader.Load(this);
+
+        OnButton.Group = _buttonGroup;
+        OffButton.Group = _buttonGroup;
+    }
+
+    public void UpdateWindow(PowerChargeBoundUserInterface bui, string title)
+    {
+        Title = title;
+
+        OnButton.OnPressed += _ => bui.SetPowerSwitch(true);
+        OffButton.OnPressed += _ => bui.SetPowerSwitch(false);
+
+        EntityView.SetEntity(bui.Owner);
+    }
+
+    public void UpdateState(PowerChargeState state)
+    {
+        if (state.On)
+            OnButton.Pressed = true;
+        else
+            OffButton.Pressed = true;
+
+        PowerLabel.Text = Loc.GetString(
+            "power-charge-window-power-label",
+            ("draw", state.PowerDraw),
+            ("max", state.PowerDrawMax));
+
+        PowerLabel.SetOnlyStyleClass(MathHelper.CloseTo(state.PowerDraw, state.PowerDrawMax) ? "Good" : "Caution");
+
+        ChargeBar.Value = state.Charge;
+        ChargeText.Text = (state.Charge / 255f).ToString("P0");
+        StatusLabel.Text = Loc.GetString(state.PowerStatus switch
+        {
+            PowerChargePowerStatus.Off => "power-charge-window-status-off",
+            PowerChargePowerStatus.Discharging => "power-charge-window-status-discharging",
+            PowerChargePowerStatus.Charging => "power-charge-window-status-charging",
+            PowerChargePowerStatus.FullyCharged => "power-charge-window-status-fully-charged",
+            _ => throw new ArgumentOutOfRangeException()
+        });
+
+        StatusLabel.SetOnlyStyleClass(state.PowerStatus switch
+        {
+            PowerChargePowerStatus.Off => "Danger",
+            PowerChargePowerStatus.Discharging => "Caution",
+            PowerChargePowerStatus.Charging => "Caution",
+            PowerChargePowerStatus.FullyCharged => "Good",
+            _ => throw new ArgumentOutOfRangeException()
+        });
+
+        EtaLabel.Text = state.EtaSeconds >= 0
+            ? Loc.GetString("power-charge-window-eta-value", ("left", TimeSpan.FromSeconds(state.EtaSeconds)))
+            : Loc.GetString("power-charge-window-eta-none");
+
+        EtaLabel.SetOnlyStyleClass(state.EtaSeconds >= 0 ? "Caution" : "Disabled");
+    }
+}
index 74641126aee89b088ca03d6ad2381eb55230070c..6aa2763888d4dab40e65b2cd135a4ad137c38370 100644 (file)
@@ -25,6 +25,9 @@ namespace Content.IntegrationTests.Tests.Gravity
   id: WeightlessGravityGeneratorDummy
   components:
   - type: GravityGenerator
+  - type: PowerCharge
+    windowTitle: gravity-generator-window-title
+    idlePower: 50
     chargeRate: 1000000000 # Set this really high so it discharges in a single tick.
     activePower: 500
   - type: ApcPowerReceiver
index 64f7a6d082085d06c040127ae809e85dfe0ba3a6..b32d6c2b8d8c28112a65a36bf585896986b4b15c 100644 (file)
@@ -21,6 +21,9 @@ namespace Content.IntegrationTests.Tests
   id: GridGravityGeneratorDummy
   components:
   - type: GravityGenerator
+  - type: PowerCharge
+    windowTitle: gravity-generator-window-title
+    idlePower: 50
     chargeRate: 1000000000 # Set this really high so it discharges in a single tick.
     activePower: 500
   - type: ApcPowerReceiver
index f946292038499ce5d9f4b3cdaa58e393c6e31235..c715a5e5f35be963e15f1803ca9caba2da0d035c 100644 (file)
@@ -8,42 +8,13 @@ namespace Content.Server.Gravity
     [Access(typeof(GravityGeneratorSystem))]
     public sealed partial class GravityGeneratorComponent : SharedGravityGeneratorComponent
     {
-        // 1% charge per second.
-        [ViewVariables(VVAccess.ReadWrite)] [DataField("chargeRate")] public float ChargeRate { get; set; } = 0.01f;
-        // The gravity generator has two power values.
-        // Idle power is assumed to be the power needed to run the control systems and interface.
-        [DataField("idlePower")] public float IdlePowerUse { get; set; }
-        // Active power is the power needed to keep the gravity field stable.
-        [DataField("activePower")] public float ActivePowerUse { get; set; }
         [DataField("lightRadiusMin")] public float LightRadiusMin { get; set; }
         [DataField("lightRadiusMax")] public float LightRadiusMax { get; set; }
 
-
-        /// <summary>
-        /// Is the power switch on?
-        /// </summary>
-        [DataField("switchedOn")]
-        public bool SwitchedOn { get; set; } = true;
-
-        /// <summary>
-        /// Is the gravity generator intact?
-        /// </summary>
-        [DataField("intact")]
-        public bool Intact { get; set; } = true;
-
-        [DataField("maxCharge")]
-        public float MaxCharge { get; set; } = 1;
-
-        // 0 -> 1
-        [ViewVariables(VVAccess.ReadWrite)] [DataField("charge")] public float Charge { get; set; } = 1;
-
         /// <summary>
         /// Is the gravity generator currently "producing" gravity?
         /// </summary>
         [ViewVariables]
         public bool GravityActive { get; set; } = false;
-
-        // Do we need a UI update even if the charge doesn't change? Used by power button.
-        [ViewVariables] public bool NeedUIUpdate { get; set; }
     }
 }
index 0b53df63fd0a7aad8b6921d0ffd905a818201bd4..5ab2dc893108b5902793a930bf74c7006202767d 100644 (file)
-using Content.Server.Administration.Logs;
-using Content.Server.Audio;
 using Content.Server.Power.Components;
-using Content.Shared.Database;
+using Content.Server.Power.EntitySystems;
 using Content.Shared.Gravity;
-using Content.Shared.Interaction;
-using Robust.Server.GameObjects;
-using Robust.Shared.Player;
 
-namespace Content.Server.Gravity
-{
-    public sealed class GravityGeneratorSystem : EntitySystem
-    {
-        [Dependency] private readonly IAdminLogManager _adminLogger = default!;
-        [Dependency] private readonly AmbientSoundSystem _ambientSoundSystem = default!;
-        [Dependency] private readonly GravitySystem _gravitySystem = default!;
-        [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
-        [Dependency] private readonly SharedPointLightSystem _lights = default!;
-        [Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
-
-        public override void Initialize()
-        {
-            base.Initialize();
-
-            SubscribeLocalEvent<GravityGeneratorComponent, ComponentInit>(OnCompInit);
-            SubscribeLocalEvent<GravityGeneratorComponent, ComponentShutdown>(OnComponentShutdown);
-            SubscribeLocalEvent<GravityGeneratorComponent, EntParentChangedMessage>(OnParentChanged); // Or just anchor changed?
-            SubscribeLocalEvent<GravityGeneratorComponent, InteractHandEvent>(OnInteractHand);
-            SubscribeLocalEvent<GravityGeneratorComponent, SharedGravityGeneratorComponent.SwitchGeneratorMessage>(
-                OnSwitchGenerator);
-        }
-
-        private void OnParentChanged(EntityUid uid, GravityGeneratorComponent component, ref EntParentChangedMessage args)
-        {
-            if (component.GravityActive && TryComp(args.OldParent, out GravityComponent? gravity))
-            {
-                _gravitySystem.RefreshGravity(args.OldParent.Value, gravity);
-            }
-        }
-
-        private void OnComponentShutdown(EntityUid uid, GravityGeneratorComponent component, ComponentShutdown args)
-        {
-            if (component.GravityActive &&
-                TryComp(uid, out TransformComponent? xform) &&
-                TryComp(xform.ParentUid, out GravityComponent? gravity))
-            {
-                component.GravityActive = false;
-                _gravitySystem.RefreshGravity(xform.ParentUid, gravity);
-            }
-        }
-
-        public override void Update(float frameTime)
-        {
-            base.Update(frameTime);
-
-            var query = EntityQueryEnumerator<GravityGeneratorComponent, ApcPowerReceiverComponent>();
-            while (query.MoveNext(out var uid, out var gravGen, out var powerReceiver))
-            {
-                var ent = (uid, gravGen, powerReceiver);
-                if (!gravGen.Intact)
-                    continue;
-
-                // Calculate charge rate based on power state and such.
-                // Negative charge rate means discharging.
-                float chargeRate;
-                if (gravGen.SwitchedOn)
-                {
-                    if (powerReceiver.Powered)
-                    {
-                        chargeRate = gravGen.ChargeRate;
-                    }
-                    else
-                    {
-                        // Scale discharge rate such that if we're at 25% active power we discharge at 75% rate.
-                        var receiving = powerReceiver.PowerReceived;
-                        var mainSystemPower = Math.Max(0, receiving - gravGen.IdlePowerUse);
-                        var ratio = 1 - mainSystemPower / (gravGen.ActivePowerUse - gravGen.IdlePowerUse);
-                        chargeRate = -(ratio * gravGen.ChargeRate);
-                    }
-                }
-                else
-                {
-                    chargeRate = -gravGen.ChargeRate;
-                }
-
-                var active = gravGen.GravityActive;
-                var lastCharge = gravGen.Charge;
-                gravGen.Charge = Math.Clamp(gravGen.Charge + frameTime * chargeRate, 0, gravGen.MaxCharge);
-                if (chargeRate > 0)
-                {
-                    // Charging.
-                    if (MathHelper.CloseTo(gravGen.Charge, gravGen.MaxCharge) && !gravGen.GravityActive)
-                    {
-                        gravGen.GravityActive = true;
-                    }
-                }
-                else
-                {
-                    // Discharging
-                    if (MathHelper.CloseTo(gravGen.Charge, 0) && gravGen.GravityActive)
-                    {
-                        gravGen.GravityActive = false;
-                    }
-                }
-
-                var updateUI = gravGen.NeedUIUpdate;
-                if (!MathHelper.CloseTo(lastCharge, gravGen.Charge))
-                {
-                    UpdateState(ent);
-                    updateUI = true;
-                }
-
-                if (updateUI)
-                    UpdateUI(ent, chargeRate);
-
-                if (active != gravGen.GravityActive &&
-                    TryComp(uid, out TransformComponent? xform) &&
-                    TryComp<GravityComponent>(xform.ParentUid, out var gravity))
-                {
-                    // Force it on in the faster path.
-                    if (gravGen.GravityActive)
-                    {
-                        _gravitySystem.EnableGravity(xform.ParentUid, gravity);
-                    }
-                    else
-                    {
-                        _gravitySystem.RefreshGravity(xform.ParentUid, gravity);
-                    }
-                }
-            }
-        }
-
-        private void SetSwitchedOn(EntityUid uid, GravityGeneratorComponent component, bool on,
-            ApcPowerReceiverComponent? powerReceiver = null, EntityUid? user = null)
-        {
-            if (!Resolve(uid, ref powerReceiver))
-                return;
-
-            if (user != null)
-                _adminLogger.Add(LogType.Action, on ? LogImpact.Medium : LogImpact.High, $"{ToPrettyString(user)} set ${ToPrettyString(uid):target} to {(on ? "on" : "off")}");
-
-            component.SwitchedOn = on;
-            UpdatePowerState(component, powerReceiver);
-            component.NeedUIUpdate = true;
-        }
-
-        private static void UpdatePowerState(
-            GravityGeneratorComponent component,
-            ApcPowerReceiverComponent powerReceiver)
-        {
-            powerReceiver.Load = component.SwitchedOn ? component.ActivePowerUse : component.IdlePowerUse;
-        }
-
-        private void UpdateUI(Entity<GravityGeneratorComponent, ApcPowerReceiverComponent> ent, float chargeRate)
-        {
-            var (_, component, powerReceiver) = ent;
-            if (!_uiSystem.IsUiOpen(ent.Owner, SharedGravityGeneratorComponent.GravityGeneratorUiKey.Key))
-                return;
-
-            var chargeTarget = chargeRate < 0 ? 0 : component.MaxCharge;
-            short chargeEta;
-            var atTarget = false;
-            if (MathHelper.CloseTo(component.Charge, chargeTarget))
-            {
-                chargeEta = short.MinValue; // N/A
-                atTarget = true;
-            }
-            else
-            {
-                var diff = chargeTarget - component.Charge;
-                chargeEta = (short) Math.Abs(diff / chargeRate);
-            }
+namespace Content.Server.Gravity;
 
-            var status = chargeRate switch
-            {
-                > 0 when atTarget => GravityGeneratorPowerStatus.FullyCharged,
-                < 0 when atTarget => GravityGeneratorPowerStatus.Off,
-                > 0 => GravityGeneratorPowerStatus.Charging,
-                < 0 => GravityGeneratorPowerStatus.Discharging,
-                _ => throw new ArgumentOutOfRangeException()
-            };
-
-            var state = new SharedGravityGeneratorComponent.GeneratorState(
-                component.SwitchedOn,
-                (byte) (component.Charge * 255),
-                status,
-                (short) Math.Round(powerReceiver.PowerReceived),
-                (short) Math.Round(powerReceiver.Load),
-                chargeEta
-            );
-
-            _uiSystem.SetUiState(
-                ent.Owner,
-                SharedGravityGeneratorComponent.GravityGeneratorUiKey.Key,
-                state);
-
-            component.NeedUIUpdate = false;
-        }
-
-        private void OnCompInit(Entity<GravityGeneratorComponent> ent, ref ComponentInit args)
-        {
-            ApcPowerReceiverComponent? powerReceiver = null;
-            if (!Resolve(ent, ref powerReceiver, false))
-                return;
-
-            UpdatePowerState(ent, powerReceiver);
-            UpdateState((ent, ent.Comp, powerReceiver));
-        }
-
-        private void OnInteractHand(EntityUid uid, GravityGeneratorComponent component, InteractHandEvent args)
-        {
-            ApcPowerReceiverComponent? powerReceiver = default!;
-            if (!Resolve(uid, ref powerReceiver))
-                return;
-
-            // Do not allow opening UI if broken or unpowered.
-            if (!component.Intact || powerReceiver.PowerReceived < component.IdlePowerUse)
-                return;
-
-            _uiSystem.OpenUi(uid, SharedGravityGeneratorComponent.GravityGeneratorUiKey.Key, args.User);
-            component.NeedUIUpdate = true;
-        }
-
-        public void UpdateState(Entity<GravityGeneratorComponent, ApcPowerReceiverComponent> ent)
-        {
-            var (uid, grav, powerReceiver) = ent;
-            var appearance = EntityManager.GetComponentOrNull<AppearanceComponent>(uid);
-            _appearance.SetData(uid, GravityGeneratorVisuals.Charge, grav.Charge, appearance);
-
-            if (_lights.TryGetLight(uid, out var pointLight))
-            {
-                _lights.SetEnabled(uid, grav.Charge > 0, pointLight);
-                _lights.SetRadius(uid, MathHelper.Lerp(grav.LightRadiusMin, grav.LightRadiusMax, grav.Charge), pointLight);
-            }
-
-            if (!grav.Intact)
-            {
-                MakeBroken((uid, grav), appearance);
-            }
-            else if (powerReceiver.PowerReceived < grav.IdlePowerUse)
-            {
-                MakeUnpowered((uid, grav), appearance);
-            }
-            else if (!grav.SwitchedOn)
-            {
-                MakeOff((uid, grav), appearance);
-            }
-            else
-            {
-                MakeOn((uid, grav), appearance);
-            }
-        }
+public sealed class GravityGeneratorSystem : EntitySystem
+{
+    [Dependency] private readonly GravitySystem _gravitySystem = default!;
+    [Dependency] private readonly SharedPointLightSystem _lights = default!;
 
-        private void MakeBroken(Entity<GravityGeneratorComponent> ent, AppearanceComponent? appearance)
-        {
-            _ambientSoundSystem.SetAmbience(ent, false);
+    public override void Initialize()
+    {
+        base.Initialize();
 
-            _appearance.SetData(ent, GravityGeneratorVisuals.State, GravityGeneratorStatus.Broken);
-        }
+        SubscribeLocalEvent<GravityGeneratorComponent, EntParentChangedMessage>(OnParentChanged);
+        SubscribeLocalEvent<GravityGeneratorComponent, ChargedMachineActivatedEvent>(OnActivated);
+        SubscribeLocalEvent<GravityGeneratorComponent, ChargedMachineDeactivatedEvent>(OnDeactivated);
+    }
 
-        private void MakeUnpowered(Entity<GravityGeneratorComponent> ent, AppearanceComponent? appearance)
+    public override void Update(float frameTime)
+    {
+        base.Update(frameTime);
+        var query = EntityQueryEnumerator<GravityGeneratorComponent, PowerChargeComponent>();
+        while (query.MoveNext(out var uid, out var grav, out var charge))
         {
-            _ambientSoundSystem.SetAmbience(ent, false);
+            if (!_lights.TryGetLight(uid, out var pointLight))
+                continue;
 
-            _appearance.SetData(ent, GravityGeneratorVisuals.State, GravityGeneratorStatus.Unpowered, appearance);
+            _lights.SetEnabled(uid, charge.Charge > 0, pointLight);
+            _lights.SetRadius(uid, MathHelper.Lerp(grav.LightRadiusMin, grav.LightRadiusMax, charge.Charge),
+                pointLight);
         }
+    }
 
-        private void MakeOff(Entity<GravityGeneratorComponent> ent, AppearanceComponent? appearance)
+    private void OnActivated(Entity<GravityGeneratorComponent> ent, ref ChargedMachineActivatedEvent args)
+    {
+        ent.Comp.GravityActive = true;
+        if (TryComp<TransformComponent>(ent, out var xform) &&
+            TryComp(xform.ParentUid, out GravityComponent? gravity))
         {
-            _ambientSoundSystem.SetAmbience(ent, false);
-
-            _appearance.SetData(ent, GravityGeneratorVisuals.State, GravityGeneratorStatus.Off, appearance);
+            _gravitySystem.EnableGravity(xform.ParentUid, gravity);
         }
+    }
 
-        private void MakeOn(Entity<GravityGeneratorComponent> ent, AppearanceComponent? appearance)
+    private void OnDeactivated(Entity<GravityGeneratorComponent> ent, ref ChargedMachineDeactivatedEvent args)
+    {
+        ent.Comp.GravityActive = false;
+        if (TryComp<TransformComponent>(ent, out var xform) &&
+            TryComp(xform.ParentUid, out GravityComponent? gravity))
         {
-            _ambientSoundSystem.SetAmbience(ent, true);
-
-            _appearance.SetData(ent, GravityGeneratorVisuals.State, GravityGeneratorStatus.On, appearance);
+            _gravitySystem.RefreshGravity(xform.ParentUid, gravity);
         }
+    }
 
-        private void OnSwitchGenerator(
-            EntityUid uid,
-            GravityGeneratorComponent component,
-            SharedGravityGeneratorComponent.SwitchGeneratorMessage args)
+    private void OnParentChanged(EntityUid uid, GravityGeneratorComponent component, ref EntParentChangedMessage args)
+    {
+        if (component.GravityActive && TryComp(args.OldParent, out GravityComponent? gravity))
         {
-            SetSwitchedOn(uid, component, args.On, user: args.Actor);
+            _gravitySystem.RefreshGravity(args.OldParent.Value, gravity);
         }
     }
 }
diff --git a/Content.Server/Power/Components/PowerChargeComponent.cs b/Content.Server/Power/Components/PowerChargeComponent.cs
new file mode 100644 (file)
index 0000000..03c6e8e
--- /dev/null
@@ -0,0 +1,66 @@
+using Content.Server.Power.EntitySystems;
+using Content.Shared.Power;
+
+namespace Content.Server.Power.Components;
+
+/// <inheritdoc cref="Content.Shared.Power.SharedPowerChargeComponent" />
+[RegisterComponent]
+[Access(typeof(PowerChargeSystem))]
+public sealed partial class PowerChargeComponent : SharedPowerChargeComponent
+{
+    /// <summary>
+    /// Change in charge per second.
+    /// </summary>
+    [DataField]
+    public float ChargeRate { get; set; } = 0.01f;
+
+    /// <summary>
+    /// Baseline power that this machine consumes.
+    /// </summary>
+    [DataField("idlePower")]
+    public float IdlePowerUse { get; set; }
+
+    /// <summary>
+    /// Power consumed when <see cref="SwitchedOn"/> is true.
+    /// </summary>
+    [DataField("activePower")]
+    public float ActivePowerUse { get; set; }
+
+    /// <summary>
+    /// Is the gravity generator intact?
+    /// </summary>
+    [DataField]
+    public bool Intact { get; set; } = true;
+
+    /// <summary>
+    /// Is the power switch on?
+    /// </summary>
+    [DataField]
+    public bool SwitchedOn { get; set; } = true;
+
+    /// <summary>
+    /// Whether or not the power is switched on and the entity has charged up.
+    /// </summary>
+    [DataField]
+    public bool Active { get; set; }
+
+    [DataField]
+    public float MaxCharge { get; set; } = 1;
+
+    /// <summary>
+    /// The UI key of the UI that's used with this machine.<br/>
+    /// This is used to allow machine power charging to be integrated into any ui
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadOnly)]
+    public Enum UiKey { get; set; } = PowerChargeUiKey.Key;
+
+    /// <summary>
+    /// Current charge value.
+    /// Goes from 0 to 1.
+    /// </summary>
+    [DataField]
+    public float Charge { get; set; } = 1;
+
+    [ViewVariables]
+    public bool NeedUIUpdate { get; set; }
+}
diff --git a/Content.Server/Power/EntitySystems/PowerChargeSystem.cs b/Content.Server/Power/EntitySystems/PowerChargeSystem.cs
new file mode 100644 (file)
index 0000000..7935af1
--- /dev/null
@@ -0,0 +1,283 @@
+using Content.Server.Administration.Logs;
+using Content.Server.Audio;
+using Content.Server.Power.Components;
+using Content.Shared.Database;
+using Content.Shared.Power;
+using Content.Shared.UserInterface;
+using Robust.Server.GameObjects;
+using Robust.Shared.Player;
+
+namespace Content.Server.Power.EntitySystems;
+
+public sealed class PowerChargeSystem : EntitySystem
+{
+    [Dependency] private readonly IAdminLogManager _adminLogger = default!;
+    [Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
+    [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+    [Dependency] private readonly AmbientSoundSystem _ambientSoundSystem = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<PowerChargeComponent, MapInitEvent>(OnMapInit);
+        SubscribeLocalEvent<PowerChargeComponent, ComponentShutdown>(OnComponentShutdown);
+        SubscribeLocalEvent<PowerChargeComponent, ActivatableUIOpenAttemptEvent>(OnUIOpenAttempt);
+        SubscribeLocalEvent<PowerChargeComponent, AfterActivatableUIOpenEvent>(OnAfterUiOpened);
+        SubscribeLocalEvent<PowerChargeComponent, AnchorStateChangedEvent>(OnAnchorStateChange);
+
+        // This needs to be ui key agnostic
+        SubscribeLocalEvent<PowerChargeComponent, SwitchChargingMachineMessage>(OnSwitchGenerator);
+    }
+
+    private void OnAnchorStateChange(EntityUid uid, PowerChargeComponent component, AnchorStateChangedEvent args)
+    {
+        if (args.Anchored || !TryComp<ApcPowerReceiverComponent>(uid, out var powerReceiverComponent))
+            return;
+
+        component.Active = false;
+        component.Charge = 0;
+        UpdateState(new Entity<PowerChargeComponent, ApcPowerReceiverComponent>(uid, component, powerReceiverComponent));
+    }
+
+    private void OnAfterUiOpened(EntityUid uid, PowerChargeComponent component, AfterActivatableUIOpenEvent args)
+    {
+        if (!TryComp<ApcPowerReceiverComponent>(uid, out var apcPowerReceiver))
+            return;
+
+        UpdateUI((uid, component, apcPowerReceiver), component.ChargeRate);
+    }
+
+    private void OnSwitchGenerator(EntityUid uid, PowerChargeComponent component, SwitchChargingMachineMessage args)
+    {
+        SetSwitchedOn(uid, component, args.On, user: args.Actor);
+    }
+
+    private void OnUIOpenAttempt(EntityUid uid, PowerChargeComponent component, ActivatableUIOpenAttemptEvent args)
+    {
+        if (!component.Intact)
+            args.Cancel();
+    }
+
+    private void OnComponentShutdown(EntityUid uid, PowerChargeComponent component, ComponentShutdown args)
+    {
+        if (!component.Active)
+            return;
+
+        component.Active = false;
+
+        var eventArgs = new ChargedMachineDeactivatedEvent();
+        RaiseLocalEvent(uid, ref eventArgs);
+    }
+
+    private void OnMapInit(Entity<PowerChargeComponent> ent, ref MapInitEvent args)
+    {
+        ApcPowerReceiverComponent? powerReceiver = null;
+        if (!Resolve(ent, ref powerReceiver, false))
+            return;
+
+        UpdatePowerState(ent, powerReceiver);
+        UpdateState((ent, ent.Comp, powerReceiver));
+    }
+
+    private void SetSwitchedOn(EntityUid uid, PowerChargeComponent component, bool on,
+        ApcPowerReceiverComponent? powerReceiver = null, EntityUid? user = null)
+    {
+        if (!Resolve(uid, ref powerReceiver))
+            return;
+
+        if (user is { } )
+            _adminLogger.Add(LogType.Action, on ? LogImpact.Medium : LogImpact.High, $"{ToPrettyString(user):player} set ${ToPrettyString(uid):target} to {(on ? "on" : "off")}");
+
+        component.SwitchedOn = on;
+        UpdatePowerState(component, powerReceiver);
+        component.NeedUIUpdate = true;
+    }
+
+    private static void UpdatePowerState(PowerChargeComponent component, ApcPowerReceiverComponent powerReceiver)
+    {
+        powerReceiver.Load = component.SwitchedOn ? component.ActivePowerUse : component.IdlePowerUse;
+    }
+
+    public override void Update(float frameTime)
+    {
+        base.Update(frameTime);
+
+        var query = EntityQueryEnumerator<PowerChargeComponent, ApcPowerReceiverComponent>();
+        while (query.MoveNext(out var uid, out var chargingMachine, out var powerReceiver))
+        {
+            var ent = (uid, gravGen: chargingMachine, powerReceiver);
+            if (!chargingMachine.Intact)
+                continue;
+
+            // Calculate charge rate based on power state and such.
+            // Negative charge rate means discharging.
+            float chargeRate;
+            if (chargingMachine.SwitchedOn)
+            {
+                if (powerReceiver.Powered)
+                {
+                    chargeRate = chargingMachine.ChargeRate;
+                }
+                else
+                {
+                    // Scale discharge rate such that if we're at 25% active power we discharge at 75% rate.
+                    var receiving = powerReceiver.PowerReceived;
+                    var mainSystemPower = Math.Max(0, receiving - chargingMachine.IdlePowerUse);
+                    var ratio = 1 - mainSystemPower / (chargingMachine.ActivePowerUse - chargingMachine.IdlePowerUse);
+                    chargeRate = -(ratio * chargingMachine.ChargeRate);
+                }
+            }
+            else
+            {
+                chargeRate = -chargingMachine.ChargeRate;
+            }
+
+            var active = chargingMachine.Active;
+            var lastCharge = chargingMachine.Charge;
+            chargingMachine.Charge = Math.Clamp(chargingMachine.Charge + frameTime * chargeRate, 0, chargingMachine.MaxCharge);
+            if (chargeRate > 0)
+            {
+                // Charging.
+                if (MathHelper.CloseTo(chargingMachine.Charge, chargingMachine.MaxCharge) && !chargingMachine.Active)
+                {
+                    chargingMachine.Active = true;
+                }
+            }
+            else
+            {
+                // Discharging
+                if (MathHelper.CloseTo(chargingMachine.Charge, 0) && chargingMachine.Active)
+                {
+                    chargingMachine.Active = false;
+                }
+            }
+
+            var updateUI = chargingMachine.NeedUIUpdate;
+            if (!MathHelper.CloseTo(lastCharge, chargingMachine.Charge))
+            {
+                UpdateState(ent);
+                updateUI = true;
+            }
+
+            if (updateUI)
+                UpdateUI(ent, chargeRate);
+
+            if (active == chargingMachine.Active)
+                continue;
+
+            if (chargingMachine.Active)
+            {
+                var eventArgs = new ChargedMachineActivatedEvent();
+                RaiseLocalEvent(uid, ref eventArgs);
+            }
+            else
+            {
+                var eventArgs = new ChargedMachineDeactivatedEvent();
+                RaiseLocalEvent(uid, ref eventArgs);
+            }
+        }
+    }
+
+    private void UpdateUI(Entity<PowerChargeComponent, ApcPowerReceiverComponent> ent, float chargeRate)
+    {
+        var (_, component, powerReceiver) = ent;
+        if (!_uiSystem.IsUiOpen(ent.Owner, component.UiKey))
+            return;
+
+        var chargeTarget = chargeRate < 0 ? 0 : component.MaxCharge;
+        short chargeEta;
+        var atTarget = false;
+        if (MathHelper.CloseTo(component.Charge, chargeTarget))
+        {
+            chargeEta = short.MinValue; // N/A
+            atTarget = true;
+        }
+        else
+        {
+            var diff = chargeTarget - component.Charge;
+            chargeEta = (short) Math.Abs(diff / chargeRate);
+        }
+
+        var status = chargeRate switch
+        {
+            > 0 when atTarget => PowerChargePowerStatus.FullyCharged,
+            < 0 when atTarget => PowerChargePowerStatus.Off,
+            > 0 => PowerChargePowerStatus.Charging,
+            < 0 => PowerChargePowerStatus.Discharging,
+            _ => throw new ArgumentOutOfRangeException()
+        };
+
+        var state = new PowerChargeState(
+            component.SwitchedOn,
+            (byte) (component.Charge * 255),
+            status,
+            (short) Math.Round(powerReceiver.PowerReceived),
+            (short) Math.Round(powerReceiver.Load),
+            chargeEta
+        );
+
+        _uiSystem.SetUiState(
+            ent.Owner,
+            component.UiKey,
+            state);
+
+        component.NeedUIUpdate = false;
+    }
+
+    private void UpdateState(Entity<PowerChargeComponent, ApcPowerReceiverComponent> ent)
+    {
+        var (uid, machine, powerReceiver) = ent;
+        var appearance = EntityManager.GetComponentOrNull<AppearanceComponent>(uid);
+        _appearance.SetData(uid, PowerChargeVisuals.Charge, machine.Charge, appearance);
+        _appearance.SetData(uid, PowerChargeVisuals.Active, machine.Active);
+
+
+        if (!machine.Intact)
+        {
+            MakeBroken((uid, machine), appearance);
+        }
+        else if (powerReceiver.PowerReceived < machine.IdlePowerUse)
+        {
+            MakeUnpowered((uid, machine), appearance);
+        }
+        else if (!machine.SwitchedOn)
+        {
+            MakeOff((uid, machine), appearance);
+        }
+        else
+        {
+            MakeOn((uid, machine), appearance);
+        }
+    }
+
+    private void MakeBroken(Entity<PowerChargeComponent> ent, AppearanceComponent? appearance)
+    {
+        _ambientSoundSystem.SetAmbience(ent, false);
+
+        _appearance.SetData(ent, PowerChargeVisuals.State, PowerChargeStatus.Broken, appearance);
+    }
+
+    private void MakeUnpowered(Entity<PowerChargeComponent> ent, AppearanceComponent? appearance)
+    {
+        _ambientSoundSystem.SetAmbience(ent, false);
+
+        _appearance.SetData(ent, PowerChargeVisuals.State, PowerChargeStatus.Unpowered, appearance);
+    }
+
+    private void MakeOff(Entity<PowerChargeComponent> ent, AppearanceComponent? appearance)
+    {
+        _ambientSoundSystem.SetAmbience(ent, false);
+
+        _appearance.SetData(ent, PowerChargeVisuals.State, PowerChargeStatus.Off, appearance);
+    }
+
+    private void MakeOn(Entity<PowerChargeComponent> ent, AppearanceComponent? appearance)
+    {
+        _ambientSoundSystem.SetAmbience(ent, true);
+
+        _appearance.SetData(ent, PowerChargeVisuals.State, PowerChargeStatus.On, appearance);
+    }
+}
+
+[ByRefEvent] public record struct ChargedMachineActivatedEvent;
+[ByRefEvent] public record struct ChargedMachineDeactivatedEvent;
diff --git a/Content.Server/Shuttles/Components/StationAnchorComponent.cs b/Content.Server/Shuttles/Components/StationAnchorComponent.cs
new file mode 100644 (file)
index 0000000..c971ed9
--- /dev/null
@@ -0,0 +1,11 @@
+using Content.Server.Shuttles.Systems;
+
+namespace Content.Server.Shuttles.Components;
+
+[RegisterComponent]
+[Access(typeof(StationAnchorSystem))]
+public sealed partial class StationAnchorComponent : Component
+{
+    [DataField("switchedOn")]
+    public bool SwitchedOn { get; set; } = true;
+}
diff --git a/Content.Server/Shuttles/Systems/StationAnchorSystem.cs b/Content.Server/Shuttles/Systems/StationAnchorSystem.cs
new file mode 100644 (file)
index 0000000..9ac1ed4
--- /dev/null
@@ -0,0 +1,86 @@
+using Content.Server.Popups;
+using Content.Server.Power.EntitySystems;
+using Content.Server.Shuttles.Components;
+using Content.Shared.Construction.Components;
+using Content.Shared.Popups;
+
+namespace Content.Server.Shuttles.Systems;
+
+public sealed class StationAnchorSystem : EntitySystem
+{
+    [Dependency] private readonly ShuttleSystem _shuttleSystem = default!;
+    [Dependency] private readonly PopupSystem _popupSystem = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<StationAnchorComponent, UnanchorAttemptEvent>(OnUnanchorAttempt);
+        SubscribeLocalEvent<StationAnchorComponent, AnchorStateChangedEvent>(OnAnchorStationChange);
+
+        SubscribeLocalEvent<StationAnchorComponent, ChargedMachineActivatedEvent>(OnActivated);
+        SubscribeLocalEvent<StationAnchorComponent, ChargedMachineDeactivatedEvent>(OnDeactivated);
+
+        SubscribeLocalEvent<StationAnchorComponent, MapInitEvent>(OnMapInit);
+    }
+
+    private void OnMapInit(Entity<StationAnchorComponent> ent, ref MapInitEvent args)
+    {
+        if (!ent.Comp.SwitchedOn)
+            return;
+
+        SetStatus(ent, true);
+    }
+
+    private void OnActivated(Entity<StationAnchorComponent> ent, ref ChargedMachineActivatedEvent args)
+    {
+        SetStatus(ent, true);
+    }
+
+    private void OnDeactivated(Entity<StationAnchorComponent> ent, ref ChargedMachineDeactivatedEvent args)
+    {
+        SetStatus(ent, false);
+    }
+
+    /// <summary>
+    /// Prevent unanchoring when anchor is active
+    /// </summary>
+    private void OnUnanchorAttempt(Entity<StationAnchorComponent> ent, ref UnanchorAttemptEvent args)
+    {
+        if (!ent.Comp.SwitchedOn)
+            return;
+
+        _popupSystem.PopupEntity(
+            Loc.GetString("station-anchor-unanchoring-failed"),
+            ent,
+            args.User,
+            PopupType.Medium);
+
+        args.Cancel();
+    }
+
+    private void OnAnchorStationChange(Entity<StationAnchorComponent> ent, ref AnchorStateChangedEvent args)
+    {
+        if (!args.Anchored)
+            SetStatus(ent, false);
+    }
+
+    private void SetStatus(Entity<StationAnchorComponent> ent, bool enabled, ShuttleComponent? shuttleComponent = default)
+    {
+        var transform = Transform(ent);
+        var grid = transform.GridUid;
+        if (!grid.HasValue || !transform.Anchored && enabled || !Resolve(grid.Value, ref shuttleComponent))
+            return;
+
+        if (enabled)
+        {
+            _shuttleSystem.Disable(grid.Value);
+        }
+        else
+        {
+            _shuttleSystem.Enable(grid.Value);
+        }
+
+        shuttleComponent.Enabled = !enabled;
+        ent.Comp.SwitchedOn = enabled;
+    }
+}
index 1f78e333f150524af3a4bd65686ff36515e28699..75b636b2fa89ac0c3e4a282a7aaee6ff11ce30c2 100644 (file)
+using Content.Shared.Power;
 using Robust.Shared.GameStates;
-using Robust.Shared.Serialization;
 
-namespace Content.Shared.Gravity
-{
-    [NetworkedComponent()]
-    [Virtual]
-    public partial class SharedGravityGeneratorComponent : Component
-    {
-        /// <summary>
-        /// A map of the sprites used by the gravity generator given its status.
-        /// </summary>
-        [DataField("spriteMap")]
-        [Access(typeof(SharedGravitySystem))]
-        public Dictionary<GravityGeneratorStatus, string> SpriteMap = new();
-
-        /// <summary>
-        /// The sprite used by the core of the gravity generator when the gravity generator is starting up.
-        /// </summary>
-        [DataField("coreStartupState")]
-        [ViewVariables(VVAccess.ReadWrite)]
-        public string CoreStartupState = "startup";
-
-        /// <summary>
-        /// The sprite used by the core of the gravity generator when the gravity generator is idle.
-        /// </summary>
-        [DataField("coreIdleState")]
-        [ViewVariables(VVAccess.ReadWrite)]
-        public string CoreIdleState = "idle";
-
-        /// <summary>
-        /// The sprite used by the core of the gravity generator when the gravity generator is activating.
-        /// </summary>
-        [DataField("coreActivatingState")]
-        [ViewVariables(VVAccess.ReadWrite)]
-        public string CoreActivatingState = "activating";
-
-        /// <summary>
-        /// The sprite used by the core of the gravity generator when the gravity generator is active.
-        /// </summary>
-        [DataField("coreActivatedState")]
-        [ViewVariables(VVAccess.ReadWrite)]
-        public string CoreActivatedState = "activated";
-
-        /// <summary>
-        ///     Sent to the server to set whether the generator should be on or off
-        /// </summary>
-        [Serializable, NetSerializable]
-        public sealed class SwitchGeneratorMessage : BoundUserInterfaceMessage
-        {
-            public bool On;
-
-            public SwitchGeneratorMessage(bool on)
-            {
-                On = on;
-            }
-        }
+namespace Content.Shared.Gravity;
 
-        [Serializable, NetSerializable]
-        public sealed class GeneratorState : BoundUserInterfaceState
-        {
-            public bool On;
-            // 0 -> 255
-            public byte Charge;
-            public GravityGeneratorPowerStatus PowerStatus;
-            public short PowerDraw;
-            public short PowerDrawMax;
-            public short EtaSeconds;
-
-            public GeneratorState(
-                bool on,
-                byte charge,
-                GravityGeneratorPowerStatus powerStatus,
-                short powerDraw,
-                short powerDrawMax,
-                short etaSeconds)
-            {
-                On = on;
-                Charge = charge;
-                PowerStatus = powerStatus;
-                PowerDraw = powerDraw;
-                PowerDrawMax = powerDrawMax;
-                EtaSeconds = etaSeconds;
-            }
-        }
-
-        [Serializable, NetSerializable]
-        public enum GravityGeneratorUiKey
-        {
-            Key
-        }
-    }
-
-    [Serializable, NetSerializable]
-    public enum GravityGeneratorVisuals
-    {
-        State,
-        Charge
-    }
-
-    [Serializable, NetSerializable]
-    public enum GravityGeneratorStatus
-    {
-        Broken,
-        Unpowered,
-        Off,
-        On
-    }
-
-    [Serializable, NetSerializable]
-    public enum GravityGeneratorPowerStatus : byte
-    {
-        Off,
-        Discharging,
-        Charging,
-        FullyCharged
-    }
+[NetworkedComponent()]
+[Virtual]
+public partial class SharedGravityGeneratorComponent : Component
+{
+    /// <summary>
+    /// A map of the sprites used by the gravity generator given its status.
+    /// </summary>
+    [DataField("spriteMap")]
+    [Access(typeof(SharedGravitySystem))]
+    public Dictionary<PowerChargeStatus, string> SpriteMap = new();
+
+    /// <summary>
+    /// The sprite used by the core of the gravity generator when the gravity generator is starting up.
+    /// </summary>
+    [DataField("coreStartupState")]
+    [ViewVariables(VVAccess.ReadWrite)]
+    public string CoreStartupState = "startup";
+
+    /// <summary>
+    /// The sprite used by the core of the gravity generator when the gravity generator is idle.
+    /// </summary>
+    [DataField("coreIdleState")]
+    [ViewVariables(VVAccess.ReadWrite)]
+    public string CoreIdleState = "idle";
+
+    /// <summary>
+    /// The sprite used by the core of the gravity generator when the gravity generator is activating.
+    /// </summary>
+    [DataField("coreActivatingState")]
+    [ViewVariables(VVAccess.ReadWrite)]
+    public string CoreActivatingState = "activating";
+
+    /// <summary>
+    /// The sprite used by the core of the gravity generator when the gravity generator is active.
+    /// </summary>
+    [DataField("coreActivatedState")]
+    [ViewVariables(VVAccess.ReadWrite)]
+    public string CoreActivatedState = "activated";
 }
diff --git a/Content.Shared/Power/SharedPowerCharge.cs b/Content.Shared/Power/SharedPowerCharge.cs
new file mode 100644 (file)
index 0000000..5599930
--- /dev/null
@@ -0,0 +1,77 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Power;
+
+/// <summary>
+///     Sent to the server to set whether the machine should be on or off
+/// </summary>
+[Serializable, NetSerializable]
+public sealed class SwitchChargingMachineMessage : BoundUserInterfaceMessage
+{
+    public bool On;
+
+    public SwitchChargingMachineMessage(bool on)
+    {
+        On = on;
+    }
+}
+
+[Serializable, NetSerializable]
+public sealed class PowerChargeState : BoundUserInterfaceState
+{
+    public bool On;
+    // 0 -> 255
+    public byte Charge;
+    public PowerChargePowerStatus PowerStatus;
+    public short PowerDraw;
+    public short PowerDrawMax;
+    public short EtaSeconds;
+
+    public PowerChargeState(
+        bool on,
+        byte charge,
+        PowerChargePowerStatus powerStatus,
+        short powerDraw,
+        short powerDrawMax,
+        short etaSeconds)
+    {
+        On = on;
+        Charge = charge;
+        PowerStatus = powerStatus;
+        PowerDraw = powerDraw;
+        PowerDrawMax = powerDrawMax;
+        EtaSeconds = etaSeconds;
+    }
+}
+
+[Serializable, NetSerializable]
+public enum PowerChargeUiKey
+{
+    Key
+}
+
+[Serializable, NetSerializable]
+public enum PowerChargeVisuals
+{
+    State,
+    Charge,
+    Active
+}
+
+[Serializable, NetSerializable]
+public enum PowerChargeStatus
+{
+    Broken,
+    Unpowered,
+    Off,
+    On
+}
+
+[Serializable, NetSerializable]
+public enum PowerChargePowerStatus : byte
+{
+    Off,
+    Discharging,
+    Charging,
+    FullyCharged
+}
diff --git a/Content.Shared/Power/SharedPowerChargeComponent.cs b/Content.Shared/Power/SharedPowerChargeComponent.cs
new file mode 100644 (file)
index 0000000..96f26a7
--- /dev/null
@@ -0,0 +1,14 @@
+namespace Content.Shared.Power;
+
+/// <summary>
+/// Component for a powered machine that slowly powers on and off over a period of time.
+/// </summary>
+public abstract partial class SharedPowerChargeComponent : Component
+{
+    /// <summary>
+    /// The title used for the default charged machine window if used
+    /// </summary>
+    [DataField]
+    public LocId WindowTitle { get; set; } = string.Empty;
+
+}
index db2cbaa138b8747fbc3d6e79dde526dc5bd2a4f7..b4034686d9786c6a0ceef1f5b560d77c7c24cef2 100644 (file)
@@ -5,6 +5,7 @@ using Content.Shared.Shuttles.UI.MapObjects;
 using Content.Shared.Whitelist;
 using Robust.Shared.Map;
 using Robust.Shared.Map.Components;
+using Robust.Shared.Physics;
 using Robust.Shared.Physics.Collision.Shapes;
 using Robust.Shared.Physics.Components;
 
@@ -125,7 +126,7 @@ public abstract partial class SharedShuttleSystem : EntitySystem
         if (!Resolve(gridUid, ref physics))
             return true;
 
-        if (physics.Mass < 10f)
+        if (physics.BodyType != BodyType.Static && physics.Mass < 10f)
         {
             return false;
         }
diff --git a/Resources/Locale/en-US/components/station-anchor-component.ftl b/Resources/Locale/en-US/components/station-anchor-component.ftl
new file mode 100644 (file)
index 0000000..fd9d5ea
--- /dev/null
@@ -0,0 +1,2 @@
+station-anchor-unanchoring-failed = Can't unanchor an active station anchor
+station-anchor-window-title = Station Anchor
diff --git a/Resources/Locale/en-US/power/components/power-charging-component.ftl b/Resources/Locale/en-US/power/components/power-charging-component.ftl
new file mode 100644 (file)
index 0000000..b4743bd
--- /dev/null
@@ -0,0 +1,22 @@
+## UI field names
+
+power-charge-window-status = Status:
+power-charge-window-power = Power:
+power-charge-window-eta = ETA:
+power-charge-window-charge = Charge:
+
+## UI statuses
+power-charge-window-status-fully-charged = Fully Charged
+power-charge-window-status-off = Off
+power-charge-window-status-charging = Charging
+power-charge-window-status-discharging = Discharging
+
+## UI Power Buttons
+power-charge-window-power-on = On
+power-charge-window-power-off = Off
+power-charge-window-power-label = { $draw } / { $max } W
+
+## UI ETA label
+
+power-charge-window-eta-none = N/A
+power-charge-window-eta-value = { TOSTRING($left, "m\\:ss") }
index 040a7daffcf623b4e96b538eac3c22d2a520d4e1..085ace797bbfdfc9683eb532d202021ac7968422 100644 (file)
       CableHV: 5
       Uranium: 2
 
+- type: entity
+  parent: BaseMachineCircuitboard
+  id: StationAnchorCircuitboard
+  name: station anchor machine board
+  description: A machine printed circuit board for a station anchor.
+  components:
+  - type: MachineBoard
+    prototype: StationAnchor
+    stackRequirements:
+      Capacitor: 4
+      MatterBin: 3
+      Steel: 10
+      Glass: 5
+      CableHV: 8
+      Uranium: 2
+
 - type: entity
   parent: BaseMachineCircuitboard
   id: ReagentGrinderIndustrialMachineCircuitboard
index 6ee454c6a98798c619c4ddadecf233380991e4b2..2880f819a72adfc37a2ca64333f892541e909ea0 100644 (file)
       behaviors:
       - !type:DoActsBehavior
         acts: ["Breakage"]
-  - type: GravityGenerator
+  - type: PowerCharge
+    windowTitle: gravity-generator-window-title
     idlePower: 50
     activePower: 2500
+  - type: GravityGenerator
     lightRadiusMin: 0.75
     lightRadiusMax: 2.5
     spriteMap:
       unpowered: "off"
       off: "off"
       on: "on"
+  - type: ActivatableUI
+    key: enum.PowerChargeUiKey.Key
+  - type: ActivatableUIRequiresPower
   - type: UserInterface
     interfaces:
-        enum.GravityGeneratorUiKey.Key:
-          type: GravityGeneratorBoundUserInterface
+      enum.PowerChargeUiKey.Key:
+        type: PowerChargeBoundUserInterface
   - type: Appearance
   - type: PointLight
     radius: 2.5
     board: MiniGravityGeneratorCircuitboard
   - type: ApcPowerReceiver
     powerLoad: 500
-  - type: GravityGenerator
+  - type: PowerCharge
     idlePower: 15
     activePower: 500
+  - type: GravityGenerator
     lightRadiusMin: 0.75
     lightRadiusMax: 2.5
   - type: StaticPrice
index c606e26f8f5a8d004d9909bc2724ef1b5e16e4bf..1d2f1cdadb521934703592f3718317b82c0684a1 100644 (file)
     - SodaDispenserMachineCircuitboard
     - SpaceHeaterMachineCircuitBoard
     - CutterMachineCircuitboard
+    - StationAnchorCircuitboard
     dynamicRecipes:
       - ThermomachineFreezerMachineCircuitBoard
       - HellfireFreezerMachineCircuitBoard
diff --git a/Resources/Prototypes/Entities/Structures/Shuttles/station_anchor.yml b/Resources/Prototypes/Entities/Structures/Shuttles/station_anchor.yml
new file mode 100644 (file)
index 0000000..2f6e42b
--- /dev/null
@@ -0,0 +1,112 @@
+- type: entity
+  id: StationAnchorBase
+  abstract: true
+  name: station anchor
+  description: Prevents stations from moving
+  placement:
+    mode: AlignTileAny
+  components:
+  - type: StationAnchor
+  - type: Transform
+    anchored: true
+  - type: Physics
+    bodyType: Static
+  - type: AmbientSound
+    enabled: false
+    range: 4
+    volume: -4
+    sound:
+      path: /Audio/Effects/shuttle_thruster.ogg
+  - type: InteractionOutline
+  - type: Sprite
+    sprite: Structures/Machines/station_anchor.rsi
+    layers:
+    - state: station_anchor
+      map: ["base"]
+    - state: station_anchor_unlit
+      shader: unshaded
+      map: ["unlit"]
+  - type: GenericVisualizer
+    visuals:
+      enum.PowerChargeVisuals.Active:
+        unlit:
+          True: { visible: True }
+          False: { visible: False }
+  - type: Appearance
+  - type: Fixtures
+    fixtures:
+      fix1:
+        shape:
+          !type:PhysShapeAabb
+          bounds: "-0.7,-0.8,0.7,0.8"
+        density: 190
+        mask:
+        - LargeMobMask
+        layer:
+        - WallLayer
+
+
+- type: entity
+  id: StationAnchorIndestructible
+  parent: StationAnchorBase
+  suffix: Indestructible, Unpowered
+
+- type: entity
+  id: StationAnchor
+  parent: [StationAnchorBase, BaseMachinePowered, ConstructibleMachine]
+  name: station anchor
+  description: Prevents stations from moving
+  placement:
+    mode: AlignTileAny
+  components:
+    - type: PowerCharge
+      windowTitle: station-anchor-window-title
+      idlePower: 50
+      activePower: 2500
+      chargeRate: 0.5
+    - type: ActivatableUI
+      key: enum.PowerChargeUiKey.Key
+    - type: ActivatableUIRequiresPower
+    - type: Anchorable
+    - type: ApcPowerReceiver
+      powerLoad: 2500
+    - type: ExtensionCableReceiver
+    - type: Damageable
+      damageContainer: Inorganic
+      damageModifierSet: Metallic
+    - type: Repairable
+      fuelCost: 10
+      doAfterDelay: 5
+    - type: Destructible
+      thresholds:
+      - trigger:
+          !type:DamageTrigger
+          damage: 150
+        behaviors:
+        - !type:DoActsBehavior
+          acts: [ "Breakage" ]
+      - trigger:
+          !type:DamageTrigger
+          damage: 600
+        behaviors:
+        - !type:DoActsBehavior
+          acts: [ "Destruction" ]
+        - !type:PlaySoundBehavior
+          sound:
+            collection: MetalBreak
+    - type: StaticPrice
+      price: 10000
+    - type: Machine
+      board: StationAnchorCircuitboard
+    - type: ContainerContainer
+      containers:
+        machine_board: !type:Container
+        machine_parts: !type:Container
+    - type: Construction
+      containers:
+      - machine_parts
+      - machine_board
+    - type: UserInterface
+      interfaces:
+        enum.PowerChargeUiKey.Key:
+          type: PowerChargeBoundUserInterface
index 1bbd60e3af6e3bcb1a2f920eee03071726548c64..6b4653d43ad6b821e65c06d9989250f56a40e3dd 100644 (file)
   completetime: 6
   materials:
      Steel: 100
-
      Glass: 500
 
 - type: latheRecipe
      Glass: 500
      Gold: 100
 
+- type: latheRecipe
+  id: StationAnchorCircuitboard
+  result: StationAnchorCircuitboard
+  category: Circuitry
+  completetime: 8
+  materials:
+    Steel: 100
+    Glass: 900
+    Gold: 100
+
 - type: latheRecipe
   id: ReagentGrinderIndustrialMachineCircuitboard
   result: ReagentGrinderIndustrialMachineCircuitboard
diff --git a/Resources/Textures/Structures/Machines/station_anchor.rsi/meta.json b/Resources/Textures/Structures/Machines/station_anchor.rsi/meta.json
new file mode 100644 (file)
index 0000000..b345534
--- /dev/null
@@ -0,0 +1,17 @@
+{
+    "version": 1,
+    "license": "CC-BY-SA-3.0",
+    "copyright": "Made and posted by ubaser on the SS14 discord.",
+    "size": {
+        "x": 64,
+        "y": 64
+    },
+    "states": [
+        {
+            "name": "station_anchor"
+        },
+        {
+            "name": "station_anchor_unlit"
+        }
+    ]
+}
diff --git a/Resources/Textures/Structures/Machines/station_anchor.rsi/station_anchor.png b/Resources/Textures/Structures/Machines/station_anchor.rsi/station_anchor.png
new file mode 100644 (file)
index 0000000..694a35a
Binary files /dev/null and b/Resources/Textures/Structures/Machines/station_anchor.rsi/station_anchor.png differ
diff --git a/Resources/Textures/Structures/Machines/station_anchor.rsi/station_anchor_unlit.png b/Resources/Textures/Structures/Machines/station_anchor.rsi/station_anchor_unlit.png
new file mode 100644 (file)
index 0000000..a8f069f
Binary files /dev/null and b/Resources/Textures/Structures/Machines/station_anchor.rsi/station_anchor_unlit.png differ