]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Salvage magnet revamp (#23119)
authormetalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Thu, 4 Jan 2024 03:25:32 +0000 (14:25 +1100)
committerGitHub <noreply@github.com>
Thu, 4 Jan 2024 03:25:32 +0000 (14:25 +1100)
* Generic offering window

* More work

* weh

* Parity

* Progression meter

* magnet

* rona

* PG asteroid work

* code red

* Asteroid spawnings

* clams

* a

* Marker fixes

* More fixes

* Workings of biome asteroids

* A

* Fix this loading code

* a

* Fix masking

* weh

* Fixes

* Magnet claiming

* toe

* petogue

* magnet

* Bunch of fixes

* Fix default

* Fixes

* asteroids

* Fix offerings

* Localisation and a bunch of fixes

* a

* Fixes

* Preliminary draft

* Announcement fixes

* Fixes and bump spawn rate

* Fix asteroid spawns and UI

* More fixes

* Expeditions fix

* fix

* Gravity

* Fix announcement rounding

* a

* Offset tweak

* sus

* jankass

* Fix merge

66 files changed:
Content.Client/Entry/EntryPoint.cs
Content.Client/Salvage/SalvageMagnetComponent.cs [deleted file]
Content.Client/Salvage/UI/OfferingWindow.xaml [moved from Content.Client/Salvage/UI/SalvageExpeditionWindow.xaml with 60% similarity]
Content.Client/Salvage/UI/OfferingWindow.xaml.cs [new file with mode: 0644]
Content.Client/Salvage/UI/OfferingWindowOption.xaml [new file with mode: 0644]
Content.Client/Salvage/UI/OfferingWindowOption.xaml.cs [new file with mode: 0644]
Content.Client/Salvage/UI/SalvageExpeditionConsoleBoundUserInterface.cs
Content.Client/Salvage/UI/SalvageExpeditionWindow.xaml.cs [deleted file]
Content.Client/Salvage/UI/SalvageMagnetBoundUserInterface.cs [new file with mode: 0644]
Content.IntegrationTests/Tests/SalvageTest.cs
Content.Server/Parallax/BiomeSystem.cs
Content.Server/Physics/Controllers/MoverController.cs
Content.Server/Procedural/DungeonJob.NoiseDunGen.cs [new file with mode: 0644]
Content.Server/Procedural/DungeonJob.PostGen.cs
Content.Server/Procedural/DungeonJob.PostGenBiome.cs [new file with mode: 0644]
Content.Server/Procedural/DungeonJob.PrefabDunGen.cs [moved from Content.Server/Procedural/DungeonJob.Generator.cs with 100% similarity]
Content.Server/Procedural/DungeonJob.cs
Content.Server/Procedural/DungeonSystem.cs
Content.Server/Salvage/FultonSystem.cs
Content.Server/Salvage/Magnet/SalvageMagnetComponent.cs [new file with mode: 0644]
Content.Server/Salvage/Magnet/SalvageMagnetDataComponent.cs [new file with mode: 0644]
Content.Server/Salvage/Magnet/SalvageMagnetTargetComponent.cs [new file with mode: 0644]
Content.Server/Salvage/Magnet/SalvageMobRestrictionsComponent.cs [new file with mode: 0644]
Content.Server/Salvage/SalvageGridComponent.cs [deleted file]
Content.Server/Salvage/SalvageMagnetComponent.cs [deleted file]
Content.Server/Salvage/SalvageMapPrototype.cs [deleted file]
Content.Server/Salvage/SalvageMobRestrictionsComponent.cs [deleted file]
Content.Server/Salvage/SalvageMobRestrictionsGridComponent.cs [deleted file]
Content.Server/Salvage/SalvageMobRestrictionsSystem.cs [deleted file]
Content.Server/Salvage/SalvageRulerCommand.cs
Content.Server/Salvage/SalvageSystem.Magnet.cs [new file with mode: 0644]
Content.Server/Salvage/SalvageSystem.cs
Content.Server/Shuttles/DroneConsoleComponent.cs [moved from Content.Server/Shuttle/Components/DroneConsoleComponent.cs with 91% similarity]
Content.Server/Shuttles/Systems/ShuttleConsoleSystem.Drone.cs
Content.Server/Shuttles/Systems/ShuttleConsoleSystem.cs
Content.Server/Shuttles/Systems/SpaceGarbageSystem.cs
Content.Server/Station/Systems/StationSystem.cs
Content.Server/Xenoarchaeology/XenoArtifacts/Triggers/Systems/ArtifactMagnetTriggerSystem.cs
Content.Shared/CCVar/CCVars.cs
Content.Shared/Parallax/Biomes/BiomeComponent.cs
Content.Shared/Parallax/Biomes/SharedBiomeSystem.cs
Content.Shared/Procedural/Dungeon.cs
Content.Shared/Procedural/DungeonGenerators/NoiseDunGen.cs [new file with mode: 0644]
Content.Shared/Procedural/PostGeneration/BiomeMarkerLayerPostGen.cs [new file with mode: 0644]
Content.Shared/Procedural/PostGeneration/BiomePostGen.cs [new file with mode: 0644]
Content.Shared/Salvage/Magnet/AsteroidOffering.cs [new file with mode: 0644]
Content.Shared/Salvage/Magnet/ISalvageMagnetOffering.cs [new file with mode: 0644]
Content.Shared/Salvage/Magnet/MagnetClaimOfferEvent.cs [new file with mode: 0644]
Content.Shared/Salvage/Magnet/SalvageMagnetBoundUserInterfaceState.cs [new file with mode: 0644]
Content.Shared/Salvage/Magnet/SalvageMagnetUiKey.cs [new file with mode: 0644]
Content.Shared/Salvage/Magnet/SalvageOffering.cs [new file with mode: 0644]
Content.Shared/Salvage/SalvageMapPrototype.cs [new file with mode: 0644]
Content.Shared/Salvage/SharedSalvageMagnetComponent.cs [deleted file]
Content.Shared/Salvage/SharedSalvageSystem.Magnet.cs [new file with mode: 0644]
Content.Shared/Salvage/SharedSalvageSystem.cs
Resources/Locale/en-US/procedural/expeditions.ftl
Resources/Locale/en-US/salvage/salvage-magnet.ftl [new file with mode: 0644]
Resources/Locale/en-US/salvage/salvage-system.ftl [deleted file]
Resources/Prototypes/Entities/Stations/base.yml
Resources/Prototypes/Entities/Stations/nanotrasen.yml
Resources/Prototypes/Entities/Structures/Machines/salvage.yml
Resources/Prototypes/Entities/Structures/Walls/asteroid.yml
Resources/Prototypes/Maps/salvage.yml
Resources/Prototypes/Procedural/Magnet/asteroid.yml [new file with mode: 0644]
Resources/Prototypes/Procedural/biome_ore_templates.yml
Resources/Prototypes/Procedural/biome_templates.yml

index 04f0acac971c1e6589062a2382ff3235e2b3ae91..a9ecb3a725699bf64069399c63fd8bd38b407dc3 100644 (file)
@@ -108,7 +108,6 @@ namespace Content.Client.Entry
             _prototypeManager.RegisterIgnore("npcFaction");
             _prototypeManager.RegisterIgnore("lobbyBackground");
             _prototypeManager.RegisterIgnore("advertisementsPack");
-            _prototypeManager.RegisterIgnore("salvageMap");
             _prototypeManager.RegisterIgnore("gamePreset");
             _prototypeManager.RegisterIgnore("noiseChannel");
             _prototypeManager.RegisterIgnore("spaceBiome");
diff --git a/Content.Client/Salvage/SalvageMagnetComponent.cs b/Content.Client/Salvage/SalvageMagnetComponent.cs
deleted file mode 100644 (file)
index 83b7655..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-using Content.Shared.Salvage;
-using Robust.Shared.GameStates;
-
-namespace Content.Client.Salvage;
-
-[NetworkedComponent, RegisterComponent]
-public sealed partial class SalvageMagnetComponent : SharedSalvageMagnetComponent {}
similarity index 60%
rename from Content.Client/Salvage/UI/SalvageExpeditionWindow.xaml
rename to Content.Client/Salvage/UI/OfferingWindow.xaml
index 67280c34f975fd930145ca144e07daa573c4021e..12f1f688c89d69b7143c75ba5d8b549948bd12d8 100644 (file)
@@ -3,10 +3,24 @@
                       Title="{Loc 'salvage-expedition-window-title'}"
                       MinSize="800 360">
     <BoxContainer Orientation="Vertical">
+        <BoxContainer Orientation="Horizontal" Name="ProgressionBox" Visible="False">
+            <Label Name="ProgressionLabel"
+                   Text="{Loc 'salvage-expedition-window-progression'}"
+                   SetWidth="96"
+                   Margin="5"/>
+            <ProgressBar Name="ProgressionBar"
+                         HorizontalExpand="True"
+                         MinValue="0"
+                         MaxValue="1"
+                         SetHeight="25"/>
+            <Label Name="ProgressionText" Text="00:00"
+                   Margin="5"/>
+        </BoxContainer>
         <BoxContainer Orientation="Horizontal">
             <Label Name="NextOfferLabel"
                    Text="{Loc 'salvage-expedition-window-next'}"
-                   Margin="5"></Label>
+                   SetWidth="96"
+                   Margin="5"/>
             <ProgressBar Name="NextOfferBar"
                          HorizontalExpand="True"
                          MinValue="0"
diff --git a/Content.Client/Salvage/UI/OfferingWindow.xaml.cs b/Content.Client/Salvage/UI/OfferingWindow.xaml.cs
new file mode 100644 (file)
index 0000000..2b607b8
--- /dev/null
@@ -0,0 +1,117 @@
+using Content.Client.Computer;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Shuttles.BUIStates;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Timing;
+
+namespace Content.Client.Salvage.UI;
+
+/// <summary>
+/// Generic window for offering multiple selections with a timer.
+/// </summary>
+[GenerateTypedNameReferences]
+public sealed partial class OfferingWindow : FancyWindow,
+    IComputerWindow<EmergencyConsoleBoundUserInterfaceState>
+{
+    [Dependency] private readonly IGameTiming _timing = default!;
+
+    public bool Claimed;
+    public TimeSpan NextOffer;
+    private TimeSpan? _progression;
+
+    /// <summary>
+    /// Time between NextOffers
+    /// </summary>
+    public TimeSpan Cooldown;
+
+    /// <summary>
+    /// Time between Progressions
+    /// </summary>
+    public TimeSpan ProgressionCooldown;
+
+    /// <summary>
+    /// Secondary timer used for tracking active progress.
+    /// </summary>
+    public TimeSpan? Progression
+    {
+        get => _progression;
+        set
+        {
+            if (_progression == value)
+                return;
+
+            _progression = value;
+
+            if (value == null)
+            {
+                ProgressionBox.Visible = false;
+            }
+            else
+            {
+                ProgressionBox.Visible = true;
+            }
+        }
+    }
+
+    public OfferingWindow()
+    {
+        RobustXamlLoader.Load(this);
+        IoCManager.InjectDependencies(this);
+
+        ProgressionBar.ForegroundStyleBoxOverride = new StyleBoxFlat(Color.FromHex("#C74EBD"));
+    }
+
+    public void AddOption(OfferingWindowOption option)
+    {
+        Container.AddChild(option);
+    }
+
+    public void ClearOptions()
+    {
+        Container.DisposeAllChildren();
+    }
+
+    protected override void FrameUpdate(FrameEventArgs args)
+    {
+        base.FrameUpdate(args);
+
+        if (_progression != null)
+        {
+            var remaining = _progression.Value - _timing.CurTime;
+
+            if (remaining < TimeSpan.Zero)
+            {
+                ProgressionBar.Value = 1f;
+                ProgressionText.Text = "00:00";
+            }
+            else
+            {
+                ProgressionBar.Value = 1f - (float) (remaining / ProgressionCooldown);
+                ProgressionText.Text = $"{remaining.Minutes:00}:{remaining.Seconds:00}";
+            }
+        }
+
+        if (Claimed)
+        {
+            NextOfferBar.Value = 1f;
+            NextOfferText.Text = "00:00";
+        }
+        else
+        {
+            var remaining = NextOffer - _timing.CurTime;
+
+            if (remaining < TimeSpan.Zero)
+            {
+                NextOfferBar.Value = 1f;
+                NextOfferText.Text = "00:00";
+            }
+            else
+            {
+                NextOfferBar.Value = 1f - (float) (remaining / Cooldown);
+                NextOfferText.Text = $"{remaining.Minutes:00}:{remaining.Seconds:00}";
+            }
+        }
+    }
+}
diff --git a/Content.Client/Salvage/UI/OfferingWindowOption.xaml b/Content.Client/Salvage/UI/OfferingWindowOption.xaml
new file mode 100644 (file)
index 0000000..fc2bfaa
--- /dev/null
@@ -0,0 +1,24 @@
+<PanelContainer xmlns="https://spacestation14.io"
+                xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+                HorizontalExpand="True"
+                Name="BigPanel"
+                Margin="5 0">
+    <BoxContainer Orientation="Vertical"
+                  Margin="5 5">
+        <!-- Title box -->
+        <controls:StripeBack>
+            <Label Name="TitleStripe"
+                   HorizontalAlignment="Center"
+                   Margin="0 5 0 5"/>
+        </controls:StripeBack>
+        <BoxContainer Orientation="Vertical" Name="ContentBox"/>
+        <!-- Buffer so all claim buttons are in the same position -->
+        <Control VerticalExpand="True"/>
+        <Button Name="ClaimButton"
+                HorizontalExpand="True"
+                VerticalAlignment="Bottom"
+                ToggleMode="True"
+                Disabled="True"
+                Text="{Loc 'offering-window-claim'}"/>
+    </BoxContainer>
+</PanelContainer>
diff --git a/Content.Client/Salvage/UI/OfferingWindowOption.xaml.cs b/Content.Client/Salvage/UI/OfferingWindowOption.xaml.cs
new file mode 100644 (file)
index 0000000..7855577
--- /dev/null
@@ -0,0 +1,87 @@
+using System.Linq;
+using Content.Client.Computer;
+using Content.Client.Stylesheets;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.CCVar;
+using Content.Shared.Parallax.Biomes;
+using Content.Shared.Procedural;
+using Content.Shared.Salvage;
+using Content.Shared.Salvage.Expeditions;
+using Content.Shared.Salvage.Expeditions.Modifiers;
+using Content.Shared.Shuttles.BUIStates;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Configuration;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Salvage.UI;
+
+/// <summary>
+/// Generic window for offering multiple selections with a timer.
+/// </summary>
+[GenerateTypedNameReferences]
+public sealed partial class OfferingWindowOption : PanelContainer
+{
+    private bool _claimed;
+
+    public string? Title
+    {
+        get => TitleStripe.Text;
+        set => TitleStripe.Text = value;
+    }
+
+    public event Action<BaseButton.ButtonEventArgs>? ClaimPressed;
+
+    public OfferingWindowOption()
+    {
+        RobustXamlLoader.Load(this);
+        IoCManager.InjectDependencies(this);
+
+        LayoutContainer.SetAnchorPreset(this, LayoutContainer.LayoutPreset.Wide);
+        BigPanel.PanelOverride = new StyleBoxFlat(new Color(30, 30, 34));
+
+        ClaimButton.OnPressed += args =>
+        {
+            ClaimPressed?.Invoke(args);
+        };
+    }
+
+    public void AddContent(Control control)
+    {
+        ContentBox.AddChild(control);
+    }
+
+    public bool Disabled
+    {
+        get => ClaimButton.Disabled;
+        set => ClaimButton.Disabled = value;
+    }
+
+    public bool Claimed
+    {
+        get => _claimed;
+        set
+        {
+            if (_claimed == value)
+                return;
+
+            _claimed = value;
+
+            if (_claimed)
+            {
+                ClaimButton.AddStyleClass(StyleBase.ButtonCaution);
+                ClaimButton.Text = Loc.GetString("offering-window-claimed");
+            }
+            else
+            {
+                ClaimButton.RemoveStyleClass(StyleBase.ButtonCaution);
+                ClaimButton.Text = Loc.GetString("offering-window-claim");
+            }
+        }
+    }
+}
index d594bdd6fd1708eaf1c763307189385e0b9e2ab6..ac480614a4f0d6e065d766078d11e07bb159bb7c 100644 (file)
@@ -1,6 +1,14 @@
+using System.Linq;
+using Content.Client.Stylesheets;
+using Content.Shared.CCVar;
+using Content.Shared.Procedural;
 using Content.Shared.Salvage.Expeditions;
+using Content.Shared.Salvage.Expeditions.Modifiers;
 using JetBrains.Annotations;
-using Robust.Client.GameObjects;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Configuration;
+using Robust.Shared.Prototypes;
 
 namespace Content.Client.Salvage.UI;
 
@@ -8,23 +16,21 @@ namespace Content.Client.Salvage.UI;
 public sealed class SalvageExpeditionConsoleBoundUserInterface : BoundUserInterface
 {
     [ViewVariables]
-    private SalvageExpeditionWindow? _window;
+    private OfferingWindow? _window;
+
+    [Dependency] private readonly IConfigurationManager _cfgManager = default!;
+    [Dependency] private readonly IEntityManager _entManager = default!;
+    [Dependency] private readonly IPrototypeManager _protoManager = default!;
 
     public SalvageExpeditionConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
     {
+        IoCManager.InjectDependencies(this);
     }
 
     protected override void Open()
     {
         base.Open();
-        _window = new SalvageExpeditionWindow();
-        _window.ClaimMission += index =>
-        {
-            SendMessage(new ClaimSalvageMessage()
-            {
-                Index = index,
-            });
-        };
+        _window = new OfferingWindow();
         _window.OnClose += Close;
         _window?.OpenCenteredLeft();
     }
@@ -40,9 +46,133 @@ public sealed class SalvageExpeditionConsoleBoundUserInterface : BoundUserInterf
     {
         base.UpdateState(state);
 
-        if (state is not SalvageExpeditionConsoleState current)
+        if (state is not SalvageExpeditionConsoleState current || _window == null)
             return;
 
-        _window?.UpdateState(current);
+        _window.Progression = null;
+        _window.Cooldown = TimeSpan.FromSeconds(_cfgManager.GetCVar(CCVars.SalvageExpeditionCooldown));
+        _window.NextOffer = current.NextOffer;
+        _window.Claimed = current.Claimed;
+        _window.ClearOptions();
+        var salvage = _entManager.System<SalvageSystem>();
+
+        for (var i = 0; i < current.Missions.Count; i++)
+        {
+            var missionParams = current.Missions[i];
+
+            var offering = new OfferingWindowOption();
+            offering.Title = Loc.GetString($"salvage-expedition-type");
+
+            var difficultyId = "Moderate";
+            var difficultyProto = _protoManager.Index<SalvageDifficultyPrototype>(difficultyId);
+            // TODO: Selectable difficulty soon.
+            var mission = salvage.GetMission(difficultyProto, missionParams.Seed);
+
+            // Difficulty
+            // Details
+            offering.AddContent(new Label()
+            {
+                Text = Loc.GetString("salvage-expedition-window-difficulty")
+            });
+
+            var difficultyColor = difficultyProto.Color;
+
+            offering.AddContent(new Label
+            {
+                Text = Loc.GetString("salvage-expedition-difficulty-Moderate"),
+                FontColorOverride = difficultyColor,
+                HorizontalAlignment = Control.HAlignment.Left,
+                Margin = new Thickness(0f, 0f, 0f, 5f),
+            });
+
+            offering.AddContent(new Label
+            {
+                Text = Loc.GetString("salvage-expedition-difficulty-players"),
+                HorizontalAlignment = Control.HAlignment.Left,
+            });
+
+            offering.AddContent(new Label
+            {
+                Text = difficultyProto.RecommendedPlayers.ToString(),
+                FontColorOverride = StyleNano.NanoGold,
+                HorizontalAlignment = Control.HAlignment.Left,
+                Margin = new Thickness(0f, 0f, 0f, 5f),
+            });
+
+            // Details
+            offering.AddContent(new Label
+            {
+                Text = Loc.GetString("salvage-expedition-window-hostiles")
+            });
+
+            var faction = mission.Faction;
+
+            offering.AddContent(new Label
+            {
+                Text = faction,
+                FontColorOverride = StyleNano.NanoGold,
+                HorizontalAlignment = Control.HAlignment.Left,
+                Margin = new Thickness(0f, 0f, 0f, 5f),
+            });
+
+            // Duration
+            offering.AddContent(new Label
+            {
+                Text = Loc.GetString("salvage-expedition-window-duration")
+            });
+
+            offering.AddContent(new Label
+            {
+                Text = mission.Duration.ToString(),
+                FontColorOverride = StyleNano.NanoGold,
+                HorizontalAlignment = Control.HAlignment.Left,
+                Margin = new Thickness(0f, 0f, 0f, 5f),
+            });
+
+            // Biome
+            offering.AddContent(new Label
+            {
+                Text = Loc.GetString("salvage-expedition-window-biome")
+            });
+
+            var biome = mission.Biome;
+
+            offering.AddContent(new Label
+            {
+                Text = Loc.GetString(_protoManager.Index<SalvageBiomeModPrototype>(biome).ID),
+                FontColorOverride = StyleNano.NanoGold,
+                HorizontalAlignment = Control.HAlignment.Left,
+                Margin = new Thickness(0f, 0f, 0f, 5f),
+            });
+
+            // Modifiers
+            offering.AddContent(new Label
+            {
+                Text = Loc.GetString("salvage-expedition-window-modifiers")
+            });
+
+            var mods = mission.Modifiers;
+
+            offering.AddContent(new Label
+            {
+                Text = string.Join("\n", mods.Select(o => "- " + o)).TrimEnd(),
+                FontColorOverride = StyleNano.NanoGold,
+                HorizontalAlignment = Control.HAlignment.Left,
+                Margin = new Thickness(0f, 0f, 0f, 5f),
+            });
+
+            offering.ClaimPressed += args =>
+            {
+                SendMessage(new ClaimSalvageMessage()
+                {
+                    Index = missionParams.Index,
+                });
+            };
+
+            offering.Claimed = current.ActiveMission == missionParams.Index;
+            offering.Disabled = current.Claimed || current.Cooldown;
+
+            _window.AddOption(offering);
+        }
     }
 }
diff --git a/Content.Client/Salvage/UI/SalvageExpeditionWindow.xaml.cs b/Content.Client/Salvage/UI/SalvageExpeditionWindow.xaml.cs
deleted file mode 100644 (file)
index 7875ac6..0000000
+++ /dev/null
@@ -1,256 +0,0 @@
-using System.Linq;
-using Content.Client.Computer;
-using Content.Client.Stylesheets;
-using Content.Client.UserInterface.Controls;
-using Content.Shared.CCVar;
-using Content.Shared.Parallax.Biomes;
-using Content.Shared.Procedural;
-using Content.Shared.Salvage;
-using Content.Shared.Salvage.Expeditions;
-using Content.Shared.Salvage.Expeditions.Modifiers;
-using Content.Shared.Shuttles.BUIStates;
-using Robust.Client.AutoGenerated;
-using Robust.Client.Graphics;
-using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.XAML;
-using Robust.Shared.Configuration;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Timing;
-using Robust.Shared.Utility;
-
-namespace Content.Client.Salvage.UI;
-
-[GenerateTypedNameReferences]
-public sealed partial class SalvageExpeditionWindow : FancyWindow,
-    IComputerWindow<EmergencyConsoleBoundUserInterfaceState>
-{
-    private readonly IConfigurationManager _cfgManager;
-    private readonly IGameTiming _timing;
-    private readonly IPrototypeManager _prototype;
-    private readonly SharedSalvageSystem _salvage;
-
-    public event Action<ushort>? ClaimMission;
-    private bool _claimed;
-    private bool _cooldown;
-    private TimeSpan _nextOffer;
-
-    public SalvageExpeditionWindow()
-    {
-        RobustXamlLoader.Load(this);
-        _cfgManager = IoCManager.Resolve<IConfigurationManager>();
-        _timing = IoCManager.Resolve<IGameTiming>();
-        _prototype = IoCManager.Resolve<IPrototypeManager>();
-        _salvage = IoCManager.Resolve<IEntityManager>().EntitySysManager.GetEntitySystem<SharedSalvageSystem>();
-    }
-
-    public void UpdateState(SalvageExpeditionConsoleState state)
-    {
-        _claimed = state.Claimed;
-        _cooldown = state.Cooldown;
-        _nextOffer = state.NextOffer;
-        Container.DisposeAllChildren();
-
-        for (var i = 0; i < state.Missions.Count; i++)
-        {
-            var missionParams = state.Missions[i];
-            var difficultyId = "Moderate";
-            var difficultyProto = _prototype.Index<SalvageDifficultyPrototype>(difficultyId);
-            // TODO: Selectable difficulty soon.
-            var mission = _salvage.GetMission(difficultyProto, missionParams.Seed);
-
-            // Mission title
-            var missionStripe = new StripeBack()
-            {
-                Margin = new Thickness(0f, -5f, 0f, 0f)
-            };
-
-            missionStripe.AddChild(new Label()
-            {
-                Text = Loc.GetString($"salvage-expedition-type"),
-                HorizontalAlignment = HAlignment.Center,
-                Margin = new Thickness(0f, 5f, 0f, 5f),
-            });
-
-            var lBox = new BoxContainer()
-            {
-                Orientation = BoxContainer.LayoutOrientation.Vertical
-            };
-
-            // Difficulty
-            // Details
-            lBox.AddChild(new Label()
-            {
-                Text = Loc.GetString("salvage-expedition-window-difficulty")
-            });
-
-            var difficultyColor = difficultyProto.Color;
-
-            lBox.AddChild(new Label
-            {
-                Text = Loc.GetString("salvage-expedition-difficulty-Moderate"),
-                FontColorOverride = difficultyColor,
-                HorizontalAlignment = HAlignment.Left,
-                Margin = new Thickness(0f, 0f, 0f, 5f),
-            });
-
-            lBox.AddChild(new Label
-            {
-                Text = Loc.GetString("salvage-expedition-difficulty-players"),
-                HorizontalAlignment = HAlignment.Left,
-            });
-
-            lBox.AddChild(new Label
-            {
-                Text = difficultyProto.RecommendedPlayers.ToString(),
-                FontColorOverride = StyleNano.NanoGold,
-                HorizontalAlignment = HAlignment.Left,
-                Margin = new Thickness(0f, 0f, 0f, 5f),
-            });
-
-            // Details
-            lBox.AddChild(new Label
-            {
-                Text = Loc.GetString("salvage-expedition-window-hostiles")
-            });
-
-            var faction = mission.Faction;
-
-            lBox.AddChild(new Label
-            {
-                Text = faction,
-                FontColorOverride = StyleNano.NanoGold,
-                HorizontalAlignment = HAlignment.Left,
-                Margin = new Thickness(0f, 0f, 0f, 5f),
-            });
-
-            // Duration
-            lBox.AddChild(new Label
-            {
-                Text = Loc.GetString("salvage-expedition-window-duration")
-            });
-
-            lBox.AddChild(new Label
-            {
-                Text = mission.Duration.ToString(),
-                FontColorOverride = StyleNano.NanoGold,
-                HorizontalAlignment = HAlignment.Left,
-                Margin = new Thickness(0f, 0f, 0f, 5f),
-            });
-
-            // Biome
-            lBox.AddChild(new Label
-            {
-                Text = Loc.GetString("salvage-expedition-window-biome")
-            });
-
-            var biome = mission.Biome;
-
-            lBox.AddChild(new Label
-            {
-                Text = Loc.GetString(_prototype.Index<SalvageBiomeModPrototype>(biome).ID),
-                FontColorOverride = StyleNano.NanoGold,
-                HorizontalAlignment = HAlignment.Left,
-                Margin = new Thickness(0f, 0f, 0f, 5f),
-            });
-
-            // Modifiers
-            lBox.AddChild(new Label
-            {
-                Text = Loc.GetString("salvage-expedition-window-modifiers")
-            });
-
-            var mods = mission.Modifiers;
-
-            lBox.AddChild(new Label
-            {
-                Text = string.Join("\n", mods.Select(o => "- " + o)).TrimEnd(),
-                FontColorOverride = StyleNano.NanoGold,
-                HorizontalAlignment = HAlignment.Left,
-                Margin = new Thickness(0f, 0f, 0f, 5f),
-            });
-
-            // Claim
-            var claimButton = new Button()
-            {
-                HorizontalExpand = true,
-                VerticalAlignment = VAlignment.Bottom,
-                Pressed = state.ActiveMission == missionParams.Index,
-                ToggleMode = true,
-                Disabled = state.Claimed || state.Cooldown,
-            };
-
-            claimButton.Label.Margin = new Thickness(0f, 5f);
-
-            claimButton.OnPressed += args =>
-            {
-                ClaimMission?.Invoke(missionParams.Index);
-            };
-
-            if (state.ActiveMission == missionParams.Index)
-            {
-                claimButton.Text = Loc.GetString("salvage-expedition-window-claimed");
-                claimButton.AddStyleClass(StyleBase.ButtonCaution);
-            }
-            else
-            {
-                claimButton.Text = Loc.GetString("salvage-expedition-window-claim");
-            }
-
-            var box = new PanelContainer
-            {
-                PanelOverride = new StyleBoxFlat(new Color(30, 30, 34)),
-                HorizontalExpand = true,
-                Margin = new Thickness(5f, 0f),
-                Children =
-                {
-                    new BoxContainer
-                    {
-                        Orientation = BoxContainer.LayoutOrientation.Vertical,
-                        Children =
-                        {
-                            missionStripe,
-                            lBox,
-                            new Control() {VerticalExpand = true},
-                            claimButton,
-                        },
-                        Margin = new Thickness(5f, 5f)
-                    }
-                }
-            };
-
-            LayoutContainer.SetAnchorPreset(box, LayoutContainer.LayoutPreset.Wide);
-
-            Container.AddChild(box);
-        }
-    }
-
-    protected override void FrameUpdate(FrameEventArgs args)
-    {
-        base.FrameUpdate(args);
-
-        if (_claimed)
-        {
-            NextOfferBar.Value = 0f;
-            NextOfferText.Text = "00:00";
-            return;
-        }
-
-        var remaining = _nextOffer - _timing.CurTime;
-
-        if (remaining < TimeSpan.Zero)
-        {
-            NextOfferBar.Value = 1f;
-            NextOfferText.Text = "00:00";
-        }
-        else
-        {
-            var cooldown = _cooldown
-                ? TimeSpan.FromSeconds(_cfgManager.GetCVar(CCVars.SalvageExpeditionCooldown))
-                : TimeSpan.FromSeconds(_cfgManager.GetCVar(CCVars.SalvageExpeditionCooldown));
-
-            NextOfferBar.Value = 1f - (float) (remaining / cooldown);
-            NextOfferText.Text = $"{remaining.Minutes:00}:{remaining.Seconds:00}";
-        }
-    }
-}
diff --git a/Content.Client/Salvage/UI/SalvageMagnetBoundUserInterface.cs b/Content.Client/Salvage/UI/SalvageMagnetBoundUserInterface.cs
new file mode 100644 (file)
index 0000000..0c1994c
--- /dev/null
@@ -0,0 +1,110 @@
+using System.Linq;
+using Content.Shared.Salvage;
+using Content.Shared.Salvage.Magnet;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+
+namespace Content.Client.Salvage.UI;
+
+public sealed class SalvageMagnetBoundUserInterface : BoundUserInterface
+{
+    [Dependency] private readonly IEntityManager _entManager = default!;
+
+    private OfferingWindow? _window;
+
+    public SalvageMagnetBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+    {
+        IoCManager.InjectDependencies(this);
+    }
+
+    protected override void Open()
+    {
+        base.Open();
+        _window = new OfferingWindow();
+        _window.OnClose += Close;
+        _window.OpenCenteredLeft();
+    }
+
+    protected override void UpdateState(BoundUserInterfaceState state)
+    {
+        base.UpdateState(state);
+
+        if (state is not SalvageMagnetBoundUserInterfaceState current || _window == null)
+            return;
+
+        _window.ClearOptions();
+
+        var salvageSystem = _entManager.System<SharedSalvageSystem>();
+        _window.NextOffer = current.NextOffer;
+        _window.Progression = current.EndTime ?? TimeSpan.Zero;
+        _window.Claimed = current.EndTime != null;
+        _window.Cooldown = current.Cooldown;
+        _window.ProgressionCooldown = current.Duration;
+
+        for (var i = 0; i < current.Offers.Count; i++)
+        {
+            var seed = current.Offers[i];
+            var offer = salvageSystem.GetSalvageOffering(seed);
+            var option = new OfferingWindowOption();
+            option.MinWidth = 210f;
+            option.Disabled = current.EndTime != null;
+            option.Claimed = current.ActiveSeed == seed;
+            var claimIndex = i;
+
+            option.ClaimPressed += args =>
+            {
+                SendMessage(new MagnetClaimOfferEvent()
+                {
+                    Index = claimIndex
+                });
+            };
+
+            switch (offer)
+            {
+                case AsteroidOffering asteroid:
+                    option.Title = Loc.GetString($"dungeon-config-proto-{asteroid.DungeonConfig.ID}");
+                    var layerKeys = asteroid.MarkerLayers.Keys.ToList();
+                    layerKeys.Sort();
+
+                    foreach (var resource in layerKeys)
+                    {
+                        var count = asteroid.MarkerLayers[resource];
+
+                        var container = new BoxContainer()
+                        {
+                            Orientation = BoxContainer.LayoutOrientation.Horizontal,
+                            HorizontalExpand = true,
+                        };
+
+                        var resourceLabel = new Label()
+                        {
+                            Text = Loc.GetString("salvage-magnet-resources",
+                                ("resource", resource)),
+                            HorizontalAlignment = Control.HAlignment.Left,
+                        };
+
+                        var countLabel = new Label()
+                        {
+                            Text = Loc.GetString("salvage-magnet-resources-count", ("count", count)),
+                            HorizontalAlignment = Control.HAlignment.Right,
+                            HorizontalExpand = true,
+                        };
+
+                        container.AddChild(resourceLabel);
+                        container.AddChild(countLabel);
+
+                        option.AddContent(container);
+                    }
+
+                    break;
+                case SalvageOffering salvage:
+                    option.Title = Loc.GetString($"salvage-map-proto-{salvage.SalvageMap.ID}");
+                    break;
+                default:
+                    throw new ArgumentOutOfRangeException();
+            }
+
+            _window.AddOption(option);
+        }
+    }
+}
index 01680e34838d3afd2b395677f4da51c41238912c..9d75428beb7bbcaa0829a84f4282b4fcbc9788f4 100644 (file)
@@ -1,6 +1,7 @@
 using System.Linq;
 using Content.Server.Salvage;
 using Content.Shared.CCVar;
+using Content.Shared.Salvage;
 using Robust.Server.GameObjects;
 using Robust.Shared.Configuration;
 using Robust.Shared.GameObjects;
index fd44f24336523bb55ca44ef3b045a5d0ab31053a..09e202a76dca620625e9758f3e7ef02912527357 100644 (file)
@@ -21,14 +21,11 @@ using Robust.Shared.Configuration;
 using Robust.Shared.Console;
 using Robust.Shared.Map;
 using Robust.Shared.Map.Components;
-using Robust.Shared.Noise;
 using Robust.Shared.Physics;
-using Robust.Shared.Physics.Components;
 using Robust.Shared.Physics.Systems;
 using Robust.Shared.Player;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Random;
-using Robust.Shared.Serialization.Manager;
 using Robust.Shared.Threading;
 using Robust.Shared.Utility;
 
@@ -327,7 +324,8 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
         {
             if (_xformQuery.TryGetComponent(pSession.AttachedEntity, out var xform) &&
                 _handledEntities.Add(pSession.AttachedEntity.Value) &&
-                 _biomeQuery.TryGetComponent(xform.MapUid, out var biome))
+                 _biomeQuery.TryGetComponent(xform.MapUid, out var biome) &&
+                biome.Enabled)
             {
                 var worldPos = _transform.GetWorldPosition(xform);
                 AddChunksInRange(biome, worldPos);
@@ -343,7 +341,8 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
             {
                 if (!_handledEntities.Add(viewer) ||
                     !_xformQuery.TryGetComponent(viewer, out xform) ||
-                    !_biomeQuery.TryGetComponent(xform.MapUid, out biome))
+                    !_biomeQuery.TryGetComponent(xform.MapUid, out biome) ||
+                    !biome.Enabled)
                 {
                     continue;
                 }
@@ -363,8 +362,11 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
 
         while (loadBiomes.MoveNext(out var gridUid, out var biome, out var grid))
         {
+            if (!biome.Enabled)
+                continue;
+
             // Load new chunks
-            LoadChunks(biome, gridUid, grid, biome.Seed, _xformQuery);
+            LoadChunks(biome, gridUid, grid, biome.Seed);
             // Unload old chunks
             UnloadChunks(biome, gridUid, grid, biome.Seed);
         }
@@ -414,8 +416,29 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
         BiomeComponent component,
         EntityUid gridUid,
         MapGridComponent grid,
-        int seed,
-        EntityQuery<TransformComponent> xformQuery)
+        int seed)
+    {
+        BuildMarkerChunks(component, gridUid, grid, seed);
+
+        var active = _activeChunks[component];
+
+        foreach (var chunk in active)
+        {
+            LoadChunkMarkers(component, gridUid, grid, chunk, seed);
+
+            if (!component.LoadedChunks.Add(chunk))
+                continue;
+
+            // Load NOW!
+            LoadChunk(component, gridUid, grid, chunk, seed);
+        }
+    }
+
+    /// <summary>
+    /// Goes through all marker chunks that haven't been calculated, then calculates what spawns there are and
+    /// allocates them to the relevant actual chunks in the biome (marker chunks may be many times larger than biome chunks).
+    /// </summary>
+    private void BuildMarkerChunks(BiomeComponent component, EntityUid gridUid, MapGridComponent grid, int seed)
     {
         var markers = _markerChunks[component];
         var loadedMarkers = component.LoadedMarkers;
@@ -432,128 +455,57 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
                 if (loadedMarkers.TryGetValue(layer, out var mobChunks) && mobChunks.Contains(chunk))
                     return;
 
-                // Get the set of spawned nodes to avoid overlap.
                 var forced = component.ForcedMarkerLayers.Contains(layer);
-                var spawnSet = _tilePool.Get();
-                var frontier = new ValueList<Vector2i>(32);
 
                 // Make a temporary version and copy back in later.
                 var pending = new Dictionary<Vector2i, Dictionary<string, List<Vector2i>>>();
 
+                // Essentially get the seed + work out a buffer to adjacent chunks so we don't
+                // inadvertantly spawn too many near the edges.
                 var layerProto = ProtoManager.Index<BiomeMarkerLayerPrototype>(layer);
-                var buffer = layerProto.Radius / 2f;
                 var markerSeed = seed + chunk.X * ChunkSize + chunk.Y + localIdx;
                 var rand = new Random(markerSeed);
-
-                // We treat a null entity mask as requiring nothing else on the tile
-                var lower = (int) Math.Floor(buffer);
-                var upper = (int) Math.Ceiling(layerProto.Size - buffer);
-
-                // TODO: Need poisson but crashes whenever I use moony's due to inputs or smth idk
-                // Get the total amount of groups to spawn across the entire chunk.
-                var count = (int) ((layerProto.Size - buffer) * (layerProto.Size - buffer) /
-                                   (layerProto.Radius * layerProto.Radius));
+                var buffer = (int) (layerProto.Radius / 2f);
+                var bounds = new Box2i(chunk + buffer, chunk + layerProto.Size - buffer);
+                var count = (int) (bounds.Area / (layerProto.Radius * layerProto.Radius));
                 count = Math.Min(count, layerProto.MaxCount);
 
-                // Pick a random tile then BFS outwards from it
-                // It will bias edge tiles significantly more but will make the CPU cry less.
-                for (var i = 0; i < count; i++)
+                GetMarkerNodes(gridUid, component, grid, layerProto, forced, bounds, count, rand,
+                    out var spawnSet, out var existing);
+
+                // Forcing markers to spawn so delete any that were found to be in the way.
+                if (forced && existing.Count > 0)
                 {
-                    var groupSize = rand.Next(layerProto.MinGroupSize, layerProto.MaxGroupSize + 1);
-                    var startNodeX = rand.Next(lower, upper + 1);
-                    var startNodeY = rand.Next(lower, upper + 1);
-                    var startNode = new Vector2i(startNodeX, startNodeY);
-                    frontier.Clear();
-                    frontier.Add(startNode + chunk);
-
-                    while (groupSize >= 0 && frontier.Count > 0)
+                    // Lock something so we can delete these safely.
+                    lock (component.PendingMarkers)
                     {
-                        var frontierIndex = rand.Next(frontier.Count);
-                        var node = frontier[frontierIndex];
-                        frontier.RemoveSwap(frontierIndex);
-
-                        // Add neighbors regardless.
-                        for (var x = -1; x <= 1; x++)
-                        {
-                            for (var y = -1; y <= 1; y++)
-                            {
-                                if (x != 0 && y != 0)
-                                    continue;
-
-                                var neighbor = new Vector2i(node.X + x, node.Y + y);
-                                var chunkOffset = neighbor - chunk;
-
-                                // Check if it's inbounds.
-                                if (chunkOffset.X < lower ||
-                                    chunkOffset.Y < lower ||
-                                    chunkOffset.X > upper ||
-                                    chunkOffset.Y > upper)
-                                {
-                                    continue;
-                                }
-
-                                if (!spawnSet.Add(neighbor))
-                                    continue;
-
-                                frontier.Add(neighbor);
-                            }
-                        }
-
-                        // Check if it's a valid spawn, if so then use it.
-                        var enumerator = _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, node);
-                        enumerator.MoveNext(out var existing);
-
-                        if (!forced && existing != null)
-                            continue;
-
-                        // Check if mask matches // anything blocking.
-                        TryGetEntity(node, component, grid, out var proto);
-
-                        // If there's an existing entity and it doesn't match the mask then skip.
-                        if (layerProto.EntityMask.Count > 0 &&
-                            (proto == null ||
-                            !layerProto.EntityMask.ContainsKey(proto)))
+                        foreach (var ent in existing)
                         {
-                            continue;
-                        }
-
-                        // If it's just a flat spawn then just check for anything blocking.
-                        if (proto != null && layerProto.Prototype != null)
-                        {
-                            continue;
-                        }
-
-                        DebugTools.Assert(layerProto.EntityMask.Count == 0 || !string.IsNullOrEmpty(proto));
-                        var chunkOrigin = SharedMapSystem.GetChunkIndices(node, ChunkSize) * ChunkSize;
-
-                        if (!pending.TryGetValue(chunkOrigin, out var pendingMarkers))
-                        {
-                            pendingMarkers = new Dictionary<string, List<Vector2i>>();
-                            pending[chunkOrigin] = pendingMarkers;
+                            Del(ent);
                         }
+                    }
+                }
 
-                        if (!pendingMarkers.TryGetValue(layer, out var layerMarkers))
-                        {
-                            layerMarkers = new List<Vector2i>();
-                            pendingMarkers[layer] = layerMarkers;
-                        }
+                foreach (var node in spawnSet.Keys)
+                {
+                    var chunkOrigin = SharedMapSystem.GetChunkIndices(node, ChunkSize) * ChunkSize;
 
-                        layerMarkers.Add(node);
-                        groupSize--;
-                        spawnSet.Add(node);
+                    if (!pending.TryGetValue(chunkOrigin, out var pendingMarkers))
+                    {
+                        pendingMarkers = new Dictionary<string, List<Vector2i>>();
+                        pending[chunkOrigin] = pendingMarkers;
+                    }
 
-                        if (forced && existing != null)
-                        {
-                            // Just lock anything so we can dump this
-                            lock (component.PendingMarkers)
-                            {
-                                Del(existing.Value);
-                            }
-                        }
+                    if (!pendingMarkers.TryGetValue(layer, out var layerMarkers))
+                    {
+                        layerMarkers = new List<Vector2i>();
+                        pendingMarkers[layer] = layerMarkers;
                     }
+
+                    layerMarkers.Add(node);
                 }
 
-                lock (component.PendingMarkers)
+                lock (loadedMarkers)
                 {
                     if (!loadedMarkers.TryGetValue(layer, out var lockMobChunks))
                     {
@@ -576,28 +528,130 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
                             lockMarkers[lockLayer] = nodes;
                         }
                     }
-
-                    _tilePool.Return(spawnSet);
                 }
             });
         }
 
         component.ForcedMarkerLayers.Clear();
-        var active = _activeChunks[component];
+    }
 
-        foreach (var chunk in active)
+    /// <summary>
+    /// Gets the marker nodes for the specified area.
+    /// </summary>
+    /// <param name="emptyTiles">Should we include empty tiles when determine markers (e.g. if they are yet to be loaded)</param>
+    public void GetMarkerNodes(
+        EntityUid gridUid,
+        BiomeComponent biome,
+        MapGridComponent grid,
+        BiomeMarkerLayerPrototype layerProto,
+        bool forced,
+        Box2i bounds,
+        int count,
+        Random rand,
+        out Dictionary<Vector2i, string?> spawnSet,
+        out HashSet<EntityUid> existingEnts,
+        bool emptyTiles = true)
+    {
+        DebugTools.Assert(count > 0);
+
+        var frontier = new ValueList<Vector2i>(32);
+        // TODO: Need poisson but crashes whenever I use moony's due to inputs or smth idk
+        // Get the total amount of groups to spawn across the entire chunk.
+        // We treat a null entity mask as requiring nothing else on the tile
+
+        spawnSet = new Dictionary<Vector2i, string?>();
+        var visited = _tilePool.Get();
+        existingEnts = new HashSet<EntityUid>();
+
+        // Pick a random tile then BFS outwards from it
+        // It will bias edge tiles significantly more but will make the CPU cry less.
+        for (var i = 0; i < count; i++)
         {
-            LoadMarkerChunk(component, gridUid, grid, chunk, seed);
+            var groupSize = rand.Next(layerProto.MinGroupSize, layerProto.MaxGroupSize + 1);
+            var startNodeX = rand.Next(bounds.Left, bounds.Right);
+            var startNodeY = rand.Next(bounds.Bottom, bounds.Top);
+            var startNode = new Vector2i(startNodeX, startNodeY);
+            frontier.Clear();
+            frontier.Add(startNode);
+            visited.Add(startNode);
+
+            while (groupSize >= 0 && frontier.Count > 0)
+            {
+                var frontierIndex = rand.Next(frontier.Count);
+                var node = frontier[frontierIndex];
+                frontier.RemoveSwap(frontierIndex);
 
-            if (!component.LoadedChunks.Add(chunk))
-                continue;
+                // Add neighbors regardless.
+                for (var x = -1; x <= 1; x++)
+                {
+                    for (var y = -1; y <= 1; y++)
+                    {
+                        if (x != 0 && y != 0)
+                            continue;
 
-            // Load NOW!
-            LoadChunk(component, gridUid, grid, chunk, seed);
+                        var neighbor = new Vector2i(node.X + x, node.Y + y);
+
+                        // Check if it's inbounds.
+                        if (!bounds.Contains(neighbor))
+                            continue;
+
+                        if (!visited.Add(neighbor))
+                            continue;
+
+                        frontier.Add(neighbor);
+                    }
+                }
+
+                // Empty tile, skip if relevant.
+                if (!emptyTiles && (!_mapSystem.TryGetTile(grid, node, out var tile) || tile.IsEmpty))
+                    continue;
+
+                // Check if it's a valid spawn, if so then use it.
+                var enumerator = _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, node);
+                enumerator.MoveNext(out var existing);
+
+                if (!forced && existing != null)
+                    continue;
+
+                // Check if mask matches // anything blocking.
+                TryGetEntity(node, biome, grid, out var proto);
+
+                // If there's an existing entity and it doesn't match the mask then skip.
+                if (layerProto.EntityMask.Count > 0 &&
+                    (proto == null ||
+                     !layerProto.EntityMask.ContainsKey(proto)))
+                {
+                    continue;
+                }
+
+                // If it's just a flat spawn then just check for anything blocking.
+                if (proto != null && layerProto.Prototype != null)
+                {
+                    continue;
+                }
+
+                DebugTools.Assert(layerProto.EntityMask.Count == 0 || !string.IsNullOrEmpty(proto));
+                groupSize--;
+                spawnSet.Add(node, proto);
+
+                if (existing != null)
+                {
+                    existingEnts.Add(existing.Value);
+                }
+            }
         }
+
+        _tilePool.Return(visited);
     }
 
-    private void LoadMarkerChunk(
+    /// <summary>
+    /// Loads the pre-deteremined marker nodes for a particular chunk.
+    /// This is calculated in <see cref="BuildMarkerChunks"/>
+    /// </summary>
+    /// <remarks>
+    /// Note that the marker chunks do not correspond to this chunk.
+    /// </remarks>
+    private void LoadChunkMarkers(
         BiomeComponent component,
         EntityUid gridUid,
         MapGridComponent grid,
index 08738b9abed0e0d22b1b976d2a424d2161b86952..53d8a432a33008d419395a4f56a519cedcc32c56 100644 (file)
@@ -1,5 +1,4 @@
 using System.Numerics;
-using Content.Server.Shuttle.Components;
 using Content.Server.Shuttles.Components;
 using Content.Server.Shuttles.Systems;
 using Content.Shared.Movement.Components;
@@ -9,6 +8,7 @@ using Content.Shared.Shuttles.Systems;
 using Robust.Shared.Map;
 using Robust.Shared.Physics.Components;
 using Robust.Shared.Player;
+using DroneConsoleComponent = Content.Server.Shuttles.DroneConsoleComponent;
 
 namespace Content.Server.Physics.Controllers
 {
diff --git a/Content.Server/Procedural/DungeonJob.NoiseDunGen.cs b/Content.Server/Procedural/DungeonJob.NoiseDunGen.cs
new file mode 100644 (file)
index 0000000..f8af0de
--- /dev/null
@@ -0,0 +1,146 @@
+using System.Numerics;
+using System.Threading.Tasks;
+using Content.Shared.Maps;
+using Content.Shared.Procedural;
+using Content.Shared.Procedural.DungeonGenerators;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Random;
+using Robust.Shared.Utility;
+
+namespace Content.Server.Procedural;
+
+public sealed partial class DungeonJob
+{
+    private async Task<Dungeon> GenerateNoiseDungeon(NoiseDunGen dungen, EntityUid gridUid, MapGridComponent grid,
+        int seed)
+    {
+        var rand = new Random(seed);
+        var tiles = new List<(Vector2i, Tile)>();
+
+        foreach (var layer in dungen.Layers)
+        {
+            layer.Noise.SetSeed(seed);
+        }
+
+        // First we have to find a seed tile, then floodfill from there until we get to noise
+        // at which point we floodfill the entire noise.
+        var iterations = dungen.Iterations;
+        var area = new Box2i();
+        var frontier = new Queue<Vector2i>();
+        var rooms = new List<DungeonRoom>();
+        var tileCount = 0;
+        var tileCap = rand.NextGaussian(dungen.TileCap, dungen.CapStd);
+        var visited = new HashSet<Vector2i>();
+
+        while (iterations > 0 && tileCount < tileCap)
+        {
+            var roomTiles = new HashSet<Vector2i>();
+            iterations--;
+
+            // Get a random exterior tile to start floodfilling from.
+            var edge = rand.Next(4);
+            Vector2i seedTile;
+
+            switch (edge)
+            {
+                case 0:
+                    seedTile = new Vector2i(rand.Next(area.Left - 2, area.Right + 1), area.Bottom - 2);
+                    break;
+                case 1:
+                    seedTile = new Vector2i(area.Right + 1, rand.Next(area.Bottom - 2, area.Top + 1));
+                    break;
+                case 2:
+                    seedTile = new Vector2i(rand.Next(area.Left - 2, area.Right + 1), area.Top + 1);
+                    break;
+                case 3:
+                    seedTile = new Vector2i(area.Left - 2, rand.Next(area.Bottom - 2, area.Top + 1));
+                    break;
+                default:
+                    throw new ArgumentOutOfRangeException();
+            }
+
+            DebugTools.Assert(!visited.Contains(seedTile));
+            var noiseFill = false;
+            frontier.Clear();
+            visited.Add(seedTile);
+            frontier.Enqueue(seedTile);
+            area = area.UnionTile(seedTile);
+            Box2i roomArea = new Box2i(seedTile, seedTile + Vector2i.One);
+
+            // Time to floodfill again
+            while (frontier.TryDequeue(out var node) && tileCount < tileCap)
+            {
+                var foundNoise = false;
+
+                foreach (var layer in dungen.Layers)
+                {
+                    var value = layer.Noise.GetNoise(node.X, node.Y);
+
+                    if (value < layer.Threshold)
+                        continue;
+
+                    roomArea = roomArea.UnionTile(node);
+                    foundNoise = true;
+                    noiseFill = true;
+                    var tileDef = _tileDefManager[layer.Tile];
+                    var variant = rand.NextByte(tileDef.Variants);
+
+                    tiles.Add((node, new Tile(tileDef.TileId, variant: variant)));
+                    roomTiles.Add(node);
+                    tileCount++;
+                    break;
+                }
+
+                // Don't get neighbors if they don't have noise.
+                // only if we've already found any noise.
+                if (noiseFill && !foundNoise)
+                    continue;
+
+                for (var x = -1; x <= 1; x++)
+                {
+                    for (var y = -1; y <= 1; y++)
+                    {
+                        // Cardinals only
+                        if (x != 0 && y != 0)
+                            continue;
+
+                        var neighbor = new Vector2i(node.X + x, node.Y + y);
+
+                        if (!visited.Add(neighbor))
+                            continue;
+
+                        area = area.UnionTile(neighbor);
+                        frontier.Enqueue(neighbor);
+                    }
+                }
+
+                await SuspendIfOutOfTime();
+                ValidateResume();
+            }
+
+            var center = Vector2.Zero;
+
+            foreach (var tile in roomTiles)
+            {
+                center += tile + grid.TileSizeHalfVector;
+            }
+
+            center /= roomTiles.Count;
+            rooms.Add(new DungeonRoom(roomTiles, center, roomArea, new HashSet<Vector2i>()));
+            await SuspendIfOutOfTime();
+            ValidateResume();
+        }
+
+        grid.SetTiles(tiles);
+
+        var dungeon = new Dungeon(rooms);
+
+        foreach (var tile in tiles)
+        {
+            dungeon.RoomTiles.Add(tile.Item1);
+        }
+
+        return dungeon;
+    }
+}
index 4718de7c0c0e61502713ec79f3e401020bcc52ef..559d5fedb91d6e9a0ac9176a54b1ee1ed3c55008 100644 (file)
@@ -790,7 +790,7 @@ public sealed partial class DungeonJob
             foreach (var entrance in room.Entrances)
             {
                 // Just so we can still actually get in to the entrance we won't deter from a tile away from it.
-                var normal = ((Vector2) entrance + grid.TileSizeHalfVector - room.Center).ToWorldAngle().GetCardinalDir().ToIntVec();
+                var normal = (entrance + grid.TileSizeHalfVector - room.Center).ToWorldAngle().GetCardinalDir().ToIntVec();
                 deterredTiles.Remove(entrance + normal);
             }
         }
diff --git a/Content.Server/Procedural/DungeonJob.PostGenBiome.cs b/Content.Server/Procedural/DungeonJob.PostGenBiome.cs
new file mode 100644 (file)
index 0000000..4d3f573
--- /dev/null
@@ -0,0 +1,138 @@
+using System.Threading.Tasks;
+using Content.Server.Parallax;
+using Content.Shared.Parallax.Biomes;
+using Content.Shared.Parallax.Biomes.Markers;
+using Content.Shared.Procedural;
+using Content.Shared.Procedural.PostGeneration;
+using Content.Shared.Random.Helpers;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Utility;
+
+namespace Content.Server.Procedural;
+
+public sealed partial class DungeonJob
+{
+    /*
+     * Handles PostGen code for marker layers + biomes.
+     */
+
+    private async Task PostGen(BiomePostGen postGen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, Random random)
+    {
+        if (_entManager.TryGetComponent(gridUid, out BiomeComponent? biomeComp))
+            return;
+
+        biomeComp = _entManager.AddComponent<BiomeComponent>(gridUid);
+        var biomeSystem = _entManager.System<BiomeSystem>();
+        biomeSystem.SetTemplate(gridUid, biomeComp, _prototype.Index(postGen.BiomeTemplate));
+        var seed = random.Next();
+        var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
+
+        foreach (var node in dungeon.RoomTiles)
+        {
+            // Need to set per-tile to override data.
+            if (biomeSystem.TryGetTile(node, biomeComp.Layers, seed, grid, out var tile))
+            {
+                _maps.SetTile(gridUid, grid, node, tile.Value);
+            }
+
+            if (biomeSystem.TryGetDecals(node, biomeComp.Layers, seed, grid, out var decals))
+            {
+                foreach (var decal in decals)
+                {
+                    _decals.TryAddDecal(decal.ID, new EntityCoordinates(gridUid, decal.Position), out _);
+                }
+            }
+
+            if (biomeSystem.TryGetEntity(node, biomeComp, grid, out var entityProto))
+            {
+                var ent = _entManager.SpawnEntity(entityProto, new EntityCoordinates(gridUid, node + grid.TileSizeHalfVector));
+                var xform = xformQuery.Get(ent);
+
+                if (!xform.Comp.Anchored)
+                {
+                    _transform.AnchorEntity(ent, xform);
+                }
+
+                // TODO: Engine bug with SpawnAtPosition
+                DebugTools.Assert(xform.Comp.Anchored);
+            }
+
+            await SuspendIfOutOfTime();
+            ValidateResume();
+        }
+
+        biomeComp.Enabled = false;
+    }
+
+    private async Task PostGen(BiomeMarkerLayerPostGen postGen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, Random random)
+    {
+        if (!_entManager.TryGetComponent(gridUid, out BiomeComponent? biomeComp))
+            return;
+
+        var biomeSystem = _entManager.System<BiomeSystem>();
+        var weightedRandom = _prototype.Index(postGen.MarkerTemplate);
+        var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
+        var templates = new Dictionary<string, int>();
+
+        for (var i = 0; i < postGen.Count; i++)
+        {
+            var template = weightedRandom.Pick(random);
+            var count = templates.GetOrNew(template);
+            count++;
+            templates[template] = count;
+        }
+
+        foreach (var (template, count) in templates)
+        {
+            var markerTemplate = _prototype.Index<BiomeMarkerLayerPrototype>(template);
+
+            var bounds = new Box2i();
+
+            foreach (var tile in dungeon.RoomTiles)
+            {
+                bounds = bounds.UnionTile(tile);
+            }
+
+            await SuspendIfOutOfTime();
+            ValidateResume();
+
+            biomeSystem.GetMarkerNodes(gridUid, biomeComp, grid, markerTemplate, true, bounds, count,
+                random, out var spawnSet, out var existing, false);
+
+            await SuspendIfOutOfTime();
+            ValidateResume();
+
+            foreach (var ent in existing)
+            {
+                _entManager.DeleteEntity(ent);
+            }
+
+            await SuspendIfOutOfTime();
+            ValidateResume();
+
+            foreach (var (node, mask) in spawnSet)
+            {
+                string? proto;
+
+                if (mask != null && markerTemplate.EntityMask.TryGetValue(mask, out var maskedProto))
+                {
+                    proto = maskedProto;
+                }
+                else
+                {
+                    proto = markerTemplate.Prototype;
+                }
+
+                var ent = _entManager.SpawnAtPosition(proto, new EntityCoordinates(gridUid, node + grid.TileSizeHalfVector));
+                var xform = xformQuery.Get(ent);
+
+                if (!xform.Comp.Anchored)
+                    _transform.AnchorEntity(ent, xform);
+
+                await SuspendIfOutOfTime();
+                ValidateResume();
+            }
+        }
+    }
+}
index 4da585586a04f1576903614a2c25aac990c9fc09..55c4474d31b0ba6dfbe0eb21e2348dd83b212380 100644 (file)
@@ -27,6 +27,7 @@ public sealed partial class DungeonJob : Job<Dungeon>
     private readonly DecalSystem _decals;
     private readonly DungeonSystem _dungeon;
     private readonly EntityLookupSystem _lookup;
+    private readonly SharedMapSystem _maps;
     private readonly SharedTransformSystem _transform;
     private EntityQuery<TagComponent> _tagQuery;
 
@@ -68,6 +69,7 @@ public sealed partial class DungeonJob : Job<Dungeon>
         _decals = decals;
         _dungeon = dungeon;
         _lookup = lookup;
+        _maps = _entManager.System<SharedMapSystem>();
         _transform = transform;
         _tagQuery = _entManager.GetEntityQuery<TagComponent>();
 
@@ -86,15 +88,18 @@ public sealed partial class DungeonJob : Job<Dungeon>
 
         switch (_gen.Generator)
         {
+            case NoiseDunGen noise:
+                dungeon = await GenerateNoiseDungeon(noise, _gridUid, _grid, _seed);
+                break;
             case PrefabDunGen prefab:
                 dungeon = await GeneratePrefabDungeon(prefab, _gridUid, _grid, _seed);
+                DebugTools.Assert(dungeon.RoomExteriorTiles.Count > 0);
                 break;
             default:
                 throw new NotImplementedException();
         }
 
         DebugTools.Assert(dungeon.RoomTiles.Count > 0);
-        DebugTools.Assert(dungeon.RoomExteriorTiles.Count > 0);
 
         // To make it slightly more deterministic treat this RNG as separate ig.
         var random = new Random(_seed);
@@ -108,6 +113,9 @@ public sealed partial class DungeonJob : Job<Dungeon>
                 case AutoCablingPostGen cabling:
                     await PostGen(cabling, dungeon, _gridUid, _grid, random);
                     break;
+                case BiomePostGen biome:
+                    await PostGen(biome, dungeon, _gridUid, _grid, random);
+                    break;
                 case BoundaryWallPostGen boundary:
                     await PostGen(boundary, dungeon, _gridUid, _grid, random);
                     break;
@@ -138,6 +146,9 @@ public sealed partial class DungeonJob : Job<Dungeon>
                 case InternalWindowPostGen internalWindow:
                     await PostGen(internalWindow, dungeon, _gridUid, _grid, random);
                     break;
+                case BiomeMarkerLayerPostGen markerPost:
+                    await PostGen(markerPost, dungeon, _gridUid, _grid, random);
+                    break;
                 case RoomEntrancePostGen rEntrance:
                     await PostGen(rEntrance, dungeon, _gridUid, _grid, random);
                     break;
@@ -154,6 +165,7 @@ public sealed partial class DungeonJob : Job<Dungeon>
                 break;
         }
 
+        // Defer splitting so they don't get spammed and so we don't have to worry about tracking the grid along the way.
         _grid.CanSplit = true;
         _entManager.System<GridFixtureSystem>().CheckSplits(_gridUid);
         return dungeon;
index ba69f1ea5f8146d7b1bd2b150a7022bc28968a27..d8377940f8eadb41e809e2af860ad5b78aca5ad9 100644 (file)
@@ -227,7 +227,6 @@ public sealed partial class DungeonSystem : SharedDungeonSystem
 
         _dungeonJobs.Add(job, cancelToken);
         _dungeonJobQueue.EnqueueJob(job);
-        job.Run();
         await job.AsTask;
 
         if (job.Exception != null)
index 60e1d3b47251d9845546b7b0eb1898be1f3d2fcc..323b71daeaeadfbfe8772a6c3ff9c1f426157ffd 100644 (file)
@@ -5,6 +5,9 @@ using Robust.Shared.Random;
 
 namespace Content.Server.Salvage;
 
+/// <summary>
+/// Transports attached entities to the linked beacon after a timer has elapsed.
+/// </summary>
 public sealed class FultonSystem : SharedFultonSystem
 {
     [Dependency] private readonly IRobustRandom _random = default!;
diff --git a/Content.Server/Salvage/Magnet/SalvageMagnetComponent.cs b/Content.Server/Salvage/Magnet/SalvageMagnetComponent.cs
new file mode 100644 (file)
index 0000000..6c1abf3
--- /dev/null
@@ -0,0 +1,7 @@
+namespace Content.Server.Salvage.Magnet;
+
+[RegisterComponent]
+public sealed partial class SalvageMagnetComponent : Component
+{
+
+}
diff --git a/Content.Server/Salvage/Magnet/SalvageMagnetDataComponent.cs b/Content.Server/Salvage/Magnet/SalvageMagnetDataComponent.cs
new file mode 100644 (file)
index 0000000..4c4a222
--- /dev/null
@@ -0,0 +1,57 @@
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Server.Salvage.Magnet;
+
+/// <summary>
+/// Added to the station to hold salvage magnet data.
+/// </summary>
+[RegisterComponent]
+public sealed partial class SalvageMagnetDataComponent : Component
+{
+    // May be multiple due to splitting.
+
+    /// <summary>
+    /// Entities currently magnetised.
+    /// </summary>
+    [DataField]
+    public List<EntityUid>? ActiveEntities;
+
+    /// <summary>
+    /// If the magnet is currently active when does it end.
+    /// </summary>
+    [DataField(customTypeSerializer:typeof(TimeOffsetSerializer))]
+    public TimeSpan? EndTime;
+
+    [DataField(customTypeSerializer:typeof(TimeOffsetSerializer))]
+    public TimeSpan NextOffer;
+
+    /// <summary>
+    /// How long salvage will be active for before despawning.
+    /// </summary>
+    [DataField]
+    public TimeSpan ActiveTime = TimeSpan.FromMinutes(6);
+
+    /// <summary>
+    /// Cooldown between offerings after one ends.
+    /// </summary>
+    [DataField]
+    public TimeSpan OfferCooldown = TimeSpan.FromMinutes(3);
+
+    /// <summary>
+    /// Seeds currently offered
+    /// </summary>
+    [DataField]
+    public List<int> Offered = new();
+
+    [DataField]
+    public int OfferCount = 6;
+
+    [DataField]
+    public int ActiveSeed;
+
+    /// <summary>
+    /// Final countdown announcement.
+    /// </summary>
+    [DataField]
+    public bool Announced;
+}
diff --git a/Content.Server/Salvage/Magnet/SalvageMagnetTargetComponent.cs b/Content.Server/Salvage/Magnet/SalvageMagnetTargetComponent.cs
new file mode 100644 (file)
index 0000000..01ecb7c
--- /dev/null
@@ -0,0 +1,14 @@
+namespace Content.Server.Salvage.Magnet;
+
+/// <summary>
+/// Indicates the entity is a salvage target for tracking.
+/// </summary>
+[RegisterComponent]
+public sealed partial class SalvageMagnetTargetComponent : Component
+{
+    /// <summary>
+    /// Entity that spawned us.
+    /// </summary>
+    [DataField]
+    public EntityUid DataTarget;
+}
diff --git a/Content.Server/Salvage/Magnet/SalvageMobRestrictionsComponent.cs b/Content.Server/Salvage/Magnet/SalvageMobRestrictionsComponent.cs
new file mode 100644 (file)
index 0000000..62ce5c6
--- /dev/null
@@ -0,0 +1,12 @@
+namespace Content.Server.Salvage.Magnet;
+
+// This is dumb
+/// <summary>
+/// Deletes the attached entity if the linked entity is deleted.
+/// </summary>
+[RegisterComponent]
+public sealed partial class SalvageMobRestrictionsComponent : Component
+{
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    public EntityUid LinkedEntity;
+}
diff --git a/Content.Server/Salvage/SalvageGridComponent.cs b/Content.Server/Salvage/SalvageGridComponent.cs
deleted file mode 100644 (file)
index af7a851..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-namespace Content.Server.Salvage
-{
-    /// <summary>
-    /// A grid spawned by a salvage magnet.
-    /// </summary>
-    [RegisterComponent]
-    public sealed partial class SalvageGridComponent : Component
-    {
-        /// <summary>
-        /// The magnet that spawned this grid.
-        /// </summary>
-        public EntityUid? SpawnerMagnet;
-    }
-}
diff --git a/Content.Server/Salvage/SalvageMagnetComponent.cs b/Content.Server/Salvage/SalvageMagnetComponent.cs
deleted file mode 100644 (file)
index d9a5b79..0000000
+++ /dev/null
@@ -1,138 +0,0 @@
-using Content.Shared.Radio;
-using Content.Shared.Random;
-using Content.Shared.Salvage;
-using Robust.Shared.GameStates;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Server.Salvage
-{
-    /// <summary>
-    /// A salvage magnet.
-    /// </summary>
-    [NetworkedComponent, RegisterComponent]
-    [Access(typeof(SalvageSystem))]
-    public sealed partial class SalvageMagnetComponent : SharedSalvageMagnetComponent
-    {
-        /// <summary>
-        /// Maximum distance from the offset position that will be used as a salvage's spawnpoint.
-        /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("offsetRadiusMax")]
-        public float OffsetRadiusMax = 32;
-
-        /// <summary>
-        /// The entity attached to the magnet
-        /// </summary>
-        [ViewVariables(VVAccess.ReadOnly)]
-        [DataField("attachedEntity")]
-        public EntityUid? AttachedEntity;
-
-        /// <summary>
-        /// Current state of this magnet
-        /// </summary>
-        [ViewVariables(VVAccess.ReadOnly)]
-        [DataField("magnetState")]
-        public MagnetState MagnetState = MagnetState.Inactive;
-
-        /// <summary>
-        /// How long it takes for the magnet to pull in the debris
-        /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("baseAttachingTime")]
-        public TimeSpan BaseAttachingTime = TimeSpan.FromSeconds(30);
-
-        /// <summary>
-        /// How long it actually takes for the magnet to pull in the debris
-        /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("attachingTime")]
-        public TimeSpan AttachingTime = TimeSpan.FromSeconds(30);
-
-        /// <summary>
-        /// How long the magnet can hold the debris until it starts losing the lock
-        /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("holdTime")]
-        public TimeSpan HoldTime = TimeSpan.FromSeconds(240);
-
-        /// <summary>
-        /// How long the magnet can hold the debris while losing the lock
-        /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("detachingTime")]
-        public TimeSpan DetachingTime = TimeSpan.FromSeconds(30);
-
-        /// <summary>
-        /// How long the magnet has to cool down for after use
-        /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("baseCooldownTime")]
-        public TimeSpan BaseCooldownTime = TimeSpan.FromSeconds(60);
-
-        /// <summary>
-        /// How long the magnet actually has to cool down for after use
-        /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("cooldownTime")]
-        public TimeSpan CooldownTime = TimeSpan.FromSeconds(60);
-
-        [DataField("salvageChannel", customTypeSerializer: typeof(PrototypeIdSerializer<RadioChannelPrototype>))]
-        public string SalvageChannel = "Supply";
-
-        /// <summary>
-        /// Current how much charge the magnet currently has
-        /// </summary>
-        [DataField("chargeRemaining")]
-        public int ChargeRemaining = 5;
-
-        /// <summary>
-        /// How much capacity the magnet can hold
-        /// </summary>
-        [DataField("chargeCapacity")]
-        public int ChargeCapacity = 5;
-
-        /// <summary>
-        /// Used as a guard to prevent spamming the appearance system
-        /// </summary>
-        [DataField("previousCharge")]
-        public int PreviousCharge = 5;
-
-        /// <summary>
-        /// The chance that a random procgen asteroid will be
-        /// generated rather than a static salvage prototype.
-        /// </summary>
-        [DataField("asteroidChance"), ViewVariables(VVAccess.ReadWrite)]
-        public float AsteroidChance = 0.6f;
-
-        /// <summary>
-        /// A weighted random prototype corresponding to
-        /// what asteroid entities will be generated.
-        /// </summary>
-        [DataField("asteroidPool", customTypeSerializer: typeof(PrototypeIdSerializer<WeightedRandomEntityPrototype>)), ViewVariables(VVAccess.ReadWrite)]
-        public string AsteroidPool = "RandomAsteroidPool";
-    }
-
-    [CopyByRef, DataRecord]
-    public record struct MagnetState(MagnetStateType StateType, TimeSpan Until)
-    {
-        public static readonly MagnetState Inactive = new (MagnetStateType.Inactive, TimeSpan.Zero);
-    };
-
-    public sealed class SalvageMagnetActivatedEvent : EntityEventArgs
-    {
-        public EntityUid Magnet;
-
-        public SalvageMagnetActivatedEvent(EntityUid magnet)
-        {
-            Magnet = magnet;
-        }
-    }
-    public enum MagnetStateType
-    {
-        Inactive,
-        Attaching,
-        Holding,
-        Detaching,
-        CoolingDown,
-    }
-}
diff --git a/Content.Server/Salvage/SalvageMapPrototype.cs b/Content.Server/Salvage/SalvageMapPrototype.cs
deleted file mode 100644 (file)
index b951c47..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-using Robust.Shared.Prototypes;
-using Robust.Shared.Utility;
-
-namespace Content.Server.Salvage;
-
-[Prototype("salvageMap")]
-public sealed partial class SalvageMapPrototype : IPrototype
-{
-    [ViewVariables] [IdDataField] public string ID { get; } = default!;
-
-    /// <summary>
-    /// Relative directory path to the given map, i.e. `Maps/Salvage/template.yml`
-    /// </summary>
-    [DataField("mapPath", required: true)] public ResPath MapPath;
-
-    /// <summary>
-    /// Name for admin use
-    /// </summary>
-    [DataField("name")] public string Name = string.Empty;
-}
diff --git a/Content.Server/Salvage/SalvageMobRestrictionsComponent.cs b/Content.Server/Salvage/SalvageMobRestrictionsComponent.cs
deleted file mode 100644 (file)
index 4d1b01e..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-using Robust.Shared.GameObjects;
-using Robust.Shared.Maths;
-using Robust.Shared.Serialization.Manager.Attributes;
-using Robust.Shared.ViewVariables;
-using System;
-
-namespace Content.Server.Salvage;
-
-/// <summary>
-///     This component exists as a sort of stateful marker for a
-///     killswitch meant to keep salvage mobs from doing stuff they
-///     really shouldn't (attacking station).
-///     The main thing is that adding this component ties the mob to
-///     whatever it's currently parented to.
-/// </summary>
-[RegisterComponent]
-public sealed partial class SalvageMobRestrictionsComponent : Component
-{
-    [ViewVariables(VVAccess.ReadOnly)]
-    [DataField("linkedGridEntity")]
-    public EntityUid LinkedGridEntity = EntityUid.Invalid;
-}
-
diff --git a/Content.Server/Salvage/SalvageMobRestrictionsGridComponent.cs b/Content.Server/Salvage/SalvageMobRestrictionsGridComponent.cs
deleted file mode 100644 (file)
index 29ea7c5..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-using Robust.Shared.GameObjects;
-using Robust.Shared.Maths;
-using Robust.Shared.Serialization.Manager.Attributes;
-using Robust.Shared.ViewVariables;
-using System;
-
-namespace Content.Server.Salvage;
-
-/// <summary>
-///     This component is attached to grids when a salvage mob is
-///     spawned on them.
-///     This attachment is done by SalvageMobRestrictionsSystem.
-///     *Simply put, when this component is removed, the mobs die.*
-///     *This applies even if the mobs are off-grid at the time.*
-/// </summary>
-[RegisterComponent]
-public sealed partial class SalvageMobRestrictionsGridComponent : Component
-{
-    [ViewVariables(VVAccess.ReadOnly)]
-    [DataField("mobsToKill")]
-    public List<EntityUid> MobsToKill = new();
-}
-
diff --git a/Content.Server/Salvage/SalvageMobRestrictionsSystem.cs b/Content.Server/Salvage/SalvageMobRestrictionsSystem.cs
deleted file mode 100644 (file)
index d326c36..0000000
+++ /dev/null
@@ -1,70 +0,0 @@
-using Content.Server.Body.Systems;
-using Content.Shared.Body.Components;
-using Content.Shared.Damage;
-using Content.Shared.Mobs.Systems;
-
-namespace Content.Server.Salvage;
-
-public sealed class SalvageMobRestrictionsSystem : EntitySystem
-{
-    [Dependency] private readonly BodySystem _bodySystem = default!;
-    [Dependency] private readonly DamageableSystem _damageableSystem = default!;
-    [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
-
-    public override void Initialize()
-    {
-        base.Initialize();
-
-        SubscribeLocalEvent<SalvageMobRestrictionsComponent, ComponentInit>(OnInit);
-        SubscribeLocalEvent<SalvageMobRestrictionsComponent, ComponentRemove>(OnRemove);
-        SubscribeLocalEvent<SalvageMobRestrictionsGridComponent, ComponentRemove>(OnRemoveGrid);
-    }
-
-    private void OnInit(EntityUid uid, SalvageMobRestrictionsComponent component, ComponentInit args)
-    {
-        var gridUid = Transform(uid).ParentUid;
-        if (!EntityManager.EntityExists(gridUid))
-        {
-            // Give up, we were spawned improperly
-            return;
-        }
-        // When this code runs, the salvage magnet hasn't actually gotten ahold of the entity yet.
-        // So it therefore isn't in a position to do this.
-        if (!TryComp(gridUid, out SalvageMobRestrictionsGridComponent? rg))
-        {
-            rg = AddComp<SalvageMobRestrictionsGridComponent>(gridUid);
-        }
-        rg.MobsToKill.Add(uid);
-        component.LinkedGridEntity = gridUid;
-    }
-
-    private void OnRemove(EntityUid uid, SalvageMobRestrictionsComponent component, ComponentRemove args)
-    {
-        if (TryComp(component.LinkedGridEntity, out SalvageMobRestrictionsGridComponent? rg))
-        {
-            rg.MobsToKill.Remove(uid);
-        }
-    }
-
-    private void OnRemoveGrid(EntityUid uid, SalvageMobRestrictionsGridComponent component, ComponentRemove args)
-    {
-        var metaQuery = GetEntityQuery<MetaDataComponent>();
-        var bodyQuery = GetEntityQuery<BodyComponent>();
-        var damageQuery = GetEntityQuery<DamageableComponent>();
-        foreach (var target in component.MobsToKill)
-        {
-            if (Deleted(target, metaQuery)) continue;
-            if (_mobStateSystem.IsDead(target)) continue; // DONT WASTE BIOMASS
-            if (bodyQuery.TryGetComponent(target, out var body))
-            {
-                // Just because.
-                _bodySystem.GibBody(target, body: body);
-            }
-            else if (damageQuery.TryGetComponent(target, out var damageableComponent))
-            {
-                _damageableSystem.SetAllDamage(target, damageableComponent, 200);
-            }
-        }
-    }
-}
-
index a1fd60974116e393d40db21b85ccea64dcbe7dcd..49785e8f4171558f5ba7abe3460da7cb657aeb78 100644 (file)
@@ -44,7 +44,7 @@ sealed class SalvageRulerCommand : IConsoleCommand
         var first = true;
         foreach (var mapGrid in _maps.GetAllGrids(entityTransform.MapID))
         {
-            var aabb = _entities.GetComponent<TransformComponent>(mapGrid).WorldMatrix.TransformBox(mapGrid.Comp.LocalAABB);
+            var aabb = _entities.System<SharedTransformSystem>().GetWorldMatrix(mapGrid).TransformBox(mapGrid.Comp.LocalAABB);
             if (first)
             {
                 total = aabb;
diff --git a/Content.Server/Salvage/SalvageSystem.Magnet.cs b/Content.Server/Salvage/SalvageSystem.Magnet.cs
new file mode 100644 (file)
index 0000000..b6c188b
--- /dev/null
@@ -0,0 +1,408 @@
+using System.Linq;
+using System.Numerics;
+using System.Threading.Tasks;
+using Content.Server.Salvage.Magnet;
+using Content.Shared.Humanoid;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Radio;
+using Content.Shared.Salvage.Magnet;
+using Robust.Server.Maps;
+using Robust.Shared.Map;
+
+namespace Content.Server.Salvage;
+
+public sealed partial class SalvageSystem
+{
+    [ValidatePrototypeId<RadioChannelPrototype>]
+    private const string MagnetChannel = "Supply";
+
+    private EntityQuery<SalvageMobRestrictionsComponent> _salvMobQuery;
+
+    private void InitializeMagnet()
+    {
+        _salvMobQuery = GetEntityQuery<SalvageMobRestrictionsComponent>();
+
+        SubscribeLocalEvent<SalvageMagnetDataComponent, MapInitEvent>(OnMagnetDataMapInit);
+
+        SubscribeLocalEvent<SalvageMagnetTargetComponent, GridSplitEvent>(OnMagnetTargetSplit);
+
+        SubscribeLocalEvent<SalvageMagnetComponent, MagnetClaimOfferEvent>(OnMagnetClaim);
+        SubscribeLocalEvent<SalvageMagnetComponent, ComponentStartup>(OnMagnetStartup);
+        SubscribeLocalEvent<SalvageMagnetComponent, AnchorStateChangedEvent>(OnMagnetAnchored);
+    }
+
+    private void OnMagnetClaim(EntityUid uid, SalvageMagnetComponent component, ref MagnetClaimOfferEvent args)
+    {
+        var player = args.Session.AttachedEntity;
+
+        if (player is null)
+            return;
+
+        var station = _station.GetOwningStation(uid);
+
+        if (!TryComp(station, out SalvageMagnetDataComponent? dataComp) ||
+            dataComp.EndTime != null)
+        {
+            return;
+        }
+
+        TakeMagnetOffer((station.Value, dataComp), args.Index, (uid, component));
+    }
+
+    private void OnMagnetStartup(EntityUid uid, SalvageMagnetComponent component, ComponentStartup args)
+    {
+        UpdateMagnetUI((uid, component), Transform(uid));
+    }
+
+    private void OnMagnetAnchored(EntityUid uid, SalvageMagnetComponent component, ref AnchorStateChangedEvent args)
+    {
+        if (!args.Anchored)
+            return;
+
+        UpdateMagnetUI((uid, component), args.Transform);
+    }
+
+    private void OnMagnetDataMapInit(EntityUid uid, SalvageMagnetDataComponent component, ref MapInitEvent args)
+    {
+        CreateMagnetOffers((uid, component));
+    }
+
+    private void OnMagnetTargetSplit(EntityUid uid, SalvageMagnetTargetComponent component, ref GridSplitEvent args)
+    {
+        // Don't think I'm not onto you people splitting to make new grids.
+        if (TryComp(component.DataTarget, out SalvageMagnetDataComponent? dataComp))
+        {
+            foreach (var gridUid in args.NewGrids)
+            {
+                dataComp.ActiveEntities?.Add(gridUid);
+            }
+        }
+    }
+
+    private void UpdateMagnet()
+    {
+        var dataQuery = EntityQueryEnumerator<SalvageMagnetDataComponent>();
+        var curTime = _timing.CurTime;
+
+        while (dataQuery.MoveNext(out var uid, out var magnetData))
+        {
+            // Magnet currently active.
+            if (magnetData.EndTime != null)
+            {
+                if (magnetData.EndTime.Value < curTime)
+                {
+                    EndMagnet((uid, magnetData));
+                }
+                else if (!magnetData.Announced && (magnetData.EndTime.Value - curTime).TotalSeconds < 31)
+                {
+                    var magnet = GetMagnet((uid, magnetData));
+
+                    if (magnet != null)
+                    {
+                        Report(magnet.Value.Owner, MagnetChannel,
+                            "salvage-system-announcement-losing",
+                            ("timeLeft", (magnetData.EndTime.Value - curTime).Seconds));
+                    }
+
+                    magnetData.Announced = true;
+                }
+            }
+            if (magnetData.NextOffer < curTime)
+            {
+                CreateMagnetOffers((uid, magnetData));
+            }
+        }
+    }
+
+    /// <summary>
+    /// Ends the magnet attachment and deletes the relevant grids.
+    /// </summary>
+    private void EndMagnet(Entity<SalvageMagnetDataComponent> data)
+    {
+        if (data.Comp.ActiveEntities != null)
+        {
+            // Handle mobrestrictions getting deleted
+            var query = AllEntityQuery<SalvageMobRestrictionsComponent>();
+
+            while (query.MoveNext(out var salvUid, out var salvMob))
+            {
+                if (data.Comp.ActiveEntities.Contains(salvMob.LinkedEntity))
+                {
+                    QueueDel(salvUid);
+                }
+            }
+
+            // Uhh yeah don't delete mobs or whatever
+            var mobQuery = AllEntityQuery<HumanoidAppearanceComponent, MobStateComponent, TransformComponent>();
+
+            while (mobQuery.MoveNext(out var mobUid, out _, out _, out var xform))
+            {
+                if (xform.GridUid == null || !data.Comp.ActiveEntities.Contains(xform.GridUid.Value) || xform.MapUid == null)
+                    continue;
+
+                _transform.SetParent(mobUid, xform.MapUid.Value);
+            }
+
+            // Go and cleanup the active ents.
+            foreach (var ent in data.Comp.ActiveEntities)
+            {
+                Del(ent);
+            }
+
+            data.Comp.ActiveEntities = null;
+        }
+
+        data.Comp.EndTime = null;
+        UpdateMagnetUIs(data);
+    }
+
+    private void CreateMagnetOffers(Entity<SalvageMagnetDataComponent> data)
+    {
+        data.Comp.Offered.Clear();
+
+        for (var i = 0; i < data.Comp.OfferCount; i++)
+        {
+            var seed = _random.Next();
+
+            // Fuck with the seed to mix wrecks and asteroids.
+            seed = (int) (seed / 10f) * 10;
+
+            if (i >= data.Comp.OfferCount / 2)
+            {
+                seed++;
+            }
+
+            data.Comp.Offered.Add(seed);
+        }
+
+        data.Comp.NextOffer = _timing.CurTime + data.Comp.OfferCooldown;
+        UpdateMagnetUIs(data);
+    }
+
+    // Just need something to announce.
+    private Entity<SalvageMagnetComponent>? GetMagnet(Entity<SalvageMagnetDataComponent> data)
+    {
+        var query = AllEntityQuery<SalvageMagnetComponent, TransformComponent>();
+
+        while (query.MoveNext(out var magnetUid, out var magnet, out var xform))
+        {
+            var stationUid = _station.GetOwningStation(magnetUid, xform);
+
+            if (stationUid != data.Owner)
+                continue;
+
+            return (magnetUid, magnet);
+        }
+
+        return null;
+    }
+
+    private void UpdateMagnetUI(Entity<SalvageMagnetComponent> entity, TransformComponent xform)
+    {
+        var station = _station.GetOwningStation(entity, xform);
+
+        if (!TryComp(station, out SalvageMagnetDataComponent? dataComp))
+            return;
+
+        _ui.TrySetUiState(entity, SalvageMagnetUiKey.Key,
+            new SalvageMagnetBoundUserInterfaceState(dataComp.Offered)
+            {
+                Cooldown = dataComp.OfferCooldown,
+                Duration = dataComp.ActiveTime,
+                EndTime = dataComp.EndTime,
+                NextOffer = dataComp.NextOffer,
+                ActiveSeed = dataComp.ActiveSeed,
+            });
+    }
+
+    private void UpdateMagnetUIs(Entity<SalvageMagnetDataComponent> data)
+    {
+        var query = AllEntityQuery<SalvageMagnetComponent, TransformComponent>();
+
+        while (query.MoveNext(out var magnetUid, out var magnet, out var xform))
+        {
+            var station = _station.GetOwningStation(magnetUid, xform);
+
+            if (station != data.Owner)
+                continue;
+
+            _ui.TrySetUiState(magnetUid, SalvageMagnetUiKey.Key,
+                new SalvageMagnetBoundUserInterfaceState(data.Comp.Offered)
+                {
+                    Cooldown = data.Comp.OfferCooldown,
+                    Duration = data.Comp.ActiveTime,
+                    EndTime = data.Comp.EndTime,
+                    NextOffer = data.Comp.NextOffer,
+                    ActiveSeed = data.Comp.ActiveSeed,
+                });
+        }
+    }
+
+    private async Task TakeMagnetOffer(Entity<SalvageMagnetDataComponent> data, int index, Entity<SalvageMagnetComponent> magnet)
+    {
+        var seed = data.Comp.Offered[index];
+
+        var offering = GetSalvageOffering(seed);
+        var salvMap = _mapManager.CreateMap();
+
+        // Set values while awaiting asteroid dungeon if relevant so we can't double-take offers.
+        data.Comp.ActiveSeed = seed;
+        data.Comp.EndTime = _timing.CurTime + data.Comp.ActiveTime;
+        data.Comp.NextOffer = data.Comp.EndTime.Value;
+        UpdateMagnetUIs(data);
+
+        switch (offering)
+        {
+            case AsteroidOffering asteroid:
+                var grid = _mapManager.CreateGrid(salvMap);
+                await _dungeon.GenerateDungeonAsync(asteroid.DungeonConfig, grid.Owner, grid, Vector2i.Zero, seed);
+                break;
+            case SalvageOffering wreck:
+                var salvageProto = wreck.SalvageMap;
+
+                var opts = new MapLoadOptions
+                {
+                    Offset = new Vector2(0, 0)
+                };
+
+                if (!_map.TryLoad(salvMap, salvageProto.MapPath.ToString(), out var roots, opts))
+                {
+                    Report(magnet, MagnetChannel, "salvage-system-announcement-spawn-debris-disintegrated");
+                    _mapManager.DeleteMap(salvMap);
+                    return;
+                }
+
+                break;
+            default:
+                throw new ArgumentOutOfRangeException();
+        }
+
+        Box2? bounds = null;
+        var mapXform = _xformQuery.GetComponent(_mapManager.GetMapEntityId(salvMap));
+
+        if (mapXform.ChildCount == 0)
+        {
+            Report(magnet.Owner, MagnetChannel, "salvage-system-announcement-spawn-no-debris-available");
+            return;
+        }
+
+        var mapChildren = mapXform.ChildEnumerator;
+
+        while (mapChildren.MoveNext(out var mapChild))
+        {
+            // If something went awry in dungen.
+            if (!_gridQuery.TryGetComponent(mapChild, out var childGrid))
+                continue;
+
+            var childAABB = _transform.GetWorldMatrix(mapChild).TransformBox(childGrid.LocalAABB);
+            bounds = bounds?.Union(childAABB) ?? childAABB;
+
+            // Update mass scanner names as relevant.
+            if (offering is AsteroidOffering)
+            {
+                _metaData.SetEntityName(mapChild, Loc.GetString("salvage-asteroid-name"));
+                _gravity.EnableGravity(mapChild);
+            }
+        }
+
+        var magnetGridUid = _xformQuery.GetComponent(magnet.Owner).GridUid;
+        Box2 attachedBounds = Box2.Empty;
+        MapId mapId = MapId.Nullspace;
+
+        if (magnetGridUid != null)
+        {
+            var magnetGridXform = _xformQuery.GetComponent(magnetGridUid.Value);
+            attachedBounds = _transform.GetWorldMatrix(magnetGridXform)
+                .TransformBox(_gridQuery.GetComponent(magnetGridUid.Value).LocalAABB);
+
+            mapId = magnetGridXform.MapID;
+        }
+
+        if (!TryGetSalvagePlacementLocation(mapId, attachedBounds, bounds!.Value, out var spawnLocation, out var spawnAngle))
+        {
+            Report(magnet.Owner, MagnetChannel, "salvage-system-announcement-spawn-no-debris-available");
+            _mapManager.DeleteMap(salvMap);
+            return;
+        }
+
+        data.Comp.ActiveEntities = null;
+        mapChildren = mapXform.ChildEnumerator;
+
+        // It worked, move it into position and cleanup values.
+        while (mapChildren.MoveNext(out var mapChild))
+        {
+            var salvXForm = _xformQuery.GetComponent(mapChild);
+            var localPos = salvXForm.LocalPosition;
+            _transform.SetParent(mapChild, salvXForm, _mapManager.GetMapEntityId(spawnLocation.MapId));
+            _transform.SetWorldPositionRotation(mapChild, spawnLocation.Position + localPos, spawnAngle, salvXForm);
+
+            data.Comp.ActiveEntities ??= new List<EntityUid>();
+            data.Comp.ActiveEntities?.Add(mapChild);
+
+            // Handle mob restrictions
+            var children = salvXForm.ChildEnumerator;
+
+            while (children.MoveNext(out var child))
+            {
+                if (!_salvMobQuery.TryGetComponent(child, out var salvMob))
+                    continue;
+
+                salvMob.LinkedEntity = mapChild;
+            }
+        }
+
+        Report(magnet.Owner, MagnetChannel, "salvage-system-announcement-arrived", ("timeLeft", data.Comp.ActiveTime.TotalSeconds));
+        _mapManager.DeleteMap(salvMap);
+
+        data.Comp.Announced = false;
+
+        var active = new SalvageMagnetActivatedEvent()
+        {
+            Magnet = magnet,
+        };
+
+        RaiseLocalEvent(ref active);
+    }
+
+    private bool TryGetSalvagePlacementLocation(MapId mapId, Box2 attachedBounds, Box2 bounds, out MapCoordinates coords, out Angle angle)
+    {
+        const float OffsetRadiusMin = 4f;
+        const float OffsetRadiusMax = 16f;
+
+        var minDistance = (attachedBounds.Height < attachedBounds.Width ? attachedBounds.Width : attachedBounds.Height) / 2f;
+        var minActualDistance = bounds.Height < bounds.Width ? minDistance + bounds.Width / 2f : minDistance + bounds.Height / 2f;
+
+        var attachedCenter = attachedBounds.Center;
+
+        angle = _random.NextAngle();
+
+        // Thanks 20kdc
+        for (var i = 0; i < 20; i++)
+        {
+            var randomPos = attachedCenter +
+                            _random.NextAngle().ToVec() * (minActualDistance +
+                                                           _random.NextFloat(OffsetRadiusMin, OffsetRadiusMax));
+            var finalCoords = new MapCoordinates(randomPos, mapId);
+
+            var box2 = Box2.CenteredAround(finalCoords.Position, bounds.Size);
+            var box2Rot = new Box2Rotated(box2, angle, finalCoords.Position);
+
+            // This doesn't stop it from spawning on top of random things in space
+            // Might be better like this, ghosts could stop it before
+            if (_mapManager.FindGridsIntersecting(finalCoords.MapId, box2Rot).Any())
+                continue;
+
+            coords = finalCoords;
+            return true;
+        }
+
+        coords = MapCoordinates.Nullspace;
+        return false;
+    }
+}
+
+[ByRefEvent]
+public record struct SalvageMagnetActivatedEvent
+{
+    public EntityUid Magnet;
+}
index eb98e1f2e1fc34bbf3ac6b66e3b920f48a0bbda4..545a385bff8584f2a1ee01a1db528d2fb38baaf8 100644 (file)
@@ -17,6 +17,7 @@ using Robust.Shared.Prototypes;
 using Robust.Shared.Random;
 using Robust.Shared.Utility;
 using Content.Server.Chat.Managers;
+using Content.Server.Gravity;
 using Content.Server.Parallax;
 using Content.Server.Procedural;
 using Content.Server.Shuttles.Systems;
@@ -46,39 +47,29 @@ namespace Content.Server.Salvage
         [Dependency] private readonly AnchorableSystem _anchorable = default!;
         [Dependency] private readonly BiomeSystem _biome = default!;
         [Dependency] private readonly DungeonSystem _dungeon = default!;
+        [Dependency] private readonly GravitySystem _gravity = default!;
         [Dependency] private readonly MapLoaderSystem _map = default!;
-        [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
+        [Dependency] private readonly MetaDataSystem _metaData = default!;
         [Dependency] private readonly RadioSystem _radioSystem = default!;
-        [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
         [Dependency] private readonly SharedAudioSystem _audio = default!;
         [Dependency] private readonly SharedTransformSystem _transform = default!;
         [Dependency] private readonly ShuttleSystem _shuttle = default!;
         [Dependency] private readonly ShuttleConsoleSystem _shuttleConsoles = default!;
         [Dependency] private readonly StationSystem _station = default!;
         [Dependency] private readonly UserInterfaceSystem _ui = default!;
-        [Dependency] private readonly MetaDataSystem _metaData = default!;
-
-        private const int SalvageLocationPlaceAttempts = 25;
 
-        // TODO: This is probably not compatible with multi-station
-        private readonly Dictionary<EntityUid, SalvageGridState> _salvageGridStates = new();
+        private EntityQuery<MapGridComponent> _gridQuery;
+        private EntityQuery<TransformComponent> _xformQuery;
 
         public override void Initialize()
         {
             base.Initialize();
 
-            SubscribeLocalEvent<SalvageMagnetComponent, InteractHandEvent>(OnInteractHand);
-            SubscribeLocalEvent<SalvageMagnetComponent, RefreshPartsEvent>(OnRefreshParts);
-            SubscribeLocalEvent<SalvageMagnetComponent, UpgradeExamineEvent>(OnUpgradeExamine);
-            SubscribeLocalEvent<SalvageMagnetComponent, ExaminedEvent>(OnExamined);
-            SubscribeLocalEvent<SalvageMagnetComponent, ToolUseAttemptEvent>(OnToolUseAttempt);
-            SubscribeLocalEvent<SalvageMagnetComponent, ComponentShutdown>(OnMagnetRemoval);
-            SubscribeLocalEvent<GridRemovalEvent>(OnGridRemoval);
-
-            // Can't use RoundRestartCleanupEvent, I need to clean up before the grid, and components are gone to prevent the announcements
-            SubscribeLocalEvent<GameRunLevelChangedEvent>(OnRoundEnd);
+            _gridQuery = GetEntityQuery<MapGridComponent>();
+            _xformQuery = GetEntityQuery<TransformComponent>();
 
             InitializeExpeditions();
+            InitializeMagnet();
             InitializeRunner();
         }
 
@@ -88,327 +79,6 @@ namespace Content.Server.Salvage
             ShutdownExpeditions();
         }
 
-        private void OnRoundEnd(GameRunLevelChangedEvent ev)
-        {
-            if(ev.New != GameRunLevel.InRound)
-            {
-                _salvageGridStates.Clear();
-            }
-        }
-
-        private void UpdateAppearance(EntityUid uid, SalvageMagnetComponent? component = null)
-        {
-            if (!Resolve(uid, ref component, false))
-                return;
-
-            _appearanceSystem.SetData(uid, SalvageMagnetVisuals.ReadyBlinking, component.MagnetState.StateType == MagnetStateType.Attaching);
-            _appearanceSystem.SetData(uid, SalvageMagnetVisuals.Ready, component.MagnetState.StateType == MagnetStateType.Holding);
-            _appearanceSystem.SetData(uid, SalvageMagnetVisuals.Unready, component.MagnetState.StateType == MagnetStateType.CoolingDown);
-            _appearanceSystem.SetData(uid, SalvageMagnetVisuals.UnreadyBlinking, component.MagnetState.StateType == MagnetStateType.Detaching);
-        }
-
-        private void UpdateChargeStateAppearance(EntityUid uid, TimeSpan currentTime, SalvageMagnetComponent? component = null)
-        {
-            if (!Resolve(uid, ref component, false))
-                return;
-
-            var timeLeft = Convert.ToInt32(component.MagnetState.Until.TotalSeconds - currentTime.TotalSeconds);
-
-            component.ChargeRemaining = component.MagnetState.StateType switch
-            {
-                MagnetStateType.Inactive => 5,
-                MagnetStateType.Holding => timeLeft / (Convert.ToInt32(component.HoldTime.TotalSeconds) / component.ChargeCapacity) + 1,
-                MagnetStateType.Detaching => 0,
-                MagnetStateType.CoolingDown => component.ChargeCapacity - timeLeft / (Convert.ToInt32(component.CooldownTime.TotalSeconds) / component.ChargeCapacity) - 1,
-                _ => component.ChargeRemaining
-            };
-
-            if (component.PreviousCharge == component.ChargeRemaining)
-                return;
-            _appearanceSystem.SetData(uid, SalvageMagnetVisuals.ChargeState, component.ChargeRemaining);
-            component.PreviousCharge = component.ChargeRemaining;
-        }
-
-        private void OnGridRemoval(GridRemovalEvent ev)
-        {
-            // If we ever want to give magnets names, and announce them individually, we would need to loop this, before removing it.
-            if (_salvageGridStates.Remove(ev.EntityUid))
-            {
-                if (TryComp<SalvageGridComponent>(ev.EntityUid, out var salvComp) &&
-                    TryComp<SalvageMagnetComponent>(salvComp.SpawnerMagnet, out var magnet))
-                    Report(salvComp.SpawnerMagnet.Value, magnet.SalvageChannel, "salvage-system-announcement-spawn-magnet-lost");
-                // For the very unlikely possibility that the salvage magnet was on a salvage, we will not return here
-            }
-            foreach(var gridState in _salvageGridStates)
-            {
-                foreach(var magnet in gridState.Value.ActiveMagnets)
-                {
-                    if (!TryComp<SalvageMagnetComponent>(magnet, out var magnetComponent))
-                        continue;
-
-                    if (magnetComponent.AttachedEntity != ev.EntityUid)
-                        continue;
-                    magnetComponent.AttachedEntity = null;
-                    magnetComponent.MagnetState = MagnetState.Inactive;
-                    return;
-                }
-            }
-        }
-
-        private void OnMagnetRemoval(EntityUid uid, SalvageMagnetComponent component, ComponentShutdown args)
-        {
-            if (component.MagnetState.StateType == MagnetStateType.Inactive)
-                return;
-
-            var magnetTranform = Transform(uid);
-            if (magnetTranform.GridUid is not { } gridId || !_salvageGridStates.TryGetValue(gridId, out var salvageGridState))
-                return;
-
-            salvageGridState.ActiveMagnets.Remove(uid);
-            Report(uid, component.SalvageChannel, "salvage-system-announcement-spawn-magnet-lost");
-            if (component.AttachedEntity.HasValue)
-            {
-                SafeDeleteSalvage(component.AttachedEntity.Value);
-                component.AttachedEntity = null;
-                Report(uid, component.SalvageChannel, "salvage-system-announcement-lost");
-            }
-            else if (component.MagnetState is { StateType: MagnetStateType.Attaching })
-            {
-                Report(uid, component.SalvageChannel, "salvage-system-announcement-spawn-no-debris-available");
-            }
-
-            component.MagnetState = MagnetState.Inactive;
-        }
-
-        private void OnRefreshParts(EntityUid uid, SalvageMagnetComponent component, RefreshPartsEvent args)
-        {
-            var rating = args.PartRatings[component.MachinePartDelay] - 1;
-            var factor = MathF.Pow(component.PartRatingDelay, rating);
-            component.AttachingTime = component.BaseAttachingTime * factor;
-            component.CooldownTime = component.BaseCooldownTime * factor;
-        }
-
-        private void OnUpgradeExamine(EntityUid uid, SalvageMagnetComponent component, UpgradeExamineEvent args)
-        {
-            args.AddPercentageUpgrade("salvage-system-magnet-delay-upgrade", (float) (component.CooldownTime / component.BaseCooldownTime));
-        }
-
-        private void OnExamined(EntityUid uid, SalvageMagnetComponent component, ExaminedEvent args)
-        {
-            if (!args.IsInDetailsRange)
-                return;
-
-            var gotGrid = false;
-            var remainingTime = TimeSpan.Zero;
-
-            if (Transform(uid).GridUid is { } gridId &&
-                _salvageGridStates.TryGetValue(gridId, out var salvageGridState))
-            {
-                remainingTime = component.MagnetState.Until - salvageGridState.CurrentTime;
-                gotGrid = true;
-            }
-            else
-            {
-                Log.Warning("Failed to load salvage grid state, can't display remaining time");
-            }
-
-            switch (component.MagnetState.StateType)
-            {
-                case MagnetStateType.Inactive:
-                    args.PushMarkup(Loc.GetString("salvage-system-magnet-examined-inactive"));
-                    break;
-                case MagnetStateType.Attaching:
-                    args.PushMarkup(Loc.GetString("salvage-system-magnet-examined-pulling-in"));
-                    break;
-                case MagnetStateType.Detaching:
-                    args.PushMarkup(Loc.GetString("salvage-system-magnet-examined-releasing"));
-                    break;
-                case MagnetStateType.CoolingDown:
-                    if (gotGrid)
-                        args.PushMarkup(Loc.GetString("salvage-system-magnet-examined-cooling-down", ("timeLeft", Math.Ceiling(remainingTime.TotalSeconds))));
-                    break;
-                case MagnetStateType.Holding:
-                    if (gotGrid)
-                        args.PushMarkup(Loc.GetString("salvage-system-magnet-examined-active", ("timeLeft", Math.Ceiling(remainingTime.TotalSeconds))));
-                    break;
-                default:
-                    throw new ArgumentOutOfRangeException();
-            }
-        }
-
-        private void OnToolUseAttempt(EntityUid uid, SalvageMagnetComponent comp, ToolUseAttemptEvent args)
-        {
-            // prevent reconstruct exploit to "leak" wrecks or skip cooldowns
-            if (comp.MagnetState != MagnetState.Inactive)
-            {
-                args.Cancel();
-            }
-        }
-
-        private void OnInteractHand(EntityUid uid, SalvageMagnetComponent component, InteractHandEvent args)
-        {
-            if (args.Handled)
-                return;
-            args.Handled = true;
-            StartMagnet(uid, component, args.User);
-            UpdateAppearance(uid, component);
-        }
-
-        private void StartMagnet(EntityUid uid, SalvageMagnetComponent component, EntityUid user)
-        {
-            switch (component.MagnetState.StateType)
-            {
-                case MagnetStateType.Inactive:
-                    ShowPopup(uid, "salvage-system-report-activate-success", user);
-                    var magnetTransform = Transform(uid);
-                    var gridId = magnetTransform.GridUid ?? throw new InvalidOperationException("Magnet had no grid associated");
-                    if (!_salvageGridStates.TryGetValue(gridId, out var gridState))
-                    {
-                        gridState = new SalvageGridState();
-                        _salvageGridStates[gridId] = gridState;
-                    }
-                    gridState.ActiveMagnets.Add(uid);
-                    component.MagnetState = new MagnetState(MagnetStateType.Attaching, gridState.CurrentTime + component.AttachingTime);
-                    RaiseLocalEvent(new SalvageMagnetActivatedEvent(uid));
-                    Report(uid, component.SalvageChannel, "salvage-system-report-activate-success");
-                    break;
-                case MagnetStateType.Attaching:
-                case MagnetStateType.Holding:
-                    ShowPopup(uid, "salvage-system-report-already-active", user);
-                    break;
-                case MagnetStateType.Detaching:
-                case MagnetStateType.CoolingDown:
-                    ShowPopup(uid, "salvage-system-report-cooling-down", user);
-                    break;
-                default:
-                    throw new ArgumentOutOfRangeException();
-            }
-        }
-        private void ShowPopup(EntityUid uid, string messageKey, EntityUid user)
-        {
-            _popupSystem.PopupEntity(Loc.GetString(messageKey), uid, user);
-        }
-
-        private void SafeDeleteSalvage(EntityUid salvage)
-        {
-            if(!EntityManager.TryGetComponent<TransformComponent>(salvage, out var salvageTransform))
-            {
-                Log.Error("Salvage entity was missing transform component");
-                return;
-            }
-
-            if (salvageTransform.GridUid == null)
-            {
-                Log.Error( "Salvage entity has no associated grid?");
-                return;
-            }
-
-            foreach (var player in Filter.Empty().AddInGrid(salvageTransform.GridUid.Value, EntityManager).Recipients)
-            {
-                if (player.AttachedEntity.HasValue)
-                {
-                    var playerEntityUid = player.AttachedEntity.Value;
-                    if (HasComp<SalvageMobRestrictionsComponent>(playerEntityUid))
-                    {
-                        // Salvage mobs are NEVER immune (even if they're from a different salvage, they shouldn't be here)
-                        continue;
-                    }
-                    _transform.SetParent(playerEntityUid, salvageTransform.ParentUid);
-                }
-            }
-
-            // Deletion has to happen before grid traversal re-parents players.
-            Del(salvage);
-        }
-
-        private bool TryGetSalvagePlacementLocation(EntityUid uid, SalvageMagnetComponent component, Box2 bounds, out MapCoordinates coords, out Angle angle)
-        {
-            var xform = Transform(uid);
-            var smallestBound = (bounds.Height < bounds.Width
-                ? bounds.Height
-                : bounds.Width) / 2f;
-            var maxRadius = component.OffsetRadiusMax + smallestBound;
-
-            angle = Angle.Zero;
-            coords = new EntityCoordinates(uid, new Vector2(0, -maxRadius)).ToMap(EntityManager, _transform);
-
-            if (xform.GridUid is not null)
-                angle = _transform.GetWorldRotation(Transform(xform.GridUid.Value));
-
-            for (var i = 0; i < SalvageLocationPlaceAttempts; i++)
-            {
-                var randomRadius = _random.NextFloat(component.OffsetRadiusMax);
-                var randomOffset = _random.NextAngle().ToVec() * randomRadius;
-                var finalCoords = new MapCoordinates(coords.Position + randomOffset, coords.MapId);
-
-                var box2 = Box2.CenteredAround(finalCoords.Position, bounds.Size);
-                var box2Rot = new Box2Rotated(box2, angle, finalCoords.Position);
-
-                // This doesn't stop it from spawning on top of random things in space
-                // Might be better like this, ghosts could stop it before
-                if (_mapManager.FindGridsIntersecting(finalCoords.MapId, box2Rot).Any())
-                    continue;
-                coords = finalCoords;
-                return true;
-            }
-            return false;
-        }
-
-        private bool SpawnSalvage(EntityUid uid, SalvageMagnetComponent component)
-        {
-            var salvMap = _mapManager.CreateMap();
-
-            EntityUid? salvageEnt;
-            if (_random.Prob(component.AsteroidChance))
-            {
-                var asteroidProto = _prototypeManager.Index<WeightedRandomEntityPrototype>(component.AsteroidPool).Pick(_random);
-                salvageEnt = Spawn(asteroidProto, new MapCoordinates(0, 0, salvMap));
-            }
-            else
-            {
-                var forcedSalvage = _configurationManager.GetCVar(CCVars.SalvageForced);
-                var salvageProto = string.IsNullOrWhiteSpace(forcedSalvage)
-                    ? _random.Pick(_prototypeManager.EnumeratePrototypes<SalvageMapPrototype>().ToList())
-                    : _prototypeManager.Index<SalvageMapPrototype>(forcedSalvage);
-
-                var opts = new MapLoadOptions
-                {
-                    Offset = new Vector2(0, 0)
-                };
-
-                if (!_map.TryLoad(salvMap, salvageProto.MapPath.ToString(), out var roots, opts) ||
-                    roots.FirstOrNull() is not { } root)
-                {
-                    Report(uid, component.SalvageChannel, "salvage-system-announcement-spawn-debris-disintegrated");
-                    _mapManager.DeleteMap(salvMap);
-                    return false;
-                }
-
-                salvageEnt = root;
-            }
-
-            var bounds = Comp<MapGridComponent>(salvageEnt.Value).LocalAABB;
-            if (!TryGetSalvagePlacementLocation(uid, component, bounds, out var spawnLocation, out var spawnAngle))
-            {
-                Report(uid, component.SalvageChannel, "salvage-system-announcement-spawn-no-debris-available");
-                _mapManager.DeleteMap(salvMap);
-                return false;
-            }
-
-            var salvXForm = Transform(salvageEnt.Value);
-            _transform.SetParent(salvageEnt.Value, salvXForm, _mapManager.GetMapEntityId(spawnLocation.MapId));
-            _transform.SetWorldPosition(salvXForm, spawnLocation.Position);
-
-            component.AttachedEntity = salvageEnt;
-            var gridcomp = EnsureComp<SalvageGridComponent>(salvageEnt.Value);
-            gridcomp.SpawnerMagnet = uid;
-            _transform.SetWorldRotation(salvageEnt.Value, spawnAngle);
-
-            Report(uid, component.SalvageChannel, "salvage-system-announcement-arrived", ("timeLeft", component.HoldTime.TotalSeconds));
-            _mapManager.DeleteMap(salvMap);
-            return true;
-        }
-
         private void Report(EntityUid source, string channelName, string messageKey, params (string, object)[] args)
         {
             var message = args.Length == 0 ? Loc.GetString(messageKey) : Loc.GetString(messageKey, args);
@@ -416,87 +86,12 @@ namespace Content.Server.Salvage
             _radioSystem.SendRadioMessage(source, message, channel, source);
         }
 
-        private void Transition(EntityUid uid, SalvageMagnetComponent magnet, TimeSpan currentTime)
-        {
-            switch (magnet.MagnetState.StateType)
-            {
-                case MagnetStateType.Attaching:
-                    if (SpawnSalvage(uid, magnet))
-                    {
-                        magnet.MagnetState = new MagnetState(MagnetStateType.Holding, currentTime + magnet.HoldTime);
-                    }
-                    else
-                    {
-                        magnet.MagnetState = new MagnetState(MagnetStateType.CoolingDown, currentTime + magnet.CooldownTime);
-                    }
-                    break;
-                case MagnetStateType.Holding:
-                    Report(uid, magnet.SalvageChannel, "salvage-system-announcement-losing", ("timeLeft", magnet.DetachingTime.TotalSeconds));
-                    magnet.MagnetState = new MagnetState(MagnetStateType.Detaching, currentTime + magnet.DetachingTime);
-                    break;
-                case MagnetStateType.Detaching:
-                    if (magnet.AttachedEntity.HasValue)
-                    {
-                        SafeDeleteSalvage(magnet.AttachedEntity.Value);
-                    }
-                    else
-                    {
-                        Log.Error("Salvage detaching was expecting attached entity but it was null");
-                    }
-                    Report(uid, magnet.SalvageChannel, "salvage-system-announcement-lost");
-                    magnet.MagnetState = new MagnetState(MagnetStateType.CoolingDown, currentTime + magnet.CooldownTime);
-                    break;
-                case MagnetStateType.CoolingDown:
-                    magnet.MagnetState = MagnetState.Inactive;
-                    break;
-            }
-            UpdateAppearance(uid, magnet);
-            UpdateChargeStateAppearance(uid, currentTime, magnet);
-        }
-
         public override void Update(float frameTime)
         {
-            var secondsPassed = TimeSpan.FromSeconds(frameTime);
-            // Keep track of time, and state per grid
-            foreach (var (uid, state) in _salvageGridStates)
-            {
-                if (state.ActiveMagnets.Count == 0) continue;
-                // Not handling the case where the salvage we spawned got paused
-                // They both need to be paused, or it doesn't make sense
-                if (MetaData(uid).EntityPaused) continue;
-                state.CurrentTime += secondsPassed;
-
-                var deleteQueue = new RemQueue<EntityUid>();
-
-                foreach(var magnet in state.ActiveMagnets)
-                {
-                    if (!TryComp<SalvageMagnetComponent>(magnet, out var magnetComp))
-                        continue;
-
-                    UpdateChargeStateAppearance(magnet, state.CurrentTime, magnetComp);
-                    if (magnetComp.MagnetState.Until > state.CurrentTime) continue;
-                    Transition(magnet, magnetComp, state.CurrentTime);
-                    if (magnetComp.MagnetState.StateType == MagnetStateType.Inactive)
-                    {
-                        deleteQueue.Add(magnet);
-                    }
-                }
-
-                foreach(var magnet in deleteQueue)
-                {
-                    state.ActiveMagnets.Remove(magnet);
-                }
-            }
-
             UpdateExpeditions();
+            UpdateMagnet();
             UpdateRunner();
         }
     }
-
-    public sealed class SalvageGridState
-    {
-        public TimeSpan CurrentTime { get; set; }
-        public List<EntityUid> ActiveMagnets { get; } = new();
-    }
 }
 
similarity index 91%
rename from Content.Server/Shuttle/Components/DroneConsoleComponent.cs
rename to Content.Server/Shuttles/DroneConsoleComponent.cs
index 07a8fb9cbd410e64eef11876c7284180263cc1f6..371f87d61218d605c8205b85f57346177efeae48 100644 (file)
@@ -1,7 +1,7 @@
 using Content.Server.Shuttles.Components;
 using Robust.Shared.Prototypes;
 
-namespace Content.Server.Shuttle.Components;
+namespace Content.Server.Shuttles;
 
 // Primo shitcode
 /// <summary>
index b9c29cb02b6b157f2963a6cab7d880a41e81daaa..864a2fbaef25681db32ad89892d08933e2e8e679 100644 (file)
@@ -1,4 +1,3 @@
-using Content.Server.Shuttle.Components;
 using Content.Server.Shuttles.Components;
 using Content.Server.Shuttles.Events;
 using Content.Server.Station.Components;
index 04b53a505ad5b4f4509eff377e1cb91f1c8b5562..21c7ecc0500806e5ea31dd718d0f5a5e2b8b5703 100644 (file)
@@ -1,6 +1,5 @@
 using Content.Server.Power.Components;
 using Content.Server.Power.EntitySystems;
-using Content.Server.Shuttle.Components;
 using Content.Server.Shuttles.Components;
 using Content.Server.Shuttles.Events;
 using Content.Server.Station.Systems;
index 7a5b4c98708b88f46fcf6a8075519cc1b4d4d6b0..3dc014c40e6670a61b2fd7974dadb51e6558f1a6 100644 (file)
@@ -10,20 +10,25 @@ namespace Content.Server.Shuttles.Systems;
 /// </summary>
 public sealed class SpaceGarbageSystem : EntitySystem
 {
+    private EntityQuery<TransformComponent> _xformQuery;
+
     public override void Initialize()
     {
         base.Initialize();
+        _xformQuery = GetEntityQuery<TransformComponent>();
         SubscribeLocalEvent<SpaceGarbageComponent, StartCollideEvent>(OnCollide);
     }
 
     private void OnCollide(EntityUid uid, SpaceGarbageComponent component, ref StartCollideEvent args)
     {
-        if (args.OtherBody.BodyType != BodyType.Static) return;
+        if (args.OtherBody.BodyType != BodyType.Static)
+            return;
 
-        var ourXform = Transform(uid);
-        var otherXform = Transform(args.OtherEntity);
+        var ourXform = _xformQuery.GetComponent(uid);
+        var otherXform = _xformQuery.GetComponent(args.OtherEntity);
 
-        if (ourXform.GridUid == otherXform.GridUid) return;
+        if (ourXform.GridUid == otherXform.GridUid)
+            return;
 
         QueueDel(uid);
     }
index 8f9e634c098dce7b0eeed6b685aba5f4cd0a6499..7aa29551b2d6f5a8c6016a6ea447eef57ed1cf3d 100644 (file)
@@ -401,6 +401,14 @@ public sealed class StationSystem : EntitySystem
         QueueDel(station);
     }
 
+    public EntityUid? GetOwningStation(EntityUid? entity, TransformComponent? xform = null)
+    {
+        if (entity == null)
+            return null;
+
+        return GetOwningStation(entity.Value, xform);
+    }
+
     /// <summary>
     /// Gets the station that "owns" the given entity (essentially, the station the grid it's on is attached to)
     /// </summary>
index f51d617df85aef613abca4147b60eff7367665b4..92a586547dd6ff15b0b07deb0ca445420615f811 100644 (file)
@@ -12,6 +12,8 @@ public sealed class ArtifactMagnetTriggerSystem : EntitySystem
 {
     [Dependency] private readonly ArtifactSystem _artifact = default!;
 
+    private readonly List<EntityUid> _toActivate = new();
+
     /// <inheritdoc/>
     public override void Initialize()
     {
@@ -25,7 +27,7 @@ public sealed class ArtifactMagnetTriggerSystem : EntitySystem
         if (!EntityQuery<ArtifactMagnetTriggerComponent>().Any())
             return;
 
-        List<EntityUid> toActivate = new();
+        _toActivate.Clear();
 
         //assume that there's more instruments than artifacts
         var query = EntityQueryEnumerator<MagbootsComponent, TransformComponent>();
@@ -43,21 +45,20 @@ public sealed class ArtifactMagnetTriggerSystem : EntitySystem
                 if (distance > trigger.Range)
                     continue;
 
-                toActivate.Add(artifactUid);
+                _toActivate.Add(artifactUid);
             }
         }
 
-        foreach (var a in toActivate)
+        foreach (var a in _toActivate)
         {
             _artifact.TryActivateArtifact(a);
         }
     }
 
-    private void OnMagnetActivated(SalvageMagnetActivatedEvent ev)
+    private void OnMagnetActivated(ref SalvageMagnetActivatedEvent ev)
     {
         var magXform = Transform(ev.Magnet);
 
-        var toActivate = new List<EntityUid>();
         var query = EntityQueryEnumerator<ArtifactMagnetTriggerComponent, TransformComponent>();
         while (query.MoveNext(out var uid, out var artifact, out var xform))
         {
@@ -67,10 +68,10 @@ public sealed class ArtifactMagnetTriggerSystem : EntitySystem
             if (distance > artifact.Range)
                 continue;
 
-            toActivate.Add(uid);
+            _toActivate.Add(uid);
         }
 
-        foreach (var a in toActivate)
+        foreach (var a in _toActivate)
         {
             _artifact.TryActivateArtifact(a);
         }
index a3eb8246ca293910b23071b6a0eb613af17f75e2..eefce86dfb296fbe9e96ac6ce7de06617a4e34d9 100644 (file)
@@ -1638,12 +1638,6 @@ namespace Content.Shared.CCVar
          * Salvage
          */
 
-        /// <summary>
-        ///     Forced salvage map prototype name (if empty, randomly selected)
-        /// </summary>
-        public static readonly CVarDef<string>
-            SalvageForced = CVarDef.Create("salvage.forced", "", CVar.SERVERONLY);
-
         /// <summary>
         /// Duration for missions
         /// </summary>
index 18aa55312c29fc9e621b967f0a646a0540341b03..8446febfb2593bf7f58e1f98ce2fd232d01aa71c 100644 (file)
@@ -12,6 +12,12 @@ namespace Content.Shared.Parallax.Biomes;
 [RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true), Access(typeof(SharedBiomeSystem))]
 public sealed partial class BiomeComponent : Component
 {
+    /// <summary>
+    /// Do we load / deload.
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadWrite), Access(Other = AccessPermissions.ReadWriteExecute)]
+    public bool Enabled = true;
+
     [ViewVariables(VVAccess.ReadWrite), DataField("seed")]
     [AutoNetworkedField]
     public int Seed = -1;
index a65251a0f6f771027b25adf1ac353618b77ae63b..db590135f09ee3bc46530c9799308551fc8ea03e 100644 (file)
@@ -94,6 +94,14 @@ public abstract class SharedBiomeSystem : EntitySystem
             return true;
         }
 
+        return TryGetTile(indices, layers, seed, grid, out tile);
+    }
+
+    /// <summary>
+    /// Gets the underlying biome tile, ignoring any existing tile that may be there.
+    /// </summary>
+    public bool TryGetTile(Vector2i indices, List<IBiomeLayer> layers, int seed, MapGridComponent? grid, [NotNullWhen(true)] out Tile? tile)
+    {
         for (var i = layers.Count - 1; i >= 0; i--)
         {
             var layer = layers[i];
index 1a465f8fcab293a4796666a6d0747f43940ed1e3..5e976fe83d0e7751a5c5daed5c772b6adb2fe3d0 100644 (file)
@@ -2,7 +2,7 @@ namespace Content.Shared.Procedural;
 
 public sealed class Dungeon
 {
-    public readonly List<DungeonRoom> Rooms = new();
+    public readonly List<DungeonRoom> Rooms;
 
     /// <summary>
     /// Hashset of the tiles across all rooms.
@@ -14,4 +14,14 @@ public sealed class Dungeon
     public readonly HashSet<Vector2i> CorridorTiles = new();
 
     public readonly HashSet<Vector2i> CorridorExteriorTiles = new();
+
+    public Dungeon()
+    {
+        Rooms = new List<DungeonRoom>();
+    }
+
+    public Dungeon(List<DungeonRoom> rooms)
+    {
+        Rooms = rooms;
+    }
 }
diff --git a/Content.Shared/Procedural/DungeonGenerators/NoiseDunGen.cs b/Content.Shared/Procedural/DungeonGenerators/NoiseDunGen.cs
new file mode 100644 (file)
index 0000000..3ea0d98
--- /dev/null
@@ -0,0 +1,58 @@
+using Content.Shared.Maps;
+using Robust.Shared.Noise;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
+
+namespace Content.Shared.Procedural.DungeonGenerators;
+
+/// <summary>
+/// Generates dungeon flooring based on the specified noise.
+/// </summary>
+public sealed partial class NoiseDunGen : IDunGen
+{
+    /*
+     * Floodfills out from 0 until it finds a valid tile.
+     * From here it then floodfills until it can no longer fill in an area and generates a dungeon from that.
+     */
+
+    // At some point we may want layers masking each other like a simpler version of biome code but for now
+    // we'll just make it circular.
+
+    /// <summary>
+    /// How many areas of noise to fill out. Useful if we just want 1 blob area to fill out.
+    /// </summary>
+    [DataField]
+    public int Iterations = int.MaxValue;
+
+    /// <summary>
+    /// Cap on how many tiles to include.
+    /// </summary>
+    [DataField]
+    public int TileCap = 128;
+
+    /// <summary>
+    /// Standard deviation of tilecap.
+    /// </summary>
+    [DataField]
+    public float CapStd = 8f;
+
+    [DataField(required: true)]
+    public List<NoiseDunGenLayer> Layers = new();
+}
+
+[DataRecord]
+public record struct NoiseDunGenLayer
+{
+    /// <summary>
+    /// If the noise value is above this then it gets output.
+    /// </summary>
+    [DataField]
+    public float Threshold;
+
+    [DataField(required: true)]
+    public string Tile;
+
+    [DataField(required: true)]
+    public FastNoiseLite Noise;
+}
diff --git a/Content.Shared/Procedural/PostGeneration/BiomeMarkerLayerPostGen.cs b/Content.Shared/Procedural/PostGeneration/BiomeMarkerLayerPostGen.cs
new file mode 100644 (file)
index 0000000..dc64feb
--- /dev/null
@@ -0,0 +1,21 @@
+using Content.Shared.Parallax.Biomes.Markers;
+using Content.Shared.Procedural.PostGeneration;
+using Content.Shared.Random;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Procedural.PostGeneration;
+
+/// <summary>
+/// Spawns the specified marker layer on top of the dungeon rooms.
+/// </summary>
+public sealed partial class BiomeMarkerLayerPostGen : IPostDunGen
+{
+    /// <summary>
+    /// How many times to spawn marker layers; can duplicate.
+    /// </summary>
+    [DataField]
+    public int Count = 6;
+
+    [DataField(required: true)]
+    public ProtoId<WeightedRandomPrototype> MarkerTemplate;
+}
diff --git a/Content.Shared/Procedural/PostGeneration/BiomePostGen.cs b/Content.Shared/Procedural/PostGeneration/BiomePostGen.cs
new file mode 100644 (file)
index 0000000..d02de24
--- /dev/null
@@ -0,0 +1,15 @@
+using Content.Shared.Parallax.Biomes;
+using Content.Shared.Procedural.PostGeneration;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Procedural.PostGeneration;
+
+/// <summary>
+/// Generates a biome on top of valid tiles, then removes the biome when done.
+/// Only works if no existing biome is present.
+/// </summary>
+public sealed partial class BiomePostGen : IPostDunGen
+{
+    [DataField(required: true)]
+    public ProtoId<BiomeTemplatePrototype> BiomeTemplate;
+}
diff --git a/Content.Shared/Salvage/Magnet/AsteroidOffering.cs b/Content.Shared/Salvage/Magnet/AsteroidOffering.cs
new file mode 100644 (file)
index 0000000..3ab69c6
--- /dev/null
@@ -0,0 +1,16 @@
+using Content.Shared.Procedural;
+
+namespace Content.Shared.Salvage.Magnet;
+
+/// <summary>
+/// Asteroid offered for the magnet.
+/// </summary>
+public record struct AsteroidOffering : ISalvageMagnetOffering
+{
+    public DungeonConfigPrototype DungeonConfig;
+
+    /// <summary>
+    /// Calculated marker layers for the asteroid.
+    /// </summary>
+    public Dictionary<string, int> MarkerLayers;
+}
diff --git a/Content.Shared/Salvage/Magnet/ISalvageMagnetOffering.cs b/Content.Shared/Salvage/Magnet/ISalvageMagnetOffering.cs
new file mode 100644 (file)
index 0000000..7d002fe
--- /dev/null
@@ -0,0 +1,6 @@
+namespace Content.Shared.Salvage.Magnet;
+
+public interface ISalvageMagnetOffering
+{
+
+}
\ No newline at end of file
diff --git a/Content.Shared/Salvage/Magnet/MagnetClaimOfferEvent.cs b/Content.Shared/Salvage/Magnet/MagnetClaimOfferEvent.cs
new file mode 100644 (file)
index 0000000..a44854f
--- /dev/null
@@ -0,0 +1,12 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Salvage.Magnet;
+
+/// <summary>
+/// Claim an offer from the magnet UI.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed class MagnetClaimOfferEvent : BoundUserInterfaceMessage
+{
+    public int Index;
+}
diff --git a/Content.Shared/Salvage/Magnet/SalvageMagnetBoundUserInterfaceState.cs b/Content.Shared/Salvage/Magnet/SalvageMagnetBoundUserInterfaceState.cs
new file mode 100644 (file)
index 0000000..a243261
--- /dev/null
@@ -0,0 +1,22 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Salvage.Magnet;
+
+[Serializable, NetSerializable]
+public sealed class SalvageMagnetBoundUserInterfaceState : BoundUserInterfaceState
+{
+    public TimeSpan? EndTime;
+    public TimeSpan NextOffer;
+
+    public TimeSpan Cooldown;
+    public TimeSpan Duration;
+
+    public int ActiveSeed;
+
+    public List<int> Offers;
+
+    public SalvageMagnetBoundUserInterfaceState(List<int> offers)
+    {
+        Offers = offers;
+    }
+}
diff --git a/Content.Shared/Salvage/Magnet/SalvageMagnetUiKey.cs b/Content.Shared/Salvage/Magnet/SalvageMagnetUiKey.cs
new file mode 100644 (file)
index 0000000..ba7b212
--- /dev/null
@@ -0,0 +1,6 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Salvage.Magnet;
+
+[Serializable, NetSerializable]
+public enum SalvageMagnetUiKey : byte { Key }
diff --git a/Content.Shared/Salvage/Magnet/SalvageOffering.cs b/Content.Shared/Salvage/Magnet/SalvageOffering.cs
new file mode 100644 (file)
index 0000000..c49f1ff
--- /dev/null
@@ -0,0 +1,9 @@
+namespace Content.Shared.Salvage.Magnet;
+
+/// <summary>
+/// Asteroid offered for the magnet.
+/// </summary>
+public record struct SalvageOffering : ISalvageMagnetOffering
+{
+    public SalvageMapPrototype SalvageMap;
+}
diff --git a/Content.Shared/Salvage/SalvageMapPrototype.cs b/Content.Shared/Salvage/SalvageMapPrototype.cs
new file mode 100644 (file)
index 0000000..518b64d
--- /dev/null
@@ -0,0 +1,15 @@
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Salvage;
+
+[Prototype]
+public sealed class SalvageMapPrototype : IPrototype
+{
+    [ViewVariables] [IdDataField] public string ID { get; } = default!;
+
+    /// <summary>
+    /// Relative directory path to the given map, i.e. `Maps/Salvage/template.yml`
+    /// </summary>
+    [DataField(required: true)] public ResPath MapPath;
+}
diff --git a/Content.Shared/Salvage/SharedSalvageMagnetComponent.cs b/Content.Shared/Salvage/SharedSalvageMagnetComponent.cs
deleted file mode 100644 (file)
index 56942b8..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-using Content.Shared.Construction.Prototypes;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Shared.Salvage;
-
-public abstract partial class SharedSalvageMagnetComponent : Component
-{
-    /// <summary>
-    /// The machine part that affects the attaching and cooldown times
-    /// </summary>
-    [DataField("machinePartDelay", customTypeSerializer: typeof(PrototypeIdSerializer<MachinePartPrototype>)), ViewVariables(VVAccess.ReadWrite)]
-    public string MachinePartDelay = "Capacitor";
-
-    /// <summary>
-    /// A multiplier applied to the attaching and cooldown times for each level of <see cref="MachinePartDelay"/>
-    /// </summary>
-    [DataField("partRatingDelay"), ViewVariables(VVAccess.ReadWrite)]
-    public float PartRatingDelay = 0.75f;
-}
-
-[Serializable, NetSerializable]
-public enum SalvageMagnetVisuals : byte
-{
-    ChargeState,
-    Ready,
-    ReadyBlinking,
-    Unready,
-    UnreadyBlinking
-}
diff --git a/Content.Shared/Salvage/SharedSalvageSystem.Magnet.cs b/Content.Shared/Salvage/SharedSalvageSystem.Magnet.cs
new file mode 100644 (file)
index 0000000..f1fff0b
--- /dev/null
@@ -0,0 +1,69 @@
+using Content.Shared.Procedural;
+using Content.Shared.Procedural.PostGeneration;
+using Content.Shared.Random.Helpers;
+using Content.Shared.Salvage.Magnet;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Salvage;
+
+public abstract partial class SharedSalvageSystem
+{
+    private readonly List<SalvageMapPrototype> _salvageMaps = new();
+
+    private readonly List<ProtoId<DungeonConfigPrototype>> _asteroidConfigs = new()
+    {
+        "BlobAsteroid",
+        "ClusterAsteroid",
+        "SpindlyAsteroid",
+        "SwissCheeseAsteroid"
+    };
+
+    public ISalvageMagnetOffering GetSalvageOffering(int seed)
+    {
+        var rand = new System.Random(seed);
+
+        // Asteroid seed
+        if (seed % 2 == 0)
+        {
+            var config = _asteroidConfigs[rand.Next(_asteroidConfigs.Count)];
+            var layerRand = new System.Random(seed);
+            var configProto = _proto.Index(config);
+            var layers = new Dictionary<string, int>();
+
+            // If we ever add more random layers will need to Next on these.
+            foreach (var layer in configProto.PostGeneration)
+            {
+                switch (layer)
+                {
+                    case BiomeMarkerLayerPostGen marker:
+                        for (var i = 0; i < marker.Count; i++)
+                        {
+                            var proto = _proto.Index(marker.MarkerTemplate).Pick(layerRand);
+                            var layerCount = layers.GetOrNew(proto);
+                            layerCount++;
+                            layers[proto] = layerCount;
+                        }
+                        break;
+                }
+            }
+
+            return new AsteroidOffering
+            {
+                DungeonConfig = configProto,
+                MarkerLayers = layers,
+            };
+        }
+
+        // Salvage map seed
+        _salvageMaps.Clear();
+        _salvageMaps.AddRange(_proto.EnumeratePrototypes<SalvageMapPrototype>());
+        var mapIndex = rand.Next(_salvageMaps.Count);
+        var map = _salvageMaps[mapIndex];
+
+        return new SalvageOffering()
+        {
+            SalvageMap = map,
+        };
+    }
+}
index e5124ab4d03c9947d3cb10afb471c08477aa41d8..5f5fc875d62a34ec4acef082d7ee781080fdc533 100644 (file)
@@ -15,7 +15,7 @@ using Robust.Shared.Utility;
 
 namespace Content.Shared.Salvage;
 
-public abstract class SharedSalvageSystem : EntitySystem
+public abstract partial class SharedSalvageSystem : EntitySystem
 {
     [Dependency] protected readonly IConfigurationManager CfgManager = default!;
     [Dependency] private readonly IPrototypeManager _proto = default!;
index f309da1bda3e3053de85e8811ef22055910e75b8..b7bbdbce2f99fb423faaa08f5b75e5b8184251a5 100644 (file)
@@ -12,8 +12,9 @@ salvage-expedition-window-hostiles = Hostiles:
 salvage-expedition-window-duration = Duration:
 salvage-expedition-window-biome = Biome:
 salvage-expedition-window-modifiers = Modifiers:
-salvage-expedition-window-claimed = Claimed
-salvage-expedition-window-claim = Claim
+
+offering-window-claimed = Claimed
+offering-window-claim = Claim
 
 salvage-expedition-window-next = Next offer
 
diff --git a/Resources/Locale/en-US/salvage/salvage-magnet.ftl b/Resources/Locale/en-US/salvage/salvage-magnet.ftl
new file mode 100644 (file)
index 0000000..72ae64a
--- /dev/null
@@ -0,0 +1,60 @@
+salvage-system-announcement-losing = The magnet is no longer able to hold the salvagable debris. Estimated time until loss: {$timeLeft} seconds.
+salvage-system-announcement-spawn-debris-disintegrated = Debris disintegrated during orbital transfer.
+salvage-system-announcement-spawn-no-debris-available = No debris could be recovered by the salvage magnet.
+salvage-system-announcement-arrived = A piece of salvagable debris has been pulled in. Estimated hold time: {$timeLeft} seconds.
+salvage-asteroid-name = Asteroid
+
+salvage-expedition-window-progression = Progression
+
+salvage-magnet-resources = {$resource ->
+    [OreTin] Tin
+    [OreCoal] Coal
+    [OreQuartz] Quartz
+    [OreGold] Gold
+    [OreSilver] Silver
+    [OrePlasma] Plasma
+    [OreUranium] Uranium
+    *[other] {$resource}
+}
+
+salvage-magnet-resources-count = {$count ->
+    [1] (Poor)
+    [2] (Rich)
+    [3] (Rich)
+    *[other] (Extraordinary)
+}
+
+# Asteroids
+dungeon-config-proto-BlobAsteroid = Asteroid clump
+dungeon-config-proto-ClusterAsteroid = Asteroid cluster
+dungeon-config-proto-SpindlyAsteroid = Asteroid spiral
+dungeon-config-proto-SwissCheeseAsteroid = Asteroid fragments
+
+# Wrecks
+salvage-map-proto-Small1 = Engineering Storage
+salvage-map-proto-Small2 = Gaming Nook
+salvage-map-proto-Small3 = Laundromat
+salvage-map-proto-Small4 = Bar Salvage
+salvage-map-proto-SmallShip1 = Pill
+salvage-map-proto-SmallAISurveyDrone = AI Survey Drone
+
+salvage-map-proto-SmallA1 = Asteroid Plasmafire
+salvage-map-proto-Medium1 = Plasma-Trapped Cache
+salvage-map-proto-MediumVault1 = Vault
+salvage-map-proto-MediumOrchestra = Silent Orchestra
+salvage-map-proto-MediumLibraryWreck = Abandoned Library
+salvage-map-proto-MediumCargoWreck = Cargo Department Wreck
+salvage-map-proto-MediumPirateWreck = Pirate Barge Fragment
+salvage-map-proto-TickColony = Space Tick colony
+salvage-map-proto-CargoDock = Asteroid Cargo Dock
+salvage-map-proto-SpaceWaffleHome = Waffle Home
+salvage-map-proto-MediumShuttleWreck = Ruined Emergency Shuttle
+salvage-map-proto-MediumPetHospital = Pet and Bear Hospital
+salvage-map-proto-MediumCrashedShuttle = Crashed Shuttle
+salvage-map-proto-Meatball = Meatball
+
+salvage-map-proto-StationStation = Station station
+salvage-map-proto-AsteroidBase = Asteroid Base
+salvage-map-proto-RuinCargoBase = Ruined Cargo Storage
+salvage-map-proto-SecurityChunk = Security Department Chunk
+salvage-map-proto-EngineeringChunk = Engineering Department Chunk
diff --git a/Resources/Locale/en-US/salvage/salvage-system.ftl b/Resources/Locale/en-US/salvage/salvage-system.ftl
deleted file mode 100644 (file)
index 1da7980..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-salvage-system-announcement-source = Salvage Control System
-salvage-system-announcement-arrived = A piece of salvagable debris has been pulled in. Estimated hold time: {$timeLeft} seconds.
-salvage-system-announcement-losing = The magnet is no longer able to hold the salvagable debris. Estimated time until loss: {$timeLeft} seconds.
-salvage-system-announcement-lost = The salvagable debris have been lost.
-
-salvage-system-announcement-spawn-magnet-lost = The salvage magnet has been lost.
-salvage-system-announcement-spawn-no-debris-available = No debris could be recovered by the salvage magnet.
-salvage-system-announcement-spawn-debris-disintegrated = Debris disintegrated during orbital transfer.
-
-salvage-system-report-already-active = The salvage magnet is already active.
-salvage-system-report-cooling-down = The salvage magnet is cooling down.
-salvage-system-report-activate-success = The salvage magnet is pulling in a piece of debris!
-
-salvage-system-magnet-examined-inactive = The salvage magnet is inactive.
-salvage-system-magnet-examined-pulling-in = The salvage magnet is attempting to pull in salvage.
-salvage-system-magnet-examined-active = The salvage magnet is holding salvage in place. Can hold for { $timeLeft ->
-    [1] one second.
-    *[other] { $timeLeft } seconds.
-}
-salvage-system-magnet-examined-releasing = The salvage magnet is releasing the salvage.
-salvage-system-magnet-examined-cooling-down = The salvage magnet is cooling down. It will be ready in: {$timeLeft} seconds.
-salvage-system-magnet-delay-upgrade = Attaching/cooldown delay
-
index 856a37029d801ad49e6f237d217826f3d38cad1c..d68afa9b0bc932a1fd100e3f8cbcdb0efb0ab91b 100644 (file)
   components:
     - type: SalvageExpeditionData
 
+- type: entity
+  id: BaseStationMagnet
+  abstract: true
+  components:
+    - type: SalvageMagnetData
+
 - type: entity
   id: BaseStationSiliconLawCrewsimov
   abstract: true
index b83c6d0a526d0bc351b6761a9dbb2f259d71be79..313610c24eef493bc416ef8f04f00f69e3f878a1 100644 (file)
@@ -19,6 +19,7 @@
     - BaseStationCentcomm
     - BaseStationEvacuation
     - BaseStationAlertLevels
+    - BaseStationMagnet
     - BaseStationExpeditions
     - BaseStationSiliconLawCrewsimov
     - BaseStationAllEventsEligible
index 1673f980ff0a505a640193f3828976da7a6532ff..1b279782dfe314c430a3c115eb1fd071749b0e73 100644 (file)
     - state: salvage-magnet-o4
       map: ["chargeState"]
       shader: unshaded
-  - type: Appearance
-  - type: GenericVisualizer
-    visuals:
-      enum.SalvageMagnetVisuals.ChargeState:
-        chargeState:
-          0: { state: salvage-magnet-o0, shader: "unshaded", visible: false }
-          1: { state: salvage-magnet-o0, shader: "unshaded", visible: true }
-          2: { state: salvage-magnet-o1, shader: "unshaded", visible: true }
-          3: { state: salvage-magnet-o2, shader: "unshaded", visible: true }
-          4: { state: salvage-magnet-o3, shader: "unshaded", visible: true }
-          5: { state: salvage-magnet-o4, shader: "unshaded", visible: true }
-      enum.SalvageMagnetVisuals.Ready:
-        ready:
-          False: { state: salvage-magnet-ready, visible: false, shader: "unshaded" }
-          True: { state: salvage-magnet-ready, visible: true, shader: "unshaded" }
-      enum.SalvageMagnetVisuals.ReadyBlinking:
-        readyBlinking:
-          False: { state: salvage-magnet-ready-blinking, visible: false, shader: "unshaded" }
-          True: { state: salvage-magnet-ready-blinking, visible: true, shader: "unshaded" }
-      enum.SalvageMagnetVisuals.Unready:
-        unready:
-          False: { state: salvage-magnet-unready, visible: false, shader: "unshaded" }
-          True: { state: salvage-magnet-unready, visible: true, shader: "unshaded" }
-      enum.SalvageMagnetVisuals.UnreadyBlinking:
-        unreadyBlinking:
-          False: { state: salvage-magnet-unready-blinking, visible: false, shader: "unshaded" }
-          True: { state: salvage-magnet-unready-blinking, visible: true, shader: "unshaded" }
-  - type: Rotatable
+  - type: ActivatableUI
+    key: enum.SalvageMagnetUiKey.Key
+  - type: ActivatableUIRequiresPower
+  - type: UserInterface
+    interfaces:
+    - key: enum.SalvageMagnetUiKey.Key
+      type: SalvageMagnetBoundUserInterface
   - type: Transform
     noRot: false
+  - type: Appearance
+  - type: Rotatable
   - type: IntrinsicRadioReceiver
   - type: ActiveRadio
     channels:
index 2e0e15b6a6aca658d68153fffbd9bcc5872e9404..36f3d14c9eb9cff2f1c97c265b95895ff76f2b20 100644 (file)
         - FullTileMask
         layer:
         - WallLayer
-  - type: OreVein
-    oreChance: 0.2
-    oreRarityPrototypeId: RandomOreDistributionStandard
   - type: RadiationBlocker
     resistance: 2
 
+# Ore veins
+- type: entity
+  id: AsteroidRockCoal
+  parent: AsteroidRock
+  description: An ore vein rich with coal.
+  suffix: Coal
+  components:
+    - type: OreVein
+      oreChance: 1.0
+      currentOre: OreCoal
+    - type: Sprite
+      layers:
+        - state: rock_asteroid
+        - map: [ "enum.EdgeLayer.South" ]
+          state: rock_asteroid_south
+        - map: [ "enum.EdgeLayer.East" ]
+          state: rock_asteroid_east
+        - map: [ "enum.EdgeLayer.North" ]
+          state: rock_asteroid_north
+        - map: [ "enum.EdgeLayer.West" ]
+          state: rock_asteroid_west
+        - state: rock_coal
+
+- type: entity
+  id: AsteroidRockGold
+  parent: AsteroidRock
+  description: An ore vein rich with gold.
+  suffix: Gold
+  components:
+    - type: OreVein
+      oreChance: 1.0
+      currentOre: OreGold
+    - type: Sprite
+      layers:
+        - state: rock_asteroid
+        - map: [ "enum.EdgeLayer.South" ]
+          state: rock_asteroid_south
+        - map: [ "enum.EdgeLayer.East" ]
+          state: rock_asteroid_east
+        - map: [ "enum.EdgeLayer.North" ]
+          state: rock_asteroid_north
+        - map: [ "enum.EdgeLayer.West" ]
+          state: rock_asteroid_west
+        - state: rock_gold
+
+- type: entity
+  id: AsteroidRockPlasma
+  parent: AsteroidRock
+  description: An ore vein rich with plasma.
+  suffix: Plasma
+  components:
+    - type: OreVein
+      oreChance: 1.0
+      currentOre: OrePlasma
+    - type: Sprite
+      layers:
+        - state: rock_asteroid
+        - map: [ "enum.EdgeLayer.South" ]
+          state: rock_asteroid_south
+        - map: [ "enum.EdgeLayer.East" ]
+          state: rock_asteroid_east
+        - map: [ "enum.EdgeLayer.North" ]
+          state: rock_asteroid_north
+        - map: [ "enum.EdgeLayer.West" ]
+          state: rock_asteroid_west
+        - state: rock_phoron
+
+- type: entity
+  id: AsteroidRockQuartz
+  parent: AsteroidRock
+  description: An ore vein rich with quartz.
+  suffix: Quartz
+  components:
+    - type: OreVein
+      oreChance: 1.0
+      currentOre: OreSpaceQuartz
+    - type: Sprite
+      layers:
+        - state: rock_asteroid
+        - map: [ "enum.EdgeLayer.South" ]
+          state: rock_asteroid_south
+        - map: [ "enum.EdgeLayer.East" ]
+          state: rock_asteroid_east
+        - map: [ "enum.EdgeLayer.North" ]
+          state: rock_asteroid_north
+        - map: [ "enum.EdgeLayer.West" ]
+          state: rock_asteroid_west
+        - state: rock_quartz
+
+- type: entity
+  id: AsteroidRockSilver
+  parent: AsteroidRock
+  description: An ore vein rich with silver.
+  suffix: Silver
+  components:
+    - type: OreVein
+      oreChance: 1.0
+      currentOre: OreSilver
+    - type: Sprite
+      layers:
+        - state: rock_asteroid
+        - map: [ "enum.EdgeLayer.South" ]
+          state: rock_asteroid_south
+        - map: [ "enum.EdgeLayer.East" ]
+          state: rock_asteroid_east
+        - map: [ "enum.EdgeLayer.North" ]
+          state: rock_asteroid_north
+        - map: [ "enum.EdgeLayer.West" ]
+          state: rock_asteroid_west
+        - state: rock_silver
+
+# Yes I know it drops steel but we may get smelting at some point
+- type: entity
+  id: AsteroidRockTin
+  parent: AsteroidRock
+  description: An ore vein rich with steel.
+  suffix: Steel
+  components:
+    - type: OreVein
+      oreChance: 1.0
+      currentOre: OreSteel
+    - type: Sprite
+      layers:
+        - state: rock_asteroid
+        - map: [ "enum.EdgeLayer.South" ]
+          state: rock_asteroid_south
+        - map: [ "enum.EdgeLayer.East" ]
+          state: rock_asteroid_east
+        - map: [ "enum.EdgeLayer.North" ]
+          state: rock_asteroid_north
+        - map: [ "enum.EdgeLayer.West" ]
+          state: rock_asteroid_west
+        - state: rock_tin
+
+- type: entity
+  id: AsteroidRockUranium
+  parent: AsteroidRock
+  description: An ore vein rich with uranium.
+  suffix: Uranium
+  components:
+    - type: OreVein
+      oreChance: 1.0
+      currentOre: OreUranium
+    - type: Sprite
+      layers:
+        - state: rock_asteroid
+        - map: [ "enum.EdgeLayer.South" ]
+          state: rock_asteroid_south
+        - map: [ "enum.EdgeLayer.East" ]
+          state: rock_asteroid_east
+        - map: [ "enum.EdgeLayer.North" ]
+          state: rock_asteroid_north
+        - map: [ "enum.EdgeLayer.West" ]
+          state: rock_asteroid_west
+        - state: rock_uranium
+
+
+- type: entity
+  id: AsteroidRockBananium
+  parent: AsteroidRock
+  description: An ore vein rich with bananium.
+  suffix: Bananium
+  components:
+    - type: OreVein
+      oreChance: 1.0
+      currentOre: OreBananium
+    - type: Sprite
+      layers:
+        - state: rock_asteroid
+        - map: [ "enum.EdgeLayer.South" ]
+          state: rock_asteroid_south
+        - map: [ "enum.EdgeLayer.East" ]
+          state: rock_asteroid_east
+        - map: [ "enum.EdgeLayer.North" ]
+          state: rock_asteroid_north
+        - map: [ "enum.EdgeLayer.West" ]
+          state: rock_asteroid_west
+        - state: rock_bananium
+
+- type: entity
+  id: AsteroidRockArtifactFragment
+  parent: AsteroidRock
+  description: A rock wall. What's that sticking out of it?
+  suffix: Artifact Fragment
+  components:
+    - type: OreVein
+      oreChance: 1.0
+      currentOre: OreArtifactFragment
+    - type: Sprite
+      layers:
+        - state: rock_asteroid
+        - map: [ "enum.EdgeLayer.South" ]
+          state: rock_asteroid_south
+        - map: [ "enum.EdgeLayer.East" ]
+          state: rock_asteroid_east
+        - map: [ "enum.EdgeLayer.North" ]
+          state: rock_asteroid_north
+        - map: [ "enum.EdgeLayer.West" ]
+          state: rock_asteroid_west
+        - state: rock_artifact_fragment
+
 - type: entity
   id: AsteroidRockMining
   parent: AsteroidRock
index 7e4d9c2d4441ecf00107fbd7e7798b82fd15275e..144c557743cd6fef37ebcd343a9b8bbcf6f2e283 100644 (file)
-# So the way things are done as of now is that Salvages are measured by world AABBs in their maps.
-# Remember, first two coordinates should be the minimum X/Y, second two should be maximum.
-# You can also use the salvageruler command to get these, once you're sure the transform's been reset.
-# Ultimately, you should still be keeping the maps centred.
+# Dear god please stop adding ones with thousands of entities, learn how procgen works.
 
 # "Small"-class maps - Max size square: 7x7, indicated size: 3.5
 
 - type: salvageMap
   id: Small1
-  name: "Small / Engineering Storage 1"
   mapPath: /Maps/Salvage/small-1.yml
 
 - type: salvageMap
   id: Small2
-  name: "Small / Gaming Nook 1"
   mapPath: /Maps/Salvage/small-2.yml
 
 - type: salvageMap
-  id: Small-ship-1
-  name: "Small / Ship 1 (Pill)"
+  id: SmallShip1
   mapPath: /Maps/Salvage/small-ship-1.yml
 
 - type: salvageMap
   id: Small3
-  name: "Small / Laundromat 1"
   mapPath: /Maps/Salvage/small-3.yml
 
 - type: salvageMap
   id: SmallAISurveyDrone
-  name: "Small / AI Survey Drone"
   mapPath: /Maps/Salvage/small-ai-survey-drone.yml
 
 - type: salvageMap
   id: Small4
-  name: "Small / Bar Salvage"
   mapPath: /Maps/Salvage/small-4.yml
 
 # Small - Asteroids
 
 - type: salvageMap
   id: SmallA1
-  name: "Small / Asteroid 1 Plasmafire"
   mapPath: /Maps/Salvage/small-a-1.yml
 
 # "Medium"-class maps - Max size square: 15x15, indicated size: 7.5
 
 - type: salvageMap
   id: Medium1
-  name: "Medium / Plasma-Trapped Cache 1"
   mapPath: /Maps/Salvage/medium-1.yml
 
 - type: salvageMap
   id: MediumVault1
-  name: "Medium / Vault 1"
   mapPath: /Maps/Salvage/medium-vault-1.yml
 
 - type: salvageMap
   id: MediumOrchestra
-  name: "Medium / Silent Orchestra"
   mapPath: /Maps/Salvage/medium-silent-orchestra.yml
 
 - type: salvageMap
   id: MediumLibraryWreck
-  name: "Medium / Abandoned Library"
   mapPath: /Maps/Salvage/medium-library.yml
 
 - type: salvageMap
   id: MediumCargoWreck
-  name: "Medium / Cargo Department Wreck"
   mapPath: /Maps/Salvage/cargo-1.yml
 
 - type: salvageMap
   id: MediumPirateWreck
-  name: "Medium / Pirate Barge Fragment"
   mapPath: /Maps/Salvage/medium-pirate.yml
 
 - type: salvageMap
   id: TickColony
-  name: "Space Tick colony"
   mapPath: /Maps/Salvage/tick-colony.yml
 
 - type: salvageMap
   id: CargoDock
-  name: "Asteroid Cargo Dock"
   mapPath: /Maps/Salvage/medium-dock.yml
 
 - type: salvageMap
   id: SpaceWaffleHome
-  name: "Waffle Home"
   mapPath: /Maps/Salvage/wh-salvage.yml
 
 - type: salvageMap
   id: MediumShuttleWreck
-  name: "Medium / Ruined Emergency Shuttle"
   mapPath: /Maps/Salvage/medium-ruined-emergency-shuttle.yml
 
 - type: salvageMap
-  id: mediumPetHospital
-  name: "Medium / Pet and Bear Hospital"
+  id: MediumPetHospital
   mapPath: /Maps/Salvage/medium-pet-hospital.yml
 
 - type: salvageMap
   id: MediumCrashedShuttle
-  name: "Crashed Shuttle"
   mapPath: /Maps/Salvage/medium-crashed-shuttle.yml
 
 - type: salvageMap
   id: Meatball
-  name: "Meatball"
   mapPath: /Maps/Salvage/meatball.yml
 
 # """Large""" maps
 
 - type: salvageMap
   id: StationStation
-  name: "StationStation"
   mapPath: /Maps/Salvage/stationstation.yml
 
 - type: salvageMap
   id: AsteroidBase
-  name: "Asteroid Base"
   mapPath: /Maps/Salvage/asteroid-base.yml
 
 - type: salvageMap
   id: RuinCargoBase
-  name: "Ruined Cargo Storage"
   mapPath: /Maps/Salvage/ruin-cargo-salvage.yml
 
 - type: salvageMap
   id: SecurityChunk
-  name: "Security Department Chunk"
   mapPath: /Maps/Salvage/security-chunk.yml
 
 - type: salvageMap
   id: EngineeringChunk
-  name: "Engineering Department Chunk"
   mapPath: /Maps/Salvage/engineering-chunk.yml
diff --git a/Resources/Prototypes/Procedural/Magnet/asteroid.yml b/Resources/Prototypes/Procedural/Magnet/asteroid.yml
new file mode 100644 (file)
index 0000000..6fb7499
--- /dev/null
@@ -0,0 +1,116 @@
+- type: weightedRandom
+  id: AsteroidOre
+  weights:
+    OreTin: 1.0
+    OreQuartz: 1.0
+    OreCoal: 1.0
+    OreGold: 0.25
+    OreSilver: 0.25
+    OrePlasma: 0.15
+    OreUranium: 0.15
+
+# Large asteroids, typically 1
+- type: dungeonConfig
+  id: BlobAsteroid
+  # Floor generation
+  generator: !type:NoiseDunGen
+    tileCap: 1500
+    capStd: 32
+    iterations: 1
+    layers:
+      - tile: FloorAsteroidSand
+        threshold: 0.30
+        noise:
+          frequency: 0.020
+          noiseType: OpenSimplex2
+          fractalType: FBm
+          octaves: 2
+          lacunarity: 2
+  # Everything else
+  postGeneration:
+    # Generate biome
+    - !type:BiomePostGen
+      biomeTemplate: Asteroid
+
+    # Generate ore veins
+    - !type:MarkerLayerPostGen
+      markerTemplate: AsteroidOre
+
+# Multiple smaller asteroids
+# This is a pain so we generate fewer tiles
+- type: dungeonConfig
+  id: ClusterAsteroid
+  # Floor generation
+  generator: !type:NoiseDunGen
+    tileCap: 1000
+    capStd: 32
+    layers:
+      - tile: FloorAsteroidSand
+        threshold: 0.10
+        noise:
+          frequency: 0.130
+          noiseType: OpenSimplex2
+          fractalType: FBm
+          octaves: 2
+          lacunarity: 2
+  # Everything else
+  postGeneration:
+    # Generate biome
+    - !type:BiomePostGen
+      biomeTemplate: Asteroid
+
+    # Generate ore veins
+    - !type:MarkerLayerPostGen
+      markerTemplate: AsteroidOre
+
+# Long and spindly, less smooth than blob
+- type: dungeonConfig
+  id: SpindlyAsteroid
+  # Floor generation
+  generator: !type:NoiseDunGen
+    tileCap: 1500
+    capStd: 32
+    layers:
+      - tile: FloorAsteroidSand
+        threshold: -0.50
+        noise:
+          frequency: 0.055
+          noiseType: Cellular
+          fractalType: FBm
+          octaves: 3
+          lacunarity: 2
+          cellularDistanceFunction: Euclidean
+  postGeneration:
+    # Generate biome
+    - !type:BiomePostGen
+      biomeTemplate: Asteroid
+
+    # Generate ore veins
+    - !type:MarkerLayerPostGen
+      markerTemplate: AsteroidOre
+
+# Lots of holes in it
+- type: dungeonConfig
+  id: SwissCheeseAsteroid
+  # Floor generation
+  generator: !type:NoiseDunGen
+    tileCap: 1500
+    capStd: 32
+    layers:
+      - tile: FloorAsteroidSand
+        threshold: -0.10
+        noise:
+          frequency: 0.155
+          noiseType: OpenSimplex2
+          fractalType: FBm
+          octaves: 2
+          lacunarity: 2
+  # Everything else
+  postGeneration:
+    # Generate biome
+    - !type:BiomePostGen
+      biomeTemplate: Asteroid
+
+    # Generate ore veins
+    - !type:MarkerLayerPostGen
+      markerTemplate: AsteroidOre
index 517d52de8a1a21c7dbe37d30078ae06cd4211ba7..cebc4cb3c449122dd06ff3bd79751740b757bae5 100644 (file)
@@ -2,6 +2,7 @@
 - type: biomeMarkerLayer
   id: OreTin
   entityMask:
+    AsteroidRock: AsteroidRockTin
     WallRock: WallRockTin
     WallRockBasalt: WallRockBasaltTin
     WallRockChromite: WallRockChromiteTin
@@ -15,6 +16,7 @@
 - type: biomeMarkerLayer
   id: OreQuartz
   entityMask:
+    AsteroidRock: AsteroidRockQuartz
     WallRock: WallRockQuartz
     WallRockBasalt: WallRockBasaltQuartz
     WallRockChromite: WallRockChromiteQuartz
@@ -27,6 +29,7 @@
 - type: biomeMarkerLayer
   id: OreCoal
   entityMask:
+    AsteroidRock: AsteroidRockCoal
     WallRock: WallRockCoal
     WallRockBasalt: WallRockBasaltCoal
     WallRockChromite: WallRockChromiteCoal
@@ -42,6 +45,7 @@
 - type: biomeMarkerLayer
   id: OreGold
   entityMask:
+    AsteroidRock: AsteroidRockGold
     WallRock: WallRockGold
     WallRockBasalt: WallRockBasaltGold
     WallRockChromite: WallRockChromiteGold
@@ -56,6 +60,7 @@
 - type: biomeMarkerLayer
   id: OreSilver
   entityMask:
+    AsteroidRock: AsteroidRockSilver
     WallRock: WallRockSilver
     WallRockBasalt: WallRockBasaltSilver
     WallRockChromite: WallRockChromiteSilver
@@ -71,6 +76,7 @@
 - type: biomeMarkerLayer
   id: OrePlasma
   entityMask:
+    AsteroidRock: AsteroidRockPlasma
     WallRock: WallRockPlasma
     WallRockBasalt: WallRockBasaltPlasma
     WallRockChromite: WallRockChromitePlasma
@@ -85,6 +91,7 @@
 - type: biomeMarkerLayer
   id: OreUranium
   entityMask:
+    AsteroidRock: AsteroidRockUranium
     WallRock: WallRockUranium
     WallRockBasalt: WallRockBasaltUranium
     WallRockChromite: WallRockChromiteUranium
 - type: biomeMarkerLayer
   id: OreBananium
   entityMask:
+    AsteroidRock: AsteroidRockBananium
     WallRock: WallRockBananium
     WallRockBasalt: WallRockBasaltBananium
     WallRockChromite: WallRockChromiteBananium
 - type: biomeMarkerLayer
   id: OreArtifactFragment
   entityMask:
+    AsteroidRock: AsteroidRockArtifactFragment
     WallRock: WallRockArtifactFragment
     WallRockBasalt: WallRockBasaltArtifactFragment
     WallRockChromite: WallRockChromiteArtifactFragment
index 9f0a4222e2683f8aa5ffbcff8472f2e441124a63..997ae33aa55517afa716688b7312941060a89180 100644 (file)
         noiseType: OpenSimplex2
         frequency: 2
       tile: FloorAsteroidSand
+
+# Asteroid
+- type: biomeTemplate
+  id: Asteroid
+  layers:
+    - !type:BiomeEntityLayer
+      threshold: 0.85
+      noise:
+        seed: 2
+        noiseType: OpenSimplex2
+        fractalType: PingPong
+      allowedTiles:
+        - FloorAsteroidSand
+      entities:
+        - CrystalGreen
+        - CrystalPink
+        - CrystalOrange
+        - CrystalBlue
+        - CrystalCyan
+    - !type:BiomeEntityLayer
+      threshold: 0.95
+      noise:
+        seed: 1
+        noiseType: OpenSimplex2
+        frequency: 1
+      allowedTiles:
+        - FloorAsteroidSand
+      entities:
+        - FloraStalagmite1
+        - FloraStalagmite2
+        - FloraStalagmite3
+        - FloraStalagmite4
+        - FloraStalagmite5
+        - FloraStalagmite6
+    - !type:BiomeEntityLayer
+      threshold: -0.6
+      invert: true
+      noise:
+        seed: 0
+        noiseType: Perlin
+        fractalType: Ridged
+        octaves: 1
+        frequency: 0.1
+        gain: 0.5
+      allowedTiles:
+        - FloorAsteroidSand
+      entities:
+        - AsteroidRock
+    - !type:BiomeTileLayer
+      threshold: -1.0
+      tile: FloorAsteroidSand
+      variants:
+        - 0
+    - !type:BiomeTileLayer
+      threshold: 0.5
+      noise:
+        seed: 1
+        noiseType: OpenSimplex2
+        frequency: 2
+      tile: FloorAsteroidSand