diff --git a/Content.Server/Salvage/Expeditions/SalvageEliminationExpeditionComponent.cs b/Content.Server/Salvage/Expeditions/SalvageEliminationExpeditionComponent.cs
new file mode 100644
index 00000000000..a3ec66ff302
--- /dev/null
+++ b/Content.Server/Salvage/Expeditions/SalvageEliminationExpeditionComponent.cs
@@ -0,0 +1,16 @@
+using Content.Shared.Salvage;
+
+namespace Content.Server.Salvage.Expeditions.Structure;
+
+///
+/// Tracks expedition data for
+///
+[RegisterComponent, Access(typeof(SalvageSystem), typeof(SpawnSalvageMissionJob))]
+public sealed partial class SalvageEliminationExpeditionComponent : Component
+{
+ ///
+ /// List of mobs that need to be killed for the mission to be complete.
+ ///
+ [DataField("megafauna")]
+ public List Megafauna = new();
+}
diff --git a/Content.Server/Salvage/Expeditions/SalvageExpeditionComponent.cs b/Content.Server/Salvage/Expeditions/SalvageExpeditionComponent.cs
index 2ac369eaaf6..819eecaac6b 100644
--- a/Content.Server/Salvage/Expeditions/SalvageExpeditionComponent.cs
+++ b/Content.Server/Salvage/Expeditions/SalvageExpeditionComponent.cs
@@ -1,23 +1,10 @@
-// SPDX-FileCopyrightText: 2023 DrSmugleaf
-// SPDX-FileCopyrightText: 2023 Pieter-Jan Briers
-// SPDX-FileCopyrightText: 2023 deltanedas <39013340+deltanedas@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2023 deltanedas <@deltanedas:kde.org>
-// SPDX-FileCopyrightText: 2023 metalgearsloth
-// SPDX-FileCopyrightText: 2024 CMDR-JohnAlex <94056103+CMDR-JohnAlex@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2024 Kira Bridgeton <161087999+Verbalase@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2024 Pieter-Jan Briers
-// SPDX-FileCopyrightText: 2024 Piras314
-// SPDX-FileCopyrightText: 2024 PoTeletubby
-// SPDX-FileCopyrightText: 2024 metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 pathetic meowmeow
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
using System.Numerics;
+using Content.Shared.Salvage;
using Content.Shared.Salvage.Expeditions;
using Robust.Shared.Audio;
+using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Server.Salvage.Expeditions;
@@ -70,4 +57,18 @@ public sealed partial class SalvageExpeditionComponent : SharedSalvageExpedition
///
[DataField]
public ResolvedSoundSpecifier SelectedSong;
-}
\ No newline at end of file
+
+ // Frontier: expedition difficulty and rewards
+ ///
+ /// The difficulty this mission had or, in the future, was selected.
+ ///
+ [ViewVariables(VVAccess.ReadWrite), DataField("difficulty")]
+ public DifficultyRating Difficulty;
+
+ ///
+ /// List of items to order on mission completion
+ ///
+ [ViewVariables(VVAccess.ReadWrite), DataField("rewards", customTypeSerializer: typeof(PrototypeIdListSerializer))]
+ public List Rewards = default!;
+ // End Frontier: expedition difficulty and rewards
+}
diff --git a/Content.Server/Salvage/Expeditions/SalvageMiningExpeditionComponent.cs b/Content.Server/Salvage/Expeditions/SalvageMiningExpeditionComponent.cs
new file mode 100644
index 00000000000..b9a1379aab9
--- /dev/null
+++ b/Content.Server/Salvage/Expeditions/SalvageMiningExpeditionComponent.cs
@@ -0,0 +1,16 @@
+using Content.Shared.Salvage;
+
+namespace Content.Server.Salvage.Expeditions;
+
+///
+/// Tracks expedition data for
+///
+[RegisterComponent, Access(typeof(SalvageSystem))]
+public sealed partial class SalvageMiningExpeditionComponent : Component
+{
+ ///
+ /// Entities that were present on the shuttle and match the loot tax.
+ ///
+ [DataField("exemptEntities")]
+ public List ExemptEntities = new();
+}
diff --git a/Content.Server/Salvage/Expeditions/SalvageShuttleComponent.cs b/Content.Server/Salvage/Expeditions/SalvageShuttleComponent.cs
index 7a10a935375..99f63d41da9 100644
--- a/Content.Server/Salvage/Expeditions/SalvageShuttleComponent.cs
+++ b/Content.Server/Salvage/Expeditions/SalvageShuttleComponent.cs
@@ -1,9 +1,3 @@
-// SPDX-FileCopyrightText: 2023 DrSmugleaf
-// SPDX-FileCopyrightText: 2023 metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
-//
-// SPDX-License-Identifier: MIT
-
namespace Content.Server.Salvage.Expeditions;
///
@@ -13,4 +7,4 @@ namespace Content.Server.Salvage.Expeditions;
public sealed partial class SalvageShuttleComponent : Component
{
-}
\ No newline at end of file
+}
diff --git a/Content.Server/Salvage/Expeditions/SalvageStructureComponent.cs b/Content.Server/Salvage/Expeditions/SalvageStructureComponent.cs
index c91bb77a8eb..f79d33412a5 100644
--- a/Content.Server/Salvage/Expeditions/SalvageStructureComponent.cs
+++ b/Content.Server/Salvage/Expeditions/SalvageStructureComponent.cs
@@ -1,9 +1,3 @@
-// SPDX-FileCopyrightText: 2023 DrSmugleaf
-// SPDX-FileCopyrightText: 2023 metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
-//
-// SPDX-License-Identifier: MIT
-
namespace Content.Server.Salvage.Expeditions.Structure;
///
@@ -13,4 +7,4 @@ namespace Content.Server.Salvage.Expeditions.Structure;
public sealed partial class SalvageStructureComponent : Component
{
-}
\ No newline at end of file
+}
diff --git a/Content.Server/Salvage/Expeditions/SalvageStructureExpeditionComponent.cs b/Content.Server/Salvage/Expeditions/SalvageStructureExpeditionComponent.cs
new file mode 100644
index 00000000000..0dec1f81adf
--- /dev/null
+++ b/Content.Server/Salvage/Expeditions/SalvageStructureExpeditionComponent.cs
@@ -0,0 +1,13 @@
+using Content.Shared.Salvage;
+
+namespace Content.Server.Salvage.Expeditions.Structure;
+
+///
+/// Tracks expedition data for
+///
+[RegisterComponent, Access(typeof(SalvageSystem), typeof(SpawnSalvageMissionJob))]
+public sealed partial class SalvageStructureExpeditionComponent : Component
+{
+ [DataField("structures")]
+ public List Structures = new();
+}
diff --git a/Content.Server/Salvage/SalvageRulerCommand.cs b/Content.Server/Salvage/SalvageRulerCommand.cs
index 95c57a1123c..b445358c375 100644
--- a/Content.Server/Salvage/SalvageRulerCommand.cs
+++ b/Content.Server/Salvage/SalvageRulerCommand.cs
@@ -1,13 +1,3 @@
-// SPDX-FileCopyrightText: 2022 20kdc
-// SPDX-FileCopyrightText: 2023 DrSmugleaf
-// SPDX-FileCopyrightText: 2023 Leon Friedrich <60421075+ElectroJr@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2023 Visne <39844191+Visne@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2024 Brandon Hu <103440971+Brandon-Huu@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2024 metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
using Content.Server.Administration;
using Content.Shared.Administration;
using Robust.Shared.Console;
@@ -71,3 +61,4 @@ public void Execute(IConsoleShell shell, string argStr, string[] args)
shell.WriteLine(total.ToString());
}
}
+
diff --git a/Content.Server/Salvage/SalvageSystem.ExpeditionConsole.cs b/Content.Server/Salvage/SalvageSystem.ExpeditionConsole.cs
index 25bed3b63af..1d7c4a44c36 100644
--- a/Content.Server/Salvage/SalvageSystem.ExpeditionConsole.cs
+++ b/Content.Server/Salvage/SalvageSystem.ExpeditionConsole.cs
@@ -1,48 +1,223 @@
-// SPDX-FileCopyrightText: 2023 DrSmugleaf
-// SPDX-FileCopyrightText: 2024 0x6273 <0x40@keemail.me>
-// SPDX-FileCopyrightText: 2024 MilenVolf <63782763+MilenVolf@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2024 Piras314
-// SPDX-FileCopyrightText: 2024 SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2024 metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2024 metalgearsloth
-// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-using Content.Shared.Procedural;
+using Content.Server.Station.Components;
+using Content.Shared.Popups;
+using Content.Shared.Shuttles.Components;
using Content.Shared.Salvage.Expeditions;
-using Content.Shared.Dataset;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Physics.Components;
using Robust.Shared.Prototypes;
+using Content.Server.Salvage.Expeditions; // Frontier
+using Content.Shared._NF.CCVar; // Frontier
+using Content.Shared.Mind.Components; // Frontier
+using Content.Shared.Mobs.Components; // Frontier
+using Content.Shared.NPC.Components; // Frontier
+using Content.Shared.IdentityManagement; // Frontier
+using Content.Shared.NPC; // Frontier
+using Content.Server._NF.Salvage; // Frontier
+using Content.Server.Shuttles.Components;
namespace Content.Server.Salvage;
public sealed partial class SalvageSystem
{
- public static readonly EntProtoId CoordinatesDisk = "CoordinatesDisk";
- public static readonly ProtoId PlanetNames = "NamesBorer";
+ [ValidatePrototypeId]
+ public const string CoordinatesDisk = "CoordinatesDisk";
+ private const float ShuttleFTLRange = 256f;
+ private const float ShuttleFTLMassThreshold = 100f;
+
+ [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
private void OnSalvageClaimMessage(EntityUid uid, SalvageExpeditionConsoleComponent component, ClaimSalvageMessage args)
{
var station = _station.GetOwningStation(uid);
- if (!TryComp(station, out var data) || data.Claimed)
+ // Frontier
+ if (!TryComp(station, out var data) || data.Claimed) // Moved up before the active expedition count
return;
+ var activeExpeditionCount = 0;
+ var expeditionQuery = AllEntityQuery();
+ while (expeditionQuery.MoveNext(out var expeditionUid, out _, out _))
+ if (TryComp(expeditionUid, out var expeditionData) && expeditionData.Claimed)
+ activeExpeditionCount++;
+
+ if (activeExpeditionCount >= _cfgManager.GetCVar(NFCCVars.SalvageExpeditionMaxActive))
+ {
+ PlayDenySound(uid, component);
+ _popupSystem.PopupEntity(Loc.GetString("shuttle-ftl-too-many"), uid, PopupType.MediumCaution);
+ UpdateConsoles(station.Value, data);
+ return;
+ }
+ // End Frontier
+
if (!data.Missions.TryGetValue(args.Index, out var missionparams))
return;
- var cdUid = Spawn(CoordinatesDisk, Transform(uid).Coordinates);
- SpawnMission(missionparams, station.Value, cdUid);
+ // Frontier: FTL travel is currently restricted to expeditions and such, and so we need to put this here
+ // until FTL changes for us in some way.
+ if (!component.Debug) // Skip the test
+ {
+ if (!TryComp(station, out var stationData))
+ return;
+ if (_station.GetLargestGrid(stationData) is not { Valid: true } grid)
+ return;
+ if (!TryComp(grid, out var gridComp))
+ return;
+
+ // Frontier: check for FTL component - if one exists, the station won't be taken into FTL.
+ if (HasComp(grid))
+ {
+ PlayDenySound(uid, component);
+ _popupSystem.PopupEntity(Loc.GetString("shuttle-ftl-recharge"), uid, PopupType.MediumCaution);
+ UpdateConsoles(station.Value, data); // Sure, why not?
+ return;
+ }
+
+ var xform = Transform(grid);
+ var bounds = xform.WorldMatrix.TransformBox(gridComp.LocalAABB).Enlarged(ShuttleFTLRange);
+ var bodyQuery = GetEntityQuery();
+ // Keep track of docked grids to exclude them from the proximity check
+ var dockedGrids = new HashSet();
+
+ // Find all docked grids by looking for DockingComponents on the shuttle
+ var dockQuery = EntityQueryEnumerator();
+ while (dockQuery.MoveNext(out var dockUid, out var dock, out var dockXform))
+ {
+ // Only consider docks on our grid
+ if (dockXform.GridUid != grid || !dock.Docked || dock.DockedWith == null)
+ continue;
+
+ // If we have a docked entity, get its grid
+ if (TryComp(dock.DockedWith.Value, out var dockedXform) && dockedXform.GridUid != null)
+ {
+ dockedGrids.Add(dockedXform.GridUid.Value);
+
+ // Check if we're docked to another grid
+ var parentGridUid = dockedXform.GridUid.Value;
+
+ // Find all other grids docked to this parent grid
+ // These should also be excluded from the proximity check so we can
+ // still FTL even when other ships are docked to the same station/grid
+ var parentDockQuery = EntityQueryEnumerator();
+ while (parentDockQuery.MoveNext(out var parentDockUid, out var parentDock, out var parentDockXform))
+ {
+ // Only consider docks on the parent grid
+ if (parentDockXform.GridUid != parentGridUid || !parentDock.Docked || parentDock.DockedWith == null)
+ continue;
+
+ // If we have a docked entity and it's not our grid, add its grid to the exclusion list
+ if (TryComp(parentDock.DockedWith.Value, out var siblingDockedXform) &&
+ siblingDockedXform.GridUid != null &&
+ siblingDockedXform.GridUid != grid)
+ {
+ dockedGrids.Add(siblingDockedXform.GridUid.Value);
+ }
+ }
+ }
+ }
+
+ foreach (var other in _mapManager.FindGridsIntersecting(xform.MapID, bounds))
+ {
+ if (other.Owner == grid ||
+ dockedGrids.Contains(other.Owner) || // Skip grids that are docked to us or to the same parent grid
+ !bodyQuery.TryGetComponent(other.Owner, out var body) ||
+ body.Mass < ShuttleFTLMassThreshold)
+ {
+ continue;
+ }
+
+ PlayDenySound(uid, component);
+ _popupSystem.PopupEntity(Loc.GetString("shuttle-ftl-proximity"), uid, PopupType.Medium);
+ UpdateConsoles(station.Value, data);
+ return;
+ }
+ }
+ // End Frontier
+
+ // Frontier change - disable coordinate disks for expedition missions
+ //var cdUid = Spawn(CoordinatesDisk, Transform(uid).Coordinates);
+ SpawnMission(missionparams, station.Value, null);
data.ActiveMission = args.Index;
- var mission = GetMission(_prototypeManager.Index(missionparams.Difficulty), missionparams.Seed);
+ var mission = GetMission(missionparams.MissionType, missionparams.Difficulty, missionparams.Seed);
data.NextOffer = _timing.CurTime + mission.Duration + TimeSpan.FromSeconds(1);
- _labelSystem.Label(cdUid, GetFTLName(_prototypeManager.Index(PlanetNames), missionparams.Seed));
- _audio.PlayPvs(component.PrintSound, uid);
+ // Frontier change - disable coordinate disks for expedition missions
+ //_labelSystem.Label(cdUid, GetFTLName(_prototypeManager.Index("NamesBorer"), missionparams.Seed));
+ //_audio.PlayPvs(component.PrintSound, uid);
+
+ UpdateConsoles(station.Value, data); // Frontier: add station
+ }
+
+ // Frontier: early expedition end
+ private void OnSalvageFinishMessage(EntityUid entity, SalvageExpeditionConsoleComponent component, FinishSalvageMessage e)
+ {
+ var station = _station.GetOwningStation(entity);
+ if (!TryComp(station, out var data) || !data.CanFinish)
+ return;
+
+ // Based on SalvageSystem.Runner:OnConsoleFTLAttempt
+ if (!TryComp(entity, out TransformComponent? xform)) // Get the console's grid (if you move it, rip you)
+ {
+ PlayDenySound(entity, component);
+ _popupSystem.PopupEntity(Loc.GetString("salvage-expedition-shuttle-not-found"), entity, PopupType.MediumCaution);
+ UpdateConsoles(station.Value, data);
+ return;
+ }
+
+ // Frontier: check if any player characters or friendly ghost roles are outside
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var mindContainer, out var _, out var mobXform))
+ {
+ if (mobXform.MapUid != xform.MapUid)
+ continue;
+
+ // Not player controlled (ghosted)
+ if (!mindContainer.HasMind)
+ continue;
+
+ // NPC, definitely not a person
+ if (HasComp(uid) || HasComp(uid))
+ continue;
+
+ // Hostile ghost role, continue
+ if (TryComp(uid, out NpcFactionMemberComponent? npcFaction))
+ {
+ var hostileFactions = npcFaction.HostileFactions;
+ if (hostileFactions.Contains("NanoTrasen")) // Nasty - what if we need pirate expeditions?
+ continue;
+ }
+
+ // Okay they're on salvage, so are they on the shuttle.
+ if (mobXform.GridUid != xform.GridUid)
+ {
+ PlayDenySound(entity, component);
+ _popupSystem.PopupEntity(Loc.GetString("salvage-expedition-not-everyone-aboard", ("target", Identity.Entity(uid, EntityManager))), entity, PopupType.MediumCaution);
+ UpdateConsoles(station.Value, data);
+ return;
+ }
+ }
+ // End SalvageSystem.Runner:OnConsoleFTLAttempt
+
+ data.CanFinish = false;
+ UpdateConsoles(station.Value, data);
+
+ var map = Transform(entity).MapUid;
- UpdateConsoles((station.Value, data));
+ if (!TryComp(map, out var expedition))
+ return;
+
+ const int departTime = 20;
+ var newEndTime = _timing.CurTime + TimeSpan.FromSeconds(departTime);
+
+ if (expedition.EndTime <= newEndTime)
+ return;
+
+ expedition.EndTime = newEndTime;
+ expedition.Stage = ExpeditionStage.FinalCountdown;
+
+ Announce(map.Value, Loc.GetString("salvage-expedition-announcement-early-finish", ("departTime", departTime)));
}
+ // End Frontier: early expedition end
private void OnSalvageConsoleInit(Entity console, ref ComponentInit args)
{
@@ -54,7 +229,7 @@ private void OnSalvageConsoleParent(Entity co
UpdateConsole(console);
}
- private void UpdateConsoles(Entity component)
+ private void UpdateConsoles(EntityUid stationUid, SalvageExpeditionDataComponent component)
{
var state = GetState(component);
@@ -63,9 +238,18 @@ private void UpdateConsoles(Entity component)
{
var station = _station.GetOwningStation(uid, xform);
- if (station != component.Owner)
+ if (station != stationUid)
continue;
+ // Frontier: if we have a lingering FTL component, we cannot start a new mission
+ if (!TryComp(station, out var stationData) ||
+ _station.GetLargestGrid(stationData) is not { Valid: true } grid ||
+ HasComp(grid))
+ {
+ state.Cooldown = true; //Hack: disable buttons
+ }
+ // End Frontier
+
_ui.SetUiState((uid, uiComp), SalvageConsoleUiKey.Expedition, state);
}
}
@@ -81,9 +265,23 @@ private void UpdateConsole(Entity component)
}
else
{
- state = new SalvageExpeditionConsoleState(TimeSpan.Zero, false, true, 0, new List());
+ state = new SalvageExpeditionConsoleState(TimeSpan.Zero, false, true, false, 0, new List()); // Frontier: add false as 4th param
}
+ // Frontier: if we have a lingering FTL component, we cannot start a new mission
+ if (!TryComp(station, out var stationData) ||
+ _station.GetLargestGrid(stationData) is not { Valid: true } grid ||
+ HasComp(grid))
+ {
+ state.Cooldown = true; //Hack: disable buttons
+ }
+ // End Frontier
+
_ui.SetUiState(component.Owner, SalvageConsoleUiKey.Expedition, state);
}
-}
\ No newline at end of file
+
+ private void PlayDenySound(EntityUid uid, SalvageExpeditionConsoleComponent component)
+ {
+ _audio.PlayPvs(_audio.GetSound(component.ErrorSound), uid);
+ }
+}
diff --git a/Content.Server/Salvage/SalvageSystem.Expeditions.cs b/Content.Server/Salvage/SalvageSystem.Expeditions.cs
index 45c747e3e3f..cbd5e7fdf9f 100644
--- a/Content.Server/Salvage/SalvageSystem.Expeditions.cs
+++ b/Content.Server/Salvage/SalvageSystem.Expeditions.cs
@@ -1,38 +1,31 @@
-// SPDX-FileCopyrightText: 2023 DrSmugleaf
-// SPDX-FileCopyrightText: 2023 Moony
-// SPDX-FileCopyrightText: 2023 Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2023 Pieter-Jan Briers
-// SPDX-FileCopyrightText: 2023 TemporalOroboros
-// SPDX-FileCopyrightText: 2023 Visne <39844191+Visne@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2023 deltanedas <39013340+deltanedas@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2023 deltanedas <@deltanedas:kde.org>
-// SPDX-FileCopyrightText: 2023 moonheart08
-// SPDX-FileCopyrightText: 2024 0x6273 <0x40@keemail.me>
-// SPDX-FileCopyrightText: 2024 ElectroJr
-// SPDX-FileCopyrightText: 2024 Leon Friedrich <60421075+ElectroJr@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2024 Pieter-Jan Briers
-// SPDX-FileCopyrightText: 2024 Piras314
-// SPDX-FileCopyrightText: 2024 SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2024 Vasilis
-// SPDX-FileCopyrightText: 2024 metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2024 metalgearsloth
-// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 pathetic meowmeow
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
using System.Linq;
using System.Threading;
+using Content.Server._NF.Salvage; // Frontier: graceful exped spawn failures
+using Content.Server.Cargo.Components;
+using Content.Server.Cargo.Systems;
using Content.Server.Salvage.Expeditions;
using Content.Server.Salvage.Expeditions.Structure;
using Content.Shared.CCVar;
+using Content.Shared._NF.CCVar; // Frontier
using Content.Shared.Examine;
+using Content.Shared.Random.Helpers;
using Content.Shared.Salvage.Expeditions;
-using Content.Shared.Shuttles.Components;
using Robust.Shared.Audio;
using Robust.Shared.CPUJob.JobQueues;
using Robust.Shared.CPUJob.JobQueues.Queues;
+using Content.Server.Shuttles.Systems;
+using Content.Server.Station.Components;
+using Content.Server.Station.Systems;
+using Content.Shared.Coordinates;
+using Content.Shared.Procedural;
+using Content.Shared.Salvage;
using Robust.Shared.GameStates;
+using Content.Server.Weather;
+using Content.Shared.Weather;
+using Robust.Shared.Random;
+using Robust.Shared.Map;
+using Content.Shared.Shuttles.Components; // Frontier
+using Robust.Shared.Configuration; // Frontier
namespace Content.Server.Salvage;
@@ -42,28 +35,36 @@ public sealed partial class SalvageSystem
* Handles setup / teardown of salvage expeditions.
*/
- private const int MissionLimit = 3;
+ private const int MissionLimit = 5;
+ [Dependency] private readonly IConfigurationManager _cfgManager = default!; // Frontier
private readonly JobQueue _salvageQueue = new();
private readonly List<(SpawnSalvageMissionJob Job, CancellationTokenSource CancelToken)> _salvageJobs = new();
+ private readonly List _missionDifficulties = [DifficultyRating.Moderate, DifficultyRating.Hazardous, DifficultyRating.Extreme]; // Frontier
private const double SalvageJobTime = 0.002;
private float _cooldown;
+ private float _failedCooldown;
private void InitializeExpeditions()
{
SubscribeLocalEvent(OnSalvageConsoleInit);
SubscribeLocalEvent(OnSalvageConsoleParent);
SubscribeLocalEvent(OnSalvageClaimMessage);
+ SubscribeLocalEvent(OnExpeditionSpawnComplete); // Frontier: more gracefully handle expedition generation failures
+ SubscribeLocalEvent(OnSalvageFinishMessage); // Frontier: For early finish
SubscribeLocalEvent(OnExpeditionMapInit);
+ // SubscribeLocalEvent(OnDataUnpaused); // Frontier
+
SubscribeLocalEvent(OnExpeditionShutdown);
+ // SubscribeLocalEvent(OnExpeditionUnpaused); // Frontier
SubscribeLocalEvent(OnExpeditionGetState);
SubscribeLocalEvent(OnStructureExamine);
- _cooldown = _configurationManager.GetCVar(CCVars.SalvageExpeditionCooldown);
- Subs.CVar(_configurationManager, CCVars.SalvageExpeditionCooldown, SetCooldownChange);
+ Subs.CVar(_cfgManager, CCVars.SalvageExpeditionCooldown, SetCooldownChange, true); // Frontier
+ Subs.CVar(_cfgManager, NFCCVars.SalvageExpeditionFailedCooldown, SetFailedCooldownChange, true); // Frontier
}
private void OnExpeditionGetState(EntityUid uid, SalvageExpeditionComponent component, ref ComponentGetState args)
@@ -74,6 +75,14 @@ private void OnExpeditionGetState(EntityUid uid, SalvageExpeditionComponent comp
};
}
+ // Frontier
+ private void ShutdownExpeditions()
+ {
+ _cfgManager.UnsubValueChanged(CCVars.SalvageExpeditionCooldown, SetCooldownChange);
+ _cfgManager.UnsubValueChanged(NFCCVars.SalvageExpeditionFailedCooldown, SetFailedCooldownChange);
+ }
+ // End Frontier
+
private void SetCooldownChange(float obj)
{
// Update the active cooldowns if we change it.
@@ -89,6 +98,20 @@ private void SetCooldownChange(float obj)
_cooldown = obj;
}
+ private void SetFailedCooldownChange(float obj)
+ {
+ var diff = obj - _failedCooldown;
+
+ var query = AllEntityQuery();
+
+ while (query.MoveNext(out var comp))
+ {
+ comp.NextOffer += TimeSpan.FromSeconds(diff);
+ }
+
+ _failedCooldown = obj;
+ }
+
private void OnExpeditionMapInit(EntityUid uid, SalvageExpeditionComponent component, MapInitEvent args)
{
component.SelectedSong = _audio.ResolveSound(component.Sound);
@@ -98,15 +121,6 @@ private void OnExpeditionShutdown(EntityUid uid, SalvageExpeditionComponent comp
{
component.Stream = _audio.Stop(component.Stream);
- // First wipe any disks referencing us
- var disks = AllEntityQuery();
- while (disks.MoveNext(out var disk, out var diskComp)
- && diskComp.Destination == uid)
- {
- diskComp.Destination = null;
- Dirty(disk, diskComp);
- }
-
foreach (var (job, cancelToken) in _salvageJobs.ToArray())
{
if (job.Station == component.Station)
@@ -122,10 +136,20 @@ private void OnExpeditionShutdown(EntityUid uid, SalvageExpeditionComponent comp
// Finish mission
if (TryComp(component.Station, out var data))
{
- FinishExpedition((component.Station, data), uid);
+ FinishExpedition(data, uid, component, component.Station); // Frontier: null currentTime || comp.Claimed)
continue;
- comp.Cooldown = false;
- comp.NextOffer += TimeSpan.FromSeconds(_cooldown);
+ if (!HasComp(_station.GetLargestGrid(Comp(uid)))) // Frontier
+ comp.Cooldown = false;
+ //comp.NextOffer += TimeSpan.FromSeconds(_cooldown); // Frontier
+ comp.NextOffer = currentTime + TimeSpan.FromSeconds(_cooldown); // Frontier
GenerateMissions(comp);
- UpdateConsoles((uid, comp));
+ UpdateConsoles(uid, comp);
}
}
- private void FinishExpedition(Entity expedition, EntityUid uid)
+ private void FinishExpedition(SalvageExpeditionDataComponent component, EntityUid uid, SalvageExpeditionComponent expedition, EntityUid? shuttle)
{
- var component = expedition.Comp;
component.NextOffer = _timing.CurTime + TimeSpan.FromSeconds(_cooldown);
- Announce(uid, Loc.GetString("salvage-expedition-completed"));
+ Announce(uid, Loc.GetString("salvage-expedition-mission-completed"));
+ // Finish mission cleanup.
+ switch (expedition.MissionParams.MissionType)
+ {
+ // Handles the mining taxation.
+ case SalvageMissionType.Mining:
+ expedition.Completed = true;
+
+ if (shuttle != null && TryComp(uid, out var mining))
+ {
+ var xformQuery = GetEntityQuery();
+ var entities = new List();
+ MiningTax(entities, shuttle.Value, mining, xformQuery);
+
+ var tax = GetMiningTax(expedition.MissionParams.Difficulty);
+ _random.Shuffle(entities);
+
+ // TODO: urgh this pr is already taking so long I'll do this later
+ for (var i = 0; i < Math.Ceiling(entities.Count * tax); i++)
+ {
+ // QueueDel(entities[i]);
+ }
+ }
+
+ break;
+ }
+
+ // Handle payout after expedition has finished
+ if (expedition.Completed)
+ {
+ Log.Debug($"Completed mission {expedition.MissionParams.MissionType} with seed {expedition.MissionParams.Seed}");
+ component.NextOffer = _timing.CurTime + TimeSpan.FromSeconds(_cooldown);
+ Announce(uid, Loc.GetString("salvage-expedition-mission-completed"));
+ GiveRewards(expedition);
+ }
+ else
+ {
+ Log.Debug($"Failed mission {expedition.MissionParams.MissionType} with seed {expedition.MissionParams.Seed}");
+ component.NextOffer = _timing.CurTime + TimeSpan.FromSeconds(_failedCooldown);
+ Announce(uid, Loc.GetString("salvage-expedition-mission-failed"));
+ }
+
+
component.ActiveMission = 0;
component.Cooldown = true;
- UpdateConsoles(expedition);
+ if (shuttle != null) // Frontier
+ UpdateConsoles(shuttle.Value, component); // Frontier
+ }
+
+ ///
+ /// Deducts ore tax for mining.
+ ///
+ private void MiningTax(List entities, EntityUid entity, SalvageMiningExpeditionComponent mining, EntityQuery xformQuery)
+ {
+ if (!mining.ExemptEntities.Contains(entity))
+ {
+ entities.Add(entity);
+ }
+
+ var xform = xformQuery.GetComponent(entity);
+ var children = xform.ChildEnumerator;
+
+ while (children.MoveNext(out var child))
+ {
+ MiningTax(entities, child, mining, xformQuery);
+ }
}
private void GenerateMissions(SalvageExpeditionDataComponent component)
{
component.Missions.Clear();
+ var configs = Enum.GetValues().ToList();
+
+ // Temporarily removed coz it SUCKS
+ configs.Remove(SalvageMissionType.Mining);
+
+ // this doesn't support having more missions than types of ratings
+ // but the previous system didn't do that either.
+ var allDifficulties = _missionDifficulties; // Frontier: Enum.GetValues() < _missionDifficulties
+ _random.Shuffle(allDifficulties);
+ var difficulties = allDifficulties.Take(MissionLimit).ToList();
+ // difficulties.Sort(); // Frontier: sort later
+
+ // Frontier: multiple missions per difficulty
+ // If we support more missions than there are accepted types, pick more until you're up to MissionLimit
+ while (difficulties.Count < MissionLimit)
+ {
+ var difficultyIndex = _random.Next(_missionDifficulties.Count);
+ difficulties.Add(_missionDifficulties[difficultyIndex]);
+ }
+ difficulties.Sort();
+ // End Frontier: multiple missions per difficulty
+
+ if (configs.Count == 0)
+ return;
for (var i = 0; i < MissionLimit; i++)
{
- var mission = new SalvageMissionParams
- {
- Index = component.NextIndex,
- Seed = _random.Next(),
- Difficulty = "Moderate",
- };
+ _random.Shuffle(configs);
+ var rating = difficulties[i];
- component.Missions[component.NextIndex++] = mission;
+ foreach (var config in configs)
+ {
+ var mission = new SalvageMissionParams
+ {
+ Index = component.NextIndex,
+ MissionType = config,
+ Seed = _random.Next(),
+ Difficulty = rating,
+ };
+
+ component.Missions[component.NextIndex++] = mission;
+ break;
+ }
}
}
private SalvageExpeditionConsoleState GetState(SalvageExpeditionDataComponent component)
{
var missions = component.Missions.Values.ToList();
- return new SalvageExpeditionConsoleState(component.NextOffer, component.Claimed, component.Cooldown, component.ActiveMission, missions);
+ //return new SalvageExpeditionConsoleState(component.NextOffer, component.Claimed, component.Cooldown, component.ActiveMission, missions);
+ return new SalvageExpeditionConsoleState(component.NextOffer, component.Claimed, component.Cooldown, component.CanFinish, component.ActiveMission, missions); // Frontier
}
private void SpawnMission(SalvageMissionParams missionParams, EntityUid station, EntityUid? coordinatesDisk)
@@ -195,12 +315,17 @@ private void SpawnMission(SalvageMissionParams missionParams, EntityUid station,
SalvageJobTime,
EntityManager,
_timing,
- _logManager,
+ _mapManager,
_prototypeManager,
_anchorable,
_biome,
+ _weather,
_dungeon,
+ _shuttle,
+ _station,
_metaData,
+ this,
+ _transform,
_mapSystem,
station,
coordinatesDisk,
@@ -215,4 +340,40 @@ private void OnStructureExamine(EntityUid uid, SalvageStructureComponent compone
{
args.PushMarkup(Loc.GetString("salvage-expedition-structure-examine"));
}
-}
\ No newline at end of file
+
+ private void GiveRewards(SalvageExpeditionComponent comp)
+ {
+ if (!_cfgManager.GetCVar(NFCCVars.SalvageExpeditionRewardsEnabled))
+ return;
+
+ var palletList = new List();
+ var pallets = EntityQueryEnumerator(); // Frontier CargoPalletComponent 0))
+ return;
+
+ foreach (var reward in comp.Rewards)
+ {
+ Spawn(reward, (Transform(_random.Pick(palletList)).MapPosition));
+ }
+ }
+
+ // Frontier: handle exped spawn job failures gracefully - reset the console
+ private void OnExpeditionSpawnComplete(EntityUid uid, SalvageExpeditionDataComponent component, ExpeditionSpawnCompleteEvent ev)
+ {
+ if (component.ActiveMission == ev.MissionIndex && !ev.Success)
+ {
+ component.ActiveMission = 0;
+ component.Cooldown = false;
+ UpdateConsoles(uid, component);
+ }
+ }
+ // End Frontier
+}
diff --git a/Content.Server/Salvage/SalvageSystem.Magnet.cs b/Content.Server/Salvage/SalvageSystem.Magnet.cs
index 4ba3d0a5e34..07014d56920 100644
--- a/Content.Server/Salvage/SalvageSystem.Magnet.cs
+++ b/Content.Server/Salvage/SalvageSystem.Magnet.cs
@@ -1,32 +1,3 @@
-// SPDX-FileCopyrightText: 2024 Emisse <99158783+Emisse@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2024 Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2024 Plykiya <58439124+Plykiya@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2024 metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2024 plykiya
-// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 Aidenkrz
-// SPDX-FileCopyrightText: 2025 Aineias1
-// SPDX-FileCopyrightText: 2025 FaDeOkno <143940725+FaDeOkno@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 GoobBot
-// SPDX-FileCopyrightText: 2025 Leon Friedrich <60421075+ElectroJr@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 McBosserson <148172569+McBosserson@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 Milon
-// SPDX-FileCopyrightText: 2025 Piras314
-// SPDX-FileCopyrightText: 2025 Rouden <149893554+Roudenn@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 SX_7
-// SPDX-FileCopyrightText: 2025 TheBorzoiMustConsume <197824988+TheBorzoiMustConsume@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 Unlumination <144041835+Unlumy@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 Winkarst <74284083+Winkarst-cpu@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 deltanedas <39013340+deltanedas@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
-// SPDX-FileCopyrightText: 2025 gluesniffler <159397573+gluesniffler@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 gluesniffler
-// SPDX-FileCopyrightText: 2025 username <113782077+whateverusername0@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 whateverusername0
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
@@ -37,7 +8,6 @@
using Content.Shared.Salvage.Magnet;
using Robust.Shared.Exceptions;
using Robust.Shared.Map;
-using Robust.Shared.Prototypes;
namespace Content.Server.Salvage;
@@ -45,17 +15,16 @@ public sealed partial class SalvageSystem
{
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
- private static readonly ProtoId MagnetChannel = "Supply";
+ [ValidatePrototypeId]
+ private const string MagnetChannel = "Supply";
private EntityQuery _salvMobQuery;
- private EntityQuery _mobStateQuery;
private List<(Entity Entity, EntityUid MapUid, Vector2 LocalPosition)> _detachEnts = new();
private void InitializeMagnet()
{
_salvMobQuery = GetEntityQuery();
- _mobStateQuery = GetEntityQuery();
SubscribeLocalEvent(OnMagnetDataMapInit);
@@ -77,12 +46,11 @@ private void OnMagnetClaim(EntityUid uid, SalvageMagnetComponent component, ref
}
var index = args.Index;
- var actor = args.Actor;
async void TryTakeMagnetOffer()
{
try
{
- await TakeMagnetOffer((station.Value, dataComp), index, (uid, component), actor); // DeltaV: pass the user entity
+ await TakeMagnetOffer((station.Value, dataComp), index, (uid, component));
}
catch (Exception e)
{
@@ -165,11 +133,11 @@ private void EndMagnet(Entity data)
if (data.Comp.ActiveEntities != null)
{
// Handle mobrestrictions getting deleted
- var query = AllEntityQuery();
+ var query = AllEntityQuery();
- while (query.MoveNext(out var salvUid, out var salvMob, out var salvMobState))
+ while (query.MoveNext(out var salvUid, out var salvMob))
{
- if (data.Comp.ActiveEntities.Contains(salvMob.LinkedEntity) && _mobState.IsAlive(salvUid, salvMobState))
+ if (data.Comp.ActiveEntities.Contains(salvMob.LinkedEntity))
{
QueueDel(salvUid);
}
@@ -187,20 +155,6 @@ private void EndMagnet(Entity data)
if (_salvMobQuery.HasComp(mobUid))
continue;
- bool CheckParents(EntityUid uid)
- {
- do
- {
- uid = _transform.GetParentUid(uid);
- if (_mobStateQuery.HasComp(uid))
- return true;
- } while (uid != xform.GridUid && uid != EntityUid.Invalid);
- return false;
- }
-
- if (CheckParents(mobUid))
- continue;
-
// Can't parent directly to map as it runs grid traversal.
_detachEnts.Add(((mobUid, xform), xform.MapUid.Value, _transform.GetWorldPosition(xform)));
_transform.DetachEntity(mobUid, xform);
@@ -308,15 +262,11 @@ private void UpdateMagnetUIs(Entity data)
}
}
- private async Task TakeMagnetOffer(Entity data, int index, Entity magnet, EntityUid user) // DeltaV: add user param
+ private async Task TakeMagnetOffer(Entity data, int index, Entity magnet)
{
var seed = data.Comp.Offered[index];
var offering = GetSalvageOffering(seed);
- // Begin DeltaV Addition: make wrecks cost mining points to pull
- if (offering.Cost > 0 && !(_points.TryFindIdCard(user) is {} idCard && _points.RemovePoints(idCard, offering.Cost)))
- return;
- // End DeltaV Addition
var salvMap = _mapSystem.CreateMap();
var salvMapXform = Transform(salvMap);
@@ -330,12 +280,12 @@ private async Task TakeMagnetOffer(Entity data, int
{
case AsteroidOffering asteroid:
var grid = _mapManager.CreateGridEntity(salvMap);
- await _dungeon.GenerateDungeonAsync(asteroid.DungeonConfig, grid.Owner, grid.Comp, Vector2i.Zero, seed);
+ await _dungeon.GenerateDungeonAsync(asteroid.DungeonConfig, asteroid.Id, grid.Owner, grid.Comp, Vector2i.Zero, seed); // Frontier: added asteroid.Id - FIXME: value makes no sense.
break;
case DebrisOffering debris:
var debrisProto = _prototypeManager.Index(debris.Id);
var debrisGrid = _mapManager.CreateGridEntity(salvMap);
- await _dungeon.GenerateDungeonAsync(debrisProto, debrisGrid.Owner, debrisGrid.Comp, Vector2i.Zero, seed);
+ await _dungeon.GenerateDungeonAsync(debrisProto, debrisProto.ID, debrisGrid.Owner, debrisGrid.Comp, Vector2i.Zero, seed); // Frontier: debrisProto.ID
break;
case SalvageOffering wreck:
var salvageProto = wreck.SalvageMap;
@@ -495,4 +445,4 @@ private bool TryGetSalvagePlacementLocation(Entity magne
public record struct SalvageMagnetActivatedEvent
{
public EntityUid Magnet;
-}
\ No newline at end of file
+}
diff --git a/Content.Server/Salvage/SalvageSystem.Runner.cs b/Content.Server/Salvage/SalvageSystem.Runner.cs
index 056aef52730..2c83571e7fc 100644
--- a/Content.Server/Salvage/SalvageSystem.Runner.cs
+++ b/Content.Server/Salvage/SalvageSystem.Runner.cs
@@ -1,19 +1,7 @@
-// SPDX-FileCopyrightText: 2023 Pieter-Jan Briers
-// SPDX-FileCopyrightText: 2023 deltanedas <39013340+deltanedas@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2023 deltanedas <@deltanedas:kde.org>
-// SPDX-FileCopyrightText: 2023 metalgearsloth
-// SPDX-FileCopyrightText: 2024 Aidenkrz
-// SPDX-FileCopyrightText: 2024 DrSmugleaf <10968691+DrSmugleaf@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2024 Leon Friedrich <60421075+ElectroJr@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2024 chavonadelal <156101927+chavonadelal@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2024 metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 pathetic meowmeow
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
using System.Numerics;
+using Content.Server.GameTicking;
using Content.Server.Salvage.Expeditions;
+using Content.Server.Salvage.Expeditions.Structure;
using Content.Server.Shuttles.Components;
using Content.Server.Shuttles.Events;
using Content.Server.Station.Components;
@@ -22,10 +10,13 @@
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Salvage.Expeditions;
+using Robust.Shared.Map;
using Content.Shared.Shuttles.Components;
using Content.Shared.Localizations;
using Robust.Shared.Map.Components;
using Robust.Shared.Player;
+using Robust.Shared.Utility;
+using Content.Shared.Coordinates;
namespace Content.Server.Salvage;
@@ -36,7 +27,7 @@ public sealed partial class SalvageSystem
*/
[Dependency] private readonly MobStateSystem _mobState = default!;
-
+ [Dependency] private readonly GameTicker _gameTicker = default!;
private void InitializeRunner()
{
SubscribeLocalEvent(OnFTLRequest);
@@ -56,7 +47,7 @@ private void OnConsoleFTLAttempt(ref ConsoleFTLAttemptEvent ev)
// TODO: This is terrible but need bluespace harnesses or something.
var query = EntityQueryEnumerator();
- while (query.MoveNext(out var uid, out _, out var mobState, out var mobXform))
+ while (query.MoveNext(out var uid, out var _, out var mobState, out var mobXform))
{
if (mobXform.MapUid != xform.MapUid)
continue;
@@ -112,13 +103,21 @@ private void OnFTLCompleted(ref FTLCompletedEvent args)
if (!TryComp(args.MapUid, out var component))
return;
+ // Frontier
+ if (TryComp(component.Station, out var data))
+ {
+ data.CanFinish = true;
+ UpdateConsoles(component.Station, data);
+ }
+ // Frontier
+
// Someone FTLd there so start announcement
if (component.Stage != ExpeditionStage.Added)
return;
Announce(args.MapUid, Loc.GetString("salvage-expedition-announcement-countdown-minutes", ("duration", (component.EndTime - _timing.CurTime).Minutes)));
- var directionLocalization = ContentLocalizationManager.FormatDirection(component.DungeonLocation.GetDir()).ToLower();
+ var directionLocalization = ContentLocalizationManager.FormatDirection(component.DungeonLocation.GetDir()).ToLower();
if (component.DungeonLocation != Vector2.Zero)
Announce(args.MapUid, Loc.GetString("salvage-expedition-announcement-dungeon", ("direction", directionLocalization)));
@@ -129,12 +128,25 @@ private void OnFTLCompleted(ref FTLCompletedEvent args)
private void OnFTLStarted(ref FTLStartedEvent ev)
{
+ // Started a mining mission so work out exempt entities
+ if (TryComp(
+ _mapManager.GetMapEntityId(ev.TargetCoordinates.ToMap(EntityManager, _transform).MapId),
+ out var mining))
+ {
+ var ents = new List();
+ var xformQuery = GetEntityQuery();
+ MiningTax(ents, ev.Entity, mining, xformQuery);
+ mining.ExemptEntities = ents;
+ }
+
if (!TryComp(ev.FromMapUid, out var expedition) ||
!TryComp(expedition.Station, out var station))
{
return;
}
+ station.CanFinish = false; // Frontier
+
// Check if any shuttles remain.
var query = EntityQueryEnumerator();
@@ -166,7 +178,7 @@ private void UpdateRunner()
Dirty(uid, comp);
Announce(uid, Loc.GetString("salvage-expedition-announcement-countdown-seconds", ("duration", TimeSpan.FromSeconds(45).Seconds)));
}
- else if (comp.Stream == null && remaining < audioLength)
+ else if (comp.Stage < ExpeditionStage.MusicCountdown && comp.Stream == null && remaining < audioLength) // Frontier
{
var audio = _audio.PlayPvs(comp.Sound, uid);
comp.Stream = audio?.Entity;
@@ -175,7 +187,7 @@ private void UpdateRunner()
Dirty(uid, comp);
Announce(uid, Loc.GetString("salvage-expedition-announcement-countdown-minutes", ("duration", audioLength.Minutes)));
}
- else if (comp.Stage < ExpeditionStage.Countdown && remaining < TimeSpan.FromMinutes(4))
+ else if (comp.Stage < ExpeditionStage.Countdown && remaining < TimeSpan.FromMinutes(5))
{
comp.Stage = ExpeditionStage.Countdown;
Dirty(uid, comp);
@@ -203,7 +215,51 @@ private void UpdateRunner()
if (shuttleXform.MapUid != uid || HasComp(shuttleUid))
continue;
- _shuttle.FTLToDock(shuttleUid, shuttle, member, ftlTime);
+ // Frontier: try to find a potential destination for ship that doesn't collide with other grids.
+ var mapId = _gameTicker.DefaultMap;
+ if (!_mapSystem.TryGetMap(mapId, out var mapUid))
+ {
+ Log.Error($"Could not get DefaultMap EntityUID, shuttle {shuttleUid} may be stuck on expedition.");
+ continue;
+ }
+
+ // Destination generator parameters (move to CVAR?)
+ int numRetries = 20; // Maximum number of retries
+ float minDistance = 200f; // Minimum distance from another object, in meters
+ float minRange = 750f; // Minimum distance from sector centre, in meters
+ float maxRange = 3500f; // Maximum distance from sector centre, in meters
+
+ // Get a list of all grid positions on the destination map
+ List gridCoords = new();
+ var gridQuery = EntityManager.AllEntityQueryEnumerator();
+ while (gridQuery.MoveNext(out var _, out _, out var xform))
+ {
+ if (xform.MapID == mapId)
+ gridCoords.Add(_transform.GetWorldPosition(xform));
+ }
+
+ Vector2 dropLocation = _random.NextVector2(minRange, maxRange);
+ for (int i = 0; i < numRetries; i++)
+ {
+ bool positionIsValid = true;
+ foreach (var station in gridCoords)
+ {
+ if (Vector2.Distance(station, dropLocation) < minDistance)
+ {
+ positionIsValid = false;
+ break;
+ }
+ }
+
+ if (positionIsValid)
+ break;
+
+ // No good position yet, pick another random position.
+ dropLocation = _random.NextVector2(minRange, maxRange);
+ }
+
+ _shuttle.FTLToCoordinates(shuttleUid, shuttle, new EntityCoordinates(mapUid.Value, dropLocation), 0f, 5.5f, 50f);
+ // End Frontier: try to find a potential destination for ship that doesn't collide with other grids.
}
break;
@@ -216,5 +272,72 @@ private void UpdateRunner()
QueueDel(uid);
}
}
+
+ // Mining missions: NOOP since it's handled after ftling
+
+ // Structure missions
+ var structureQuery = EntityQueryEnumerator();
+
+ while (structureQuery.MoveNext(out var uid, out var structure, out var comp))
+ {
+ if (comp.Completed)
+ continue;
+
+ var structureAnnounce = false;
+
+ for (var i = 0; i < structure.Structures.Count; i++)
+ {
+ var objective = structure.Structures[i];
+
+ if (Deleted(objective))
+ {
+ structure.Structures.RemoveSwap(i);
+ structureAnnounce = true;
+ }
+ }
+
+ if (structureAnnounce)
+ {
+ Announce(uid, Loc.GetString("salvage-expedition-structure-remaining", ("count", structure.Structures.Count)));
+ }
+
+ if (structure.Structures.Count == 0)
+ {
+ comp.Completed = true;
+ Announce(uid, Loc.GetString("salvage-expedition-completed"));
+ }
+ }
+
+ // Elimination missions
+ var eliminationQuery = EntityQueryEnumerator();
+ while (eliminationQuery.MoveNext(out var uid, out var elimination, out var comp))
+ {
+ if (comp.Completed)
+ continue;
+
+ var announce = false;
+
+ for (var i = 0; i < elimination.Megafauna.Count; i++)
+ {
+ var mob = elimination.Megafauna[i];
+
+ if (Deleted(mob) || _mobState.IsDead(mob))
+ {
+ elimination.Megafauna.RemoveSwap(i);
+ announce = true;
+ }
+ }
+
+ if (announce)
+ {
+ Announce(uid, Loc.GetString("salvage-expedition-megafauna-remaining", ("count", elimination.Megafauna.Count)));
+ }
+
+ if (elimination.Megafauna.Count == 0)
+ {
+ comp.Completed = true;
+ Announce(uid, Loc.GetString("salvage-expedition-completed"));
+ }
+ }
}
-}
\ No newline at end of file
+}
diff --git a/Content.Server/Salvage/SalvageSystem.cs b/Content.Server/Salvage/SalvageSystem.cs
index 91688b99932..2f691df59e2 100644
--- a/Content.Server/Salvage/SalvageSystem.cs
+++ b/Content.Server/Salvage/SalvageSystem.cs
@@ -1,63 +1,4 @@
-// SPDX-FileCopyrightText: 2021 Vera Aguilera Puerto <6766154+Zumorica@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2022 20kdc
-// SPDX-FileCopyrightText: 2022 Alex Evgrashin
-// SPDX-FileCopyrightText: 2022 Alexander Evgrashin
-// SPDX-FileCopyrightText: 2022 Chris V
-// SPDX-FileCopyrightText: 2022 Errant <35878406+dmnct@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2022 Jackson <8786660+jacksonzck@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2022 Justin Trotter
-// SPDX-FileCopyrightText: 2022 Júlio César Ueti <52474532+Mirino97@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2022 Kevin Zheng
-// SPDX-FileCopyrightText: 2022 Moony
-// SPDX-FileCopyrightText: 2022 Morbo <14136326+Morb0@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2022 Paul Ritter
-// SPDX-FileCopyrightText: 2022 Rane <60792108+Elijahrane@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2022 SpaceManiac
-// SPDX-FileCopyrightText: 2022 Veritius
-// SPDX-FileCopyrightText: 2022 metalgearsloth
-// SPDX-FileCopyrightText: 2022 mirrorcult
-// SPDX-FileCopyrightText: 2022 wrexbe <81056464+wrexbe@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2023 Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2023 Pieter-Jan Briers
-// SPDX-FileCopyrightText: 2023 Slava0135 <40753025+Slava0135@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2023 Visne <39844191+Visne@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2023 Vordenburg <114301317+Vordenburg@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2023 deltanedas
-// SPDX-FileCopyrightText: 2023 dmnct
-// SPDX-FileCopyrightText: 2024 0x6273 <0x40@keemail.me>
-// SPDX-FileCopyrightText: 2024 ElectroJr
-// SPDX-FileCopyrightText: 2024 Pieter-Jan Briers
-// SPDX-FileCopyrightText: 2024 SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2024 Vasilis
-// SPDX-FileCopyrightText: 2024 metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2024 metalgearsloth
-// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 Aidenkrz
-// SPDX-FileCopyrightText: 2025 Aineias1
-// SPDX-FileCopyrightText: 2025 FaDeOkno <143940725+FaDeOkno@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 GoobBot
-// SPDX-FileCopyrightText: 2025 Leon Friedrich <60421075+ElectroJr@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 McBosserson <148172569+McBosserson@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 Milon
-// SPDX-FileCopyrightText: 2025 Piras314
-// SPDX-FileCopyrightText: 2025 Rouden <149893554+Roudenn@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 TheBorzoiMustConsume <197824988+TheBorzoiMustConsume@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 Unlumination <144041835+Unlumy@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 deltanedas <39013340+deltanedas@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
-// SPDX-FileCopyrightText: 2025 gluesniffler <159397573+gluesniffler@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 gluesniffler
-// SPDX-FileCopyrightText: 2025 username <113782077+whateverusername0@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 whateverusername0
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
using Content.Server.Radio.EntitySystems;
-using Content.Shared._DV.Salvage.Systems; // DeltaV
-using Content.Shared.Examine;
-using Content.Shared.Interaction;
-using Content.Shared.Popups;
using Content.Shared.Radio;
using Content.Shared.Salvage;
using Robust.Server.GameObjects;
@@ -75,28 +16,26 @@
using Robust.Shared.Audio.Systems;
using Robust.Shared.Map.Components;
using Robust.Shared.Timing;
-using Content.Shared.Labels.EntitySystems;
using Robust.Shared.EntitySerialization.Systems;
+using Content.Server.Weather;
+using Content.Shared.Weather;
namespace Content.Server.Salvage
{
public sealed partial class SalvageSystem : SharedSalvageSystem
{
[Dependency] private readonly IChatManager _chat = default!;
- [Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
- [Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly AnchorableSystem _anchorable = default!;
[Dependency] private readonly BiomeSystem _biome = default!;
+ [Dependency] private readonly WeatherSystem _weather = default!;
[Dependency] private readonly DungeonSystem _dungeon = default!;
[Dependency] private readonly GravitySystem _gravity = default!;
- [Dependency] private readonly LabelSystem _labelSystem = default!;
[Dependency] private readonly MapLoaderSystem _loader = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
- [Dependency] private readonly MiningPointsSystem _points = default!; // DeltaV
[Dependency] private readonly RadioSystem _radioSystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
@@ -121,6 +60,14 @@ public override void Initialize()
InitializeRunner();
}
+ // Frontier
+ public override void Shutdown()
+ {
+ ShutdownExpeditions();
+ base.Shutdown();
+ }
+ // End Frontier
+
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);
@@ -136,3 +83,4 @@ public override void Update(float frameTime)
}
}
}
+
diff --git a/Content.Server/Salvage/SpawnSalvageMissionJob.cs b/Content.Server/Salvage/SpawnSalvageMissionJob.cs
index 3673aa1b561..5b7bc5913ce 100644
--- a/Content.Server/Salvage/SpawnSalvageMissionJob.cs
+++ b/Content.Server/Salvage/SpawnSalvageMissionJob.cs
@@ -1,25 +1,9 @@
-// SPDX-FileCopyrightText: 2023 Visne <39844191+Visne@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2023 deltanedas <39013340+deltanedas@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2023 deltanedas <@deltanedas:kde.org>
-// SPDX-FileCopyrightText: 2024 0x6273 <0x40@keemail.me>
-// SPDX-FileCopyrightText: 2024 ElectroJr
-// SPDX-FileCopyrightText: 2024 Kara
-// SPDX-FileCopyrightText: 2024 Leon Friedrich <60421075+ElectroJr@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2024 MilenVolf <63782763+MilenVolf@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2024 Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2024 Piras314
-// SPDX-FileCopyrightText: 2024 SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2024 Vasilis
-// SPDX-FileCopyrightText: 2024 metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2024 metalgearsloth
-// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
using System.Linq;
using System.Numerics;
using System.Threading;
using System.Threading.Tasks;
+using Content.Server._NF.Salvage; // Frontier: job complete event
+using Content.Server.Atmos;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Robust.Shared.CPUJob.JobQueues;
@@ -27,6 +11,11 @@
using Content.Server.Parallax;
using Content.Server.Procedural;
using Content.Server.Salvage.Expeditions;
+using Content.Server.Salvage.Expeditions.Structure;
+using Content.Server.Shuttles.Components;
+using Content.Server.Shuttles.Systems;
+using Content.Server.Station.Components;
+using Content.Server.Station.Systems;
using Content.Shared.Atmos;
using Content.Shared.Construction.EntitySystems;
using Content.Shared.Dataset;
@@ -35,19 +24,21 @@
using Content.Shared.Physics;
using Content.Shared.Procedural;
using Content.Shared.Procedural.Loot;
-using Content.Shared.Random;
using Content.Shared.Salvage;
using Content.Shared.Salvage.Expeditions;
using Content.Shared.Salvage.Expeditions.Modifiers;
using Content.Shared.Shuttles.Components;
-using Robust.Shared.Collections;
+using Content.Shared.Storage;
+using Content.Server.Weather;
+using Content.Shared.Weather;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
+using Robust.Shared.Physics.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
-using Robust.Shared.Utility;
-using Content.Server.Shuttles.Components;
+using Robust.Shared.GameObjects;
+using Content.Shared._Crescent.SpaceBiomes;
namespace Content.Server.Salvage;
@@ -55,29 +46,51 @@ public sealed class SpawnSalvageMissionJob : Job
{
private readonly IEntityManager _entManager;
private readonly IGameTiming _timing;
+ private readonly IMapManager _mapManager;
private readonly IPrototypeManager _prototypeManager;
private readonly AnchorableSystem _anchorable;
private readonly BiomeSystem _biome;
+ private readonly WeatherSystem _weather;
private readonly DungeonSystem _dungeon;
private readonly MetaDataSystem _metaData;
+ private readonly ShuttleSystem _shuttle;
+ private readonly StationSystem _stationSystem;
+ private readonly SalvageSystem _salvage;
+ private readonly SharedTransformSystem _xforms;
private readonly SharedMapSystem _map;
public readonly EntityUid Station;
public readonly EntityUid? CoordinatesDisk;
private readonly SalvageMissionParams _missionParams;
- private readonly ISawmill _sawmill;
+ // Frontier: Used for saving state between async job
+#pragma warning disable IDE1006 // suppressing _ prefix complaints to reduce merge conflict area
+ private EntityUid mapUid = EntityUid.Invalid;
+#pragma warning restore IDE1006
+ // End Frontier
+
+ // Mono
+ private const float MassConstant = 50f; // Arbitrary, at this value massMultiplier = 0.65
+ private const float MassMultiplierMin = 0.5f;
+ private const float MassMultiplierMax = 5f;
+ private const float StartupTime = 5.5f;
+ private const float HyperSpaceTime = 50f;
public SpawnSalvageMissionJob(
double maxTime,
IEntityManager entManager,
IGameTiming timing,
- ILogManager logManager,
+ IMapManager mapManager,
IPrototypeManager protoManager,
AnchorableSystem anchorable,
BiomeSystem biome,
+ WeatherSystem weather,
DungeonSystem dungeon,
+ ShuttleSystem shuttle,
+ StationSystem stationSystem,
MetaDataSystem metaData,
+ SalvageSystem salvage,
+ SharedTransformSystem xform,
SharedMapSystem map,
EntityUid station,
EntityUid? coordinatesDisk,
@@ -86,25 +99,56 @@ public SpawnSalvageMissionJob(
{
_entManager = entManager;
_timing = timing;
+ _mapManager = mapManager;
_prototypeManager = protoManager;
_anchorable = anchorable;
_biome = biome;
+ _weather = weather;
_dungeon = dungeon;
+ _shuttle = shuttle;
+ _stationSystem = stationSystem;
_metaData = metaData;
+ _salvage = salvage;
+ _xforms = xform;
_map = map;
Station = station;
CoordinatesDisk = coordinatesDisk;
_missionParams = missionParams;
- _sawmill = logManager.GetSawmill("salvage_job");
-#if !DEBUG
- _sawmill.Level = LogLevel.Info;
-#endif
}
protected override async Task Process()
{
- _sawmill.Debug("salvage", $"Spawning salvage mission with seed {_missionParams.Seed}");
- var mapUid = _map.CreateMap(out var mapId, runMapInit: false);
+ // Frontier: gracefully handle expedition failures
+ bool success = true;
+ string? errorStackTrace = null;
+ try
+ {
+ await InternalProcess().ContinueWith((t) => { success = false; errorStackTrace = t.Exception?.InnerException?.StackTrace; }, TaskContinuationOptions.OnlyOnFaulted);
+ }
+ finally
+ {
+ ExpeditionSpawnCompleteEvent ev = new(Station, success, _missionParams.Index);
+ _entManager.EventBus.RaiseLocalEvent(Station, ev);
+ if (errorStackTrace != null)
+ Logger.ErrorS("salvage", $"Expedition generation failed with exception: {errorStackTrace}!");
+ if (!success)
+ {
+ // Invalidate station, expedition cancellation will be handled by task handler
+ if (_entManager.TryGetComponent(mapUid, out var salvage))
+ salvage.Station = EntityUid.Invalid;
+
+ _entManager.QueueDeleteEntity(mapUid);
+ }
+ }
+ return success;
+ // End Frontier: gracefully handle expedition failures
+ }
+
+ private async Task InternalProcess() // Frontier: make process an internal function (for a try block indenting an entire), add "out EntityUid mapUid" param
+ {
+ Logger.DebugS("salvage", $"Spawning salvage mission with seed {_missionParams.Seed}");
+ var config = _missionParams.MissionType;
+ mapUid = _map.CreateMap(out var mapId, runMapInit: false); // Frontier: remove "var"
MetaDataComponent? metadata = null;
var grid = _entManager.EnsureComponent(mapUid);
var random = new Random(_missionParams.Seed);
@@ -114,7 +158,7 @@ protected override async Task Process()
destComp.Enabled = true;
_metaData.SetEntityName(
mapUid,
- _entManager.System().GetFTLName(_prototypeManager.Index(SalvageSystem.PlanetNames), _missionParams.Seed));
+ _entManager.System().GetFTLName(_prototypeManager.Index("NamesBorer"), _missionParams.Seed));
_entManager.AddComponent(mapUid);
// Saving the mission mapUid to a CD is made optional, in case one is somehow made in a process without a CD entity
@@ -127,17 +171,17 @@ protected override async Task Process()
// Setup mission configs
// As we go through the config the rating will deplete so we'll go for most important to least important.
- var difficultyId = "Moderate";
- var difficultyProto = _prototypeManager.Index(difficultyId);
var mission = _entManager.System()
- .GetMission(difficultyProto, _missionParams.Seed);
+ .GetMission(_missionParams.MissionType, _missionParams.Difficulty, _missionParams.Seed);
+ var missionWeather = _prototypeManager.Index(mission.Weather);
var missionBiome = _prototypeManager.Index(mission.Biome);
+ BiomeComponent? biome = null;
if (missionBiome.BiomePrototype != null)
{
- var biome = _entManager.AddComponent(mapUid);
+ biome = _entManager.AddComponent(mapUid);
var biomeSystem = _entManager.System();
biomeSystem.SetTemplate(mapUid, biome, _prototypeManager.Index(missionBiome.BiomePrototype));
biomeSystem.SetSeed(mapUid, biome, mission.Seed);
@@ -157,6 +201,13 @@ protected override async Task Process()
_entManager.System().SetMapSpace(mapUid, air.Space, atmos);
_entManager.System().SetMapGasMixture(mapUid, new GasMixture(moles, mission.Temperature), atmos);
+ if (!air.Space)
+ {
+ var weather = _entManager.EnsureComponent(mapUid);
+ _entManager.System().SetWeather(mapId, _prototypeManager.Index(missionWeather.WeatherPrototype), null);
+ _entManager.Dirty(mapUid, weather);
+ }
+
if (mission.Color != null)
{
var lighting = _entManager.EnsureComponent(mapUid);
@@ -165,204 +216,328 @@ protected override async Task Process()
}
}
- _map.InitializeMap(mapId);
- _map.SetPaused(mapUid, true);
+ _mapManager.DoMapInitialize(mapId);
+ _mapManager.SetMapPaused(mapId, true);
// Setup expedition
var expedition = _entManager.AddComponent(mapUid);
expedition.Station = Station;
expedition.EndTime = _timing.CurTime + mission.Duration;
expedition.MissionParams = _missionParams;
+ expedition.Difficulty = _missionParams.Difficulty;
+ expedition.Rewards = mission.Rewards;
+
+ // On Frontier, we cant share our locations it breaks ftl in a bad bad way
+ // Don't want consoles to have the incorrect name until refreshed.
+ /*var ftlUid = _entManager.CreateEntityUninitialized("FTLPoint", new EntityCoordinates(mapUid, grid.TileSizeHalfVector));
+ _metaData.SetEntityName(ftlUid, SharedSalvageSystem.GetFTLName(_prototypeManager.Index("NamesBorer"), _missionParams.Seed));
+ _entManager.InitializeAndStartEntity(ftlUid);*/
+
+ // so we just gunna yeet them there instead why not. they chose this life.
+ /*var stationData = _entManager.GetComponent(Station);
+ var shuttleUid = _stationSystem.GetLargestGrid(stationData);
+ if (shuttleUid is { Valid : true } vesselUid)
+ {
+ var shuttle = _entManager.GetComponent(vesselUid);
+ _shuttle.FTLToCoordinates(vesselUid, shuttle, new EntityCoordinates(mapUid, Vector2.Zero), 0f, 5.5f, 50f);
+ }*/
- var landingPadRadius = 24;
+ var landingPadRadius = 4; // Frontier: 24<4 - using this as a margin (4-16), not a radius
var minDungeonOffset = landingPadRadius + 4;
// We'll use the dungeon rotation as the spawn angle
var dungeonRotation = _dungeon.GetDungeonRotation(_missionParams.Seed);
- var maxDungeonOffset = minDungeonOffset + 12;
- var dungeonOffsetDistance = minDungeonOffset + (maxDungeonOffset - minDungeonOffset) * random.NextFloat();
- var dungeonOffset = new Vector2(0f, dungeonOffsetDistance);
- dungeonOffset = dungeonRotation.RotateVec(dungeonOffset);
- var dungeonMod = _prototypeManager.Index(mission.Dungeon);
- var dungeonConfig = _prototypeManager.Index(dungeonMod.Proto);
- var dungeons = await WaitAsyncTask(_dungeon.GenerateDungeonAsync(dungeonConfig, mapUid, grid, (Vector2i)dungeonOffset,
- _missionParams.Seed));
+ Dungeon dungeon = default!; // Frontier: explicitly type as Dungeon
+
+ Vector2 dungeonOffset = new Vector2(); // Frontier: needed for dungeon offset
+ if (config != SalvageMissionType.Mining) // Frontier: why?
+ {
+ var maxDungeonOffset = minDungeonOffset + 12;
+ var dungeonOffsetDistance = minDungeonOffset + (maxDungeonOffset - minDungeonOffset) * random.NextFloat();
+ dungeonOffset = new Vector2(0f, dungeonOffsetDistance);
+ dungeonOffset = dungeonRotation.RotateVec(dungeonOffset);
+ var dungeonMod = _prototypeManager.Index(mission.Dungeon);
+ var dungeonConfig = _prototypeManager.Index(dungeonMod.Proto);
+ var dungeons = await WaitAsyncTask(_dungeon.GenerateDungeonAsync(dungeonConfig, dungeonConfig.ID, mapUid, grid, (Vector2i) dungeonOffset, // Frontier: add dungeonConfig.ID
+ _missionParams.Seed));
+
+ dungeon = dungeons.First();
+
+ // Aborty
+ if (dungeon.Rooms.Count == 0)
+ {
+ return false;
+ }
- var dungeon = dungeons.First();
+ expedition.DungeonLocation = dungeonOffset;
+ }
- // Aborty
- if (dungeon.Rooms.Count == 0)
+ // Frontier: get map bounding box
+ Box2 dungeonBox = new Box2(dungeonOffset, dungeonOffset);
+ foreach (var tile in dungeon.AllTiles)
{
- return false;
+ dungeonBox = dungeonBox.ExtendToContain(tile);
}
- expedition.DungeonLocation = dungeonOffset;
+ var stationData = _entManager.GetComponent(Station);
- List reservedTiles = new();
+ // Frontier: get ship bounding box relative to largest grid coords
+ var shuttleUid = _stationSystem.GetLargestGrid(stationData);
+ Box2 shuttleBox = new Box2();
- foreach (var tile in _map.GetTilesIntersecting(mapUid, grid, new Circle(Vector2.Zero, landingPadRadius), false))
+ if (shuttleUid is { Valid: true } vesselUid &&
+ _entManager.TryGetComponent(vesselUid, out var gridComp))
{
- if (!_biome.TryGetBiomeTile(mapUid, grid, tile.GridIndices, out _))
- continue;
-
- reservedTiles.Add(tile.GridIndices);
+ shuttleBox = gridComp.LocalAABB;
}
- var budgetEntries = new List();
-
- /*
- * GUARANTEED LOOT
- */
+ // Frontier: offset ship spawn point from bounding boxes
+ Vector2 dungeonProjection = new Vector2(dungeonBox.Width * (float) -Math.Sin(dungeonRotation) / 2, dungeonBox.Height * (float) Math.Cos(dungeonRotation) / 2); // Project boxes to get relevant offset for dungeon rotation.
+ Vector2 shuttleProjection = new Vector2(shuttleBox.Width * (float) -Math.Sin(dungeonRotation) / 2, shuttleBox.Height * (float) Math.Cos(dungeonRotation) / 2); // Note: sine is negative because of CCW rotation (starting north, then west)
+ Vector2 coords = dungeonBox.Center - dungeonProjection - dungeonOffset - shuttleProjection - shuttleBox.Center; // Coordinates to spawn the ship at to center it with the dungeon's bounding boxes
+ coords = coords.Rounded(); // Ensure grid is aligned to map coords
- // We'll always add this loot if possible
- // mainly used for ore layers.
- foreach (var lootProto in _prototypeManager.EnumeratePrototypes())
+ // Frontier: delay ship FTL
+ if (shuttleUid is { Valid: true })
{
- if (!lootProto.Guaranteed)
- continue;
-
- try
- {
- await SpawnDungeonLoot(lootProto, mapUid);
- }
- catch (Exception e)
- {
- _sawmill.Error($"Failed to spawn guaranteed loot {lootProto.ID}: {e}");
- }
+ var shuttle = _entManager.GetComponent(shuttleUid.Value);
+ MassAdjustFTLExpedStartup(shuttleUid, out var massStartupTime);
+ _shuttle.FTLToCoordinates(shuttleUid.Value, shuttle, new EntityCoordinates(mapUid, coords), 0f, massStartupTime, HyperSpaceTime);
}
- // Handle boss loot (when relevant).
+ List reservedTiles = new();
- // Handle mob loot.
+ // Frontier: no need for intersecting tiles, we offset the map
- // Handle remaining loot
+ // Vector2 clearBoxCenter = dungeonBox.Center - dungeonProjection - dungeonOffset - shuttleProjection;
+ // float clearBoxHalfWidth = shuttleBox.Width / 2.0f + 4.0f;
+ // float clearBoxHalfHeight = shuttleBox.Height / 2.0f + 4.0f;
+ // Box2 shuttleClearBox = new Box2(clearBoxCenter.X - clearBoxHalfWidth,
+ // clearBoxCenter.Y - clearBoxHalfHeight,
+ // clearBoxCenter.X + clearBoxHalfWidth,
+ // clearBoxCenter.Y + clearBoxHalfHeight);
- /*
- * MOB SPAWNS
- */
+ // foreach (var tile in _map.GetTilesIntersecting(mapUid, grid, new Circle(Vector2.Zero, landingPadRadius), false))
+ // {
+ // if (!_biome.TryGetBiomeTile(mapUid, grid, tile.GridIndices, out _))
+ // continue;
- var mobBudget = difficultyProto.MobBudget;
- var faction = _prototypeManager.Index(mission.Faction);
- var randomSystem = _entManager.System();
+ // reservedTiles.Add(tile.GridIndices);
+ // }
+ // End Frontier
- foreach (var entry in faction.MobGroups)
+ // Mission setup
+ switch (config)
{
- budgetEntries.Add(entry);
+ case SalvageMissionType.Mining:
+ await SetupMining(mission, mapUid);
+ break;
+ case SalvageMissionType.Destruction:
+ await SetupStructure(mission, dungeon, mapUid, grid, random);
+ break;
+ case SalvageMissionType.Elimination:
+ await SetupElimination(mission, dungeon, mapUid, grid, random);
+ break;
+ default:
+ throw new NotImplementedException();
}
- var probSum = budgetEntries.Sum(x => x.Prob);
-
- while (mobBudget > 0f)
+ // Handle loot
+ // We'll always add this loot if possible
+ foreach (var lootProto in _prototypeManager.EnumeratePrototypes())
{
- var entry = randomSystem.GetBudgetEntry(ref mobBudget, ref probSum, budgetEntries, random);
- if (entry == null)
- break;
-
- try
- {
- await SpawnRandomEntry((mapUid, grid), entry, dungeon, random);
- }
- catch (Exception e)
- {
- _sawmill.Error($"Failed to spawn mobs for {entry.Proto}: {e}");
- }
+ if (!lootProto.Guaranteed)
+ continue;
+ await SpawnDungeonLoot(dungeon, missionBiome, lootProto, mapUid, grid, random, reservedTiles);
}
+ return true;
+ }
- var allLoot = _prototypeManager.Index(SharedSalvageSystem.ExpeditionsLootProto);
- var lootBudget = difficultyProto.LootBudget;
+ private void MassAdjustFTLExpedStartup(EntityUid? shuttleUid, out float massStartupTime)
+ {
+ var massMultiplier = 1f;
+ if (_entManager.TryGetComponent(shuttleUid, out PhysicsComponent? shuttlePhysics))
+ {
+ var mass = shuttlePhysics.Mass;
+ massMultiplier = float.Log(float.Sqrt(mass / MassConstant + float.E));
+ massMultiplier = float.Clamp(massMultiplier, MassMultiplierMin, MassMultiplierMax);
+ }
+ massStartupTime = StartupTime * massMultiplier;
+ }
- foreach (var rule in allLoot.LootRules)
+ private async Task SpawnDungeonLoot(Dungeon? dungeon, SalvageBiomeModPrototype biomeMod, SalvageLootPrototype loot, EntityUid gridUid, MapGridComponent grid, Random random, List reservedTiles)
+ {
+ for (var i = 0; i < loot.LootRules.Count; i++)
{
+ var rule = loot.LootRules[i];
+
switch (rule)
{
- case RandomSpawnsLoot randomLoot:
- budgetEntries.Clear();
-
- foreach (var entry in randomLoot.Entries)
+ case BiomeMarkerLoot biomeLoot:
{
- budgetEntries.Add(entry);
+ if (_entManager.TryGetComponent(gridUid, out var biome) &&
+ biomeLoot.Prototype.TryGetValue(biomeMod.ID, out var mod))
+ {
+ _biome.AddMarkerLayer(gridUid, biome, mod);
+ }
}
-
- probSum = budgetEntries.Sum(x => x.Prob);
-
- while (lootBudget > 0f)
+ break;
+ case BiomeTemplateLoot biomeLoot:
{
- var entry = randomSystem.GetBudgetEntry(ref lootBudget, ref probSum, budgetEntries, random);
- if (entry == null)
- break;
-
- _sawmill.Debug($"Spawning dungeon loot {entry.Proto}");
- await SpawnRandomEntry((mapUid, grid), entry, dungeon, random);
+ if (_entManager.TryGetComponent(gridUid, out var biome))
+ {
+ _biome.AddTemplate(gridUid, biome, "Loot", _prototypeManager.Index(biomeLoot.Prototype), i);
+ }
}
break;
- default:
- throw new NotImplementedException();
}
}
+ }
- return true;
+ #region Mission Specific
+
+ private async Task SetupMining(
+ SalvageMission mission,
+ EntityUid gridUid)
+ {
+ var faction = _prototypeManager.Index(mission.Faction);
+
+ if (_entManager.TryGetComponent(gridUid, out var biome))
+ {
+ // TODO: Better
+ for (var i = 0; i < _salvage.GetDifficulty(mission.Difficulty); i++)
+ {
+ _biome.AddMarkerLayer(gridUid, biome, faction.Configs["Mining"]);
+ }
+ }
}
- private async Task SpawnRandomEntry(Entity grid, IBudgetEntry entry, Dungeon dungeon, Random random)
+ private async Task SetupStructure(
+ SalvageMission mission,
+ Dungeon dungeon,
+ EntityUid gridUid,
+ MapGridComponent grid,
+ Random random)
{
- await SuspendIfOutOfTime();
+ var structureComp = _entManager.EnsureComponent(gridUid);
+ var availableRooms = dungeon.Rooms.ToList();
+ var faction = _prototypeManager.Index(mission.Faction);
+ await SpawnMobsRandomRooms(mission, dungeon, faction, grid, random);
- var availableRooms = new ValueList(dungeon.Rooms);
- var availableTiles = new List();
+ var structureCount = _salvage.GetStructureCount(mission.Difficulty);
+ var shaggy = faction.Configs["DefenseStructure"];
+ var validSpawns = new List();
- while (availableRooms.Count > 0)
+ // Spawn the objectives
+ for (var i = 0; i < structureCount; i++)
{
- availableTiles.Clear();
- var roomIndex = random.Next(availableRooms.Count);
- var room = availableRooms.RemoveSwap(roomIndex);
- availableTiles.AddRange(room.Tiles);
+ var structureRoom = availableRooms[random.Next(availableRooms.Count)];
+ validSpawns.Clear();
+ validSpawns.AddRange(structureRoom.Tiles);
+ random.Shuffle(validSpawns);
- while (availableTiles.Count > 0)
+ while (validSpawns.Count > 0)
{
- var tile = availableTiles.RemoveSwap(random.Next(availableTiles.Count));
+ var spawnTile = validSpawns[^1];
+ validSpawns.RemoveAt(validSpawns.Count - 1);
- if (!_anchorable.TileFree(grid, tile, (int)CollisionGroup.MachineLayer,
- (int)CollisionGroup.MachineLayer))
+ if (!_anchorable.TileFree(grid, spawnTile, (int) CollisionGroup.MachineLayer,
+ (int) CollisionGroup.MachineMask)) // Frontier: MachineLayer(uid);
- _entManager.RemoveComponent(uid);
- return;
+ var spawnPosition = _map.GridTileToLocal(mapUid, grid, spawnTile);
+ var uid = _entManager.SpawnEntity(shaggy, spawnPosition);
+ _entManager.AddComponent(uid);
+ structureComp.Structures.Add(uid);
+ break;
}
}
+ }
+
+ private async Task SetupElimination(
+ SalvageMission mission,
+ Dungeon dungeon,
+ EntityUid gridUid,
+ MapGridComponent grid,
+ Random random)
+ {
+ // spawn megafauna in a random place
+ var roomIndex = random.Next(dungeon.Rooms.Count);
+ var room = dungeon.Rooms[roomIndex];
+ var tile = room.Tiles.ElementAt(random.Next(room.Tiles.Count));
+ var position = _map.GridTileToLocal(mapUid, grid, tile);
- // oh noooooooooooo
+ var faction = _prototypeManager.Index(mission.Faction);
+ var prototype = faction.Configs["Megafauna"];
+ var uid = _entManager.SpawnEntity(prototype, position);
+ // not removing ghost role since its 1 megafauna, expect that you won't be able to cheese it.
+ var eliminationComp = _entManager.EnsureComponent(gridUid);
+ eliminationComp.Megafauna.Add(uid);
+
+ // spawn less mobs than usual since there's megafauna to deal with too
+ await SpawnMobsRandomRooms(mission, dungeon, faction, grid, random, 0.5f);
}
- private async Task SpawnDungeonLoot(SalvageLootPrototype loot, EntityUid gridUid)
+ private async Task SpawnMobsRandomRooms(SalvageMission mission, Dungeon dungeon, SalvageFactionPrototype faction, MapGridComponent grid, Random random, float scale = 1f)
{
- for (var i = 0; i < loot.LootRules.Count; i++)
+ // scale affects how many groups are spawned, not the size of the groups themselves
+ var groupSpawns = _salvage.GetSpawnCount(mission.Difficulty) * scale;
+ var groupSum = faction.MobGroups.Sum(o => o.Prob);
+ var validSpawns = new List();
+
+ for (var i = 0; i < groupSpawns; i++)
{
- var rule = loot.LootRules[i];
+ var roll = random.NextFloat() * groupSum;
+ var value = 0f;
- switch (rule)
+ foreach (var group in faction.MobGroups)
{
- case BiomeMarkerLoot biomeLoot:
- {
- if (_entManager.TryGetComponent(gridUid, out var biome))
- {
- _biome.AddMarkerLayer(gridUid, biome, biomeLoot.Prototype);
- }
- }
- break;
- case BiomeTemplateLoot biomeLoot:
+ value += group.Prob;
+
+ if (value < roll)
+ continue;
+
+ var mobGroupIndex = random.Next(faction.MobGroups.Count);
+ var mobGroup = faction.MobGroups[mobGroupIndex];
+
+ var spawnRoomIndex = random.Next(dungeon.Rooms.Count);
+ var spawnRoom = dungeon.Rooms[spawnRoomIndex];
+ validSpawns.Clear();
+ validSpawns.AddRange(spawnRoom.Tiles);
+ random.Shuffle(validSpawns);
+
+ foreach (var entry in EntitySpawnCollection.GetSpawns(mobGroup.Entries, random))
+ {
+ while (validSpawns.Count > 0)
{
- if (_entManager.TryGetComponent(gridUid, out var biome))
+ var spawnTile = validSpawns[^1];
+ validSpawns.RemoveAt(validSpawns.Count - 1);
+
+ if (!_anchorable.TileFree(grid, spawnTile, (int)CollisionGroup.MachineLayer,
+ (int)CollisionGroup.MachineLayer))
{
- _biome.AddTemplate(gridUid, biome, "Loot", _prototypeManager.Index(biomeLoot.Prototype), i);
+ continue;
}
+
+ var spawnPosition = _map.GridTileToLocal(mapUid, grid, spawnTile); // Frontier: grid<_map
+
+ var uid = _entManager.CreateEntityUninitialized(entry, spawnPosition);
+ _entManager.RemoveComponent(uid);
+ _entManager.RemoveComponent(uid);
+ _entManager.InitializeAndStartEntity(uid);
+
+ break;
}
- break;
+ }
+
+ await SuspendIfOutOfTime();
+ break;
}
}
}
-}
\ No newline at end of file
+
+ #endregion
+}
diff --git a/Content.Server/_NF/Salvage/ExpeditionSpawnCompleteEvent.cs b/Content.Server/_NF/Salvage/ExpeditionSpawnCompleteEvent.cs
new file mode 100644
index 00000000000..20582dbbccc
--- /dev/null
+++ b/Content.Server/_NF/Salvage/ExpeditionSpawnCompleteEvent.cs
@@ -0,0 +1,17 @@
+namespace Content.Server._NF.Salvage;
+
+///
+/// This event is raised when an expedition spawn job has completed (either successfully or in failure), and informs whether the job was successful or not.
+///
+public sealed class ExpeditionSpawnCompleteEvent : EntityEventArgs
+{
+ public EntityUid Station;
+ public bool Success;
+ public ushort MissionIndex;
+ public ExpeditionSpawnCompleteEvent(EntityUid station, bool success, ushort missionIndex)
+ {
+ Station = station;
+ Success = success;
+ MissionIndex = missionIndex;
+ }
+}
diff --git a/Content.Server/_NF/Salvage/NFSalvageMobRestrictionsComponent.cs b/Content.Server/_NF/Salvage/NFSalvageMobRestrictionsComponent.cs
new file mode 100644
index 00000000000..3d3985fdc8c
--- /dev/null
+++ b/Content.Server/_NF/Salvage/NFSalvageMobRestrictionsComponent.cs
@@ -0,0 +1,77 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Server._NF.Salvage;
+
+///
+/// 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.
+///
+[RegisterComponent]
+public sealed partial class NFSalvageMobRestrictionsComponent : Component
+{
+ [DataField, ViewVariables(VVAccess.ReadOnly)]
+ public EntityUid LinkedGridEntity = EntityUid.Invalid;
+
+ ///
+ /// If set to false, this mob will not be despawned when its linked entity is despawned.
+ /// Useful for event ghost roles, for instance.
+ ///
+ [DataField]
+ public bool DespawnIfOffLinkedGrid = false;
+
+ // On walking off grid
+ [DataField]
+ public string LeaveGridPopup = "dungeon-boss-grid-warning";
+
+ ///
+ /// Components to be added when the mob leave the grid.
+ ///
+ [DataField]
+ public ComponentRegistry AddComponentsLeaveGrid { get; set; } = new();
+
+ ///
+ /// Components to be removed when the mob leave the grid.
+ ///
+ [DataField]
+ public ComponentRegistry RemoveComponentsLeaveGrid { get; set; } = new();
+
+ ///
+ /// Components to be added when the mob return to the grid.
+ ///
+ [DataField]
+ public ComponentRegistry AddComponentsReturnGrid { get; set; } = new();
+
+ ///
+ /// Components to be removed when the mob return to the grid.
+ ///
+ [DataField]
+ public ComponentRegistry RemoveComponentsReturnGrid { get; set; } = new();
+
+ // On death
+ ///
+ /// Components to be added on death.
+ ///
+ [DataField]
+ public ComponentRegistry AddComponentsOnDeath { get; set; } = new();
+
+ ///
+ /// Components to be removed on death.
+ ///
+ [DataField]
+ public ComponentRegistry RemoveComponentsOnDeath { get; set; } = new();
+
+ ///
+ /// Components to be added on revivel.
+ ///
+ [DataField]
+ public ComponentRegistry AddComponentsOnRevival { get; set; } = new();
+
+ ///
+ /// Components to be removed on revival.
+ ///
+ [DataField]
+ public ComponentRegistry RemoveComponentsOnRevival { get; set; } = new();
+}
diff --git a/Content.Server/_NF/Salvage/SalvageMobRestrictionsGridComponent.cs b/Content.Server/_NF/Salvage/SalvageMobRestrictionsGridComponent.cs
new file mode 100644
index 00000000000..251f868eb5a
--- /dev/null
+++ b/Content.Server/_NF/Salvage/SalvageMobRestrictionsGridComponent.cs
@@ -0,0 +1,16 @@
+namespace Content.Server._NF.Salvage;
+
+///
+/// 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.*
+///
+[RegisterComponent]
+public sealed partial class SalvageMobRestrictionsGridComponent : Component
+{
+ [ViewVariables(VVAccess.ReadOnly)]
+ [DataField("mobsToKill")]
+ public List MobsToKill = new();
+}
diff --git a/Content.Server/_NF/Salvage/SalvageMobRestrictionsSystem.cs b/Content.Server/_NF/Salvage/SalvageMobRestrictionsSystem.cs
new file mode 100644
index 00000000000..3fd1497cd2d
--- /dev/null
+++ b/Content.Server/_NF/Salvage/SalvageMobRestrictionsSystem.cs
@@ -0,0 +1,152 @@
+using Content.Shared.Body.Components;
+using Content.Server.Body.Systems;
+using Content.Server.Explosion.EntitySystems;
+using Content.Shared.Mobs;
+using Content.Server.Administration.Logs;
+using Content.Server.Chat.Managers;
+using Content.Server.Popups;
+using Content.Shared.Database;
+using Content.Shared.Popups;
+using Robust.Shared.Player;
+
+namespace Content.Server._NF.Salvage;
+
+public sealed class SalvageMobRestrictionsSystem : EntitySystem
+{
+ [Dependency] private readonly BodySystem _body = default!;
+ [Dependency] private readonly ExplosionSystem _explosion = default!;
+ [Dependency] private readonly IAdminLogManager _adminLogger = default!;
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnInit);
+ SubscribeLocalEvent(OnRemove);
+ SubscribeLocalEvent(OnRemoveGrid);
+ SubscribeLocalEvent(OnMobState);
+ SubscribeLocalEvent(OnParentChanged);
+ }
+
+ private void OnInit(EntityUid uid, NFSalvageMobRestrictionsComponent component, ComponentInit args)
+ {
+ var gridUid = Transform(uid).ParentUid;
+ if (!EntityManager.EntityExists(gridUid))
+ {
+ // Give up, we were spawned improperly
+ return;
+ }
+ // When this code runs, the system hasn't actually gotten ahold of the grid entity yet.
+ // So it therefore isn't in a position to do this.
+ if (!TryComp(gridUid, out SalvageMobRestrictionsGridComponent? rg))
+ {
+ rg = AddComp(gridUid);
+ }
+ rg!.MobsToKill.Add(uid);
+ component.LinkedGridEntity = gridUid;
+ }
+
+ private void OnRemove(EntityUid uid, NFSalvageMobRestrictionsComponent component, ComponentRemove args)
+ {
+ if (TryComp(component.LinkedGridEntity, out SalvageMobRestrictionsGridComponent? rg))
+ {
+ rg.MobsToKill.Remove(uid);
+ }
+ }
+
+ private void OnRemoveGrid(EntityUid uid, SalvageMobRestrictionsGridComponent component, ComponentRemove args)
+ {
+ foreach (EntityUid target in component.MobsToKill)
+ {
+ // Don't destroy yourself, don't destroy things being destroyed.
+ if (uid == target || MetaData(target).EntityLifeStage >= EntityLifeStage.Terminating)
+ continue;
+
+ // Mono - fix
+ if (TryComp(target, out var mobRestrictions) && !mobRestrictions.DespawnIfOffLinkedGrid)
+ continue;
+
+ if (TryComp(target, out BodyComponent? body))
+ {
+ // Creates a pool of blood on death, but remove the organs.
+ var gibs = _body.GibBody(target, body: body, gibOrgans: true);
+ foreach (var gib in gibs)
+ Del(gib);
+ }
+ else
+ {
+ // No body, probably a robot - explode it and delete the body
+ _explosion.QueueExplosion(target, ExplosionSystem.DefaultExplosionPrototypeId, 5, 10, 5);
+ Del(target);
+ }
+ }
+ }
+
+ private void OnMobState(EntityUid uid, NFSalvageMobRestrictionsComponent component, MobStateChangedEvent args)
+ {
+ // If this entity is being destroyed, no need to fiddle with components
+ if (Terminating(uid))
+ return;
+
+ if (args.NewMobState == MobState.Dead)
+ {
+ EntityManager.AddComponents(uid, component.AddComponentsOnDeath);
+ EntityManager.RemoveComponents(uid, component.RemoveComponentsOnDeath);
+ }
+ else if (args.OldMobState == MobState.Dead)
+ {
+ EntityManager.AddComponents(uid, component.AddComponentsOnRevival);
+ EntityManager.RemoveComponents(uid, component.RemoveComponentsOnRevival);
+ }
+ }
+
+ private void OnParentChanged(EntityUid uid, NFSalvageMobRestrictionsComponent component, ref EntParentChangedMessage args)
+ {
+ // If this entity is being destroyed, no need to fiddle with components
+ if (Terminating(uid))
+ return;
+
+ var gridUid = Transform(uid).GridUid;
+ var popupMessage = Loc.GetString(component.LeaveGridPopup);
+
+ if (component.LinkedGridEntity == gridUid && HasComp(gridUid))
+ {
+ EntityManager.AddComponents(uid, component.AddComponentsReturnGrid);
+ EntityManager.RemoveComponents(uid, component.RemoveComponentsReturnGrid);
+
+ if (!EntityManager.TryGetComponent(uid, out ActorComponent? actor))
+ return;
+
+ if (actor.PlayerSession.AttachedEntity == null)
+ return;
+
+ if (component.DespawnIfOffLinkedGrid)
+ _adminLogger.Add(LogType.AdminMessage, LogImpact.Low, $"{ToPrettyString(actor.PlayerSession.AttachedEntity.Value):player} returned to dungeon grid");
+ }
+ else
+ {
+ EntityManager.AddComponents(uid, component.AddComponentsLeaveGrid);
+ EntityManager.RemoveComponents(uid, component.RemoveComponentsLeaveGrid);
+
+ if (!EntityManager.TryGetComponent(uid, out ActorComponent? actor))
+ return;
+
+ if (actor.PlayerSession.AttachedEntity == null)
+ return;
+
+ if (component.DespawnIfOffLinkedGrid)
+ {
+ _adminLogger.Add(LogType.AdminMessage, LogImpact.Low, $"{ToPrettyString(actor.PlayerSession.AttachedEntity.Value):player} left the dungeon grid");
+ _popupSystem.PopupEntity(popupMessage, actor.PlayerSession.AttachedEntity.Value, actor.PlayerSession, PopupType.MediumCaution);
+ }
+ }
+ }
+
+ // Returns true if the given entity is invalid or terminating
+ private bool Terminating(EntityUid uid)
+ {
+ return !TryComp(uid, out MetaDataComponent? meta) || meta.EntityLifeStage >= EntityLifeStage.Terminating;
+ }
+}
+
diff --git a/Content.Shared/Procedural/SalvageDifficultyPrototype.cs b/Content.Shared/Procedural/SalvageDifficultyPrototype.cs
index ac00a427d06..54b151f9266 100644
--- a/Content.Shared/Procedural/SalvageDifficultyPrototype.cs
+++ b/Content.Shared/Procedural/SalvageDifficultyPrototype.cs
@@ -1,10 +1,3 @@
-// SPDX-FileCopyrightText: 2023 DrSmugleaf
-// SPDX-FileCopyrightText: 2023 metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 Tayrtahn
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
using Robust.Shared.Prototypes;
namespace Content.Shared.Procedural;
diff --git a/Content.Shared/Salvage/SharedSalvageSystem.cs b/Content.Shared/Salvage/SharedSalvageSystem.cs
index c8225d775dd..5c1790945c2 100644
--- a/Content.Shared/Salvage/SharedSalvageSystem.cs
+++ b/Content.Shared/Salvage/SharedSalvageSystem.cs
@@ -1,38 +1,84 @@
-// SPDX-FileCopyrightText: 2023 Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2023 Vordenburg <114301317+Vordenburg@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2023 deltanedas <39013340+deltanedas@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2023 deltanedas <@deltanedas:kde.org>
-// SPDX-FileCopyrightText: 2023 metalgearsloth
-// SPDX-FileCopyrightText: 2024 MilenVolf <63782763+MilenVolf@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2024 Piras314
-// SPDX-FileCopyrightText: 2024 chavonadelal <156101927+chavonadelal@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2024 metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
-// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-using System.Linq;
-using Content.Shared.CCVar;
using Content.Shared.Dataset;
-using Content.Shared.Procedural;
-using Content.Shared.Procedural.Loot;
+using Content.Shared.Random;
+using Content.Shared.Random.Helpers;
using Content.Shared.Salvage.Expeditions;
using Content.Shared.Salvage.Expeditions.Modifiers;
-using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
+using Robust.Shared.Serialization;
+using System.Linq; // Frontier
namespace Content.Shared.Salvage;
public abstract partial class SharedSalvageSystem : EntitySystem
{
- [Dependency] protected readonly IConfigurationManager CfgManager = default!;
+ [Dependency] private readonly ILocalizationManager _loc = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
+ #region Descriptions
+
+ public string GetMissionDescription(SalvageMission mission)
+ {
+ // Hardcoded in coooooz it's dynamic based on difficulty and I'm lazy.
+ switch (mission.Mission)
+ {
+ case SalvageMissionType.Mining:
+ // Taxation: , ("tax", $"{GetMiningTax(mission.Difficulty) * 100f:0}")
+ return Loc.GetString("salvage-expedition-desc-mining");
+ case SalvageMissionType.Destruction:
+ var proto = _proto.Index(mission.Faction).Configs["DefenseStructure"];
+
+ return Loc.GetString("salvage-expedition-desc-structure",
+ ("count", GetStructureCount(mission.Difficulty)),
+ ("structure", _loc.GetEntityData(proto).Name));
+ case SalvageMissionType.Elimination:
+ return Loc.GetString("salvage-expedition-desc-elimination");
+ default:
+ throw new NotImplementedException();
+ }
+ }
+
+ public float GetMiningTax(DifficultyRating baseRating)
+ {
+ return 0.6f + (int) baseRating * 0.05f;
+ }
+
///
- /// Main loot table for salvage expeditions.
+ /// Gets the amount of structures to destroy.
///
- public static readonly ProtoId ExpeditionsLootProto = "SalvageLoot";
+ public int GetStructureCount(DifficultyRating baseRating)
+ {
+ return 1 + (int) baseRating * 2;
+ }
+
+ #endregion
+
+ public int GetDifficulty(DifficultyRating rating)
+ {
+ switch (rating)
+ {
+ case DifficultyRating.Minimal:
+ return 8;
+ case DifficultyRating.Minor:
+ return 12;
+ case DifficultyRating.Moderate:
+ return 16;
+ case DifficultyRating.Hazardous:
+ return 20;
+ case DifficultyRating.Extreme:
+ return 30;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(rating), rating, null);
+ }
+ }
+
+ ///