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); + } + } + + /// + /// How many groups of mobs to spawn for a mission. + /// + public float GetSpawnCount(DifficultyRating difficulty) + { + return ((int)difficulty + 1) * 2; // Frontier: add one to difficulty (no empty expeditions) + } public string GetFTLName(LocalizedDatasetPrototype dataset, int seed) { @@ -40,24 +86,23 @@ public string GetFTLName(LocalizedDatasetPrototype dataset, int seed) return $"{Loc.GetString(dataset.Values[random.Next(dataset.Values.Count)])}-{random.Next(10, 100)}-{(char) (65 + random.Next(26))}"; } - public SalvageMission GetMission(SalvageDifficultyPrototype difficulty, int seed) + public SalvageMission GetMission(SalvageMissionType config, DifficultyRating difficulty, int seed) { // This is on shared to ensure the client display for missions and what the server generates are consistent - var modifierBudget = difficulty.ModifierBudget; + var rating = (float) GetDifficulty(difficulty); + // Don't want easy missions to have any negative modifiers but also want + // easy to be a 1 for difficulty. + rating -= 1f; var rand = new System.Random(seed); // Run budget in order of priority // - Biome // - Lighting // - Atmos - var biome = GetMod(rand, ref modifierBudget); - var light = GetBiomeMod(biome.ID, rand, ref modifierBudget); - var temp = GetBiomeMod(biome.ID, rand, ref modifierBudget); - var air = GetBiomeMod(biome.ID, rand, ref modifierBudget); - var dungeon = GetBiomeMod(biome.ID, rand, ref modifierBudget); - var factionProtos = _proto.EnumeratePrototypes().ToList(); - factionProtos.Sort((x, y) => string.Compare(x.ID, y.ID, StringComparison.Ordinal)); - var faction = factionProtos[rand.Next(factionProtos.Count)]; + var faction = GetMod(rand, ref rating); + var biome = GetMod(rand, ref rating); + var air = GetBiomeMod(biome.ID, rand, ref rating); + var dungeon = GetBiomeMod(biome.ID, rand, ref rating); var mods = new List(); @@ -67,19 +112,38 @@ public SalvageMission GetMission(SalvageDifficultyPrototype difficulty, int seed } // only show the description if there is an atmosphere since wont matter otherwise + var temp = GetBiomeMod(biome.ID, rand, ref rating); if (temp.Description != string.Empty && !air.Space) { mods.Add(Loc.GetString(temp.Description)); } + // only show the description if there is an atmosphere since wont matter otherwise + var weather = GetBiomeMod(biome.ID, rand, ref rating); + if (weather.Description != string.Empty && !air.Space) + { + mods.Add(Loc.GetString(weather.Description)); + } + + var light = GetBiomeMod(biome.ID, rand, ref rating); if (light.Description != string.Empty) { mods.Add(Loc.GetString(light.Description)); } - var duration = TimeSpan.FromSeconds(CfgManager.GetCVar(CCVars.SalvageExpeditionDuration)); + var time = GetMod(rand, ref rating); + // Round the duration to nearest 15 seconds. + var exactDuration = MathHelper.Lerp(time.MinDuration, time.MaxDuration, rand.NextFloat()); + exactDuration = MathF.Round(exactDuration / 15f) * 15f; + var duration = TimeSpan.FromSeconds(exactDuration); + + if (!time.Hidden && time.Description != string.Empty) + { + mods.Add(Loc.GetString(time.Description)); + } - return new SalvageMission(seed, dungeon.ID, faction.ID, biome.ID, air.ID, temp.Temperature, light.Color, duration, mods); + var rewards = GetRewards(difficulty, rand); + return new SalvageMission(seed, difficulty, dungeon.ID, faction.ID, config, biome.ID, weather.ID, air.ID, temp.Temperature, light.Color, duration, rewards, mods); } public T GetBiomeMod(string biome, System.Random rand, ref float rating) where T : class, IPrototype, IBiomeSpecificMod @@ -119,4 +183,76 @@ public T GetMod(System.Random rand, ref float rating) where T : class, IProto throw new InvalidOperationException(); } + + private List GetRewards(DifficultyRating difficulty, System.Random rand) + { + var rewards = new List(3); + var ids = RewardsForDifficulty(difficulty); + + foreach (var id in ids) + { + // pick a random reward to give + var weights = _proto.Index(id); + rewards.Add(weights.Pick(rand)); + } + + return rewards; + } + + /// + /// Get a list of WeightedRandomEntityPrototype IDs with the rewards for a certain difficulty. + /// Frontier: added uncommon and legendary reward tiers, limited amount of rewards to 1 per difficulty rating + /// + private string[] RewardsForDifficulty(DifficultyRating rating) + { + var t1 = "ExpeditionRewardT1"; // Frontier - Update tiers + var t2 = "ExpeditionRewardT2"; // Frontier - Update tiers + var t3 = "ExpeditionRewardT3"; // Frontier - Update tiers + var t4 = "ExpeditionRewardT4"; // Frontier - Update tiers + var t5 = "ExpeditionRewardT5"; // Frontier - Update tiers + switch (rating) + { + case DifficultyRating.Minimal: + return new string[] { t1 }; // Frontier - Update tiers // Frontier + case DifficultyRating.Minor: + return new string[] { t2 }; // Frontier - Update tiers // Frontier + case DifficultyRating.Moderate: + return new string[] { t3 }; // Frontier - Update tiers + case DifficultyRating.Hazardous: + return new string[] { t4 }; // Frontier - Update tiers + case DifficultyRating.Extreme: + return new string[] { t5 }; // Frontier - Update tiers + default: + throw new NotImplementedException(); + } + } +} + +[Serializable, NetSerializable] +public enum SalvageMissionType : byte +{ + /// + /// No dungeon, just ore loot and random mob spawns. + /// + Mining, + + /// + /// Destroy the specified structures in a dungeon. + /// + Destruction, + + /// + /// Kill a large creature in a dungeon. + /// + Elimination, +} + +[Serializable, NetSerializable] +public enum DifficultyRating : byte +{ + Minimal, + Minor, + Moderate, + Hazardous, + Extreme, } diff --git a/Content.Shared/_Crescent/SpaceBiomes/SpaceBiomePrototype.cs b/Content.Shared/_Crescent/SpaceBiomes/SpaceBiomePrototype.cs new file mode 100644 index 00000000000..7efb39431ff --- /dev/null +++ b/Content.Shared/_Crescent/SpaceBiomes/SpaceBiomePrototype.cs @@ -0,0 +1,16 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared._Crescent.SpaceBiomes; + +[Prototype("ambientSpaceBiome")] +public sealed class SpaceBiomePrototype : IPrototype +{ + [IdDataField] + public string ID { get; private set; } = default!; + + [DataField(required: true)] + public string Name = ""; + + [DataField(required: false)] + public string Description = ""; +} diff --git a/Content.Shared/_Crescent/SpaceBiomes/SpaceBiomeSourceComponent.cs b/Content.Shared/_Crescent/SpaceBiomes/SpaceBiomeSourceComponent.cs new file mode 100644 index 00000000000..ed156036fc6 --- /dev/null +++ b/Content.Shared/_Crescent/SpaceBiomes/SpaceBiomeSourceComponent.cs @@ -0,0 +1,29 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Shared._Crescent.SpaceBiomes; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class SpaceBiomeSourceComponent : Component +{ + [AutoNetworkedField] + [DataField(required: true)] + public ProtoId Id; + + /// + /// Distance at which swap should begin + /// null = infinite distance + /// + [AutoNetworkedField] + [DataField(required: true)] + public float? SwapDistance; + + + /// + /// If multiple biomes are overlapping, biome with the highest priority is applied + /// + [AutoNetworkedField] + [DataField] + public float Priority; +} diff --git a/Content.Shared/_NF/CCVar/NFCCVars.cs b/Content.Shared/_NF/CCVar/NFCCVars.cs new file mode 100644 index 00000000000..48f7879268a --- /dev/null +++ b/Content.Shared/_NF/CCVar/NFCCVars.cs @@ -0,0 +1,239 @@ +using Robust.Shared.Configuration; + +namespace Content.Shared._NF.CCVar; + +[CVarDefs] +public sealed class NFCCVars +{ + /* + * Respawn + */ + /// + /// Whether or not respawning is enabled. + /// + public static readonly CVarDef RespawnEnabled = + CVarDef.Create("nf14.respawn.enabled", true, CVar.SERVER | CVar.REPLICATED); + + /// + /// Respawn time, how long the player has to wait in seconds after going into cryosleep. Should be small, misclicks happen. + /// + public static readonly CVarDef RespawnCryoFirstTime = + CVarDef.Create("nf14.respawn.cryo_first_time", 20.0f, CVar.SERVER | CVar.REPLICATED); + + /// + /// Respawn time, how long the player has to wait in seconds after death, or on subsequent cryo attempts. + /// + public static readonly CVarDef RespawnTime = + CVarDef.Create("nf14.respawn.time", 1200.0f, CVar.SERVER | CVar.REPLICATED); + + /// + /// Whether or not returning from cryosleep is enabled. + /// + public static readonly CVarDef CryoReturnEnabled = + CVarDef.Create("nf14.uncryo.enabled", true, CVar.SERVER | CVar.REPLICATED); + + /// + /// The time in seconds after which a cryosleeping body is considered expired and can be deleted from the storage map. + /// + public static readonly CVarDef CryoExpirationTime = + CVarDef.Create("nf14.uncryo.maxtime", 180 * 60f, CVar.SERVER | CVar.REPLICATED); + + /* + * Public Transit + */ + /// + /// Whether public transit is enabled. + /// + public static readonly CVarDef PublicTransit = + CVarDef.Create("nf14.publictransit.enabled", true, CVar.SERVERONLY); + + /// + /// The map to use for the public bus. + /// Mono: Changed to _Mono busdart.yml + /// + public static readonly CVarDef PublicTransitBusMap = + CVarDef.Create("nf14.publictransit.bus_map", "/Maps/_Mono/Shuttles/Bus/busdart.yml", CVar.SERVERONLY); + + /// + /// The amount of time the bus waits at a station. + /// + public static readonly CVarDef PublicTransitWaitTime = + CVarDef.Create("nf14.publictransit.wait_time", 40f, CVar.SERVERONLY); + + /// + /// The amount of time the bus flies through FTL space. + /// This does nothing because the transit system is bugged in our favor (instant travel) + /// + public static readonly CVarDef PublicTransitFlyTime = + CVarDef.Create("nf14.publictransit.fly_time", 15f, CVar.SERVERONLY); + + /* + * World Gen + */ + /// + /// The number of Trade Stations to spawn in every round + /// + public static readonly CVarDef MarketStations = + CVarDef.Create("nf14.worldgen.market_stations", 1, CVar.SERVERONLY); + + /// + /// The number of Cargo Depots to spawn in every round + /// + public static readonly CVarDef CargoDepots = + CVarDef.Create("nf14.worldgen.cargo_depots", 4, CVar.SERVERONLY); + + /// + /// The number of Optional Points Of Interest to spawn in every round + /// + public static readonly CVarDef OptionalStations = + CVarDef.Create("nf14.worldgen.optional_stations", 6, CVar.SERVERONLY); + + /// + /// The multiplier to add to distance spawning calculations for a smidge of server setting variance + /// + public static readonly CVarDef POIDistanceModifier = + CVarDef.Create("nf14.worldgen.distance_modifier", 1f, CVar.SERVERONLY); + + /// + /// The rough minimum distance between POIs in meters. + /// + public static readonly CVarDef MinPOIDistance = + CVarDef.Create("nf14.worldgen.min_poi_distance", 400f, CVar.SERVERONLY); + + /// + /// The maximum number of times to retry POI placement during world generation. + /// + public static readonly CVarDef POIPlacementRetries = + CVarDef.Create("nf14.worldgen.poi_placement_retries", 10, CVar.SERVERONLY); + + /* + * Shipyard + */ + /// + /// Whether the Shipyard is enabled. + /// + public static readonly CVarDef Shipyard = + CVarDef.Create("shuttle.shipyard", true, CVar.SERVERONLY); + + /// + /// Base sell rate (multiplier: 0.85 = 85%) + /// + public static readonly CVarDef ShipyardSellRate = + CVarDef.Create("shuttle.shipyard_base_sell_rate", 0.85f, CVar.SERVERONLY); + + /* + * Salvage + */ + /// + /// The maximum number of shuttles able to go on expedition at once. + /// + public static readonly CVarDef SalvageExpeditionMaxActive = + CVarDef.Create("nf14.salvage.expedition_max_active", 15, CVar.REPLICATED); + + /// + /// Cooldown for failed missions. + /// + public static readonly CVarDef SalvageExpeditionFailedCooldown = + CVarDef.Create("salvage.expedition_failed_cooldown", 450f, CVar.REPLICATED); //Mono 1200->450 + + /// + /// Whether salvage expedition rewards is enabled. + /// + public static readonly CVarDef SalvageExpeditionRewardsEnabled = + CVarDef.Create("nf14.salvage.expedition_rewards_enabled", false, CVar.REPLICATED); + + /* + * Smuggling + */ + /// + /// The maximum number of smuggling drop pods to be out at once. + /// Taking another dead drop note will cause the oldest one to be destroyed. + /// + public static readonly CVarDef SmugglingMaxSimultaneousPods = + CVarDef.Create("nf14.smuggling.max_simultaneous_pods", 5, CVar.REPLICATED); + /// + /// The maximum number of dead drops (places to get smuggling notes) to place at once. + /// + public static readonly CVarDef SmugglingMaxDeadDrops = + CVarDef.Create("nf14.smuggling.max_sector_dead_drops", 10, CVar.REPLICATED); + /// + /// The minimum number of FMCs to spawn for anti-smuggling work. + /// + public static readonly CVarDef SmugglingMinFMCPayout = + CVarDef.Create("nf14.smuggling.min_fmc_payout", 1, CVar.REPLICATED); + /// + /// The shortest time to wait before a dead drop spawns a new smuggling note. + /// + public static readonly CVarDef DeadDropMinTimeout = + CVarDef.Create("nf14.smuggling.min_timeout", 900, CVar.REPLICATED); + /// + /// The longest time to wait before a dead drop spawns a new smuggling note. + /// + public static readonly CVarDef DeadDropMaxTimeout = + CVarDef.Create("nf14.smuggling.max_timeout", 5400, CVar.REPLICATED); + /// + /// The shortest distance that a smuggling pod will spawn away from Colonial Outpost. + /// + public static readonly CVarDef DeadDropMinDistance = + CVarDef.Create("nf14.smuggling.min_distance", 6500, CVar.REPLICATED); + /// + /// The longest distance that a smuggling pod will spawn away from Colonial Outpost. + /// + public static readonly CVarDef DeadDropMaxDistance = + CVarDef.Create("nf14.smuggling.max_distance", 8000, CVar.REPLICATED); + /// + /// The smallest number of dead drop hints (paper clues to dead drop locations) at round start. + /// + public static readonly CVarDef DeadDropMinHints = + CVarDef.Create("nf14.smuggling.min_hints", 0, CVar.REPLICATED); // Used with BasicDeadDropHintVariationPass + /// + /// The largest number of dead drop hints (paper clues to dead drop locations) at round start. + /// + public static readonly CVarDef DeadDropMaxHints = + CVarDef.Create("nf14.smuggling.max_hints", 0, CVar.REPLICATED); // Used with BasicDeadDropHintVariationPass + + /* + * Discord + */ + /// + /// URL of the Discord webhook which will send round status notifications. + /// + public static readonly CVarDef DiscordRoundWebhook = + CVarDef.Create("discord.round_webhook", string.Empty, CVar.SERVERONLY); + + /// + /// Discord ID of role which will be pinged on new round start message. + /// + public static readonly CVarDef DiscordRoundRoleId = + CVarDef.Create("discord.round_roleid", string.Empty, CVar.SERVERONLY); + + /// + /// Send notifications only about a new round begins. + /// + public static readonly CVarDef DiscordRoundStartOnly = + CVarDef.Create("discord.round_start_only", false, CVar.SERVERONLY); + + /// + /// URL of the Discord webhook which will relay all round end messages. + /// + public static readonly CVarDef DiscordLeaderboardWebhook = + CVarDef.Create("discord.leaderboard_webhook", string.Empty, CVar.SERVERONLY); + + /* + * Auth + */ + public static readonly CVarDef ServerAuthList = + CVarDef.Create("frontier.auth_servers", "", CVar.CONFIDENTIAL | CVar.SERVERONLY); + + public static readonly CVarDef AllowMultiConnect = + CVarDef.Create("frontier.allow_multi_connect", true, CVar.CONFIDENTIAL | CVar.SERVERONLY); + + /* + * Events + */ + /// + /// A scale factor applied to a grid's bounds when trying to find a spot to randomly generate a crate for bluespace events. + /// + public static readonly CVarDef CrateGenerationGridBoundsScale = + CVarDef.Create("nf14.events.crate_generation_grid_bounds_scale", 0.6f, CVar.SERVERONLY); +}