From 982f27051898c7d03c713c9b0049419884c1c3f3 Mon Sep 17 00:00:00 2001 From: fenndragon Date: Sun, 26 Apr 2026 14:39:47 -0600 Subject: [PATCH 01/49] first step --- .../SectorWorldExpeditionIntegrationTest.cs | 179 +++++++ .../Gateway/Systems/GatewayGeneratorSystem.cs | 46 +- .../Gateway/Systems/GatewaySystem.cs | 18 +- .../Medical/SuitSensors/SuitSensorSystem.cs | 4 +- .../Expeditions/SalvageExpeditionComponent.cs | 2 +- .../SalvageSystem.ExpeditionConsole.cs | 10 +- .../Salvage/SalvageSystem.Expeditions.cs | 5 +- .../Salvage/SalvageSystem.Runner.cs | 48 +- .../SalvageSystem.SectorExpeditionResolver.cs | 87 ++++ Content.Server/Salvage/SalvageSystem.cs | 2 + .../Salvage/SpawnSalvageMissionJob.cs | 99 ++-- Content.Server/Trash/TrashCleanupSystem.cs | 7 +- .../Components/SectorChunkCarverComponent.cs | 52 +++ .../SectorExpeditionSiteComponent.cs | 22 + .../Components/SectorWorldComponent.cs | 177 +++++++ .../Components/WorldLoaderComponent.cs | 3 +- .../Worldgen/Components/WorldSeedComponent.cs | 14 + .../Worldgen/Systems/NoiseIndexSystem.cs | 48 +- .../Systems/SectorChunkCarverSystem.cs | 181 +++++++ .../Worldgen/Systems/SectorWorldSystem.cs | 441 ++++++++++++++++++ .../Worldgen/Systems/WorldControllerSystem.cs | 9 + .../_Mono/FireControl/FireControlSystem.cs | 7 +- Resources/Prototypes/Entities/World/chunk.yml | 1 + .../Prototypes/World/worldgen_default.yml | 43 +- .../Prototypes/_NF/World/worldgen_default.yml | 46 +- SECTOR_WORLDGEN_REWRITE.md | 198 ++++++++ 26 files changed, 1649 insertions(+), 100 deletions(-) create mode 100644 Content.IntegrationTests/Tests/SectorWorldExpeditionIntegrationTest.cs create mode 100644 Content.Server/Salvage/SalvageSystem.SectorExpeditionResolver.cs create mode 100644 Content.Server/Worldgen/Components/SectorChunkCarverComponent.cs create mode 100644 Content.Server/Worldgen/Components/SectorExpeditionSiteComponent.cs create mode 100644 Content.Server/Worldgen/Components/SectorWorldComponent.cs create mode 100644 Content.Server/Worldgen/Components/WorldSeedComponent.cs create mode 100644 Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs create mode 100644 Content.Server/Worldgen/Systems/SectorWorldSystem.cs create mode 100644 SECTOR_WORLDGEN_REWRITE.md diff --git a/Content.IntegrationTests/Tests/SectorWorldExpeditionIntegrationTest.cs b/Content.IntegrationTests/Tests/SectorWorldExpeditionIntegrationTest.cs new file mode 100644 index 00000000000..8f3a12e2372 --- /dev/null +++ b/Content.IntegrationTests/Tests/SectorWorldExpeditionIntegrationTest.cs @@ -0,0 +1,179 @@ +using System.Collections.Generic; +using System.Numerics; +using System.Linq; +using Content.Server.Atmos.Components; +using Content.Server.Atmos.EntitySystems; +using Content.Server.Salvage; +using Content.Server.Salvage.Expeditions; +using Content.Server.Shuttles.Events; +using Content.Server.Worldgen.Components; +using Content.Server.Worldgen.Systems; +using Content.Shared.Atmos; +using Content.Shared.Gravity; +using Content.Shared.Maps; +using Content.Shared.Mobs.Components; +using Content.Shared.Shuttles.Components; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Maths; + +namespace Content.IntegrationTests.Tests; + +[TestFixture] +public sealed class SectorWorldExpeditionIntegrationTest +{ + private static List CreatePlanetTypes() => + [ + new SectorPlanetTypeDefinition + { + Id = "lava", + Name = "Lava", + BiomeTemplate = "NFVGRoidLava", + SurfaceTiles = ["FloorBasalt"], + MinTemperature = 700f, + MaxTemperature = 700f, + MinOxygen = 0f, + MaxOxygen = 0f, + MinNitrogen = 0f, + MaxNitrogen = 0f, + MinCarbonDioxide = 18f, + MaxCarbonDioxide = 18f, + }, + new SectorPlanetTypeDefinition + { + Id = "tundra", + Name = "Tundra", + BiomeTemplate = "NFVGRoidSnow", + SurfaceTiles = ["FloorSnow"], + MinTemperature = 255f, + MaxTemperature = 255f, + MinOxygen = 18f, + MaxOxygen = 18f, + MinNitrogen = 60f, + MaxNitrogen = 60f, + MinCarbonDioxide = 1f, + MaxCarbonDioxide = 1f, + } + ]; + + [Test] + public async Task PersistentPlanetTypeMapsHaveConfiguredAtmosphereTest() + { + await using var pair = await PoolManager.GetServerClient(); + var server = pair.Server; + EntityUid sectorMap = default; + + await server.WaitPost(() => + { + var entMan = server.ResolveDependency(); + var mapSystem = entMan.System(); + var sectorWorld = entMan.System(); + sectorMap = mapSystem.CreateMap(out _); + var sector = entMan.EnsureComponent(sectorMap); + sector.UniverseSeed = 1337; + sector.PlanetTypes = CreatePlanetTypes(); + sectorWorld.TryGetPlanetAtPosition(sectorMap, Vector2.Zero, out _, sector); + }); + + await pair.RunTicksSync(1); + + await server.WaitPost(() => + { + var entMan = server.ResolveDependency(); + var atmos = entMan.System(); + var sector = entMan.GetComponent(sectorMap); + + Assert.That(sector.SpaceMap, Is.EqualTo(sectorMap)); + Assert.That(sector.FtlMap, Is.Not.Null); + Assert.That(sector.ColCommMap, Is.Not.Null); + Assert.That(sector.PlanetTypeMaps.Count, Is.EqualTo(sector.PlanetTypes.Count)); + + foreach (var planet in sector.Planets) + { + Assert.That(sector.PlanetTypeMaps.TryGetValue(planet.PlanetTypeId, out var layerMap), Is.True, planet.PlanetTypeId); + Assert.That(entMan.TryGetComponent(layerMap, out var mapAtmos), Is.True, planet.PlanetTypeId); + Assert.That(entMan.TryGetComponent(layerMap, out var gravity), Is.True, planet.PlanetTypeId); + var mix = atmos.GetTileMixture(null, (layerMap, mapAtmos), Vector2i.Zero); + + Assert.That(gravity.Enabled, Is.True, planet.PlanetTypeId); + Assert.That(mapAtmos!.Space, Is.False, planet.PlanetTypeId); + Assert.That(mix, Is.Not.Null, planet.PlanetTypeId); + Assert.That(mix!.Temperature, Is.EqualTo(planet.Temperature).Within(0.01f), planet.PlanetTypeId); + Assert.That(mix.GetMoles(Gas.Oxygen), Is.EqualTo(planet.Oxygen).Within(0.01f), planet.PlanetTypeId); + Assert.That(mix.GetMoles(Gas.Nitrogen), Is.EqualTo(planet.Nitrogen).Within(0.01f), planet.PlanetTypeId); + Assert.That(mix.GetMoles(Gas.CarbonDioxide), Is.EqualTo(planet.CarbonDioxide).Within(0.01f), planet.PlanetTypeId); + } + + Assert.That(entMan.TryGetComponent(sector.FtlMap!.Value, out var ftlAtmos), Is.True); + Assert.That(ftlAtmos!.Space, Is.True); + }); + + await pair.CleanReturnAsync(); + } + + [Test] + public async Task SharedPlanetMapExpeditionsDoNotBlockOtherConsoleFtlAttemptTest() + { + await using var pair = await PoolManager.GetServerClient(); + var server = pair.Server; + EntityUid sectorMap = default; + + await server.WaitPost(() => + { + var entMan = server.ResolveDependency(); + var mapSystem = entMan.System(); + var sectorWorld = entMan.System(); + sectorMap = mapSystem.CreateMap(out _); + var sector = entMan.EnsureComponent(sectorMap); + sector.UniverseSeed = 7331; + sector.PlanetTypes = CreatePlanetTypes(); + sectorWorld.TryGetPlanetAtPosition(sectorMap, Vector2.Zero, out _, sector); + }); + + await pair.RunTicksSync(1); + + await server.WaitPost(() => + { + var entMan = server.ResolveDependency(); + var mapMan = server.ResolveDependency(); + var xform = entMan.System(); + var sector = entMan.GetComponent(sectorMap); + + var hostPlanet = sector.Planets.First(); + var hostMap = sector.PlanetTypeMaps[hostPlanet.PlanetTypeId]; + var hostMapId = entMan.GetComponent(hostMap).MapId; + + var expeditionA = mapMan.CreateGridEntity(hostMapId); + var expeditionB = mapMan.CreateGridEntity(hostMapId); + + xform.SetCoordinates(expeditionA.Owner, new EntityCoordinates(hostMap, Vector2.Zero)); + xform.SetCoordinates(expeditionB.Owner, new EntityCoordinates(hostMap, new Vector2(1024f, 0f))); + + entMan.AddComponent(expeditionA.Owner); + entMan.AddComponent(expeditionB.Owner); + + var siteA = entMan.EnsureComponent(expeditionA.Owner); + siteA.SectorMap = hostMap; + siteA.PlanetId = hostPlanet.PlanetId; + siteA.Center = Vector2.Zero; + siteA.Radius = 196f; + + var siteB = entMan.EnsureComponent(expeditionB.Owner); + siteB.SectorMap = hostMap; + siteB.PlanetId = hostPlanet.PlanetId; + siteB.Center = new Vector2(1024f, 0f); + siteB.Radius = 196f; + + var crew = entMan.SpawnEntity("MobHuman", new EntityCoordinates(expeditionB.Owner, Vector2.Zero)); + Assert.That(entMan.HasComponent(crew), Is.True); + + var ev = new ConsoleFTLAttemptEvent(expeditionA.Owner, false, string.Empty); + entMan.EventBus.RaiseLocalEvent(expeditionA.Owner, ref ev); + + Assert.That(ev.Cancelled, Is.False, "Crew on a different expedition in the same host map should not block FTL."); + }); + + await pair.CleanReturnAsync(); + } +} \ No newline at end of file diff --git a/Content.Server/Gateway/Systems/GatewayGeneratorSystem.cs b/Content.Server/Gateway/Systems/GatewayGeneratorSystem.cs index 8d4c182beac..7f4a599f9af 100644 --- a/Content.Server/Gateway/Systems/GatewayGeneratorSystem.cs +++ b/Content.Server/Gateway/Systems/GatewayGeneratorSystem.cs @@ -2,6 +2,8 @@ using Content.Server.Gateway.Components; using Content.Server.Parallax; using Content.Server.Procedural; +using Content.Server.Worldgen.Components; +using Content.Server.Worldgen.Systems; using Content.Shared.CCVar; using Content.Shared.Dataset; using Content.Shared.Maps; @@ -15,6 +17,7 @@ using Robust.Shared.Random; using Robust.Shared.Timing; using Robust.Shared.Utility; +using Robust.Shared.GameObjects; namespace Content.Server.Gateway.Systems; @@ -36,6 +39,8 @@ public sealed class GatewayGeneratorSystem : EntitySystem [Dependency] private readonly SharedMapSystem _maps = default!; [Dependency] private readonly SharedSalvageSystem _salvage = default!; [Dependency] private readonly TileSystem _tile = default!; + [Dependency] private readonly SectorWorldSystem _sectorWorld = default!; + [Dependency] private readonly SharedTransformSystem _xform = default!; private static readonly ProtoId PlanetNamesId = "NamesBorer"; private static readonly ProtoId ContinentalId = "Continental"; @@ -97,21 +102,46 @@ private void GenerateDestination(EntityUid uid, GatewayGeneratorComponent? gener return; var tileDef = _tileDefManager["FloorSteel"]; - const int MaxOffset = 256; var tiles = new List<(Vector2i Index, Tile Tile)>(); var seed = _random.Next(); var random = new Random(seed); - var mapId = _mapManager.CreateMap(); - var mapUid = _mapManager.GetMapEntityId(mapId); + + if (!_sectorWorld.TryGetDefaultSectorMap(out _, out var sector) || sector.Planets.Count == 0) + return; + + var hostPlanet = sector.Planets[random.Next(sector.Planets.Count)]; + if (!_sectorWorld.TryGetPersistentMap(hostPlanet.PlanetTypeId, out var hostMapUid, out _)) + return; + + var mapId = Comp(hostMapUid).MapId; + var siteGrid = _mapManager.CreateGridEntity(mapId); + var mapUid = siteGrid.Owner; + + if (!_sectorWorld.TryReserveExpeditionSite(seed, mapUid, hostPlanet.PlanetTypeId, out var placement)) + { + QueueDel(mapUid); + return; + } var gatewayName = _salvage.GetFTLName(_protoManager.Index(PlanetNamesId), seed); _metadata.SetEntityName(mapUid, gatewayName); + _xform.SetCoordinates(mapUid, new EntityCoordinates(placement.SectorMap, placement.Center)); - var origin = new Vector2i(random.Next(-MaxOffset, MaxOffset), random.Next(-MaxOffset, MaxOffset)); + var site = EnsureComp(mapUid); + site.SectorMap = placement.SectorMap; + site.PlanetId = placement.Planet.PlanetId; + site.Center = placement.Center; + site.Radius = placement.ReservationRadius; - _biome.EnsurePlanet(mapUid, _protoManager.Index(ContinentalId), seed); + var biome = EnsureComp(mapUid); + var biomeTemplate = string.IsNullOrWhiteSpace(hostPlanet.BiomeTemplate) + ? ContinentalId + : new ProtoId(hostPlanet.BiomeTemplate); + _biome.SetTemplate(mapUid, biome, _protoManager.Index(biomeTemplate)); + _biome.SetSeed(mapUid, biome, seed); - var grid = Comp(mapUid); + var origin = Vector2i.Zero; + var grid = siteGrid.Comp; for (var x = -2; x <= 2; x++) { @@ -167,7 +197,7 @@ private void OnGeneratorOpen(Entity ent, r GenerateDestination(ent.Comp.Generator); } - if (!TryComp(args.MapUid, out MapGridComponent? grid)) + if (!TryComp(ent.Owner, out MapGridComponent? grid)) return; ent.Comp.Locked = false; @@ -181,7 +211,7 @@ private void OnGeneratorOpen(Entity ent, r var dungeonRotation = _dungeon.GetDungeonRotation(seed); var dungeonPosition = (origin + dungeonRotation.RotateVec(new Vector2i(0, dungeonDistance))).Floored(); - _dungeon.GenerateDungeon(_protoManager.Index(ExperimentDungeonId), "Experiment", args.MapUid, grid, dungeonPosition, seed); // Frontier: add "Experiment" arg + _dungeon.GenerateDungeon(_protoManager.Index(ExperimentDungeonId), "Experiment", ent.Owner, grid, dungeonPosition, seed); // Frontier: add "Experiment" arg // TODO: Dungeon mobs + loot. diff --git a/Content.Server/Gateway/Systems/GatewaySystem.cs b/Content.Server/Gateway/Systems/GatewaySystem.cs index 6f30c098dbb..0534f9c0f54 100644 --- a/Content.Server/Gateway/Systems/GatewaySystem.cs +++ b/Content.Server/Gateway/Systems/GatewaySystem.cs @@ -94,8 +94,11 @@ private void UpdateUserInterface(EntityUid uid, GatewayComponent comp, Transform // - If our map is a generated destination then use the generator that made it if (TryComp(_stations.GetOwningStation(uid), out GatewayGeneratorComponent? generatorComp) || - (TryComp(xform.MapUid, out GatewayGeneratorDestinationComponent? generatorDestination) && - TryComp(generatorDestination.Generator, out generatorComp))) + (xform.GridUid != null && + TryComp(xform.GridUid.Value, out GatewayGeneratorDestinationComponent? generatorDestination) && + TryComp(generatorDestination.Generator, out generatorComp)) || + (TryComp(xform.MapUid, out GatewayGeneratorDestinationComponent? mapGeneratorDestination) && + TryComp(mapGeneratorDestination.Generator, out generatorComp))) { nextUnlock = generatorComp.NextUnlock; unlockTime = generatorComp.UnlockCooldown; @@ -110,7 +113,11 @@ private void UpdateUserInterface(EntityUid uid, GatewayComponent comp, Transform continue; // Show destination if either no destination comp on the map or it's ours. - TryComp(destXform.MapUid, out var gatewayDestination); + GatewayGeneratorDestinationComponent? gatewayDestination = null; + if (destXform.GridUid != null) + TryComp(destXform.GridUid.Value, out gatewayDestination); + + gatewayDestination ??= CompOrNull(destXform.MapUid); var isDockingArm = TryComp(destUid, out var dockingArmDestination); if (isDockingArm) @@ -269,8 +276,9 @@ private void OpenPortal(EntityUid uid, GatewayComponent comp, EntityUid dest, Ga if (!Resolve(dest, ref destXform) || destXform.MapUid == null) return; + var destinationTarget = destXform.GridUid ?? destXform.MapUid.Value; var ev = new AttemptGatewayOpenEvent(destXform.MapUid.Value, dest); - RaiseLocalEvent(destXform.MapUid.Value, ref ev); + RaiseLocalEvent(destinationTarget, ref ev); if (ev.Cancelled) return; @@ -287,7 +295,7 @@ private void OpenPortal(EntityUid uid, GatewayComponent comp, EntityUid dest, Ga targetPortal.RandomTeleport = false; var openEv = new GatewayOpenEvent(destXform.MapUid.Value, dest); - RaiseLocalEvent(destXform.MapUid.Value, ref openEv); + RaiseLocalEvent(destinationTarget, ref openEv); // for ui comp.NextReady = _timing.CurTime + comp.Cooldown; diff --git a/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs b/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs index 7a6d04ec06f..a6c21940092 100644 --- a/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs +++ b/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs @@ -26,6 +26,7 @@ using Robust.Shared.Timing; using Content.Shared.DeviceNetwork.Components; using Content.Server.Salvage.Expeditions; // Frontier +using Content.Server.Salvage; using Content.Server._NF.Medical.SuitSensors; // Frontier using Content.Shared.Emp; using Content.Shared.FloofStation; @@ -50,6 +51,7 @@ public sealed class SuitSensorSystem : EntitySystem [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly InventorySystem _inventory = default!; + [Dependency] private readonly SalvageSystem _salvage = default!; public override void Initialize() { @@ -475,7 +477,7 @@ public void SetAllSensors(EntityUid target, SuitSensorMode mode, SlotFlags slots _transform.GetInvWorldMatrix(xformQuery.GetComponent(transform.GridUid.Value), xformQuery))); // Frontier: check if sensor is on expedition - if (TryComp(transform.MapUid, out var salvageComp)) + if (_salvage.IsOnExpedition(uid, transform)) locationName = Loc.GetString("suit-sensor-location-expedition"); else if (TryComp(transform.GridUid, out MetaDataComponent? meta)) locationName = meta.EntityName; diff --git a/Content.Server/Salvage/Expeditions/SalvageExpeditionComponent.cs b/Content.Server/Salvage/Expeditions/SalvageExpeditionComponent.cs index 24ab38b74db..a6a3441e677 100644 --- a/Content.Server/Salvage/Expeditions/SalvageExpeditionComponent.cs +++ b/Content.Server/Salvage/Expeditions/SalvageExpeditionComponent.cs @@ -27,7 +27,7 @@ public sealed partial class SalvageExpeditionComponent : SharedSalvageExpedition /// [ViewVariables(VVAccess.ReadWrite), DataField("endTime", customTypeSerializer: typeof(TimeOffsetSerializer))] [AutoPausedField] - public TimeSpan EndTime; + public TimeSpan? EndTime; /// /// Station whose mission this is. diff --git a/Content.Server/Salvage/SalvageSystem.ExpeditionConsole.cs b/Content.Server/Salvage/SalvageSystem.ExpeditionConsole.cs index 2eb32a0d5f3..0620e1320bb 100644 --- a/Content.Server/Salvage/SalvageSystem.ExpeditionConsole.cs +++ b/Content.Server/Salvage/SalvageSystem.ExpeditionConsole.cs @@ -257,11 +257,10 @@ private void OnSalvageFinishMessage(EntityUid entity, SalvageExpeditionConsoleCo /// public bool TryEndExpeditionEarlyFromConsole(EntityUid consoleUid) { - if (!TryComp(consoleUid, out TransformComponent? xform) || xform.MapUid == null) + if (!TryComp(consoleUid, out TransformComponent? xform)) return false; - var expeditionMapUid = xform.MapUid.Value; - if (!TryComp(expeditionMapUid, out SalvageExpeditionComponent? expedition)) + if (!TryGetExpeditionForEntity(consoleUid, out var expeditionMapUid, out SalvageExpeditionComponent? expedition, xform)) return false; // HardLight: Return has already been queued; treat this as handled to avoid duplicate countdown timers. @@ -318,7 +317,7 @@ private void TriggerExpeditionFTLHome(EntityUid expeditionMapUid, SalvageExpedit // Find shuttles on the expedition map and FTL them home while (shuttleQuery.MoveNext(out var shuttleUid, out var shuttle, out var shuttleXform, out _)) { - if (shuttleXform.MapUid != expeditionMapUid || TryComp(shuttleUid, out FTLComponent? _)) + if (!IsEntityOnExpedition(shuttleUid, expeditionMapUid, shuttleXform) || TryComp(shuttleUid, out FTLComponent? _)) continue; var dropLocation = PickExpeditionReturnDropLocation(existingPositions); // HardLight @@ -434,14 +433,17 @@ private void SpawnMissionForConsole(SalvageMissionParams missionParams, EntityUi EntityManager, _timing, _logManager, + _mapManager, _prototypeManager, _anchorable, + _audio, _biome, _dungeon, _metaData, _mapSystem, _station, _shuttle, + _sectorWorld, this, missionStation, consoleUid, // HARDLIGHT: Pass console reference for FTL targeting diff --git a/Content.Server/Salvage/SalvageSystem.Expeditions.cs b/Content.Server/Salvage/SalvageSystem.Expeditions.cs index f9d1476762e..c02c19b7ba7 100644 --- a/Content.Server/Salvage/SalvageSystem.Expeditions.cs +++ b/Content.Server/Salvage/SalvageSystem.Expeditions.cs @@ -416,14 +416,17 @@ private void SpawnMission(SalvageMissionParams missionParams, EntityUid station, EntityManager, _timing, _logManager, + _mapManager, _prototypeManager, _anchorable, + _audio, _biome, _dungeon, _metaData, _mapSystem, _station, // Frontier _shuttle, // Frontier + _sectorWorld, this, // Frontier station, console, @@ -500,7 +503,7 @@ private void OnMapTerminating(EntityUid uid, SalvageExpeditionComponent componen var newCoords = new MapCoordinates(Vector2.Zero, _gameTicker.DefaultMap); while (ghosts.MoveNext(out var ghostUid, out _, out var xform)) { - if (xform.MapUid == uid) + if (IsEntityOnExpedition(ghostUid, uid, xform)) _transform.SetMapCoordinates(ghostUid, newCoords); } } diff --git a/Content.Server/Salvage/SalvageSystem.Runner.cs b/Content.Server/Salvage/SalvageSystem.Runner.cs index 9bf41a29f03..1c535f008f1 100644 --- a/Content.Server/Salvage/SalvageSystem.Runner.cs +++ b/Content.Server/Salvage/SalvageSystem.Runner.cs @@ -46,7 +46,7 @@ private void InitializeRunner() private void OnConsoleFTLAttempt(ref ConsoleFTLAttemptEvent ev) { if (!TryComp(ev.Uid, out TransformComponent? xform) || - !TryComp(xform.MapUid, out var salvage)) + !TryGetExpeditionForEntity(ev.Uid, out var expeditionUid, out _, xform)) { return; } @@ -56,7 +56,7 @@ private void OnConsoleFTLAttempt(ref ConsoleFTLAttemptEvent ev) while (query.MoveNext(out var uid, out _, out var mobState, out var mobXform)) { - if (mobXform.MapUid != xform.MapUid) + if (!IsEntityOnExpedition(uid, expeditionUid, mobXform)) continue; // Don't count unidentified humans (loot) or anyone you murdered so you can still maroon them once dead. @@ -83,8 +83,14 @@ private void Announce(EntityUid mapUid, string text) // gone" and "MapId no longer registered" are normal during cleanup, so log at Debug. if (!TryComp(mapUid, out var map)) { - Log.Debug($"Skipping salvage announcement for {ToPrettyString(mapUid)} because the map component is no longer available."); - return; + var xform = Transform(mapUid); + if (xform.MapUid is not { } parentMap || !TryComp(parentMap, out map)) + { + Log.Debug($"Skipping salvage announcement for {ToPrettyString(mapUid)} because the map component is no longer available."); + return; + } + + mapUid = parentMap; } var mapId = map.MapId; @@ -127,7 +133,7 @@ private void OnFTLRequest(ref FTLRequestEvent ev) private void OnFTLCompleted(ref FTLCompletedEvent args) { - if (!TryComp(args.MapUid, out var component)) + if (!TryGetExpeditionForEntity(args.Entity, out var expeditionUid, out var component)) return; EnsureComp(args.Entity); @@ -149,21 +155,22 @@ private void OnFTLCompleted(ref FTLCompletedEvent args) } else { - Log.Warning($"FTL completed but no valid console reference found for expedition on {args.MapUid}"); + Log.Warning($"FTL completed but no valid console reference found for expedition on {expeditionUid}"); } - Announce(args.MapUid, Loc.GetString("salvage-expedition-announcement-countdown-minutes", ("duration", (component.EndTime - _timing.CurTime).Minutes))); + if (component.EndTime is { } endTime) + Announce(expeditionUid, Loc.GetString("salvage-expedition-announcement-countdown-minutes", ("duration", (endTime - _timing.CurTime).Minutes))); var directionLocalization = ContentLocalizationManager.FormatDirection(component.DungeonLocation.GetDir()).ToLower(); if (component.DungeonLocation != Vector2.Zero) - Announce(args.MapUid, Loc.GetString("salvage-expedition-announcement-dungeon", ("direction", directionLocalization))); + Announce(expeditionUid, Loc.GetString("salvage-expedition-announcement-dungeon", ("direction", directionLocalization))); // Frontier: type-specific announcement switch (component.MissionParams.MissionType) { case SalvageMissionType.Destruction: - if (TryComp(args.MapUid, out var destruction) + if (TryComp(expeditionUid, out var destruction) && destruction.Structures.Count > 0 && TryComp(destruction.Structures[0], out MetaDataComponent? structureMeta) && structureMeta.EntityPrototype != null) @@ -172,11 +179,11 @@ private void OnFTLCompleted(ref FTLCompletedEvent args) if (string.IsNullOrWhiteSpace(name)) name = Loc.GetString("salvage-expedition-announcement-destruction-entity-fallback"); // Assuming all structures are of the same type. - Announce(args.MapUid, Loc.GetString("salvage-expedition-announcement-destruction", ("structure", name), ("count", destruction.Structures.Count))); + Announce(expeditionUid, Loc.GetString("salvage-expedition-announcement-destruction", ("structure", name), ("count", destruction.Structures.Count))); } break; case SalvageMissionType.Elimination: - if (TryComp(args.MapUid, out var elimination) + if (TryComp(expeditionUid, out var elimination) && elimination.Megafauna.Count > 0 && TryComp(elimination.Megafauna[0], out MetaDataComponent? targetMeta) && targetMeta.EntityPrototype != null) @@ -185,7 +192,7 @@ private void OnFTLCompleted(ref FTLCompletedEvent args) if (string.IsNullOrWhiteSpace(name)) name = Loc.GetString("salvage-expedition-announcement-elimination-entity-fallback"); // Assuming all megafauna are of the same type. - Announce(args.MapUid, Loc.GetString("salvage-expedition-announcement-elimination", ("target", name), ("count", elimination.Megafauna.Count))); + Announce(expeditionUid, Loc.GetString("salvage-expedition-announcement-elimination", ("target", name), ("count", elimination.Megafauna.Count))); } break; default: @@ -194,12 +201,12 @@ private void OnFTLCompleted(ref FTLCompletedEvent args) // End Frontier component.Stage = ExpeditionStage.Running; - Dirty(args.MapUid, component); + Dirty(expeditionUid, component); } private void OnFTLStarted(ref FTLStartedEvent ev) { - if (ev.FromMapUid is not { } expeditionMapUid || !TryComp(expeditionMapUid, out var expedition)) + if (!TryGetExpeditionForEntity(ev.Entity, out var expeditionMapUid, out var expedition)) return; // HardLight: only the wall SalvageExpeditionConsole flow keeps station-side @@ -241,7 +248,10 @@ private void UpdateRunner() // Run the basic mission timers (e.g. announcements, auto-FTL, completion, etc) while (query.MoveNext(out var uid, out var comp)) { - var remaining = comp.EndTime - _timing.CurTime; + if (comp.EndTime == null) + continue; + + var remaining = comp.EndTime.Value - _timing.CurTime; var audioLength = _audio.GetAudioLength(comp.SelectedSong); if (comp.Stage < ExpeditionStage.FinalCountdown && remaining < TimeSpan.FromSeconds(45)) @@ -293,7 +303,7 @@ private void UpdateRunner() // This ensures shuttles get sent home even with the new console system while (shuttleQuery.MoveNext(out var shuttleUid, out var shuttle, out var shuttleXform, out _)) { - if (shuttleXform.MapUid != uid || HasComp(shuttleUid)) + if (!IsEntityOnExpedition(shuttleUid, uid, shuttleXform) || HasComp(shuttleUid)) continue; var dropLocation = PickExpeditionReturnDropLocation(existingPositions); // HardLight @@ -551,9 +561,9 @@ private bool HasExpeditionParticipantShuttlesOnMap(EntityUid expeditionMapUid) { var shuttleQuery = EntityQueryEnumerator(); - while (shuttleQuery.MoveNext(out _, out _, out var shuttleXform, out _)) + while (shuttleQuery.MoveNext(out var shuttleUid, out _, out var shuttleXform, out _)) { - if (shuttleXform.MapUid == expeditionMapUid) + if (IsEntityOnExpedition(shuttleUid, expeditionMapUid, shuttleXform)) return true; } @@ -579,7 +589,7 @@ private void FTLAllShuttlesHome(EntityUid expeditionMapUid, float? hyperspaceTim while (shuttleQuery.MoveNext(out var shuttleUid, out var shuttle, out var shuttleXform, out _)) { - if (shuttleXform.MapUid != expeditionMapUid || HasComp(shuttleUid)) + if (!IsEntityOnExpedition(shuttleUid, expeditionMapUid, shuttleXform) || HasComp(shuttleUid)) continue; var dropLocation = PickExpeditionReturnDropLocation(existingPositions); diff --git a/Content.Server/Salvage/SalvageSystem.SectorExpeditionResolver.cs b/Content.Server/Salvage/SalvageSystem.SectorExpeditionResolver.cs new file mode 100644 index 00000000000..9e758b63a85 --- /dev/null +++ b/Content.Server/Salvage/SalvageSystem.SectorExpeditionResolver.cs @@ -0,0 +1,87 @@ +using Content.Server.Salvage.Expeditions; +using Content.Server.Worldgen.Components; + +namespace Content.Server.Salvage; + +public sealed partial class SalvageSystem +{ + private const float ExpeditionResolvePadding = 256f; + + public bool IsOnExpedition(EntityUid entity, TransformComponent? xform = null) + { + return TryGetExpeditionForEntity(entity, out _, out _, xform); + } + + private bool TryGetExpeditionForEntity(EntityUid entity, out EntityUid expeditionUid, out SalvageExpeditionComponent expedition, TransformComponent? xform = null) + { + expeditionUid = EntityUid.Invalid; + expedition = default!; + + if (TryComp(entity, out var expeditionComp) && expeditionComp != null) + { + expeditionUid = entity; + expedition = expeditionComp; + return true; + } + + if (!Resolve(entity, ref xform, false)) + return false; + + if (xform.GridUid is { } gridUid && TryComp(gridUid, out expeditionComp) && expeditionComp != null) + { + expeditionUid = gridUid; + expedition = expeditionComp; + return true; + } + + if (xform.MapUid is not { } mapUid) + return false; + + if (TryComp(mapUid, out expeditionComp) && expeditionComp != null) + { + expeditionUid = mapUid; + expedition = expeditionComp; + return true; + } + + var worldPos = _transform.GetWorldPosition(xform, _xformQuery); + var maxDistanceSquared = float.MaxValue; + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var nearbyExpedition, out var site, out var expeditionXform)) + { + if (expeditionXform.MapUid != mapUid) + continue; + + var resolveRadius = site.Radius + ExpeditionResolvePadding; + var distanceSquared = (site.Center - worldPos).LengthSquared(); + if (distanceSquared > resolveRadius * resolveRadius || distanceSquared >= maxDistanceSquared) + continue; + + maxDistanceSquared = distanceSquared; + expeditionUid = uid; + expedition = nearbyExpedition; + } + + return expeditionUid != EntityUid.Invalid; + } + + private bool IsEntityOnExpedition(EntityUid entity, EntityUid expeditionUid, TransformComponent? xform = null) + { + if (!Resolve(entity, ref xform, false)) + return false; + + if (xform.GridUid == expeditionUid) + return true; + + if (!TryComp(expeditionUid, out SectorExpeditionSiteComponent? site)) + return xform.MapUid == expeditionUid; + + if (xform.MapUid != site.SectorMap) + return false; + + var worldPos = _transform.GetWorldPosition(xform, _xformQuery); + var resolveRadius = site.Radius + ExpeditionResolvePadding; + return (site.Center - worldPos).LengthSquared() <= resolveRadius * resolveRadius; + } +} \ No newline at end of file diff --git a/Content.Server/Salvage/SalvageSystem.cs b/Content.Server/Salvage/SalvageSystem.cs index 4e66938feb1..665e465be22 100644 --- a/Content.Server/Salvage/SalvageSystem.cs +++ b/Content.Server/Salvage/SalvageSystem.cs @@ -20,6 +20,7 @@ using Content.Shared.Labels.EntitySystems; using Content.Server.GameTicking; using Robust.Shared.EntitySerialization.Systems; +using Content.Server.Worldgen.Systems; namespace Content.Server.Salvage { @@ -43,6 +44,7 @@ public sealed partial class SalvageSystem : SharedSalvageSystem [Dependency] private readonly SharedMapSystem _mapSystem = default!; [Dependency] private readonly ShuttleSystem _shuttle = default!; [Dependency] private readonly StationSystem _station = default!; + [Dependency] private readonly SectorWorldSystem _sectorWorld = default!; [Dependency] private readonly UserInterfaceSystem _ui = default!; private EntityQuery _gridQuery; diff --git a/Content.Server/Salvage/SpawnSalvageMissionJob.cs b/Content.Server/Salvage/SpawnSalvageMissionJob.cs index 089bc6e5967..490a3e5c23b 100644 --- a/Content.Server/Salvage/SpawnSalvageMissionJob.cs +++ b/Content.Server/Salvage/SpawnSalvageMissionJob.cs @@ -39,6 +39,9 @@ using Content.Server.Station.Systems; // Frontier using Content.Server.Shuttles.Systems; using Content.Server._NF.Salvage.Expeditions.Structure; // Frontier +using Content.Server.Worldgen.Components; +using Content.Server.Worldgen.Systems; +using Robust.Shared.Audio.Systems; namespace Content.Server.Salvage; @@ -46,14 +49,17 @@ 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 SharedAudioSystem _audio; private readonly BiomeSystem _biome; private readonly DungeonSystem _dungeon; private readonly MetaDataSystem _metaData; private readonly SharedMapSystem _map; private readonly StationSystem _station; // Frontier private readonly ShuttleSystem _shuttle; // Frontier + private readonly SectorWorldSystem _sectorWorld; private readonly SalvageSystem _salvage; // Frontier public readonly EntityUid Station; @@ -76,14 +82,17 @@ public SpawnSalvageMissionJob( IEntityManager entManager, IGameTiming timing, ILogManager logManager, + IMapManager mapManager, IPrototypeManager protoManager, AnchorableSystem anchorable, + SharedAudioSystem audio, BiomeSystem biome, DungeonSystem dungeon, MetaDataSystem metaData, SharedMapSystem map, StationSystem stationSystem, // Frontier ShuttleSystem shuttleSystem, // Frontier + SectorWorldSystem sectorWorld, SalvageSystem salvageSystem, // Frontier EntityUid station, EntityUid? console, // HARDLIGHT: Console that initiated this mission @@ -93,14 +102,17 @@ public SpawnSalvageMissionJob( { _entManager = entManager; _timing = timing; + _mapManager = mapManager; _prototypeManager = protoManager; _anchorable = anchorable; + _audio = audio; _biome = biome; _dungeon = dungeon; _metaData = metaData; _map = map; _station = stationSystem; // Frontier _shuttle = shuttleSystem; // Frontier + _sectorWorld = sectorWorld; _salvage = salvageSystem; // Frontier Station = station; Console = console; // HARDLIGHT: Store console reference @@ -118,19 +130,22 @@ public SpawnSalvageMissionJob( IEntityManager entManager, IGameTiming timing, ILogManager logManager, + IMapManager mapManager, IPrototypeManager protoManager, AnchorableSystem anchorable, + SharedAudioSystem audio, BiomeSystem biome, DungeonSystem dungeon, MetaDataSystem metaData, SharedMapSystem map, StationSystem stationSystem, ShuttleSystem shuttleSystem, + SectorWorldSystem sectorWorld, SalvageSystem salvageSystem, EntityUid station, SalvageMissionParams missionParams, CancellationToken cancellation = default) - : this(maxTime, entManager, timing, logManager, protoManager, anchorable, biome, dungeon, metaData, map, stationSystem, shuttleSystem, salvageSystem, station, null, null, missionParams, cancellation) + : this(maxTime, entManager, timing, logManager, mapManager, protoManager, anchorable, audio, biome, dungeon, metaData, map, stationSystem, shuttleSystem, sectorWorld, salvageSystem, station, null, null, missionParams, cancellation) { // Intentionally empty } @@ -185,28 +200,9 @@ protected override async Task Process() private async Task InternalProcess() // Frontier: make process an internal function (for a try block indenting an entire), add "out EntityUid mapUid" param { _sawmill.Debug($"Spawning salvage mission with seed {_missionParams.Seed}"); - 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); - // HARDLIGHT: Make expedition destination globally FTL-accessible without requiring disks or beacons. - // Previously this required a coordinates disk and was beacon-limited which prevented ad-hoc rescue / support. - var destComp = _entManager.AddComponent(mapUid); - destComp.BeaconsOnly = false; // Allow direct FTL targeting anywhere on the expedition map. - destComp.RequireCoordinateDisk = CoordinatesDisk.HasValue; // Disk missions require a coordinates disk. - destComp.Enabled = true; // Keep enabled for entire expedition so multiple ships can jump in. - _metaData.SetEntityName( - mapUid, - _entManager.System().GetFTLName(_prototypeManager.Index(PlanetNamesId), _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 - if (CoordinatesDisk.HasValue) - { - var cd = _entManager.EnsureComponent(CoordinatesDisk.Value); - cd.Destination = _entManager.GetNetEntity(mapUid); - _entManager.Dirty(CoordinatesDisk.Value, cd); - } // Setup mission configs // As we go through the config the rating will deplete so we'll go for most important to least important. @@ -219,6 +215,46 @@ private async Task InternalProcess() // Frontier: make process an internal .GetMission(_missionParams.MissionType, difficultyProto, _missionParams.Seed); // Frontier: add MissionType var missionBiome = _prototypeManager.Index(mission.Biome); + _sectorWorld.TryResolvePlanetTypeForBiome(missionBiome.BiomePrototype, out var planetTypeId); + + if (!_sectorWorld.TryGetPersistentMap(planetTypeId, out var hostMapUid, out var hostPlanet)) + return false; + + var mapId = _entManager.GetComponent(hostMapUid).MapId; + var spawnedGrid = _mapManager.CreateGridEntity(mapId); + mapUid = spawnedGrid.Owner; + var grid = spawnedGrid.Comp; + + if (!_sectorWorld.TryReserveExpeditionSite(_missionParams.Seed, mapUid, planetTypeId, out var placement)) + { + _entManager.QueueDeleteEntity(mapUid); + return false; + } + + _entManager.System().SetCoordinates(mapUid, new EntityCoordinates(placement.SectorMap, placement.Center)); + var site = _entManager.EnsureComponent(mapUid); + site.SectorMap = placement.SectorMap; + site.PlanetId = placement.Planet.PlanetId; + site.Center = placement.Center; + site.Radius = placement.ReservationRadius; + + // HARDLIGHT: Make expedition destination globally FTL-accessible without requiring disks or beacons. + // Previously this required a coordinates disk and was beacon-limited which prevented ad-hoc rescue / support. + var destComp = _entManager.AddComponent(mapUid); + destComp.BeaconsOnly = false; + destComp.RequireCoordinateDisk = CoordinatesDisk.HasValue; + destComp.Enabled = true; + _metaData.SetEntityName( + mapUid, + $"{placement.Planet.Name} Expedition {_missionParams.Index}"); + _entManager.AddComponent(mapUid); + + if (CoordinatesDisk.HasValue) + { + var cd = _entManager.EnsureComponent(CoordinatesDisk.Value); + cd.Destination = _entManager.GetNetEntity(mapUid); + _entManager.Dirty(CoordinatesDisk.Value, cd); + } if (missionBiome.BiomePrototype != null) { @@ -232,33 +268,14 @@ private async Task InternalProcess() // Frontier: make process an internal var gravity = _entManager.EnsureComponent(mapUid); gravity.Enabled = true; _entManager.Dirty(mapUid, gravity, metadata); - - // Atmos - var air = _prototypeManager.Index(mission.Air); - // copy into a new array since the yml deserialization discards the fixed length - var moles = new float[Atmospherics.AdjustedNumberOfGases]; - air.Gases.CopyTo(moles, 0); - var atmos = _entManager.EnsureComponent(mapUid); - _entManager.System().SetMapSpace(mapUid, air.Space, atmos); - _entManager.System().SetMapGasMixture(mapUid, new GasMixture(moles, mission.Temperature), atmos); - - if (mission.Color != null) - { - var lighting = _entManager.EnsureComponent(mapUid); - lighting.AmbientLightColor = mission.Color.Value; - _entManager.Dirty(mapUid, lighting); - } } - _map.InitializeMap(mapId); - _map.SetPaused(mapUid, true); - // Setup expedition var expedition = _entManager.AddComponent(mapUid); expedition.Station = Station; expedition.Console = Console; // HARDLIGHT: Store console reference for FTL targeting - expedition.EndTime = _timing.CurTime + mission.Duration; expedition.MissionParams = _missionParams; + expedition.SelectedSong = _audio.ResolveSound(expedition.Sound); var landingPadRadius = 4; // Frontier: 24<4 - using this as a margin (4-16), not a radius var minDungeonOffset = landingPadRadius + 4; diff --git a/Content.Server/Trash/TrashCleanupSystem.cs b/Content.Server/Trash/TrashCleanupSystem.cs index 08cbad1d013..82365813f78 100644 --- a/Content.Server/Trash/TrashCleanupSystem.cs +++ b/Content.Server/Trash/TrashCleanupSystem.cs @@ -1,3 +1,4 @@ +using Content.Server._Mono.Cleanup; using Content.Server.Station.Components; using System.Linq; using Content.Server.Worldgen.Components; @@ -155,8 +156,7 @@ private List GetProtectedZones() { if (session.AttachedEntity is not { } playerEntity) continue; - if (!TryComp(playerEntity, out var xform)) - continue; + var xform = Transform(playerEntity); if (HasComp(playerEntity)) continue; @@ -203,6 +203,9 @@ private List GetProtectedZones() /// private bool ShouldProtectGrid(EntityUid gridUid) { + if (HasComp(gridUid)) + return true; + // Protect station grids if (HasComp(gridUid)) return true; diff --git a/Content.Server/Worldgen/Components/SectorChunkCarverComponent.cs b/Content.Server/Worldgen/Components/SectorChunkCarverComponent.cs new file mode 100644 index 00000000000..ab65540a02c --- /dev/null +++ b/Content.Server/Worldgen/Components/SectorChunkCarverComponent.cs @@ -0,0 +1,52 @@ +using System.Numerics; + +namespace Content.Server.Worldgen.Components; + +/// +/// Carves solid asteroid mass into the shared sector grid for a streamed chunk. +/// +[RegisterComponent] +public sealed partial class SectorChunkCarverComponent : Component +{ + [DataField] + public string DensityNoiseChannel = "Density"; + + [DataField] + public string CarveNoiseChannel = "Carver"; + + [DataField] + public string IslandNoiseChannel = "Wreck"; + + [DataField] + public float SparseFieldScale = 104f; + + [DataField] + public float IslandFieldScale = 22f; + + [DataField] + public float DetailFieldScale = 9f; + + [DataField] + public float SparseThreshold = 0.978f; + + [DataField] + public float DensityThreshold = 0.79f; + + [DataField] + public Vector2 CarveRange = new(0.46f, 0.54f); + + [DataField] + public float IslandThreshold = 0.9f; + + [DataField] + public float DensitySharpness = 2.35f; + + [DataField] + public float PlanetFalloff = 0.08f; + + [ViewVariables] + public HashSet GeneratedTiles = new(); + + [ViewVariables] + public HashSet GeneratedEntities = new(); +} \ No newline at end of file diff --git a/Content.Server/Worldgen/Components/SectorExpeditionSiteComponent.cs b/Content.Server/Worldgen/Components/SectorExpeditionSiteComponent.cs new file mode 100644 index 00000000000..2c910d53f84 --- /dev/null +++ b/Content.Server/Worldgen/Components/SectorExpeditionSiteComponent.cs @@ -0,0 +1,22 @@ +using System.Numerics; + +namespace Content.Server.Worldgen.Components; + +/// +/// Marks an expedition grid as occupying a reserved site in the streamed sector. +/// +[RegisterComponent] +public sealed partial class SectorExpeditionSiteComponent : Component +{ + [DataField] + public EntityUid SectorMap; + + [DataField] + public string PlanetId = string.Empty; + + [DataField] + public Vector2 Center; + + [DataField] + public float Radius; +} \ No newline at end of file diff --git a/Content.Server/Worldgen/Components/SectorWorldComponent.cs b/Content.Server/Worldgen/Components/SectorWorldComponent.cs new file mode 100644 index 00000000000..84701c842eb --- /dev/null +++ b/Content.Server/Worldgen/Components/SectorWorldComponent.cs @@ -0,0 +1,177 @@ +using System.Numerics; +using Content.Shared.Parallax.Biomes; + +namespace Content.Server.Worldgen.Components; + +/// +/// Authoritative runtime state for a streamed sector map. +/// +[RegisterComponent] +public sealed partial class SectorWorldComponent : Component +{ + [DataField] + public int UniverseSeed; + + [DataField] + public List PlanetTypes = new(); + + [DataField] + public float MissionReservationRadius = 196f; + + [DataField] + public float MissionReservationPadding = 128f; + + [ViewVariables] + public EntityUid? SectorGrid; + + [ViewVariables] + public EntityUid? SpaceMap; + + [ViewVariables] + public EntityUid? FtlMap; + + [ViewVariables] + public EntityUid? ColCommMap; + + [ViewVariables] + public Dictionary PlanetTypeMaps = new(); + + [ViewVariables] + public List Planets = new(); + + [ViewVariables] + public Dictionary Reservations = new(); + + [ViewVariables] + public List StartupLoaders = new(); +} + +[DataDefinition] +public sealed partial class SectorPlanetTypeDefinition +{ + [DataField(required: true)] + public string Id = string.Empty; + + [DataField(required: true)] + public string Name = string.Empty; + + [DataField(required: true)] + public string BiomeTemplate = string.Empty; + + [DataField(required: true)] + public List SurfaceTiles = new(); + + [DataField] + public float MinRadius = 900f; + + [DataField] + public float MaxRadius = 1400f; + + [DataField] + public float MinTemperature = 240f; + + [DataField] + public float MaxTemperature = 360f; + + [DataField] + public float MinOxygen = 0f; + + [DataField] + public float MaxOxygen = 24f; + + [DataField] + public float MinNitrogen = 0f; + + [DataField] + public float MaxNitrogen = 80f; + + [DataField] + public float MinCarbonDioxide = 0f; + + [DataField] + public float MaxCarbonDioxide = 8f; + + [DataField] + public string? WeatherPrototype; +} + +[DataDefinition] +public sealed partial class SectorPlanetDescriptor +{ + [DataField] + public string PlanetId = string.Empty; + + [DataField] + public string Name = string.Empty; + + [DataField] + public string PlanetTypeId = string.Empty; + + [DataField] + public string BiomeTemplate = string.Empty; + + [DataField] + public string SurfaceTile = "FloorSteel"; + + [DataField] + public Vector2 Center; + + [DataField] + public float Radius; + + [DataField] + public int Seed; + + [DataField] + public float Temperature; + + [DataField] + public float Oxygen; + + [DataField] + public float Nitrogen; + + [DataField] + public float CarbonDioxide; + + [DataField] + public string TimeOfDay = "Dawn"; + + [DataField] + public string? WeatherPrototype; +} + +[DataDefinition] +public sealed partial class SectorExpeditionReservation +{ + [DataField] + public EntityUid ExpeditionUid; + + [DataField] + public string PlanetId = string.Empty; + + [DataField] + public Vector2 Center; + + [DataField] + public float Radius; +} + +[DataDefinition] +public sealed partial class SectorExpeditionPlacement +{ + [DataField] + public EntityUid SectorMap; + + [DataField] + public string PlanetTypeId = string.Empty; + + [DataField] + public Vector2 Center; + + [DataField] + public float ReservationRadius; + + [DataField] + public SectorPlanetDescriptor Planet = new(); +} \ No newline at end of file diff --git a/Content.Server/Worldgen/Components/WorldLoaderComponent.cs b/Content.Server/Worldgen/Components/WorldLoaderComponent.cs index 7848aac469f..95f76866956 100644 --- a/Content.Server/Worldgen/Components/WorldLoaderComponent.cs +++ b/Content.Server/Worldgen/Components/WorldLoaderComponent.cs @@ -6,12 +6,13 @@ namespace Content.Server.Worldgen.Components; /// This is used for allowing some objects to load the game world. /// [RegisterComponent] -[Access(typeof(WorldControllerSystem))] +[Access(typeof(WorldControllerSystem), typeof(SectorWorldSystem))] public sealed partial class WorldLoaderComponent : Component { /// /// The radius in which the loader loads the world. /// + [Access(typeof(WorldControllerSystem), typeof(SectorWorldSystem))] [ViewVariables(VVAccess.ReadWrite)] [DataField("radius")] public int Radius = 64; diff --git a/Content.Server/Worldgen/Components/WorldSeedComponent.cs b/Content.Server/Worldgen/Components/WorldSeedComponent.cs new file mode 100644 index 00000000000..e5743184c76 --- /dev/null +++ b/Content.Server/Worldgen/Components/WorldSeedComponent.cs @@ -0,0 +1,14 @@ +namespace Content.Server.Worldgen.Components; + +/// +/// Stores the root seed for a streamed world map so chunk noise can be reproduced deterministically. +/// +[RegisterComponent] +public sealed partial class WorldSeedComponent : Component +{ + /// + /// Root seed for this world map. A value of 0 means it has not been initialized yet. + /// + [DataField] + public int Seed; +} \ No newline at end of file diff --git a/Content.Server/Worldgen/Systems/NoiseIndexSystem.cs b/Content.Server/Worldgen/Systems/NoiseIndexSystem.cs index 5a7e02c803a..9fdfdc4cf5d 100644 --- a/Content.Server/Worldgen/Systems/NoiseIndexSystem.cs +++ b/Content.Server/Worldgen/Systems/NoiseIndexSystem.cs @@ -26,11 +26,57 @@ public NoiseGenerator Get(EntityUid holder, string protoId) if (idx.Generators.TryGetValue(protoId, out var generator)) return generator; var proto = _prototype.Index(protoId); - var gen = new NoiseGenerator(proto, _random.Next()); + var gen = new NoiseGenerator(proto, GetSeed(holder, protoId)); idx.Generators[protoId] = gen; return gen; } + private int GetSeed(EntityUid holder, string protoId) + { + if (TryComp(holder, out var chunk)) + { + var worldSeed = GetWorldSeed(chunk.Map); + return HashCode.Combine(worldSeed, StableHash(protoId)); + } + + var xform = Transform(holder); + if (xform.MapUid is { } mapUid) + { + var worldSeed = GetWorldSeed(mapUid); + return HashCode.Combine(worldSeed, StableHash(protoId)); + } + + return HashCode.Combine(_random.Next(), holder.GetHashCode(), StableHash(protoId)); + } + + private int GetWorldSeed(EntityUid mapUid) + { + var worldSeed = EnsureComp(mapUid); + + if (worldSeed.Seed == 0) + worldSeed.Seed = _random.Next(1, int.MaxValue); + + return worldSeed.Seed; + } + + private static int StableHash(string value) + { + unchecked + { + const int offsetBasis = unchecked((int) 2166136261); + const int prime = 16777619; + + var hash = offsetBasis; + foreach (var ch in value) + { + hash ^= ch; + hash *= prime; + } + + return hash; + } + } + /// /// Attempts to evaluate the given noise channel using the generator on the given entity. /// diff --git a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs new file mode 100644 index 00000000000..21a0791ca62 --- /dev/null +++ b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs @@ -0,0 +1,181 @@ +using System.Numerics; +using Content.Server.Worldgen.Components; +using Content.Shared.Maps; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Prototypes; + +namespace Content.Server.Worldgen.Systems; + +/// +/// Materializes streamed sector chunk geometry into a single persistent grid on the sector map. +/// +public sealed class SectorChunkCarverSystem : EntitySystem +{ + private static readonly string[] OreSuffixes = + [ + "Coal", + "Tin", + "Quartz", + "Salt", + "Gold", + "Silver", + "Plasma", + "Uranium", + "Bananium", + "ArtifactFragment", + "Bluespace", + ]; + + [Dependency] private readonly SectorWorldSystem _sectorWorld = default!; + [Dependency] private readonly SharedMapSystem _mapSystem = default!; + [Dependency] private readonly ITileDefinitionManager _tileDefs = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnChunkLoaded); + SubscribeLocalEvent(OnChunkUnloaded); + } + + private void OnChunkLoaded(Entity ent, ref WorldChunkLoadedEvent args) + { + if (!TryComp(args.Chunk, out var chunk)) + return; + + if (!TryComp(chunk.Map, out var sector)) + return; + + if (!_sectorWorld.TryGetSectorGrid(chunk.Map, out var gridUid, sector)) + return; + + var grid = EnsureComp(gridUid); + var chunkOrigin = chunk.Coordinates * WorldGen.ChunkSize; + var tiles = new List<(Vector2i, Tile)>(WorldGen.ChunkSize * WorldGen.ChunkSize / 3); + + ent.Comp.GeneratedTiles.Clear(); + ent.Comp.GeneratedEntities.Clear(); + + for (var x = 0; x < WorldGen.ChunkSize; x++) + { + for (var y = 0; y < WorldGen.ChunkSize; y++) + { + var indices = chunkOrigin + new Vector2i(x, y); + var worldPos = indices + new Vector2(0.5f, 0.5f); + + if (!_sectorWorld.IsSolidAt(chunk.Map, ent.Owner, ent.Comp, worldPos, out var planet)) + continue; + + if (!_sectorWorld.TryGetSurfaceTile(planet, out var tileId)) + tileId = "FloorSteel"; + + var tileDef = (ContentTileDefinition) _tileDefs[tileId]; + tiles.Add((indices, new Tile(tileDef.TileId))); + ent.Comp.GeneratedTiles.Add(indices); + } + } + + if (tiles.Count > 0) + _mapSystem.SetTiles(gridUid, grid, tiles); + + foreach (var indices in ent.Comp.GeneratedTiles) + { + if (!TryGetPlanetWallPrototype(gridUid, grid, indices, out var wallPrototype)) + continue; + + var spawned = Spawn(wallPrototype, new EntityCoordinates(gridUid, indices + new Vector2(0.5f, 0.5f))); + ent.Comp.GeneratedEntities.Add(spawned); + } + } + + private void OnChunkUnloaded(Entity ent, ref WorldChunkUnloadedEvent args) + { + if (!TryComp(args.Chunk, out var chunk)) + return; + + if (!TryComp(chunk.Map, out var sector)) + return; + + if (ent.Comp.GeneratedTiles.Count == 0) + return; + + foreach (var generated in ent.Comp.GeneratedEntities) + { + if (Exists(generated)) + QueueDel(generated); + } + + ent.Comp.GeneratedEntities.Clear(); + + if (!_sectorWorld.TryGetSectorGrid(chunk.Map, out var gridUid, sector)) + return; + + var grid = EnsureComp(gridUid); + var tiles = new List<(Vector2i, Tile)>(ent.Comp.GeneratedTiles.Count); + + foreach (var indices in ent.Comp.GeneratedTiles) + { + tiles.Add((indices, Tile.Empty)); + } + + _mapSystem.SetTiles(gridUid, grid, tiles); + ent.Comp.GeneratedTiles.Clear(); + } + + private bool TryGetPlanetWallPrototype(EntityUid gridUid, MapGridComponent grid, Vector2i indices, out string prototype) + { + prototype = string.Empty; + + if (!grid.TryGetTileRef(indices, out var tile)) + return false; + + var tileId = tile.Tile.GetContentTileDefinition(_tileDefs).ID; + var baseWall = GetBaseWallPrototype(tileId); + if (baseWall == null) + return false; + + var hash = HashCode.Combine(tileId, indices.X, indices.Y); + if ((hash & 0xF) < 12) + { + prototype = baseWall; + return _proto.HasIndex(prototype); + } + + var suffix = OreSuffixes[Math.Abs(hash) % OreSuffixes.Length]; + var oreWall = $"{baseWall}{suffix}"; + if (_proto.HasIndex(oreWall)) + { + prototype = oreWall; + return true; + } + + prototype = baseWall; + return _proto.HasIndex(prototype); + } + + private static string? GetBaseWallPrototype(string tileId) + { + if (tileId.Contains("Basalt", StringComparison.OrdinalIgnoreCase)) + return "NFWallBasaltCobblebrick"; + + if (tileId.Contains("Chromite", StringComparison.OrdinalIgnoreCase)) + return "NFWallChromiteCobblebrick"; + + if (tileId.Contains("Andesite", StringComparison.OrdinalIgnoreCase) || tileId.Contains("Drought", StringComparison.OrdinalIgnoreCase)) + return "NFWallAndesiteCobblebrick"; + + if (tileId.Contains("Snow", StringComparison.OrdinalIgnoreCase)) + return "NFWallSnowCobblebrick"; + + if (tileId.Contains("Ice", StringComparison.OrdinalIgnoreCase)) + return "NFWallIce"; + + if (tileId.Contains("Sand", StringComparison.OrdinalIgnoreCase)) + return "NFWallSandCobblebrick"; + + if (tileId.Contains("Asteroid", StringComparison.OrdinalIgnoreCase)) + return "NFWallAsteroidCobblebrick"; + + return "NFWallCobblebrick"; + } +} \ No newline at end of file diff --git a/Content.Server/Worldgen/Systems/SectorWorldSystem.cs b/Content.Server/Worldgen/Systems/SectorWorldSystem.cs new file mode 100644 index 00000000000..73ae58dbd71 --- /dev/null +++ b/Content.Server/Worldgen/Systems/SectorWorldSystem.cs @@ -0,0 +1,441 @@ +using System.Numerics; +using System.Linq; +using Content.Server._Mono.Cleanup; +using Content.Server.Atmos.EntitySystems; +using Content.Server.GameTicking; +using Content.Server.Parallax; +using Content.Server.Weather; +using Content.Server.Worldgen.Components; +using Content.Shared.Atmos; +using Content.Shared.Gravity; +using Content.Shared.Light.Components; +using Content.Shared.Maps; +using Content.Shared.Parallax.Biomes; +using Content.Shared.Shuttles.Components; +using Content.Shared.Weather; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Server.Worldgen.Systems; + +/// +/// Owns streamed sector metadata, deterministic planet descriptors, and expedition site reservations. +/// +public sealed class SectorWorldSystem : EntitySystem +{ + [Dependency] private readonly GameTicker _gameTicker = default!; + [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly NoiseIndexSystem _noiseIndex = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly AtmosphereSystem _atmosphere = default!; + [Dependency] private readonly BiomeSystem _biome = default!; + [Dependency] private readonly MetaDataSystem _metaData = default!; + [Dependency] private readonly SharedMapSystem _mapSystem = default!; + [Dependency] private readonly ITileDefinitionManager _tileDefs = default!; + [Dependency] private readonly WeatherSystem _weather = default!; + [Dependency] private readonly WorldControllerSystem _worldController = default!; + + private static readonly string[] TimeOfDayStates = ["Dawn", "Day", "Dusk", "Night"]; + + public override void Initialize() + { + SubscribeLocalEvent(OnSectorStartup); + SubscribeLocalEvent(OnExpeditionSiteShutdown); + } + + private void OnSectorStartup(Entity ent, ref ComponentStartup args) + { + EnsureInitialized(ent); + } + + private void OnExpeditionSiteShutdown(Entity ent, ref ComponentShutdown args) + { + if (!TryComp(ent.Comp.SectorMap, out SectorWorldComponent? sector)) + return; + + sector.Reservations.Remove(ent.Owner); + } + + public bool TryGetDefaultSectorMap(out EntityUid sectorMap, out SectorWorldComponent sector) + { + sectorMap = EntityUid.Invalid; + sector = default!; + + if (!_mapSystem.TryGetMap(_gameTicker.DefaultMap, out var mapUid) || mapUid is not { } resolved) + return false; + + if (!TryComp(resolved, out var resolvedSector) || resolvedSector == null) + return false; + + sector = resolvedSector; + sectorMap = resolved; + EnsureInitialized((sectorMap, sector)); + return true; + } + + public bool TryGetPersistentMap(string? planetTypeId, out EntityUid mapUid, out SectorPlanetDescriptor? planet, SectorWorldComponent? sector = null) + { + mapUid = EntityUid.Invalid; + planet = null; + + if (!TryGetDefaultSectorMap(out var sectorMap, out sector)) + return false; + + EnsureInitialized((sectorMap, sector)); + + if (string.IsNullOrWhiteSpace(planetTypeId)) + { + mapUid = sector.SpaceMap ?? sectorMap; + return true; + } + + planet = sector.Planets.FirstOrDefault(candidate => candidate.PlanetTypeId == planetTypeId); + if (planet == null) + return false; + + if (!sector.PlanetTypeMaps.TryGetValue(planetTypeId, out mapUid)) + return false; + + return true; + } + + public bool TryResolvePlanetTypeForBiome(string? biomeTemplateId, out string? planetTypeId, SectorWorldComponent? sector = null) + { + planetTypeId = null; + + if (string.IsNullOrWhiteSpace(biomeTemplateId)) + return false; + + if (!TryGetDefaultSectorMap(out var sectorMap, out sector)) + return false; + + EnsureInitialized((sectorMap, sector)); + var match = sector.PlanetTypes.FirstOrDefault(candidate => candidate.BiomeTemplate == biomeTemplateId); + if (match == null) + return false; + + planetTypeId = match.Id; + return true; + } + + public bool TryGetPlanetAtPosition(EntityUid sectorMap, Vector2 worldPos, out SectorPlanetDescriptor planet, SectorWorldComponent? sector = null) + { + planet = default!; + + if (!Resolve(sectorMap, ref sector, false)) + return false; + + EnsureInitialized((sectorMap, sector)); + + foreach (var candidate in sector.Planets) + { + if ((worldPos - candidate.Center).LengthSquared() <= candidate.Radius * candidate.Radius) + { + planet = candidate; + return true; + } + } + + return false; + } + + public bool TryGetSectorGrid(EntityUid sectorMap, out EntityUid gridUid, SectorWorldComponent? sector = null) + { + gridUid = EntityUid.Invalid; + + if (!Resolve(sectorMap, ref sector, false)) + return false; + + EnsureInitialized((sectorMap, sector)); + + if (sector.SectorGrid is not { } resolvedGrid || !Exists(resolvedGrid)) + return false; + + gridUid = resolvedGrid; + return true; + } + + public bool TryGetSurfaceTile(SectorPlanetDescriptor planet, out string tileId) + { + tileId = planet.SurfaceTile; + return _tileDefs.TryGetDefinition(tileId, out _); + } + + public bool IsSolidAt(EntityUid sectorMap, EntityUid noiseHolder, SectorChunkCarverComponent carver, Vector2 worldPos, out SectorPlanetDescriptor planet) + { + if (!TryGetPlanetAtPosition(sectorMap, worldPos, out planet)) + return false; + + var localPos = worldPos - planet.Center; + var sparseSample = localPos / MathF.Max(carver.SparseFieldScale, 1f); + var islandSample = localPos / MathF.Max(carver.IslandFieldScale, 1f); + var detailSample = localPos / MathF.Max(carver.DetailFieldScale, 1f); + + var sparse = _noiseIndex.Evaluate(noiseHolder, carver.IslandNoiseChannel, sparseSample * 0.73f + new Vector2(-11.75f, 6.25f)); + if (sparse < carver.SparseThreshold) + return false; + + var density = _noiseIndex.Evaluate(noiseHolder, carver.DensityNoiseChannel, islandSample * 1.07f + new Vector2(3.25f, -1.75f)); + var carve = _noiseIndex.Evaluate(noiseHolder, carver.CarveNoiseChannel, detailSample * 1.33f + new Vector2(7.5f, -4.25f)); + var islands = _noiseIndex.Evaluate(noiseHolder, carver.IslandNoiseChannel, islandSample * 1.61f + new Vector2(-3.75f, 5.5f)); + + var radialDistance = (worldPos - planet.Center).Length() / MathF.Max(planet.Radius, 1f); + var densityBias = radialDistance * carver.PlanetFalloff; + + var sparseStrength = (sparse - carver.SparseThreshold) / MathF.Max(1f - carver.SparseThreshold, 0.001f); + sparseStrength = Math.Clamp(sparseStrength, 0f, 1f); + sparseStrength = MathF.Pow(sparseStrength, carver.DensitySharpness); + + var ridge = 1f - MathF.Abs(carve - 0.5f) * 2f; + ridge = Math.Clamp(ridge, 0f, 1f); + + var signedDensity = density * 0.58f + islands * 0.22f + ridge * 0.2f + sparseStrength * 0.32f - densityBias; + var baseMass = signedDensity >= carver.DensityThreshold || (sparseStrength >= carver.IslandThreshold && density >= carver.DensityThreshold - 0.08f); + var carvedOut = carve >= carver.CarveRange.X && carve <= carver.CarveRange.Y && sparseStrength < 0.94f; + + return baseMass && !carvedOut; + } + + public bool TryReserveExpeditionSite(int seed, EntityUid expeditionUid, string? planetTypeId, out SectorExpeditionPlacement placement) + { + placement = default!; + + if (!TryGetDefaultSectorMap(out var sectorMap, out var sector)) + return false; + + var rng = new Random(seed); + var planets = sector.Planets + .Where(planet => string.IsNullOrWhiteSpace(planetTypeId) || planet.PlanetTypeId == planetTypeId) + .OrderBy(_ => rng.Next()) + .ToList(); + var reservationRadius = sector.MissionReservationRadius; + + foreach (var planet in planets) + { + if (!TryGetPersistentMap(planet.PlanetTypeId, out var targetMap, out _ , sector)) + continue; + + var placementOrigin = targetMap == (sector.SpaceMap ?? sectorMap) + ? planet.Center + : Vector2.Zero; + + for (var attempt = 0; attempt < 32; attempt++) + { + var angle = rng.NextSingle() * MathF.Tau; + var distance = MathF.Sqrt(rng.NextSingle()) * MathF.Max(planet.Radius - reservationRadius, 64f); + var candidate = placementOrigin + new Vector2(MathF.Cos(angle), MathF.Sin(angle)) * distance; + + if (!IsReservationFree(sector, candidate, reservationRadius)) + continue; + + var reservation = new SectorExpeditionReservation + { + ExpeditionUid = expeditionUid, + PlanetId = planet.PlanetId, + Center = candidate, + Radius = reservationRadius, + }; + + sector.Reservations[expeditionUid] = reservation; + placement = new SectorExpeditionPlacement + { + SectorMap = targetMap, + PlanetTypeId = planet.PlanetTypeId, + Center = candidate, + ReservationRadius = reservationRadius, + Planet = planet, + }; + return true; + } + } + + return false; + } + + private bool IsReservationFree(SectorWorldComponent sector, Vector2 center, float radius) + { + foreach (var reservation in sector.Reservations.Values) + { + var minDistance = radius + reservation.Radius + sector.MissionReservationPadding; + if ((reservation.Center - center).LengthSquared() < minDistance * minDistance) + return false; + } + + return true; + } + + private void EnsureInitialized(Entity ent) + { + ent.Comp.SpaceMap ??= ent.Owner; + + if ((ent.Comp.SectorGrid == null || !Exists(ent.Comp.SectorGrid.Value)) && TryComp(ent.Owner, out var mapComp)) + { + var sectorGrid = _mapManager.CreateGridEntity(mapComp.MapId); + ent.Comp.SectorGrid = sectorGrid.Owner; + EnsureComp(sectorGrid.Owner); + _metaData.SetEntityName(sectorGrid.Owner, $"{MetaData(ent.Owner).EntityName} Sector Grid"); + } + + if (ent.Comp.UniverseSeed == 0) + ent.Comp.UniverseSeed = _random.Next(1, int.MaxValue); + + if (ent.Comp.Planets.Count > 0 || ent.Comp.PlanetTypes.Count == 0) + return; + + var rng = new Random(ent.Comp.UniverseSeed); + var ringStep = 2400f; + + for (var index = 0; index < ent.Comp.PlanetTypes.Count; index++) + { + var type = ent.Comp.PlanetTypes[index]; + var radius = MathHelper.Lerp(type.MinRadius, type.MaxRadius, rng.NextSingle()); + var distance = 1800f + index * ringStep + rng.NextSingle() * 900f; + var angle = (MathF.Tau / ent.Comp.PlanetTypes.Count) * index + (rng.NextSingle() - 0.5f) * 0.45f; + var center = new Vector2(MathF.Cos(angle), MathF.Sin(angle)) * distance; + var tileId = type.SurfaceTiles.Count > 0 + ? type.SurfaceTiles[rng.Next(type.SurfaceTiles.Count)] + : "FloorSteel"; + + if (!_proto.TryIndex(type.BiomeTemplate, out _)) + continue; + + ent.Comp.Planets.Add(new SectorPlanetDescriptor + { + PlanetId = $"{type.Id}-{index + 1}", + Name = $"{type.Name} {index + 1}", + PlanetTypeId = type.Id, + BiomeTemplate = type.BiomeTemplate, + SurfaceTile = tileId, + Center = center, + Radius = radius, + Seed = rng.Next(), + Temperature = MathHelper.Lerp(type.MinTemperature, type.MaxTemperature, rng.NextSingle()), + Oxygen = MathHelper.Lerp(type.MinOxygen, type.MaxOxygen, rng.NextSingle()), + Nitrogen = MathHelper.Lerp(type.MinNitrogen, type.MaxNitrogen, rng.NextSingle()), + CarbonDioxide = MathHelper.Lerp(type.MinCarbonDioxide, type.MaxCarbonDioxide, rng.NextSingle()), + TimeOfDay = TimeOfDayStates[rng.Next(TimeOfDayStates.Length)], + WeatherPrototype = type.WeatherPrototype, + }); + } + + EnsurePersistentLayerMaps(ent); + EnsureStartupPlanetLoaders(ent); + } + + private void EnsureStartupPlanetLoaders(Entity ent) + { + if (!TryGetSectorGrid(ent.Owner, out var sectorGrid, ent.Comp)) + return; + + if (ent.Comp.StartupLoaders.Count == ent.Comp.Planets.Count && ent.Comp.StartupLoaders.All(Exists)) + return; + + ent.Comp.StartupLoaders.RemoveAll(loader => !Exists(loader)); + + foreach (var planet in ent.Comp.Planets) + { + var loader = Spawn(null, new EntityCoordinates(sectorGrid, planet.Center)); + EnsureComp(loader); + _worldController.SetLoaderRadius(loader, (int) MathF.Ceiling(planet.Radius + WorldGen.ChunkSize)); + ent.Comp.StartupLoaders.Add(loader); + } + } + + private void EnsurePersistentLayerMaps(Entity ent) + { + ent.Comp.FtlMap ??= CreateLayerMap($"{MetaData(ent.Owner).EntityName} FTL", space: true, gravity: false); + ent.Comp.ColCommMap ??= CreateLayerMap($"{MetaData(ent.Owner).EntityName} ColComm", space: false, gravity: true, mixture: CreateStandardAirMixture(), timeOfDay: "Day"); + + foreach (var planet in ent.Comp.Planets) + { + if (ent.Comp.PlanetTypeMaps.ContainsKey(planet.PlanetTypeId)) + continue; + + ent.Comp.PlanetTypeMaps[planet.PlanetTypeId] = CreateLayerMap( + $"{planet.Name} Surface", + space: false, + gravity: true, + mixture: CreatePlanetMixture(planet), + timeOfDay: planet.TimeOfDay, + weatherPrototype: planet.WeatherPrototype, + biomeTemplateId: planet.BiomeTemplate, + biomeSeed: planet.Seed); + } + } + + private EntityUid CreateLayerMap( + string name, + bool space, + bool gravity, + GasMixture? mixture = null, + string? timeOfDay = null, + string? weatherPrototype = null, + string? biomeTemplateId = null, + int? biomeSeed = null) + { + var mapUid = _mapSystem.CreateMap(out _); + EnsureComp(mapUid); + _metaData.SetEntityName(mapUid, name); + + if (!space && !string.IsNullOrWhiteSpace(biomeTemplateId) && _proto.TryIndex(biomeTemplateId, out var biomeTemplate)) + { + _biome.EnsurePlanet(mapUid, biomeTemplate, biomeSeed, mapLight: GetAmbientLightForTimeOfDay(timeOfDay)); + } + + if (mixture != null) + _atmosphere.SetMapAtmosphere(mapUid, space, mixture); + else if (space) + _atmosphere.SetMapAtmosphere(mapUid, true, GasMixture.SpaceGas); + + var gravityComp = EnsureComp(mapUid); + gravityComp.Enabled = gravity; + gravityComp.Inherent = gravity; + + var light = EnsureComp(mapUid); + light.AmbientLightColor = GetAmbientLightForTimeOfDay(timeOfDay); + + EnsureComp(mapUid); + EnsureComp(mapUid); + EnsureComp(mapUid); + + if (!string.IsNullOrWhiteSpace(weatherPrototype) && + _proto.TryIndex(weatherPrototype, out var weather) && + TryComp(mapUid, out var mapComp)) + { + _weather.SetWeather(mapComp.MapId, weather, null); + } + + return mapUid; + } + + private static GasMixture CreateStandardAirMixture() + { + var moles = new float[Atmospherics.AdjustedNumberOfGases]; + moles[(int) Gas.Oxygen] = 21.824779f; + moles[(int) Gas.Nitrogen] = 82.10312f; + return new GasMixture(moles, Atmospherics.T20C); + } + + private static GasMixture CreatePlanetMixture(SectorPlanetDescriptor planet) + { + var moles = new float[Atmospherics.AdjustedNumberOfGases]; + moles[(int) Gas.Oxygen] = MathF.Max(planet.Oxygen, 0f); + moles[(int) Gas.Nitrogen] = MathF.Max(planet.Nitrogen, 0f); + moles[(int) Gas.CarbonDioxide] = MathF.Max(planet.CarbonDioxide, 0f); + return new GasMixture(moles, MathF.Max(planet.Temperature, Atmospherics.TCMB)); + } + + private static Color GetAmbientLightForTimeOfDay(string? timeOfDay) + { + return timeOfDay switch + { + "Night" => Color.FromHex("#2B3143"), + "Dusk" => Color.FromHex("#A34931"), + "Day" => Color.FromHex("#E6CB8B"), + _ => Color.FromHex("#D8B059"), + }; + } +} \ No newline at end of file diff --git a/Content.Server/Worldgen/Systems/WorldControllerSystem.cs b/Content.Server/Worldgen/Systems/WorldControllerSystem.cs index 3d4da865861..5a7d6188c16 100644 --- a/Content.Server/Worldgen/Systems/WorldControllerSystem.cs +++ b/Content.Server/Worldgen/Systems/WorldControllerSystem.cs @@ -34,6 +34,15 @@ public override void Initialize() SubscribeLocalEvent(OnChunkShutdown); } + public void SetLoaderRadius(EntityUid uid, int radius, WorldLoaderComponent? loader = null) + { + if (!Resolve(uid, ref loader, false)) + return; + + loader.Radius = radius; + Dirty(uid, loader); + } + /// /// Handles deleting chunks properly. /// diff --git a/Content.Server/_Mono/FireControl/FireControlSystem.cs b/Content.Server/_Mono/FireControl/FireControlSystem.cs index 6434f2baf1f..faadb686cfc 100644 --- a/Content.Server/_Mono/FireControl/FireControlSystem.cs +++ b/Content.Server/_Mono/FireControl/FireControlSystem.cs @@ -19,7 +19,7 @@ using Content.Shared.Interaction; using Content.Shared._Mono.ShipGuns; using Content.Shared.Examine; -using Content.Server.Salvage.Expeditions; +using Content.Server.Salvage; namespace Content.Server._Mono.FireControl; @@ -31,6 +31,7 @@ public sealed partial class FireControlSystem : EntitySystem [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly PowerReceiverSystem _power = default!; [Dependency] private readonly RotateToFaceSystem _rotateToFace = default!; + [Dependency] private readonly SalvageSystem _salvage = default!; /// /// Dictionary of entities that have visualization enabled @@ -417,7 +418,7 @@ public bool CanFireWeapons(EntityUid grid) var gridXform = Transform(grid); // Check if the weapon is an expedition - if (gridXform.MapUid != null && HasComp(gridXform.MapUid.Value)) + if (_salvage.IsOnExpedition(grid, gridXform)) return false; return true; @@ -516,7 +517,7 @@ public bool AttemptFire(EntityUid weapon, EntityUid user, EntityCoordinates coor var weaponXform = Transform(weapon); var weaponCoords = _xform.GetMapCoordinates(weaponXform); var weaponPos = weaponCoords.Position; - var targetCoords = coords.ToMap(EntityManager, _xform); + var targetCoords = _xform.ToMapCoordinates(coords); var targetPos = targetCoords.Position; // Calculate direction diff --git a/Resources/Prototypes/Entities/World/chunk.yml b/Resources/Prototypes/Entities/World/chunk.yml index 718139dcc76..709cb765d9d 100644 --- a/Resources/Prototypes/Entities/World/chunk.yml +++ b/Resources/Prototypes/Entities/World/chunk.yml @@ -8,6 +8,7 @@ categories: [ HideSpawnMenu ] components: - type: WorldChunk + - type: SectorChunkCarver - type: Sprite sprite: Markers/cross.rsi layers: diff --git a/Resources/Prototypes/World/worldgen_default.yml b/Resources/Prototypes/World/worldgen_default.yml index 45e9773d63f..3bc0faead83 100644 --- a/Resources/Prototypes/World/worldgen_default.yml +++ b/Resources/Prototypes/World/worldgen_default.yml @@ -1,9 +1,42 @@ - type: worldgenConfig id: Default components: + - type: WorldSeed - type: WorldController - - type: BiomeSelection - biomes: - - AsteroidsFallback - - Failsafe - - AsteroidsStandard + - type: SectorWorld + planetTypes: + - id: lava + name: Cinder + biomeTemplate: NFVGRoidLava + surfaceTiles: [FloorBasalt] + weatherPrototype: Ashfall + minTemperature: 340 + maxTemperature: 500 + - id: tundra + name: Rime + biomeTemplate: NFVGRoidSnow + surfaceTiles: [FloorSnow, FloorIce] + weatherPrototype: SnowfallHeavy + minTemperature: 190 + maxTemperature: 255 + minOxygen: 2 + maxOxygen: 12 + minNitrogen: 8 + maxNitrogen: 36 + - id: shadow + name: Umbra + biomeTemplate: NFVGRoidShadow + surfaceTiles: [FloorChromite] + minTemperature: 250 + maxTemperature: 320 + minOxygen: 0 + maxOxygen: 8 + minNitrogen: 0 + maxNitrogen: 18 + - id: scrap + name: Husk + biomeTemplate: NFVGRoidScrapyard + surfaceTiles: [Lattice] + weatherPrototype: Rain + minTemperature: 260 + maxTemperature: 335 diff --git a/Resources/Prototypes/_NF/World/worldgen_default.yml b/Resources/Prototypes/_NF/World/worldgen_default.yml index 0edb1d7c34c..057d9a2a34d 100644 --- a/Resources/Prototypes/_NF/World/worldgen_default.yml +++ b/Resources/Prototypes/_NF/World/worldgen_default.yml @@ -1,12 +1,42 @@ - type: worldgenConfig id: NFDefault components: + - type: WorldSeed - type: WorldController - - type: BiomeSelection - biomes: - - NFAsteroidsNear - - NFAsteroidsMid - - NFAsteroidsFar - - MonoWorldgenVeryFarT1 # Mono - - MonoWorldgenVeryFarT2Inner # Mono - - MonoWorldgenVeryFarT2Middle # Mono + - type: SectorWorld + planetTypes: + - id: lava + name: Cinder + biomeTemplate: NFVGRoidLava + surfaceTiles: [FloorBasalt] + weatherPrototype: Ashfall + minTemperature: 340 + maxTemperature: 500 + - id: tundra + name: Rime + biomeTemplate: NFVGRoidSnow + surfaceTiles: [FloorSnow, FloorIce] + weatherPrototype: SnowfallHeavy + minTemperature: 190 + maxTemperature: 255 + minOxygen: 2 + maxOxygen: 12 + minNitrogen: 8 + maxNitrogen: 36 + - id: shadow + name: Umbra + biomeTemplate: NFVGRoidShadow + surfaceTiles: [FloorChromite] + minTemperature: 250 + maxTemperature: 320 + minOxygen: 0 + maxOxygen: 8 + minNitrogen: 0 + maxNitrogen: 18 + - id: scrap + name: Husk + biomeTemplate: NFVGRoidScrapyard + surfaceTiles: [Lattice] + weatherPrototype: Rain + minTemperature: 260 + maxTemperature: 335 diff --git a/SECTOR_WORLDGEN_REWRITE.md b/SECTOR_WORLDGEN_REWRITE.md new file mode 100644 index 00000000000..e18b05139b7 --- /dev/null +++ b/SECTOR_WORLDGEN_REWRITE.md @@ -0,0 +1,198 @@ +# Sector Worldgen Rewrite + +## Goal + +Replace separate per-expedition map spawning with a single seeded sector model that: + +- builds explorable space from deterministic noise and carvers, +- keeps negative space usable for ship travel and construction, +- streams chunks in and out around players, +- preserves discovered and modified areas server-side, +- spawns expeditions and dungeons inside seeded planetary regions instead of on isolated maps. + +## Existing Systems To Reuse + +The current codebase already has the core of a streaming world pipeline: + +- `WorldControllerSystem` creates and loads chunk entities around players and `WorldLoaderComponent` entities. +- `BiomeSelectionSystem` selects chunk behavior from noise channels. +- `DebrisFeaturePlacerSystem` progressively populates chunks as they load. +- `LocalityLoaderSystem` delays structure activation until nearby chunks are actually loaded. +- `RoundPersistenceSystem` already stores expedition-related state and can be extended for sector discovery and chunk deltas. + +The current salvage path is still separate-map based: + +- `SpawnSalvageMissionJob` creates a new map for each expedition. +- `DungeonSystem` generates a dungeon directly into that map. + +That job should become a consumer of sector coordinates, not the owner of procedural map creation. + +## Recommended Architecture + +### 1. Sector root state + +Add a server-side sector authority for each world map. + +- `SectorWorldComponent` + - `UniverseSeed` + - `ChunkSize` + - `PlanetRegions` + - `GeneratedChunks` + - `DirtyChunks` +- `SectorWorldSystem` + - generates the root seed on server start, + - creates stable planet descriptors on startup, + - answers queries for chunk content, planet conditions, and valid landing zones. + +This should sit on the same world map that already uses `WorldControllerComponent`. + +### 2. Planet descriptors instead of ad hoc expedition maps + +Each planet should be a deterministic descriptor, not a separate always-loaded map. + +- `PlanetDescriptor` + - `PlanetId` + - `DisplayName` + - `PlanetSeed` + - `Bounds` + - `PrimaryBiome` + - `Temperature` + - `Atmosphere` + - `TimeOfDay` + - `WeatherBands` + +Generate all planet descriptors at server startup from `UniverseSeed`. +Expeditions then target a descriptor plus a coordinate inside that descriptor. + +### 3. Chunk pipeline + +Replace debris-only chunk population with a layered chunk generation pass: + +1. Evaluate macro noise for region ownership. +2. Evaluate density and connectivity noise. +3. Run a carver pass to form continuous asteroid belts, caverns, void lanes, and approach corridors. +4. Materialize tiles, anchored rocks, hazards, and landmarks for that chunk. +5. Apply saved deltas from persistence. + +Suggested chunk stages: + +- `EmptySpace` +- `RegionResolved` +- `BaseGeometryGenerated` +- `StructuresPlaced` +- `PersistenceApplied` + +### 4. Carver model + +Do not treat each asteroid as a separate procgen result. +Instead, build chunk geometry from a signed-density field: + +- positive density = solid asteroid mass, +- near-zero density = edge band, +- negative density = traversable space. + +Recommended inputs: + +- continent noise for large asteroid landmasses, +- ridge noise for belts and spines, +- warp noise to break grid regularity, +- corridor noise to guarantee flyable paths, +- exclusion masks for POIs, dungeons, and player-built protected areas. + +The current `NoiseRangeCarverSystem` and distance-based carvers can remain as secondary filters, but the new primary carver should operate on chunk geometry rather than point-cancelling debris spawns. + +### 5. Persistence model + +Do not save full maps for the sector. +Save chunk deltas. + +Each persisted chunk record should contain only what differs from deterministic generation: + +- removed generated entities, +- spawned player entities, +- tile changes, +- anchored structure changes, +- dungeon placements, +- discovery metadata, +- selected landing zones. + +Recommended file layout: + +- `data/sector//world.json` +- `data/sector//chunks/_.yml` +- `data/sector//planets/.json` + +### 6. Expeditions as sector placements + +Expeditions should become placements inside a planet region. + +Flow: + +1. Player picks a planet. +2. Server resolves visible and already-discovered landing zones. +3. If a new mission is needed, the server finds an unoccupied valid area inside the planet bounds. +4. `DungeonSystem` generates into a chunk-backed sector location instead of a separate map. +5. The placement is recorded as a persistent region reservation. + +This preserves concurrent expeditions and allows the map to become progressively discovered. + +### 7. UI map support + +A landing-zone UI should read from server-side sector discovery, not client-side scans. + +The UI state should expose: + +- planet list, +- discovered regions, +- blocked regions, +- active expeditions, +- recommended landing zones, +- atmospheric and thermal warnings. + +## Migration Plan + +### Phase 1: deterministic seed foundation + +- ensure world chunk noise is reproducible, +- add map-level world seed, +- audit every worldgen system that currently uses ad hoc random seeds. + +### Phase 2: sector authority + +- add `SectorWorldComponent` and `SectorWorldSystem`, +- create startup planet descriptors, +- expose APIs for chunk queries and planet metadata. + +### Phase 3: chunk geometry generation + +- add a geometry-first chunk generator, +- stop using debris placement as the primary asteroid model, +- store generated chunk metadata and apply chunk deltas. + +### Phase 4: expedition migration + +- change `SpawnSalvageMissionJob` to request a sector placement, +- generate dungeons into the live sector, +- persist reservations and expedition footprints. + +### Phase 5: discovery and landing UI + +- track explored chunks per planet, +- expose landing-zone selection, +- show persistent expedition markers and discovered sites. + +## First Implementation Targets + +The safest next code changes are: + +1. Add `SectorWorldComponent` to worldgen maps. +2. Add a `SectorWorldSystem` that creates planet descriptors on startup. +3. Extend chunk load events so a sector generator can populate chunk geometry before debris placement. +4. Move salvage mission destination selection from `SpawnSalvageMissionJob` into `SectorWorldSystem`. + +## Notes + +- The existing world chunk controller is the correct base abstraction. Reuse it. +- The existing separate expedition map flow should be treated as a compatibility layer and removed last. +- Full-map saves for an infinite world will become too expensive. Save deltas only. +- Deterministic noise and stable seeds are non-negotiable for chunk unload/reload and server restarts. \ No newline at end of file From 7f5ea93bf2b16bb998552a4d92ee605c2b28df46 Mon Sep 17 00:00:00 2001 From: fenndragon Date: Mon, 27 Apr 2026 16:43:40 -0600 Subject: [PATCH 02/49] new touches --- .../GridSplit/OrphanedGridCleanupSystem.cs | 22 ++ Content.Server/Parallax/BiomeSystem.cs | 5 +- .../Components/SectorChunkCarverComponent.cs | 27 ++ .../Components/SectorWorldComponent.cs | 3 + .../SectorAsteroidBiomePrototype.cs | 39 +++ .../Systems/SectorChunkCarverSystem.cs | 305 ++++++++++++++++-- .../Worldgen/Systems/SectorWorldSystem.cs | 91 ++++-- Resources/Prototypes/Entities/World/chunk.yml | 23 ++ Resources/Prototypes/World/noise_channels.yml | 17 + Resources/Prototypes/World/sector_biomes.yml | 252 +++++++++++++++ 10 files changed, 744 insertions(+), 40 deletions(-) create mode 100644 Content.Server/Worldgen/Prototypes/SectorAsteroidBiomePrototype.cs create mode 100644 Resources/Prototypes/World/sector_biomes.yml diff --git a/Content.Server/GridSplit/OrphanedGridCleanupSystem.cs b/Content.Server/GridSplit/OrphanedGridCleanupSystem.cs index 9768007a099..6ed0c00058c 100644 --- a/Content.Server/GridSplit/OrphanedGridCleanupSystem.cs +++ b/Content.Server/GridSplit/OrphanedGridCleanupSystem.cs @@ -1,6 +1,8 @@ using System.Linq; using Content.Server.Power.Components; using Content.Server.Procedural; +using Content.Server._Mono.Cleanup; +using Content.Server.Worldgen.Components; using Content.Server.Station.Components; using Content.Shared.CCVar; using Content.Shared.Doors.Components; @@ -163,6 +165,9 @@ private int CleanupEmptyGrids(TimeSpan curTime) /// private bool ShouldCleanupEmptyGrid(EntityUid gridUid, MapGridComponent grid, MetaDataComponent meta) { + if (ShouldPreserveGrid(gridUid)) + return false; + // Count tiles var tileCount = _mapSystem.GetAllTiles(gridUid, grid).Count(); @@ -227,6 +232,9 @@ private bool ShouldDeleteGrid(EntityUid gridUid) if (!TryComp(gridUid, out var grid)) return false; + if (ShouldPreserveGrid(gridUid)) + return false; + // Count total tiles by iterating through all tiles var tileCount = _mapSystem.GetAllTiles(gridUid, grid).Count(); @@ -259,6 +267,9 @@ private bool HasImportantEntities(EntityUid gridUid) while (children.MoveNext(out var child)) { + if (HasComp(child)) + return true; + // Check for players if (HasComp(child)) return true; @@ -291,6 +302,17 @@ private bool HasImportantEntities(EntityUid gridUid) return false; } + private bool ShouldPreserveGrid(EntityUid gridUid) + { + if (HasComp(gridUid)) + return true; + + if (HasComp(gridUid)) + return true; + + return HasImportantEntities(gridUid); + } + /// /// Sets the minimum tile count threshold for grid cleanup. diff --git a/Content.Server/Parallax/BiomeSystem.cs b/Content.Server/Parallax/BiomeSystem.cs index 8dac0bbb758..7399bc385cf 100644 --- a/Content.Server/Parallax/BiomeSystem.cs +++ b/Content.Server/Parallax/BiomeSystem.cs @@ -1,6 +1,7 @@ using System.Linq; using System.Numerics; using System.Threading.Tasks; +using Content.Server._Mono.Cleanup; using Content.Server.Atmos; using Content.Server.Atmos.Components; using Content.Server.Atmos.EntitySystems; @@ -1010,7 +1011,9 @@ public void EnsurePlanet(EntityUid mapUid, BiomeTemplatePrototype biomeTemplate, if (!Resolve(mapUid, ref metadata)) return; - EnsureComp(mapUid); + var mapGrid = EnsureComp(mapUid); + mapGrid.CanSplit = false; + EnsureComp(mapUid); var biome = (BiomeComponent) EntityManager.ComponentFactory.GetComponent(typeof(BiomeComponent)); seed ??= _random.Next(); SetSeed(mapUid, biome, seed.Value, false); diff --git a/Content.Server/Worldgen/Components/SectorChunkCarverComponent.cs b/Content.Server/Worldgen/Components/SectorChunkCarverComponent.cs index ab65540a02c..aed35dca8c8 100644 --- a/Content.Server/Worldgen/Components/SectorChunkCarverComponent.cs +++ b/Content.Server/Worldgen/Components/SectorChunkCarverComponent.cs @@ -20,6 +20,9 @@ public sealed partial class SectorChunkCarverComponent : Component [DataField] public float SparseFieldScale = 104f; + [DataField] + public float ChunkFieldScale = 5f; + [DataField] public float IslandFieldScale = 22f; @@ -29,6 +32,9 @@ public sealed partial class SectorChunkCarverComponent : Component [DataField] public float SparseThreshold = 0.978f; + [DataField] + public float ChunkThreshold = 0.45f; + [DataField] public float DensityThreshold = 0.79f; @@ -44,9 +50,30 @@ public sealed partial class SectorChunkCarverComponent : Component [DataField] public float PlanetFalloff = 0.08f; + [DataField] + public List Biomes = + [ + "SectorRock", + "SectorIce", + "SectorAndesite", + "SectorBasalt", + "SectorSand", + "SectorChromite", + "SectorRust", + "SectorScrap", + "SectorWreck", + "SectorBrass", + ]; + [ViewVariables] public HashSet GeneratedTiles = new(); [ViewVariables] public HashSet GeneratedEntities = new(); + + [ViewVariables] + public bool Materialized; + + [ViewVariables] + public string? CacheFilePath; } \ No newline at end of file diff --git a/Content.Server/Worldgen/Components/SectorWorldComponent.cs b/Content.Server/Worldgen/Components/SectorWorldComponent.cs index 84701c842eb..b0d192de374 100644 --- a/Content.Server/Worldgen/Components/SectorWorldComponent.cs +++ b/Content.Server/Worldgen/Components/SectorWorldComponent.cs @@ -21,6 +21,9 @@ public sealed partial class SectorWorldComponent : Component [DataField] public float MissionReservationPadding = 128f; + [DataField] + public float CentralClearRadius = 500f; + [ViewVariables] public EntityUid? SectorGrid; diff --git a/Content.Server/Worldgen/Prototypes/SectorAsteroidBiomePrototype.cs b/Content.Server/Worldgen/Prototypes/SectorAsteroidBiomePrototype.cs new file mode 100644 index 00000000000..c9e531a93e5 --- /dev/null +++ b/Content.Server/Worldgen/Prototypes/SectorAsteroidBiomePrototype.cs @@ -0,0 +1,39 @@ +using System.Linq; +using Content.Server.Worldgen.Tools; +using Content.Shared.Maps; +using Content.Shared.Storage; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; + +namespace Content.Server.Worldgen.Prototypes; + +[Prototype("sectorAsteroidBiome")] +public sealed partial class SectorAsteroidBiomePrototype : IPrototype +{ + private Dictionary? _caches; + + [IdDataField] + public string ID { get; private set; } = string.Empty; + + [DataField("floorTiles", required: true)] + public List FloorTiles = new(); + + [DataField("entries", required: true, + customTypeSerializer: typeof(PrototypeIdDictionarySerializer, ContentTileDefinition>))] + private Dictionary> _entries = default!; + + public Dictionary Caches + { + get + { + if (_caches == null) + { + _caches = _entries + .Select(pair => new KeyValuePair(pair.Key, new EntitySpawnCollectionCache(pair.Value))) + .ToDictionary(pair => pair.Key, pair => pair.Value); + } + + return _caches; + } + } +} \ No newline at end of file diff --git a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs index 21a0791ca62..5b5e55ee24a 100644 --- a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs +++ b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs @@ -1,9 +1,15 @@ using System.Numerics; +using System.IO; +using System.Text; using Content.Server.Worldgen.Components; +using Content.Server.Worldgen.Prototypes; using Content.Shared.Maps; +using Content.Shared.Storage; +using Robust.Shared.Maths; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Prototypes; +using Robust.Shared.Random; namespace Content.Server.Worldgen.Systems; @@ -31,15 +37,40 @@ public sealed class SectorChunkCarverSystem : EntitySystem [Dependency] private readonly SharedMapSystem _mapSystem = default!; [Dependency] private readonly ITileDefinitionManager _tileDefs = default!; [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly IRobustRandom _random = default!; + + private string _cacheDirectory = string.Empty; public override void Initialize() { + _cacheDirectory = Path.Combine(Path.GetTempPath(), "HardLight", "sector-chunk-cache", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(_cacheDirectory); + SubscribeLocalEvent(OnChunkLoaded); SubscribeLocalEvent(OnChunkUnloaded); } + public override void Shutdown() + { + base.Shutdown(); + + try + { + if (!string.IsNullOrWhiteSpace(_cacheDirectory) && Directory.Exists(_cacheDirectory)) + Directory.Delete(_cacheDirectory, true); + } + catch + { + // Temp cache cleanup is best-effort. + } + } + private void OnChunkLoaded(Entity ent, ref WorldChunkLoadedEvent args) { + if (ent.Comp.Materialized) + return; + if (!TryComp(args.Chunk, out var chunk)) return; @@ -50,11 +81,24 @@ private void OnChunkLoaded(Entity ent, ref WorldChun return; var grid = EnsureComp(gridUid); - var chunkOrigin = chunk.Coordinates * WorldGen.ChunkSize; - var tiles = new List<(Vector2i, Tile)>(WorldGen.ChunkSize * WorldGen.ChunkSize / 3); - ent.Comp.GeneratedTiles.Clear(); ent.Comp.GeneratedEntities.Clear(); + var blockedGrids = GetBlockingGrids(chunk.Map, gridUid, chunk); + var chunkBiome = GetChunkBiome(ent.Comp, sector, chunk.Coordinates); + + if (!TryComp(chunk.Map, out MapComponent? mapComp) || mapComp == null) + return; + + var sectorMapId = mapComp.MapId; + + if (TryRestoreChunkFromCache((ent.Owner, ent.Comp), chunk, gridUid, grid, chunkBiome)) + { + ent.Comp.Materialized = true; + return; + } + + var chunkOrigin = chunk.Coordinates * WorldGen.ChunkSize; + var tiles = new List<(Vector2i, Tile)>(WorldGen.ChunkSize * WorldGen.ChunkSize / 3); for (var x = 0; x < WorldGen.ChunkSize; x++) { @@ -63,11 +107,13 @@ private void OnChunkLoaded(Entity ent, ref WorldChun var indices = chunkOrigin + new Vector2i(x, y); var worldPos = indices + new Vector2(0.5f, 0.5f); - if (!_sectorWorld.IsSolidAt(chunk.Map, ent.Owner, ent.Comp, worldPos, out var planet)) + if (IsBlockedByOtherGrid(worldPos, sectorMapId, blockedGrids)) + continue; + + if (!_sectorWorld.IsSolidAt(chunk.Map, ent.Owner, ent.Comp, worldPos, out _)) continue; - if (!_sectorWorld.TryGetSurfaceTile(planet, out var tileId)) - tileId = "FloorSteel"; + var tileId = GetChunkFloorTileId(chunkBiome, sector, indices); var tileDef = (ContentTileDefinition) _tileDefs[tileId]; tiles.Add((indices, new Tile(tileDef.TileId))); @@ -78,27 +124,29 @@ private void OnChunkLoaded(Entity ent, ref WorldChun if (tiles.Count > 0) _mapSystem.SetTiles(gridUid, grid, tiles); - foreach (var indices in ent.Comp.GeneratedTiles) - { - if (!TryGetPlanetWallPrototype(gridUid, grid, indices, out var wallPrototype)) - continue; + SpawnChunkEntities((ent.Owner, ent.Comp), gridUid, grid, chunkBiome); - var spawned = Spawn(wallPrototype, new EntityCoordinates(gridUid, indices + new Vector2(0.5f, 0.5f))); - ent.Comp.GeneratedEntities.Add(spawned); - } + ent.Comp.Materialized = true; } private void OnChunkUnloaded(Entity ent, ref WorldChunkUnloadedEvent args) { + if (!ent.Comp.Materialized || ent.Comp.GeneratedTiles.Count == 0) + return; + if (!TryComp(args.Chunk, out var chunk)) return; if (!TryComp(chunk.Map, out var sector)) return; - if (ent.Comp.GeneratedTiles.Count == 0) + if (!_sectorWorld.TryGetSectorGrid(chunk.Map, out var gridUid, sector)) return; + var grid = EnsureComp(gridUid); + + SaveChunkToCache((ent.Owner, ent.Comp), gridUid, grid, chunk); + foreach (var generated in ent.Comp.GeneratedEntities) { if (Exists(generated)) @@ -107,12 +155,7 @@ private void OnChunkUnloaded(Entity ent, ref WorldCh ent.Comp.GeneratedEntities.Clear(); - if (!_sectorWorld.TryGetSectorGrid(chunk.Map, out var gridUid, sector)) - return; - - var grid = EnsureComp(gridUid); var tiles = new List<(Vector2i, Tile)>(ent.Comp.GeneratedTiles.Count); - foreach (var indices in ent.Comp.GeneratedTiles) { tiles.Add((indices, Tile.Empty)); @@ -120,13 +163,235 @@ private void OnChunkUnloaded(Entity ent, ref WorldCh _mapSystem.SetTiles(gridUid, grid, tiles); ent.Comp.GeneratedTiles.Clear(); + ent.Comp.Materialized = false; + } + + private void SaveChunkToCache(Entity ent, EntityUid gridUid, MapGridComponent grid, WorldChunkComponent chunk) + { + var cachePath = GetCachePath(ent.Owner, chunk); + ent.Comp.CacheFilePath = cachePath; + + Directory.CreateDirectory(Path.GetDirectoryName(cachePath)!); + + var builder = new StringBuilder(); + builder.AppendLine("v2"); + + foreach (var indices in ent.Comp.GeneratedTiles) + { + var tileRef = _mapSystem.GetTileRef(gridUid, grid, indices); + if (tileRef.Tile.IsEmpty) + continue; + + var tileId = tileRef.Tile.GetContentTileDefinition(_tileDefs).ID; + builder.Append('t') + .Append(',') + .Append(indices.X) + .Append(',') + .Append(indices.Y) + .Append(',') + .Append(tileId) + .AppendLine(); + } + + foreach (var generated in ent.Comp.GeneratedEntities) + { + if (!Exists(generated) || !TryComp(generated, out var meta) || meta.EntityPrototype == null) + continue; + + if (!TryComp(generated, out var xform) || xform.GridUid != gridUid) + continue; + + var indices = _mapSystem.TileIndicesFor(gridUid, grid, xform.Coordinates); + builder.Append('e') + .Append(',') + .Append(indices.X) + .Append(',') + .Append(indices.Y) + .Append(',') + .Append(meta.EntityPrototype.ID) + .AppendLine(); + } + + File.WriteAllText(cachePath, builder.ToString()); + } + + private bool TryRestoreChunkFromCache(Entity ent, WorldChunkComponent chunk, EntityUid gridUid, MapGridComponent grid, SectorAsteroidBiomePrototype? chunkBiome) + { + var cachePath = ent.Comp.CacheFilePath ?? GetCachePath(ent.Owner, chunk); + ent.Comp.CacheFilePath = cachePath; + + if (!File.Exists(cachePath)) + return false; + + var tilePlacements = new List<(Vector2i, Tile)>(); + var entityPlacements = new List<(Vector2i Indices, string PrototypeId)>(); + foreach (var line in File.ReadLines(cachePath)) + { + if (string.IsNullOrWhiteSpace(line) || line == "v1" || line == "v2") + continue; + + var parts = line.Split(',', 4); + + if (parts.Length == 3) + { + RestoreCachedTile(parts[0], parts[1], parts[2], tilePlacements, ent.Comp.GeneratedTiles); + continue; + } + + if (parts.Length != 4) + continue; + + switch (parts[0]) + { + case "t": + RestoreCachedTile(parts[1], parts[2], parts[3], tilePlacements, ent.Comp.GeneratedTiles); + break; + case "e": + if (!int.TryParse(parts[1], out var entityX) || !int.TryParse(parts[2], out var entityY)) + continue; + + entityPlacements.Add((new Vector2i(entityX, entityY), parts[3])); + break; + } + } + + if (tilePlacements.Count == 0) + return false; + + _mapSystem.SetTiles(gridUid, grid, tilePlacements); + + if (entityPlacements.Count > 0) + { + foreach (var entityPlacement in entityPlacements) + { + if (!_proto.HasIndex(entityPlacement.PrototypeId)) + continue; + + var spawned = Spawn(entityPlacement.PrototypeId, new EntityCoordinates(gridUid, entityPlacement.Indices + new Vector2(0.5f, 0.5f))); + ent.Comp.GeneratedEntities.Add(spawned); + } + } + else + { + SpawnChunkEntities((ent.Owner, ent.Comp), gridUid, grid, chunkBiome); + } + + return true; + } + + private void RestoreCachedTile(string xText, string yText, string tileId, List<(Vector2i, Tile)> tilePlacements, HashSet generatedTiles) + { + if (!int.TryParse(xText, out var x) || !int.TryParse(yText, out var y)) + return; + + if (!_tileDefs.TryGetDefinition(tileId, out var tileDefBase) || tileDefBase is not ContentTileDefinition tileDef) + return; + + var indices = new Vector2i(x, y); + tilePlacements.Add((indices, new Tile(tileDef.TileId))); + generatedTiles.Add(indices); + } + + private string GetCachePath(EntityUid chunkUid, WorldChunkComponent chunk) + { + return Path.Combine(_cacheDirectory, $"chunk_{chunkUid}_{chunk.Coordinates.X}_{chunk.Coordinates.Y}.cache"); + } + + private SectorAsteroidBiomePrototype? GetChunkBiome(SectorChunkCarverComponent carver, SectorWorldComponent sector, Vector2i chunkCoords) + { + if (carver.Biomes.Count == 0) + return null; + + var index = Math.Abs(HashCode.Combine(sector.UniverseSeed, chunkCoords.X, chunkCoords.Y) % carver.Biomes.Count); + var biomeId = carver.Biomes[index]; + return _proto.TryIndex(biomeId, out var biome) ? biome : null; + } + + private string GetChunkFloorTileId(SectorAsteroidBiomePrototype? biome, SectorWorldComponent sector, Vector2i indices) + { + if (biome == null || biome.FloorTiles.Count == 0) + return "FloorSteel"; + + var index = Math.Abs(HashCode.Combine(sector.UniverseSeed, indices.X, indices.Y) % biome.FloorTiles.Count); + return biome.FloorTiles[index]; + } + + private void SpawnChunkEntities(Entity ent, EntityUid gridUid, MapGridComponent grid, SectorAsteroidBiomePrototype? biome) + { + var spawns = new List(4); + + foreach (var indices in ent.Comp.GeneratedTiles) + { + var tile = _mapSystem.GetTileRef(gridUid, grid, indices); + if (tile.Tile.IsEmpty) + continue; + + var tileId = tile.Tile.GetContentTileDefinition(_tileDefs).ID; + var handledByBiome = false; + + if (biome != null && biome.Caches.TryGetValue(tileId, out var cache)) + { + handledByBiome = true; + spawns.Clear(); + cache.GetSpawns(_random, ref spawns); + + foreach (var prototype in spawns) + { + if (prototype == null || !_proto.HasIndex(prototype)) + continue; + + var spawned = Spawn(prototype, new EntityCoordinates(gridUid, indices + new Vector2(0.5f, 0.5f))); + ent.Comp.GeneratedEntities.Add(spawned); + } + } + + if (handledByBiome) + continue; + + if (!TryGetPlanetWallPrototype(gridUid, grid, indices, out var wallPrototype)) + continue; + + var fallback = Spawn(wallPrototype, new EntityCoordinates(gridUid, indices + new Vector2(0.5f, 0.5f))); + ent.Comp.GeneratedEntities.Add(fallback); + } + } + + private List> GetBlockingGrids(EntityUid sectorMap, EntityUid sectorGridUid, WorldChunkComponent chunk) + { + var results = new List>(); + + if (!TryComp(sectorMap, out var mapComp)) + return results; + + var chunkOrigin = chunk.Coordinates * WorldGen.ChunkSize; + var worldBounds = Box2.FromDimensions(chunkOrigin, new Vector2(WorldGen.ChunkSize, WorldGen.ChunkSize)); + _mapManager.FindGridsIntersecting(mapComp.MapId, worldBounds, ref results, includeMap: false); + results.RemoveAll(grid => grid.Owner == sectorGridUid); + return results; + } + + private bool IsBlockedByOtherGrid(Vector2 worldPos, MapId mapId, List> blockedGrids) + { + if (blockedGrids.Count == 0) + return false; + + var coords = new MapCoordinates(worldPos, mapId); + foreach (var grid in blockedGrids) + { + var tile = _mapSystem.GetTileRef(grid.Owner, grid.Comp, coords); + if (!tile.Tile.IsEmpty) + return true; + } + + return false; } private bool TryGetPlanetWallPrototype(EntityUid gridUid, MapGridComponent grid, Vector2i indices, out string prototype) { prototype = string.Empty; - if (!grid.TryGetTileRef(indices, out var tile)) + var tile = _mapSystem.GetTileRef(gridUid, grid, indices); + if (tile.Tile.IsEmpty) return false; var tileId = tile.Tile.GetContentTileDefinition(_tileDefs).ID; diff --git a/Content.Server/Worldgen/Systems/SectorWorldSystem.cs b/Content.Server/Worldgen/Systems/SectorWorldSystem.cs index 73ae58dbd71..61cc820e4c6 100644 --- a/Content.Server/Worldgen/Systems/SectorWorldSystem.cs +++ b/Content.Server/Worldgen/Systems/SectorWorldSystem.cs @@ -36,7 +36,6 @@ public sealed class SectorWorldSystem : EntitySystem [Dependency] private readonly SharedMapSystem _mapSystem = default!; [Dependency] private readonly ITileDefinitionManager _tileDefs = default!; [Dependency] private readonly WeatherSystem _weather = default!; - [Dependency] private readonly WorldControllerSystem _worldController = default!; private static readonly string[] TimeOfDayStates = ["Dawn", "Day", "Dusk", "Night"]; @@ -166,14 +165,33 @@ public bool TryGetSurfaceTile(SectorPlanetDescriptor planet, out string tileId) public bool IsSolidAt(EntityUid sectorMap, EntityUid noiseHolder, SectorChunkCarverComponent carver, Vector2 worldPos, out SectorPlanetDescriptor planet) { - if (!TryGetPlanetAtPosition(sectorMap, worldPos, out planet)) + if (TryComp(sectorMap, out SectorWorldComponent? sectorComp) && + sectorComp != null && + worldPos.LengthSquared() <= sectorComp.CentralClearRadius * sectorComp.CentralClearRadius) + { + planet = default!; + return false; + } + + var chunkCoords = SharedMapSystem.GetChunkIndices(worldPos, WorldGen.ChunkSize); + if (sectorComp == null) + { + planet = default!; return false; + } - var localPos = worldPos - planet.Center; + planet = GetSectorAsteroidDescriptor(sectorComp, chunkCoords); + + var localPos = worldPos; + var chunkSample = new Vector2(chunkCoords.X + 0.5f, chunkCoords.Y + 0.5f) / MathF.Max(carver.ChunkFieldScale, 1f); var sparseSample = localPos / MathF.Max(carver.SparseFieldScale, 1f); var islandSample = localPos / MathF.Max(carver.IslandFieldScale, 1f); var detailSample = localPos / MathF.Max(carver.DetailFieldScale, 1f); + var chunkPresence = _noiseIndex.Evaluate(noiseHolder, carver.IslandNoiseChannel, chunkSample * 0.57f + new Vector2(17.5f, -12.25f)); + if (chunkPresence < carver.ChunkThreshold) + return false; + var sparse = _noiseIndex.Evaluate(noiseHolder, carver.IslandNoiseChannel, sparseSample * 0.73f + new Vector2(-11.75f, 6.25f)); if (sparse < carver.SparseThreshold) return false; @@ -182,8 +200,7 @@ public bool IsSolidAt(EntityUid sectorMap, EntityUid noiseHolder, SectorChunkCar var carve = _noiseIndex.Evaluate(noiseHolder, carver.CarveNoiseChannel, detailSample * 1.33f + new Vector2(7.5f, -4.25f)); var islands = _noiseIndex.Evaluate(noiseHolder, carver.IslandNoiseChannel, islandSample * 1.61f + new Vector2(-3.75f, 5.5f)); - var radialDistance = (worldPos - planet.Center).Length() / MathF.Max(planet.Radius, 1f); - var densityBias = radialDistance * carver.PlanetFalloff; + var densityBias = 0f; var sparseStrength = (sparse - carver.SparseThreshold) / MathF.Max(1f - carver.SparseThreshold, 0.001f); sparseStrength = Math.Clamp(sparseStrength, 0f, 1f); @@ -192,13 +209,30 @@ public bool IsSolidAt(EntityUid sectorMap, EntityUid noiseHolder, SectorChunkCar var ridge = 1f - MathF.Abs(carve - 0.5f) * 2f; ridge = Math.Clamp(ridge, 0f, 1f); - var signedDensity = density * 0.58f + islands * 0.22f + ridge * 0.2f + sparseStrength * 0.32f - densityBias; - var baseMass = signedDensity >= carver.DensityThreshold || (sparseStrength >= carver.IslandThreshold && density >= carver.DensityThreshold - 0.08f); - var carvedOut = carve >= carver.CarveRange.X && carve <= carver.CarveRange.Y && sparseStrength < 0.94f; + var islandMass = islands * 0.42f + sparseStrength * 0.26f + ridge * 0.12f; + var signedDensity = density * 0.38f + islandMass - densityBias - 0.12f; + var baseMass = islands >= carver.IslandThreshold - 0.08f + && signedDensity >= carver.DensityThreshold - 0.04f; + var carvedOut = carve >= carver.CarveRange.X && carve <= carver.CarveRange.Y && islandMass < 0.92f; return baseMass && !carvedOut; } + private SectorPlanetDescriptor GetSectorAsteroidDescriptor(SectorWorldComponent sector, Vector2i chunkCoords) + { + if (sector.Planets.Count == 0) + { + return new SectorPlanetDescriptor + { + SurfaceTile = "FloorSteel", + }; + } + + var hash = HashCode.Combine(sector.UniverseSeed, chunkCoords.X, chunkCoords.Y); + var index = Math.Abs(hash % sector.Planets.Count); + return sector.Planets[index]; + } + public bool TryReserveExpeditionSite(int seed, EntityUid expeditionUid, string? planetTypeId, out SectorExpeditionPlacement placement) { placement = default!; @@ -275,10 +309,14 @@ private void EnsureInitialized(Entity ent) { var sectorGrid = _mapManager.CreateGridEntity(mapComp.MapId); ent.Comp.SectorGrid = sectorGrid.Owner; + sectorGrid.Comp.CanSplit = false; EnsureComp(sectorGrid.Owner); _metaData.SetEntityName(sectorGrid.Owner, $"{MetaData(ent.Owner).EntityName} Sector Grid"); } + if (ent.Comp.SectorGrid is { } existingSectorGrid) + EnsurePersistentWorldGrid(existingSectorGrid); + if (ent.Comp.UniverseSeed == 0) ent.Comp.UniverseSeed = _random.Next(1, int.MaxValue); @@ -327,21 +365,16 @@ private void EnsureInitialized(Entity ent) private void EnsureStartupPlanetLoaders(Entity ent) { - if (!TryGetSectorGrid(ent.Owner, out var sectorGrid, ent.Comp)) - return; - - if (ent.Comp.StartupLoaders.Count == ent.Comp.Planets.Count && ent.Comp.StartupLoaders.All(Exists)) + if (ent.Comp.StartupLoaders.Count == 0) return; - ent.Comp.StartupLoaders.RemoveAll(loader => !Exists(loader)); - - foreach (var planet in ent.Comp.Planets) + foreach (var loader in ent.Comp.StartupLoaders) { - var loader = Spawn(null, new EntityCoordinates(sectorGrid, planet.Center)); - EnsureComp(loader); - _worldController.SetLoaderRadius(loader, (int) MathF.Ceiling(planet.Radius + WorldGen.ChunkSize)); - ent.Comp.StartupLoaders.Add(loader); + if (Exists(loader)) + QueueDel(loader); } + + ent.Comp.StartupLoaders.Clear(); } private void EnsurePersistentLayerMaps(Entity ent) @@ -349,10 +382,16 @@ private void EnsurePersistentLayerMaps(Entity ent) ent.Comp.FtlMap ??= CreateLayerMap($"{MetaData(ent.Owner).EntityName} FTL", space: true, gravity: false); ent.Comp.ColCommMap ??= CreateLayerMap($"{MetaData(ent.Owner).EntityName} ColComm", space: false, gravity: true, mixture: CreateStandardAirMixture(), timeOfDay: "Day"); + EnsurePersistentWorldGrid(ent.Comp.FtlMap.Value); + EnsurePersistentWorldGrid(ent.Comp.ColCommMap.Value); + foreach (var planet in ent.Comp.Planets) { if (ent.Comp.PlanetTypeMaps.ContainsKey(planet.PlanetTypeId)) + { + EnsurePersistentWorldGrid(ent.Comp.PlanetTypeMaps[planet.PlanetTypeId]); continue; + } ent.Comp.PlanetTypeMaps[planet.PlanetTypeId] = CreateLayerMap( $"{planet.Name} Surface", @@ -363,9 +402,22 @@ private void EnsurePersistentLayerMaps(Entity ent) weatherPrototype: planet.WeatherPrototype, biomeTemplateId: planet.BiomeTemplate, biomeSeed: planet.Seed); + + EnsurePersistentWorldGrid(ent.Comp.PlanetTypeMaps[planet.PlanetTypeId]); } } + private void EnsurePersistentWorldGrid(EntityUid mapOrGridUid) + { + if (!Exists(mapOrGridUid)) + return; + + EnsureComp(mapOrGridUid); + + if (TryComp(mapOrGridUid, out var grid)) + grid.CanSplit = false; + } + private EntityUid CreateLayerMap( string name, bool space, @@ -378,6 +430,7 @@ private EntityUid CreateLayerMap( { var mapUid = _mapSystem.CreateMap(out _); EnsureComp(mapUid); + EnsureComp(mapUid); _metaData.SetEntityName(mapUid, name); if (!space && !string.IsNullOrWhiteSpace(biomeTemplateId) && _proto.TryIndex(biomeTemplateId, out var biomeTemplate)) diff --git a/Resources/Prototypes/Entities/World/chunk.yml b/Resources/Prototypes/Entities/World/chunk.yml index 709cb765d9d..ddc5c9218a3 100644 --- a/Resources/Prototypes/Entities/World/chunk.yml +++ b/Resources/Prototypes/Entities/World/chunk.yml @@ -9,6 +9,29 @@ components: - type: WorldChunk - type: SectorChunkCarver + densityNoiseChannel: SectorDensity + islandNoiseChannel: SectorSparse + biomes: + - SectorRock + - SectorIce + - SectorAndesite + - SectorBasalt + - SectorSand + - SectorChromite + - SectorRust + - SectorScrap + - SectorWreck + - SectorBrass + sparseFieldScale: 54 + chunkFieldScale: 5 + chunkThreshold: 0.47 + islandFieldScale: 18 + detailFieldScale: 10 + sparseThreshold: 0.66 + densityThreshold: 0.5 + islandThreshold: 0.56 + densitySharpness: 2.6 + planetFalloff: 0.04 - type: Sprite sprite: Markers/cross.rsi layers: diff --git a/Resources/Prototypes/World/noise_channels.yml b/Resources/Prototypes/World/noise_channels.yml index 27d853eeb7e..10b42a084de 100644 --- a/Resources/Prototypes/World/noise_channels.yml +++ b/Resources/Prototypes/World/noise_channels.yml @@ -37,6 +37,23 @@ inputMultiplier: 16 # Makes wreck concentration very low noise at scale. outputMultiplier: 0.35 # Frontier: fewer wrecks +- type: noiseChannel + id: SectorDensity + noiseType: Perlin + fractalLacunarityByPi: 0.666666666 + remapTo0Through1: true + inputMultiplier: 6 + +- type: noiseChannel + id: SectorSparse + noiseType: Perlin + fractalLacunarityByPi: 0.666666666 + clippingRanges: + - 0.0, 0.4 + clippedValue: 0 + remapTo0Through1: true + inputMultiplier: 16 + - type: noiseChannel id: Temperature noiseType: Perlin diff --git a/Resources/Prototypes/World/sector_biomes.yml b/Resources/Prototypes/World/sector_biomes.yml new file mode 100644 index 00000000000..7a2258aecbd --- /dev/null +++ b/Resources/Prototypes/World/sector_biomes.yml @@ -0,0 +1,252 @@ +- type: sectorAsteroidBiome + id: SectorRock + floorTiles: [FloorAsteroidSand] + entries: + FloorAsteroidSand: + - id: NFRockMineralSoft + orGroup: rock + prob: 1.025 + - id: NFRockMineralHard + orGroup: rock + prob: 0.1223 + - id: NFAsteroidRoomMarker + orGroup: rock + prob: 0.0002 + +- type: sectorAsteroidBiome + id: SectorIce + floorTiles: [FloorIce] + entries: + FloorIce: + - id: NFIceMineralSoft + orGroup: rock + prob: 0.807 + - id: NFIceMineralHard + orGroup: rock + prob: 0.1305 + - id: NFSnowRoomMarker + orGroup: rock + prob: 0.0002 + +- type: sectorAsteroidBiome + id: SectorAndesite + floorTiles: [FloorCaveDrought] + entries: + FloorCaveDrought: + - id: NFAndesiteMineralSoft + orGroup: rock + prob: 0.929 + - id: NFAndesiteMineralHard + orGroup: rock + prob: 0.1317 + - id: NFAndesiteRoomMarker + orGroup: rock + prob: 0.0003 + +- type: sectorAsteroidBiome + id: SectorBasalt + floorTiles: [FloorBasalt] + entries: + FloorBasalt: + - id: NFBasaltMineralSoft + orGroup: rock + prob: 0.905 + - id: NFBasaltMineralHard + orGroup: rock + prob: 0.115 + - id: NFBasaltRoomMarker + orGroup: rock + prob: 0.0002 + +- type: sectorAsteroidBiome + id: SectorSand + floorTiles: [FloorLowDesert] + entries: + FloorLowDesert: + - id: NFSandMineralSoft + orGroup: rock + prob: 0.899 + - id: NFSandMineralHard + orGroup: rock + prob: 0.1227 + - id: NFSandRoomMarker + orGroup: rock + prob: 0.0002 + +- type: sectorAsteroidBiome + id: SectorChromite + floorTiles: [FloorChromite] + entries: + FloorChromite: + - id: NFChromiteMineralSoft + orGroup: rock + prob: 0.8225 + - id: NFChromiteMineralHard + orGroup: rock + prob: 0.11175 + - id: NFChromiteRoomMarker + orGroup: rock + prob: 0.0002 + +- type: sectorAsteroidBiome + id: SectorRust + floorTiles: [Lattice] + entries: + Lattice: + - id: NFScrapMineralSoft + orGroup: rock + prob: 0.77 + - id: WallSolidRust + orGroup: rock + prob: 0.05 + +- type: sectorAsteroidBiome + id: SectorScrap + floorTiles: [Plating, Plating, FloorSteel, Lattice] + entries: + Plating: + - prob: 3 + - id: SalvageMaterialCrateSpawner + prob: 1 + - id: SalvageCanisterSpawner + prob: 0.2 + - id: SalvageMobSpawner + prob: 0.7 + - id: WallSolid + prob: 1 + - id: Grille + prob: 0.5 + Lattice: + - prob: 2 + - id: Grille + prob: 0.2 + - id: SalvageMaterialCrateSpawner + prob: 0.3 + - id: SalvageCanisterSpawner + prob: 0.2 + FloorSteel: + - prob: 3 + - id: SalvageMaterialCrateSpawner + prob: 1 + - id: SalvageCanisterSpawner + prob: 0.2 + - id: SalvageMobSpawner + prob: 0.7 + +- type: sectorAsteroidBiome + id: SectorWreck + floorTiles: [Plating, Plating, Plating, FloorSteel, Lattice] + entries: + FloorSteel: &wreckFloor + - prob: 1 + - id: Girder + prob: 0.3 + - id: WallSolid + prob: 0.3 + - id: WallReinforced + prob: 0.2 + - id: AirlockMaint + prob: 0.01 + - id: Barricade + prob: 0.01 + - id: SalvageSpawnerScrapCommon + prob: 3.5 + - id: SalvageSpawnerTreasure + prob: 0.3 + - id: SpawnDungeonLootSeed + prob: 0.1 + - id: SalvageSpawnerTreasureValuable + prob: 0.05 + - id: SalvageSpawnerEquipment + prob: 0.05 + - id: SalvageSpawnerEquipmentValuable + prob: 0.03 + - id: SpawnDungeonClutterMedsSingle + prob: 0.03 + - id: SpawnDungeonLootBureaucracy + prob: 0.03 + - id: SpawnDungeonLootToolsHydroponics + prob: 0.03 + - id: SalvagePartsT2Spawner + prob: 0.01 + - id: SalvagePartsT3Spawner + prob: 0.005 + - id: SalvagePartsT4Spawner + prob: 0.002 + - id: NFSalvageMaterialCrateSpawner + prob: 0.9 + - id: NFSalvageChemicalBarrelSpawner + prob: 0.08 + - id: NFSalvageServiceBarrelSpawner + prob: 0.02 + - id: NFSalvageDrinkableBarrelSpawner + prob: 0.02 + - id: NFSalvageEmptyBarrelSpawner + prob: 0.03 + - id: SalvageCanisterSpawner + prob: 0.2 + - id: NFSalvageLockerSpawner + prob: 0.2 + - id: NFSalvageGeneratorSpawner + prob: 0.1 + - id: NFSalvageFurnitureSpawner + prob: 0.1 + - id: NFSalvageSuitStorageSpawner + prob: 0.1 + - id: RandomArtifactSpawner + prob: 0.05 + - id: NFSalvageTankSpawnerHighCapacity + prob: 0.0005 + - id: MedicalPodFilled + prob: 0.03 + - id: NFSalvageMobSpawner + prob: 0.1 + - id: NFWreckRoomMarker + prob: 0.01 + Plating: *wreckFloor + Lattice: + - prob: 1 + - id: Grille + prob: 0.75 + - id: GrilleBroken + prob: 0.25 + +- type: sectorAsteroidBiome + id: SectorBrass + floorTiles: [PlatingBrass, PlatingBrass, PlatingBrass, FloorBrassFilled, FloorBrassReebe] + entries: + FloorBrassFilled: &brassFloor + - prob: 3 + - id: ClockworkGirder + prob: 0.3 + - id: WallClock + prob: 0.3 + - id: ClockworkWindow + prob: 0.2 + - id: PinionAirlock + prob: 0.01 + - id: WindoorClockwork + prob: 0.005 + - id: SalvageSpawnerScrapBrass75 + prob: 1 + - id: SalvagePartsT2Spawner + prob: 0.1 + - id: SalvagePartsT3Spawner + prob: 0.05 + - id: SalvagePartsT4Spawner + prob: 0.01 + - id: NFSalvageBrassFurnitureSpawner + prob: 0.03 + - id: AltarTechnology + prob: 0.03 + - id: RipleyChassis + prob: 0.03 + - id: PlushieRatvar + prob: 0.005 + PlatingBrass: *brassFloor + FloorBrassReebe: + - prob: 1 + - id: ClockworkGrille + prob: 0.75 + - id: ClockworkGrilleBroken + prob: 0.25 \ No newline at end of file From de702d4be11f9cea92b93deac8136501959cad2a Mon Sep 17 00:00:00 2001 From: fenndragon Date: Mon, 27 Apr 2026 17:20:45 -0600 Subject: [PATCH 03/49] fix --- .../SectorAsteroidBiomePrototype.cs | 39 ------------------- .../Systems/SectorChunkCarverSystem.cs | 29 ++++++++++++-- .../SectorAsteroidBiomePrototype.cs | 20 ++++++++++ 3 files changed, 45 insertions(+), 43 deletions(-) delete mode 100644 Content.Server/Worldgen/Prototypes/SectorAsteroidBiomePrototype.cs create mode 100644 Content.Shared/Worldgen/Prototypes/SectorAsteroidBiomePrototype.cs diff --git a/Content.Server/Worldgen/Prototypes/SectorAsteroidBiomePrototype.cs b/Content.Server/Worldgen/Prototypes/SectorAsteroidBiomePrototype.cs deleted file mode 100644 index c9e531a93e5..00000000000 --- a/Content.Server/Worldgen/Prototypes/SectorAsteroidBiomePrototype.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Linq; -using Content.Server.Worldgen.Tools; -using Content.Shared.Maps; -using Content.Shared.Storage; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; - -namespace Content.Server.Worldgen.Prototypes; - -[Prototype("sectorAsteroidBiome")] -public sealed partial class SectorAsteroidBiomePrototype : IPrototype -{ - private Dictionary? _caches; - - [IdDataField] - public string ID { get; private set; } = string.Empty; - - [DataField("floorTiles", required: true)] - public List FloorTiles = new(); - - [DataField("entries", required: true, - customTypeSerializer: typeof(PrototypeIdDictionarySerializer, ContentTileDefinition>))] - private Dictionary> _entries = default!; - - public Dictionary Caches - { - get - { - if (_caches == null) - { - _caches = _entries - .Select(pair => new KeyValuePair(pair.Key, new EntitySpawnCollectionCache(pair.Value))) - .ToDictionary(pair => pair.Key, pair => pair.Value); - } - - return _caches; - } - } -} \ No newline at end of file diff --git a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs index 5b5e55ee24a..faccb688472 100644 --- a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs +++ b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs @@ -2,9 +2,10 @@ using System.IO; using System.Text; using Content.Server.Worldgen.Components; -using Content.Server.Worldgen.Prototypes; +using Content.Server.Worldgen.Tools; using Content.Shared.Maps; using Content.Shared.Storage; +using Content.Shared.Worldgen.Prototypes; using Robust.Shared.Maths; using Robust.Shared.Map; using Robust.Shared.Map.Components; @@ -41,6 +42,7 @@ public sealed class SectorChunkCarverSystem : EntitySystem [Dependency] private readonly IRobustRandom _random = default!; private string _cacheDirectory = string.Empty; + private readonly Dictionary> _biomeCaches = new(); public override void Initialize() { @@ -195,10 +197,15 @@ private void SaveChunkToCache(Entity ent, EntityUid foreach (var generated in ent.Comp.GeneratedEntities) { - if (!Exists(generated) || !TryComp(generated, out var meta) || meta.EntityPrototype == null) + if (!Exists(generated)) continue; - if (!TryComp(generated, out var xform) || xform.GridUid != gridUid) + var meta = MetaData(generated); + if (meta.EntityPrototype == null || meta.EntityLifeStage >= EntityLifeStage.Terminating) + continue; + + var xform = Transform(generated); + if (xform.GridUid != gridUid) continue; var indices = _mapSystem.TileIndicesFor(gridUid, grid, xform.Coordinates); @@ -329,7 +336,8 @@ private void SpawnChunkEntities(Entity ent, EntityUi var tileId = tile.Tile.GetContentTileDefinition(_tileDefs).ID; var handledByBiome = false; - if (biome != null && biome.Caches.TryGetValue(tileId, out var cache)) + var biomeCache = GetBiomeCache(biome); + if (biomeCache != null && biomeCache.TryGetValue(tileId, out var cache)) { handledByBiome = true; spawns.Clear(); @@ -356,6 +364,19 @@ private void SpawnChunkEntities(Entity ent, EntityUi } } + private Dictionary? GetBiomeCache(SectorAsteroidBiomePrototype? biome) + { + if (biome == null) + return null; + + if (_biomeCaches.TryGetValue(biome.ID, out var cache)) + return cache; + + cache = biome.Entries.ToDictionary(pair => pair.Key, pair => new EntitySpawnCollectionCache(pair.Value)); + _biomeCaches[biome.ID] = cache; + return cache; + } + private List> GetBlockingGrids(EntityUid sectorMap, EntityUid sectorGridUid, WorldChunkComponent chunk) { var results = new List>(); diff --git a/Content.Shared/Worldgen/Prototypes/SectorAsteroidBiomePrototype.cs b/Content.Shared/Worldgen/Prototypes/SectorAsteroidBiomePrototype.cs new file mode 100644 index 00000000000..fed6efded6c --- /dev/null +++ b/Content.Shared/Worldgen/Prototypes/SectorAsteroidBiomePrototype.cs @@ -0,0 +1,20 @@ +using Content.Shared.Maps; +using Content.Shared.Storage; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; + +namespace Content.Shared.Worldgen.Prototypes; + +[Prototype("sectorAsteroidBiome")] +public sealed partial class SectorAsteroidBiomePrototype : IPrototype +{ + [IdDataField] + public string ID { get; private set; } = string.Empty; + + [DataField("floorTiles", required: true)] + public List FloorTiles = new(); + + [DataField("entries", required: true, + customTypeSerializer: typeof(PrototypeIdDictionarySerializer, ContentTileDefinition>))] + public Dictionary> Entries = default!; +} \ No newline at end of file From 9ca82ee5fba11f269e482107c05895ced0456c47 Mon Sep 17 00:00:00 2001 From: fenndragon Date: Tue, 28 Apr 2026 16:47:09 -0600 Subject: [PATCH 04/49] planetside generation --- .../Expeditions/SalvageExpeditionComponent.cs | 21 ++++ .../SalvageSystem.ExpeditionConsole.cs | 16 +++ .../Salvage/SalvageSystem.Expeditions.cs | 31 +++++ .../Salvage/SpawnSalvageMissionJob.cs | 109 +++++++++++++----- .../Components/SectorWorldComponent.cs | 3 + .../Systems/SectorChunkCarverSystem.cs | 1 + .../Worldgen/Systems/SectorWorldSystem.cs | 5 +- .../en-US/_NF/procedural/expeditions.ftl | 1 + .../Prototypes/World/worldgen_default.yml | 27 +++++ .../Prototypes/_NF/World/worldgen_default.yml | 27 +++++ 10 files changed, 213 insertions(+), 28 deletions(-) diff --git a/Content.Server/Salvage/Expeditions/SalvageExpeditionComponent.cs b/Content.Server/Salvage/Expeditions/SalvageExpeditionComponent.cs index a6a3441e677..c6412a30830 100644 --- a/Content.Server/Salvage/Expeditions/SalvageExpeditionComponent.cs +++ b/Content.Server/Salvage/Expeditions/SalvageExpeditionComponent.cs @@ -1,7 +1,10 @@ +using System.Collections.Generic; using System.Numerics; using Content.Shared.Salvage; using Content.Shared.Salvage.Expeditions; using Robust.Shared.Audio; +using Robust.Shared.Maths; +using Robust.Shared.Map; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; @@ -50,6 +53,24 @@ public sealed partial class SalvageExpeditionComponent : SharedSalvageExpedition [ViewVariables] public bool ReturnTriggered = false; + /// + /// Persistent grid that physically hosts the expedition content. + /// + [ViewVariables] + public EntityUid HostGridUid = EntityUid.Invalid; + + /// + /// Runtime-only entity snapshot for content stamped onto a persistent host grid. + /// + [ViewVariables] + public HashSet GeneratedEntities = new(); + + /// + /// Runtime-only tile snapshot used to restore the host grid when the expedition is removed. + /// + [ViewVariables] + public Dictionary OriginalTiles = new(); + // Frontier: moved to Client /// /// Countdown audio stream. diff --git a/Content.Server/Salvage/SalvageSystem.ExpeditionConsole.cs b/Content.Server/Salvage/SalvageSystem.ExpeditionConsole.cs index 0620e1320bb..c822376b035 100644 --- a/Content.Server/Salvage/SalvageSystem.ExpeditionConsole.cs +++ b/Content.Server/Salvage/SalvageSystem.ExpeditionConsole.cs @@ -1,6 +1,7 @@ using Content.Shared.Shuttles.Components; using Content.Shared.Procedural; using Content.Shared.Salvage.Expeditions; +using Content.Shared.Salvage.Expeditions.Modifiers; using Content.Shared.Dataset; using Robust.Shared.Prototypes; using Content.Shared.Popups; // Frontier @@ -166,8 +167,23 @@ private void OnSalvageClaimMessage(EntityUid uid, SalvageExpeditionConsoleCompon { var filter = Filter.Empty().AddInGrid(consoleXform.GridUid.Value); var announcement = Loc.GetString("salvage-expedition-announcement-claimed"); + var biomeProto = _prototypeManager.Index(mission.Biome); + var biome = string.IsNullOrWhiteSpace(Loc.GetString(biomeProto.Description)) + ? Loc.GetString(biomeProto.ID) + : Loc.GetString(biomeProto.Description); + var objective = Loc.GetString($"salvage-expedition-type-{missionparams.MissionType}"); + var difficulty = Loc.GetString($"salvage-expedition-difficulty-{missionparams.Difficulty}"); _chatSystem.DispatchFilteredAnnouncement(filter, announcement, uid, sender: "Expedition Console", colorOverride: Color.LightBlue); + _chatSystem.DispatchFilteredAnnouncement( + filter, + Loc.GetString("salvage-expedition-announcement-briefing", + ("objective", objective), + ("difficulty", difficulty), + ("biome", biome)), + uid, + sender: "Expedition Console", + colorOverride: Color.LightBlue); } Log.Info($"Mission {args.Index} successfully claimed on independent console {ToPrettyString(uid)}"); diff --git a/Content.Server/Salvage/SalvageSystem.Expeditions.cs b/Content.Server/Salvage/SalvageSystem.Expeditions.cs index c02c19b7ba7..a5f08b4d357 100644 --- a/Content.Server/Salvage/SalvageSystem.Expeditions.cs +++ b/Content.Server/Salvage/SalvageSystem.Expeditions.cs @@ -147,6 +147,8 @@ private void OnExpeditionShutdown(EntityUid uid, SalvageExpeditionComponent comp } } + CleanupHostedExpeditionContent(component); + // HARDLIGHT: Handle round persistence - station might be deleted during round transitions if (Deleted(component.Station)) { @@ -169,6 +171,35 @@ private void OnExpeditionShutdown(EntityUid uid, SalvageExpeditionComponent comp } } + private void CleanupHostedExpeditionContent(SalvageExpeditionComponent component) + { + foreach (var generated in component.GeneratedEntities) + { + if (Exists(generated)) + QueueDel(generated); + } + + component.GeneratedEntities.Clear(); + + if (component.HostGridUid == EntityUid.Invalid || component.OriginalTiles.Count == 0) + return; + + if (!TryComp(component.HostGridUid, out var grid)) + { + component.OriginalTiles.Clear(); + return; + } + + var tiles = new List<(Vector2i, Tile)>(component.OriginalTiles.Count); + foreach (var (indices, tile) in component.OriginalTiles) + { + tiles.Add((indices, tile)); + } + + _mapSystem.SetTiles(component.HostGridUid, grid, tiles); + component.OriginalTiles.Clear(); + } + private void UpdateExpeditions() { var currentTime = _timing.CurTime; diff --git a/Content.Server/Salvage/SpawnSalvageMissionJob.cs b/Content.Server/Salvage/SpawnSalvageMissionJob.cs index 490a3e5c23b..c014b0474b3 100644 --- a/Content.Server/Salvage/SpawnSalvageMissionJob.cs +++ b/Content.Server/Salvage/SpawnSalvageMissionJob.cs @@ -3,6 +3,7 @@ using System.Numerics; using System.Threading; using System.Threading.Tasks; +using Content.Server._Mono.Cleanup; using Content.Server.Atmos; using Content.Server.Atmos.Components; using Content.Server.Atmos.EntitySystems; @@ -72,6 +73,7 @@ public sealed class SpawnSalvageMissionJob : Job // Frontier: Used for saving state between async job #pragma warning disable IDE1006 // suppressing prefix warnings to reduce merge conflict area private EntityUid mapUid = EntityUid.Invalid; + private EntityUid hostGridUid = EntityUid.Invalid; #pragma warning restore IDE1006 private static readonly ProtoId FallbackDifficulty = "NFModerate"; private static readonly ProtoId PlanetNamesId = "NamesBorer"; @@ -221,9 +223,9 @@ private async Task InternalProcess() // Frontier: make process an internal return false; var mapId = _entManager.GetComponent(hostMapUid).MapId; - var spawnedGrid = _mapManager.CreateGridEntity(mapId); - mapUid = spawnedGrid.Owner; - var grid = spawnedGrid.Comp; + hostGridUid = hostMapUid; + var grid = _entManager.EnsureComponent(hostGridUid); + mapUid = _entManager.SpawnEntity(null, new MapCoordinates(Vector2.Zero, mapId)); if (!_sectorWorld.TryReserveExpeditionSite(_missionParams.Seed, mapUid, planetTypeId, out var placement)) { @@ -231,7 +233,8 @@ private async Task InternalProcess() // Frontier: make process an internal return false; } - _entManager.System().SetCoordinates(mapUid, new EntityCoordinates(placement.SectorMap, placement.Center)); + _entManager.System().SetCoordinates(mapUid, new EntityCoordinates(hostGridUid, placement.Center)); + _entManager.EnsureComponent(mapUid); var site = _entManager.EnsureComponent(mapUid); site.SectorMap = placement.SectorMap; site.PlanetId = placement.Planet.PlanetId; @@ -256,26 +259,17 @@ private async Task InternalProcess() // Frontier: make process an internal _entManager.Dirty(CoordinatesDisk.Value, cd); } - if (missionBiome.BiomePrototype != null) - { - var biome = _entManager.AddComponent(mapUid); - var biomeSystem = _entManager.System(); - biomeSystem.SetTemplate(mapUid, biome, _prototypeManager.Index(missionBiome.BiomePrototype)); - biomeSystem.SetSeed(mapUid, biome, mission.Seed); - _entManager.Dirty(mapUid, biome); - - // Gravity - var gravity = _entManager.EnsureComponent(mapUid); - gravity.Enabled = true; - _entManager.Dirty(mapUid, gravity, metadata); - } - // Setup expedition var expedition = _entManager.AddComponent(mapUid); expedition.Station = Station; expedition.Console = Console; // HARDLIGHT: Store console reference for FTL targeting expedition.MissionParams = _missionParams; expedition.SelectedSong = _audio.ResolveSound(expedition.Sound); + expedition.HostGridUid = hostGridUid; + + var captureRadius = placement.ReservationRadius + 32f; + CaptureOriginalTiles(expedition, hostGridUid, grid, placement.Center, captureRadius); + var existingEntities = CaptureNearbyEntities(hostMapUid, placement.Center, captureRadius); var landingPadRadius = 4; // Frontier: 24<4 - using this as a margin (4-16), not a radius var minDungeonOffset = landingPadRadius + 4; @@ -287,9 +281,11 @@ private async Task InternalProcess() // Frontier: make process an internal var dungeonOffsetDistance = minDungeonOffset + (maxDungeonOffset - minDungeonOffset) * random.NextFloat(); var dungeonOffset = new Vector2(0f, dungeonOffsetDistance); dungeonOffset = dungeonRotation.RotateVec(dungeonOffset); + var expeditionOrigin = placement.Center.Rounded(); + var dungeonOrigin = expeditionOrigin + dungeonOffset; var dungeonMod = _prototypeManager.Index(mission.Dungeon); var dungeonConfig = _prototypeManager.Index(dungeonMod.Proto); - var dungeons = await WaitAsyncTask(_dungeon.GenerateDungeonAsync(dungeonConfig, dungeonMod.Proto, mapUid, grid, (Vector2i)dungeonOffset, // Frontier: add dungeonMod.Proto + var dungeons = await WaitAsyncTask(_dungeon.GenerateDungeonAsync(dungeonConfig, dungeonMod.Proto, hostGridUid, grid, (Vector2i)dungeonOrigin, // Frontier: add dungeonMod.Proto _missionParams.Seed)); var dungeon = dungeons.First(); @@ -300,13 +296,13 @@ private async Task InternalProcess() // Frontier: make process an internal return false; } - expedition.DungeonLocation = dungeonOffset; + expedition.DungeonLocation = dungeonOrigin - expeditionOrigin; // Frontier: map generation and offset #region Frontier map generation // Get map bounding box - Box2 dungeonBox = new Box2(dungeonOffset, dungeonOffset); + Box2 dungeonBox = new Box2(dungeonOrigin, dungeonOrigin); foreach (var tile in dungeon.AllTiles) { dungeonBox = dungeonBox.ExtendToContain(tile); @@ -395,7 +391,7 @@ private async Task InternalProcess() // Frontier: make process an internal try { - await SpawnDungeonLoot(lootProto, mapUid); + await SpawnDungeonLoot(lootProto, hostGridUid); } catch (Exception e) { @@ -432,7 +428,7 @@ private async Task InternalProcess() // Frontier: make process an internal try { - await SpawnRandomEntry((mapUid, grid), entry, dungeon, random); + await SpawnRandomEntry((hostGridUid, grid), entry, dungeon, random); } catch (Exception e) { @@ -467,7 +463,7 @@ private async Task InternalProcess() // Frontier: make process an internal break; _sawmill.Debug($"Spawning dungeon loot {entry.Proto}"); - await SpawnRandomEntry((mapUid, grid), entry, dungeon, random); + await SpawnRandomEntry((hostGridUid, grid), entry, dungeon, random); } break; default: @@ -475,6 +471,8 @@ private async Task InternalProcess() // Frontier: make process an internal } } + CaptureGeneratedEntities(expedition, existingEntities, hostMapUid, placement.Center, captureRadius); + // Frontier: delay ship FTL if (shuttleUid is { Valid: true }) { @@ -487,7 +485,7 @@ private async Task InternalProcess() // Frontier: make process an internal } else { - _shuttle.FTLToCoordinates(shuttleUid.Value, shuttle, new EntityCoordinates(mapUid, coords), 0f, 5.5f, _salvage.TravelTime); + _shuttle.FTLToCoordinates(shuttleUid.Value, shuttle, new EntityCoordinates(hostGridUid, coords), 0f, 5.5f, _salvage.TravelTime); } } // End Frontier @@ -495,6 +493,63 @@ private async Task InternalProcess() // Frontier: make process an internal return true; } + private void CaptureOriginalTiles(SalvageExpeditionComponent expedition, EntityUid gridUid, MapGridComponent grid, Vector2 center, float radius) + { + expedition.OriginalTiles.Clear(); + + foreach (var tile in _map.GetTilesIntersecting(gridUid, grid, new Circle(center, radius), false)) + { + expedition.OriginalTiles[tile.GridIndices] = tile.Tile; + } + } + + private HashSet CaptureNearbyEntities(EntityUid mapUid, Vector2 center, float radius) + { + var entities = new HashSet(); + var radiusSquared = radius * radius; + var query = _entManager.AllEntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var xform)) + { + if (uid == mapUid || uid == hostGridUid || uid == this.mapUid) + continue; + + if (xform.MapUid != mapUid) + continue; + + var pos = xform.Coordinates.Position; + if ((pos - center).LengthSquared() > radiusSquared) + continue; + + entities.Add(uid); + } + + return entities; + } + + private void CaptureGeneratedEntities(SalvageExpeditionComponent expedition, HashSet existingEntities, EntityUid mapUid, Vector2 center, float radius) + { + expedition.GeneratedEntities.Clear(); + + var radiusSquared = radius * radius; + var query = _entManager.AllEntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var xform)) + { + if (uid == mapUid || uid == hostGridUid || uid == this.mapUid || existingEntities.Contains(uid)) + continue; + + if (xform.MapUid != mapUid) + continue; + + var pos = xform.Coordinates.Position; + if ((pos - center).LengthSquared() > radiusSquared) + continue; + + expedition.GeneratedEntities.Add(uid); + } + } + private async Task SpawnRandomEntry(Entity grid, IBudgetEntry entry, Dungeon dungeon, Random random) { await SuspendIfOutOfTime(); @@ -592,7 +647,7 @@ private async Task SetupStructure( continue; } - var uid = _entManager.SpawnEntity(shaggy, _map.GridTileToLocal(mapUid, grid, tile)); + var uid = _entManager.SpawnEntity(shaggy, _map.GridTileToLocal(hostGridUid, grid, tile)); _entManager.AddComponent(uid); structureComp.Structures.Add(uid); break; @@ -633,7 +688,7 @@ private async Task SetupElimination( continue; } - uid = _entManager.SpawnAtPosition(prototype, _map.GridTileToLocal(mapUid, grid, tile)); + uid = _entManager.SpawnAtPosition(prototype, _map.GridTileToLocal(hostGridUid, grid, tile)); break; } } diff --git a/Content.Server/Worldgen/Components/SectorWorldComponent.cs b/Content.Server/Worldgen/Components/SectorWorldComponent.cs index b0d192de374..5b34e79e039 100644 --- a/Content.Server/Worldgen/Components/SectorWorldComponent.cs +++ b/Content.Server/Worldgen/Components/SectorWorldComponent.cs @@ -61,6 +61,9 @@ public sealed partial class SectorPlanetTypeDefinition [DataField(required: true)] public string BiomeTemplate = string.Empty; + [DataField] + public List BiomeAliases = new(); + [DataField(required: true)] public List SurfaceTiles = new(); diff --git a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs index faccb688472..71eba93e649 100644 --- a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs +++ b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs @@ -1,5 +1,6 @@ using System.Numerics; using System.IO; +using System.Linq; using System.Text; using Content.Server.Worldgen.Components; using Content.Server.Worldgen.Tools; diff --git a/Content.Server/Worldgen/Systems/SectorWorldSystem.cs b/Content.Server/Worldgen/Systems/SectorWorldSystem.cs index 61cc820e4c6..867f9d17d81 100644 --- a/Content.Server/Worldgen/Systems/SectorWorldSystem.cs +++ b/Content.Server/Worldgen/Systems/SectorWorldSystem.cs @@ -112,7 +112,10 @@ public bool TryResolvePlanetTypeForBiome(string? biomeTemplateId, out string? pl return false; EnsureInitialized((sectorMap, sector)); - var match = sector.PlanetTypes.FirstOrDefault(candidate => candidate.BiomeTemplate == biomeTemplateId); + var match = sector.PlanetTypes.FirstOrDefault(candidate => + string.Equals(candidate.BiomeTemplate, biomeTemplateId, StringComparison.OrdinalIgnoreCase) + || candidate.BiomeAliases.Any(alias => string.Equals(alias, biomeTemplateId, StringComparison.OrdinalIgnoreCase))); + if (match == null) return false; diff --git a/Resources/Locale/en-US/_NF/procedural/expeditions.ftl b/Resources/Locale/en-US/_NF/procedural/expeditions.ftl index 99c1a252898..5872e2a53b6 100644 --- a/Resources/Locale/en-US/_NF/procedural/expeditions.ftl +++ b/Resources/Locale/en-US/_NF/procedural/expeditions.ftl @@ -1,6 +1,7 @@ salvage-expedition-window-finish = Finish expedition salvage-expedition-window-refresh = Refresh salvage-expedition-announcement-early-finish = The expedition was completed ahead of schedule. Shuttle will depart in {$departTime} seconds. +salvage-expedition-announcement-briefing = Objective: {$objective}. Difficulty: {$difficulty}. Biome: {$biome}. salvage-expedition-announcement-destruction = { $count -> [1] Destroy the {$structure} before the expedition ends. *[others] Destroy {$count} {MAKEPLURAL($structure)} before the expedition ends. diff --git a/Resources/Prototypes/World/worldgen_default.yml b/Resources/Prototypes/World/worldgen_default.yml index 3bc0faead83..1c7ba6c89ce 100644 --- a/Resources/Prototypes/World/worldgen_default.yml +++ b/Resources/Prototypes/World/worldgen_default.yml @@ -5,9 +5,33 @@ - type: WorldController - type: SectorWorld planetTypes: + - id: grassland + name: Verdure + biomeTemplate: Grasslands + biomeAliases: [Grasslands] + surfaceTiles: [FloorPlanetGrass] + weatherPrototype: Rain + minTemperature: 270 + maxTemperature: 320 + minOxygen: 12 + maxOxygen: 24 + minNitrogen: 32 + maxNitrogen: 82 + - id: caves + name: Hollow + biomeTemplate: NFVGRoidCaves + biomeAliases: [Caves] + surfaceTiles: [FloorCaveDrought] + minTemperature: 250 + maxTemperature: 310 + minOxygen: 2 + maxOxygen: 16 + minNitrogen: 10 + maxNitrogen: 42 - id: lava name: Cinder biomeTemplate: NFVGRoidLava + biomeAliases: [Lava] surfaceTiles: [FloorBasalt] weatherPrototype: Ashfall minTemperature: 340 @@ -15,6 +39,7 @@ - id: tundra name: Rime biomeTemplate: NFVGRoidSnow + biomeAliases: [Snow] surfaceTiles: [FloorSnow, FloorIce] weatherPrototype: SnowfallHeavy minTemperature: 190 @@ -26,6 +51,7 @@ - id: shadow name: Umbra biomeTemplate: NFVGRoidShadow + biomeAliases: [Shadow] surfaceTiles: [FloorChromite] minTemperature: 250 maxTemperature: 320 @@ -36,6 +62,7 @@ - id: scrap name: Husk biomeTemplate: NFVGRoidScrapyard + biomeAliases: [Scrapyard] surfaceTiles: [Lattice] weatherPrototype: Rain minTemperature: 260 diff --git a/Resources/Prototypes/_NF/World/worldgen_default.yml b/Resources/Prototypes/_NF/World/worldgen_default.yml index 057d9a2a34d..539dc46f27d 100644 --- a/Resources/Prototypes/_NF/World/worldgen_default.yml +++ b/Resources/Prototypes/_NF/World/worldgen_default.yml @@ -5,9 +5,33 @@ - type: WorldController - type: SectorWorld planetTypes: + - id: grassland + name: Verdure + biomeTemplate: Grasslands + biomeAliases: [Grasslands] + surfaceTiles: [FloorPlanetGrass] + weatherPrototype: Rain + minTemperature: 270 + maxTemperature: 320 + minOxygen: 12 + maxOxygen: 24 + minNitrogen: 32 + maxNitrogen: 82 + - id: caves + name: Hollow + biomeTemplate: NFVGRoidCaves + biomeAliases: [Caves] + surfaceTiles: [FloorCaveDrought] + minTemperature: 250 + maxTemperature: 310 + minOxygen: 2 + maxOxygen: 16 + minNitrogen: 10 + maxNitrogen: 42 - id: lava name: Cinder biomeTemplate: NFVGRoidLava + biomeAliases: [Lava] surfaceTiles: [FloorBasalt] weatherPrototype: Ashfall minTemperature: 340 @@ -15,6 +39,7 @@ - id: tundra name: Rime biomeTemplate: NFVGRoidSnow + biomeAliases: [Snow] surfaceTiles: [FloorSnow, FloorIce] weatherPrototype: SnowfallHeavy minTemperature: 190 @@ -26,6 +51,7 @@ - id: shadow name: Umbra biomeTemplate: NFVGRoidShadow + biomeAliases: [Shadow] surfaceTiles: [FloorChromite] minTemperature: 250 maxTemperature: 320 @@ -36,6 +62,7 @@ - id: scrap name: Husk biomeTemplate: NFVGRoidScrapyard + biomeAliases: [Scrapyard] surfaceTiles: [Lattice] weatherPrototype: Rain minTemperature: 260 From b30eca61acfcea9b11eea5be26d91decae937560 Mon Sep 17 00:00:00 2001 From: fenndragon Date: Tue, 28 Apr 2026 21:14:08 -0600 Subject: [PATCH 05/49] fixes --- .../Systems/SectorChunkCarverSystem.cs | 110 +++++++++++++++++- .../Worldgen/Systems/SectorWorldSystem.cs | 13 +++ 2 files changed, 117 insertions(+), 6 deletions(-) diff --git a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs index 71eba93e649..b15df3a3e22 100644 --- a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs +++ b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs @@ -2,6 +2,7 @@ using System.IO; using System.Linq; using System.Text; +using Robust.Server.GameObjects; using Content.Server.Worldgen.Components; using Content.Server.Worldgen.Tools; using Content.Shared.Maps; @@ -10,6 +11,7 @@ using Robust.Shared.Maths; using Robust.Shared.Map; using Robust.Shared.Map.Components; +using Robust.Shared.Physics; using Robust.Shared.Prototypes; using Robust.Shared.Random; @@ -41,6 +43,8 @@ public sealed class SectorChunkCarverSystem : EntitySystem [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; private string _cacheDirectory = string.Empty; private readonly Dictionary> _biomeCaches = new(); @@ -275,8 +279,8 @@ private bool TryRestoreChunkFromCache(Entity ent, Wo if (!_proto.HasIndex(entityPlacement.PrototypeId)) continue; - var spawned = Spawn(entityPlacement.PrototypeId, new EntityCoordinates(gridUid, entityPlacement.Indices + new Vector2(0.5f, 0.5f))); - ent.Comp.GeneratedEntities.Add(spawned); + ClearChunkMaterialEntitiesAtTile((ent.Owner, ent.Comp), gridUid, grid, entityPlacement.Indices); + SpawnTrackedTileEntity((ent.Owner, ent.Comp), gridUid, grid, entityPlacement.Indices, entityPlacement.PrototypeId); } } else @@ -336,6 +340,7 @@ private void SpawnChunkEntities(Entity ent, EntityUi var tileId = tile.Tile.GetContentTileDefinition(_tileDefs).ID; var handledByBiome = false; + ClearChunkMaterialEntitiesAtTile(ent, gridUid, grid, indices); var biomeCache = GetBiomeCache(biome); if (biomeCache != null && biomeCache.TryGetValue(tileId, out var cache)) @@ -349,8 +354,7 @@ private void SpawnChunkEntities(Entity ent, EntityUi if (prototype == null || !_proto.HasIndex(prototype)) continue; - var spawned = Spawn(prototype, new EntityCoordinates(gridUid, indices + new Vector2(0.5f, 0.5f))); - ent.Comp.GeneratedEntities.Add(spawned); + SpawnTrackedTileEntity(ent, gridUid, grid, indices, prototype); } } @@ -360,11 +364,105 @@ private void SpawnChunkEntities(Entity ent, EntityUi if (!TryGetPlanetWallPrototype(gridUid, grid, indices, out var wallPrototype)) continue; - var fallback = Spawn(wallPrototype, new EntityCoordinates(gridUid, indices + new Vector2(0.5f, 0.5f))); - ent.Comp.GeneratedEntities.Add(fallback); + SpawnTrackedTileEntity(ent, gridUid, grid, indices, wallPrototype); } } + private void ClearChunkMaterialEntitiesAtTile(Entity ent, EntityUid gridUid, MapGridComponent grid, Vector2i indices) + { + var tileRef = _mapSystem.GetTileRef(gridUid, grid, indices); + + foreach (var entity in _lookup.GetLocalEntitiesIntersecting(tileRef, flags: LookupFlags.Dynamic | LookupFlags.Static | LookupFlags.StaticSundries | LookupFlags.Sundries | LookupFlags.Approximate)) + { + if (entity == gridUid) + continue; + + if (!TryComp(entity, out var meta) || meta.EntityPrototype == null) + continue; + + if (!IsChunkMaterialPrototype(meta.EntityPrototype.ID)) + continue; + + ent.Comp.GeneratedEntities.Remove(entity); + + if (Exists(entity)) + QueueDel(entity); + } + } + + private void SpawnTrackedTileEntity(Entity ent, EntityUid gridUid, MapGridComponent grid, Vector2i indices, string prototypeId) + { + var before = GetTileEntities(gridUid, grid, indices); + var spawned = Spawn(prototypeId, new EntityCoordinates(gridUid, indices + new Vector2(0.5f, 0.5f))); + var after = GetTileEntities(gridUid, grid, indices); + + foreach (var entity in after) + { + if (before.Contains(entity)) + continue; + + if (!TryComp(entity, out var meta) || meta.EntityPrototype == null) + continue; + + if (IsTransientChunkSpawnerPrototype(meta.EntityPrototype.ID)) + { + if (Exists(entity)) + QueueDel(entity); + + continue; + } + + AnchorToGrid(entity); + ent.Comp.GeneratedEntities.Add(entity); + } + + if (!ent.Comp.GeneratedEntities.Contains(spawned) && Exists(spawned)) + { + if (TryComp(spawned, out var spawnedMeta) && spawnedMeta.EntityPrototype != null) + { + if (IsTransientChunkSpawnerPrototype(spawnedMeta.EntityPrototype.ID)) + { + QueueDel(spawned); + return; + } + } + + AnchorToGrid(spawned); + ent.Comp.GeneratedEntities.Add(spawned); + } + } + + private HashSet GetTileEntities(EntityUid gridUid, MapGridComponent grid, Vector2i indices) + { + var tileRef = _mapSystem.GetTileRef(gridUid, grid, indices); + return _lookup.GetLocalEntitiesIntersecting(tileRef, flags: LookupFlags.Dynamic | LookupFlags.Static | LookupFlags.StaticSundries | LookupFlags.Sundries | LookupFlags.Approximate).ToHashSet(); + } + + private void AnchorToGrid(EntityUid entity) + { + if (!TryComp(entity, out var xform) || xform.Anchored) + return; + + _transform.AnchorEntity(entity, xform); + } + + private static bool IsTransientChunkSpawnerPrototype(string prototypeId) + { + return prototypeId.Contains("Mineral", StringComparison.OrdinalIgnoreCase) + || prototypeId.EndsWith("RoomMarker", StringComparison.OrdinalIgnoreCase) + || prototypeId.EndsWith("Spawner", StringComparison.OrdinalIgnoreCase); + } + + private static bool IsChunkMaterialPrototype(string prototypeId) + { + return prototypeId.StartsWith("Wall", StringComparison.OrdinalIgnoreCase) + || prototypeId.StartsWith("NFWall", StringComparison.OrdinalIgnoreCase) + || prototypeId.EndsWith("RoomMarker", StringComparison.OrdinalIgnoreCase) + || prototypeId.Contains("Mineral", StringComparison.OrdinalIgnoreCase) + || prototypeId.EndsWith("Spawner", StringComparison.OrdinalIgnoreCase) + || string.Equals(prototypeId, "Grille", StringComparison.OrdinalIgnoreCase); + } + private Dictionary? GetBiomeCache(SectorAsteroidBiomePrototype? biome) { if (biome == null) diff --git a/Content.Server/Worldgen/Systems/SectorWorldSystem.cs b/Content.Server/Worldgen/Systems/SectorWorldSystem.cs index 867f9d17d81..f061cd227c1 100644 --- a/Content.Server/Worldgen/Systems/SectorWorldSystem.cs +++ b/Content.Server/Worldgen/Systems/SectorWorldSystem.cs @@ -1,6 +1,7 @@ using System.Numerics; using System.Linq; using Content.Server._Mono.Cleanup; +using Content.Server._NF.Shuttles.Components; using Content.Server.Atmos.EntitySystems; using Content.Server.GameTicking; using Content.Server.Parallax; @@ -13,8 +14,11 @@ using Content.Shared.Parallax.Biomes; using Content.Shared.Shuttles.Components; using Content.Shared.Weather; +using Robust.Server.GameObjects; using Robust.Shared.Map; using Robust.Shared.Map.Components; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Components; using Robust.Shared.Prototypes; using Robust.Shared.Random; @@ -34,6 +38,7 @@ public sealed class SectorWorldSystem : EntitySystem [Dependency] private readonly BiomeSystem _biome = default!; [Dependency] private readonly MetaDataSystem _metaData = default!; [Dependency] private readonly SharedMapSystem _mapSystem = default!; + [Dependency] private readonly PhysicsSystem _physics = default!; [Dependency] private readonly ITileDefinitionManager _tileDefs = default!; [Dependency] private readonly WeatherSystem _weather = default!; @@ -416,6 +421,14 @@ private void EnsurePersistentWorldGrid(EntityUid mapOrGridUid) return; EnsureComp(mapOrGridUid); + EnsureComp(mapOrGridUid); + + var physics = EnsureComp(mapOrGridUid); + if (physics.BodyType != BodyType.Static) + _physics.SetBodyType(mapOrGridUid, BodyType.Static, body: physics); + + _physics.SetBodyStatus(mapOrGridUid, physics, BodyStatus.OnGround); + _physics.SetFixedRotation(mapOrGridUid, true, body: physics); if (TryComp(mapOrGridUid, out var grid)) grid.CanSplit = false; From 0a3302add4937fb0bc8f518da2c95461ba77aeec Mon Sep 17 00:00:00 2001 From: Kyle Tyo <36606155+VerinSenpai@users.noreply.github.com> Date: Sun, 8 Jun 2025 20:09:42 -0400 Subject: [PATCH 06/49] MapManager warnings cleanup Server Edition 2003 (#36781) * now you see me * unused depen * test fail fix attempt 1 * test fail fix attempt 2 * fix test fail attempt 3 * shot in the dark. * Does this work? * import cleanup * taking a shot at this. * Convert PersistenceSaveCommand to LocalizedEntityCommands. * requested changes * requested changes. also dealt with improperly named private const * Update Content.Server/GameTicking/GameTicker.Spawning.cs Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Update Content.Server/GameTicking/GameTicker.Spawning.cs Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Convert PlanetCommand to LocalizedEntityCommand * Update BiomeSystem.cs * Update Content.Server/Salvage/SalvageSystem.Runner.cs Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Update Content.Server/Procedural/DungeonSystem.Rooms.cs Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Update Content.Server/Salvage/SpawnSalvageMissionJob.cs Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Update Content.Server/Station/Systems/StationBiomeSystem.cs Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * revert to latest master. * slartis suggestion. * Update SetMapAtmosCommand.cs * cleanup * Update PersistenceSaveCommand.cs * finish localizing persistencesavecommand * this is icky, I change. * :sigh: * revert whatever I did here? * oh I see, some inconsistencies. * revert this * Update PlanetCommand.cs * move this ftl to the commands folder --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> --- .../Commands/PersistenceSaveCommand.cs | 7 +++---- Content.Server/Polymorph/Systems/PolymorphSystem.cs | 1 - .../Procedural/DungeonAtlasTemplateDespawnSystem.cs | 12 +----------- Content.Server/Salvage/SalvageSystem.Expeditions.cs | 6 ------ Content.Shared/Weather/SharedWeatherSystem.cs | 1 - .../en-US/commands/persistence-save-command.ftl | 3 +++ Resources/Locale/en-US/persistence/command.ftl | 1 - 7 files changed, 7 insertions(+), 24 deletions(-) create mode 100644 Resources/Locale/en-US/commands/persistence-save-command.ftl delete mode 100644 Resources/Locale/en-US/persistence/command.ftl diff --git a/Content.Server/Administration/Commands/PersistenceSaveCommand.cs b/Content.Server/Administration/Commands/PersistenceSaveCommand.cs index cae507f6d8e..9c9c0a88905 100644 --- a/Content.Server/Administration/Commands/PersistenceSaveCommand.cs +++ b/Content.Server/Administration/Commands/PersistenceSaveCommand.cs @@ -12,8 +12,8 @@ namespace Content.Server.Administration.Commands; public sealed class PersistenceSave : IConsoleCommand { [Dependency] private readonly IConfigurationManager _config = default!; - [Dependency] private readonly IEntitySystemManager _system = default!; - [Dependency] private readonly IMapManager _map = default!; + [Dependency] private readonly SharedMapSystem _map = default!; + [Dependency] private readonly MapLoaderSystem _mapLoader = default!; public string Command => "persistencesave"; public string Description => "Saves server data to a persistence file to be loaded later."; @@ -47,8 +47,7 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) return; } - var mapLoader = _system.GetEntitySystem(); - mapLoader.TrySaveMap(mapId, new ResPath(saveFilePath)); + _mapLoader.TrySaveMap(mapId, new ResPath(saveFilePath)); shell.WriteLine(Loc.GetString("cmd-savemap-success")); } } diff --git a/Content.Server/Polymorph/Systems/PolymorphSystem.cs b/Content.Server/Polymorph/Systems/PolymorphSystem.cs index e57914db67d..1c627d5526a 100644 --- a/Content.Server/Polymorph/Systems/PolymorphSystem.cs +++ b/Content.Server/Polymorph/Systems/PolymorphSystem.cs @@ -20,7 +20,6 @@ using Robust.Server.Audio; using Robust.Server.Containers; using Robust.Server.GameObjects; -using Robust.Shared.Map; using Robust.Shared.Prototypes; using Robust.Shared.Timing; using Robust.Shared.Utility; diff --git a/Content.Server/Procedural/DungeonAtlasTemplateDespawnSystem.cs b/Content.Server/Procedural/DungeonAtlasTemplateDespawnSystem.cs index 95e21620498..76bc84de067 100644 --- a/Content.Server/Procedural/DungeonAtlasTemplateDespawnSystem.cs +++ b/Content.Server/Procedural/DungeonAtlasTemplateDespawnSystem.cs @@ -1,24 +1,14 @@ using Robust.Shared.GameObjects; -using TimedDespawnComponent = Robust.Shared.Spawners.TimedDespawnComponent; namespace Content.Server.Procedural; /// -/// Ensures dungeon atlas template entities despawn after a fixed delay. +/// Dungeon atlas template entities now persist until explicitly cleaned up. /// public sealed class DungeonAtlasTemplateDespawnSystem : EntitySystem { - private static readonly TimeSpan DespawnDelay = TimeSpan.FromMinutes(30); - public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnInit); - } - - private void OnInit(Entity ent, ref ComponentInit args) - { - var despawn = EnsureComp(ent); - despawn.Lifetime = (float) DespawnDelay.TotalSeconds; } } diff --git a/Content.Server/Salvage/SalvageSystem.Expeditions.cs b/Content.Server/Salvage/SalvageSystem.Expeditions.cs index a5f08b4d357..da7fc0e9db5 100644 --- a/Content.Server/Salvage/SalvageSystem.Expeditions.cs +++ b/Content.Server/Salvage/SalvageSystem.Expeditions.cs @@ -22,7 +22,6 @@ using Robust.Shared.Configuration; using Content.Shared.Ghost; using System.Numerics; // Frontier -using TimedDespawnComponent = Robust.Shared.Spawners.TimedDespawnComponent; namespace Content.Server.Salvage; @@ -129,9 +128,6 @@ private void SetProximityCheck(bool obj) private void OnExpeditionMapInit(EntityUid uid, SalvageExpeditionComponent component, MapInitEvent args) { component.SelectedSong = _audio.ResolveSound(component.Sound); - - var despawn = EnsureComp(uid); - despawn.Lifetime = (float) TimeSpan.FromMinutes(30).TotalSeconds; } private void OnExpeditionShutdown(EntityUid uid, SalvageExpeditionComponent component, ComponentShutdown args) @@ -147,8 +143,6 @@ private void OnExpeditionShutdown(EntityUid uid, SalvageExpeditionComponent comp } } - CleanupHostedExpeditionContent(component); - // HARDLIGHT: Handle round persistence - station might be deleted during round transitions if (Deleted(component.Station)) { diff --git a/Content.Shared/Weather/SharedWeatherSystem.cs b/Content.Shared/Weather/SharedWeatherSystem.cs index 0387f50aab1..c8337e05b69 100644 --- a/Content.Shared/Weather/SharedWeatherSystem.cs +++ b/Content.Shared/Weather/SharedWeatherSystem.cs @@ -12,7 +12,6 @@ namespace Content.Shared.Weather; public abstract class SharedWeatherSystem : EntitySystem { [Dependency] protected readonly IGameTiming Timing = default!; - [Dependency] protected readonly IMapManager MapManager = default!; [Dependency] protected readonly IPrototypeManager ProtoMan = default!; [Dependency] private readonly MetaDataSystem _metadata = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; diff --git a/Resources/Locale/en-US/commands/persistence-save-command.ftl b/Resources/Locale/en-US/commands/persistence-save-command.ftl new file mode 100644 index 00000000000..d12ff8cbee3 --- /dev/null +++ b/Resources/Locale/en-US/commands/persistence-save-command.ftl @@ -0,0 +1,3 @@ +cmd-persistencesave-desc = Saves server data to a persistence file to be loaded later. +cmd-persistencesave-usage = persistencesave [mapId] [filePath - default: game.map (CCVar) ] +cmd-persistencesave-no-path = filePath was not specified and CCVar {$cvar} is not set. Manually set the filePath param in order to save the map. diff --git a/Resources/Locale/en-US/persistence/command.ftl b/Resources/Locale/en-US/persistence/command.ftl deleted file mode 100644 index b070aee1159..00000000000 --- a/Resources/Locale/en-US/persistence/command.ftl +++ /dev/null @@ -1 +0,0 @@ -cmd-persistencesave-no-path = filePath was not specified and CCVar {$cvar} is not set. Manually set the filePath param in order to save the map. From 0adcb5fe919fd9907dc2d20f8538ef3d0763d664 Mon Sep 17 00:00:00 2001 From: Red <96445749+TheShuEd@users.noreply.github.com> Date: Mon, 9 Mar 2026 02:28:48 +0300 Subject: [PATCH 07/49] Weather entities (#41427) * weather status effect entities * proto port + fixes * Update weather.yml * fix visuals * Update SharedWeatherSystem.cs * Update SharedWeatherSystem.cs * Update WeatherSystem.cs * Thanks Slarti, thats a much better * Update WeatherSystem.cs * Update StencilOverlay.Weather.cs * Update Content.Client/Overlays/StencilOverlay.Weather.cs Co-authored-by: Pok <113675512+Pok27@users.noreply.github.com> * Update Content.Shared/StatusEffectNew/Components/StatusEffectComponent.cs Co-authored-by: Pok <113675512+Pok27@users.noreply.github.com> * Update Content.Client/Overlays/StencilOverlay.Weather.cs Co-authored-by: Pok <113675512+Pok27@users.noreply.github.com> * Revise weather command help descriptions Updated weather command help texts for clarity. * Merge branch 'master' into ed-14-11-2025-weather-entities * Tayrtahn review apply * fixes and cleanup --------- Co-authored-by: Pok <113675512+Pok27@users.noreply.github.com> Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> --- .../Overlays/StencilOverlay.Weather.cs | 49 +-- Content.Client/Overlays/StencilOverlay.cs | 4 +- .../Overlays/StencilOverlaySystem.cs | 28 -- Content.Client/Weather/WeatherSystem.cs | 168 -------- .../Weather/Commands/WeatherAddCommand.cs | 89 +++++ .../Weather/Commands/WeatherRemoveCommand.cs | 82 ++++ .../Weather/Commands/WeatherSetCommand.cs | 91 +++++ Content.Server/Weather/WeatherSystem.cs | 7 +- .../Modifiers/SalvageWeatherMod.cs | 6 +- .../Components/StatusEffectComponent.cs | 59 +++ .../StatusEffectNew/StatusEffectsSystem.cs | 366 ++++++++++++++++++ .../Effects/WeatherOnTriggerComponent.cs | 25 ++ .../Trigger/Systems/WeatherTriggerSystem.cs | 29 ++ Content.Shared/Weather/SharedWeatherSystem.cs | 16 +- Content.Shared/Weather/WeatherComponent.cs | 53 --- Content.Shared/Weather/WeatherPrototype.cs | 23 -- .../Weather/WeatherStatusEffectComponent.cs | 46 +++ Resources/Locale/en-US/weather/weather.ftl | 19 +- .../Entities/StatusEffects/weather.yml | 178 +++++++++ .../Prototypes/SoundCollections/weather.yml | 5 + Resources/Prototypes/weather.yml | 138 ------- 21 files changed, 1017 insertions(+), 464 deletions(-) delete mode 100644 Content.Client/Overlays/StencilOverlaySystem.cs delete mode 100644 Content.Client/Weather/WeatherSystem.cs create mode 100644 Content.Server/Weather/Commands/WeatherAddCommand.cs create mode 100644 Content.Server/Weather/Commands/WeatherRemoveCommand.cs create mode 100644 Content.Server/Weather/Commands/WeatherSetCommand.cs create mode 100644 Content.Shared/StatusEffectNew/Components/StatusEffectComponent.cs create mode 100644 Content.Shared/StatusEffectNew/StatusEffectsSystem.cs create mode 100644 Content.Shared/Trigger/Components/Effects/WeatherOnTriggerComponent.cs create mode 100644 Content.Shared/Trigger/Systems/WeatherTriggerSystem.cs delete mode 100644 Content.Shared/Weather/WeatherComponent.cs delete mode 100644 Content.Shared/Weather/WeatherPrototype.cs create mode 100644 Content.Shared/Weather/WeatherStatusEffectComponent.cs create mode 100644 Resources/Prototypes/Entities/StatusEffects/weather.yml create mode 100644 Resources/Prototypes/SoundCollections/weather.yml delete mode 100644 Resources/Prototypes/weather.yml diff --git a/Content.Client/Overlays/StencilOverlay.Weather.cs b/Content.Client/Overlays/StencilOverlay.Weather.cs index 0a41eecf814..9e133fb3c06 100644 --- a/Content.Client/Overlays/StencilOverlay.Weather.cs +++ b/Content.Client/Overlays/StencilOverlay.Weather.cs @@ -27,37 +27,38 @@ private void DrawWeather( // Cut out the irrelevant bits via stencil // This is why we don't just use parallax; we might want specific tiles to get drawn over // particularly for planet maps or stations. - worldHandle.RenderInRenderTarget(res.Blep!, () => - { - var xformQuery = _entManager.GetEntityQuery(); - _grids.Clear(); - - // idk if this is safe to cache in a field and clear sloth help - _mapManager.FindGridsIntersecting(mapId, worldAABB, ref _grids); - - foreach (var grid in _grids) + worldHandle.RenderInRenderTarget(res.Blep!, + () => { - var matrix = _transform.GetWorldMatrix(grid, xformQuery); - var matty = Matrix3x2.Multiply(matrix, invMatrix); - worldHandle.SetTransform(matty); - _entManager.TryGetComponent(grid.Owner, out RoofComponent? roofComp); + var xformQuery = _entManager.GetEntityQuery(); + _grids.Clear(); - foreach (var tile in _map.GetTilesIntersecting(grid.Owner, grid, worldAABB)) + // idk if this is safe to cache in a field and clear sloth help + _mapManager.FindGridsIntersecting(mapId, worldAABB, ref _grids); + + foreach (var grid in _grids) { - // Ignored tiles for stencil - if (_weather.CanWeatherAffect(grid.Owner, grid, tile, roofComp)) + var matrix = _transform.GetWorldMatrix(grid, xformQuery); + var matty = Matrix3x2.Multiply(matrix, invMatrix); + worldHandle.SetTransform(matty); + _entManager.TryGetComponent(grid.Owner, out RoofComponent? roofComp); + + foreach (var tile in _map.GetTilesIntersecting(grid.Owner, grid, worldAABB)) { - continue; - } + // Ignored tiles for stencil + if (_weather.CanWeatherAffect(grid.Owner, grid, tile, roofComp)) + { + continue; + } - var gridTile = new Box2(tile.GridIndices * grid.Comp.TileSize, - (tile.GridIndices + Vector2i.One) * grid.Comp.TileSize); + var gridTile = new Box2(tile.GridIndices * grid.Comp.TileSize, + (tile.GridIndices + Vector2i.One) * grid.Comp.TileSize); - worldHandle.DrawRect(gridTile, Color.White); + worldHandle.DrawRect(gridTile, Color.White); + } } - } - - }, Color.Transparent); + }, + Color.Transparent); worldHandle.SetTransform(Matrix3x2.Identity); worldHandle.UseShader(_protoManager.Index(StencilMaskId).Instance()); diff --git a/Content.Client/Overlays/StencilOverlay.cs b/Content.Client/Overlays/StencilOverlay.cs index 2bd6331ed46..95123417b2a 100644 --- a/Content.Client/Overlays/StencilOverlay.cs +++ b/Content.Client/Overlays/StencilOverlay.cs @@ -52,7 +52,7 @@ public StencilOverlay(ParallaxSystem parallax, SharedTransformSystem transform, protected override void Draw(in OverlayDrawArgs args) { - var mapUid = _mapManager.GetMapEntityId(args.MapId); + var mapUid = _map.GetMapOrInvalid(args.MapId); var invMatrix = args.Viewport.GetWorldToLocalMatrix(); var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources()); @@ -76,9 +76,7 @@ protected override void Draw(in OverlayDrawArgs args) } if (_entManager.TryGetComponent(mapUid, out var restrictedRangeComponent)) - { DrawRestrictedRange(args, res, restrictedRangeComponent, invMatrix); - } args.WorldHandle.UseShader(null); args.WorldHandle.SetTransform(Matrix3x2.Identity); diff --git a/Content.Client/Overlays/StencilOverlaySystem.cs b/Content.Client/Overlays/StencilOverlaySystem.cs deleted file mode 100644 index 364ec0fddbf..00000000000 --- a/Content.Client/Overlays/StencilOverlaySystem.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Content.Client.Parallax; -using Content.Client.Weather; -using Robust.Client.GameObjects; -using Robust.Client.Graphics; - -namespace Content.Client.Overlays; - -public sealed class StencilOverlaySystem : EntitySystem -{ - [Dependency] private readonly IOverlayManager _overlay = default!; - [Dependency] private readonly ParallaxSystem _parallax = default!; - [Dependency] private readonly SharedTransformSystem _transform = default!; - [Dependency] private readonly SharedMapSystem _map = default!; - [Dependency] private readonly SpriteSystem _sprite = default!; - [Dependency] private readonly WeatherSystem _weather = default!; - - public override void Initialize() - { - base.Initialize(); - _overlay.AddOverlay(new StencilOverlay(_parallax, _transform, _map, _sprite, _weather)); - } - - public override void Shutdown() - { - base.Shutdown(); - _overlay.RemoveOverlay(); - } -} diff --git a/Content.Client/Weather/WeatherSystem.cs b/Content.Client/Weather/WeatherSystem.cs deleted file mode 100644 index 26def25a15f..00000000000 --- a/Content.Client/Weather/WeatherSystem.cs +++ /dev/null @@ -1,168 +0,0 @@ -using System.Numerics; -using Content.Shared.Light.Components; -using Content.Shared.Weather; -using Robust.Client.Audio; -using Robust.Client.GameObjects; -using Robust.Client.Player; -using Robust.Shared.Audio.Systems; -using Robust.Shared.GameStates; -using Robust.Shared.Map; -using Robust.Shared.Map.Components; -using Robust.Shared.Player; -using AudioComponent = Robust.Shared.Audio.Components.AudioComponent; - -namespace Content.Client.Weather; - -public sealed class WeatherSystem : SharedWeatherSystem -{ - [Dependency] private readonly IPlayerManager _playerManager = default!; - [Dependency] private readonly AudioSystem _audio = default!; - [Dependency] private readonly MapSystem _mapSystem = default!; - [Dependency] private readonly SharedTransformSystem _transform = default!; - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnWeatherHandleState); - } - - protected override void Run(EntityUid uid, WeatherData weather, WeatherPrototype weatherProto, float frameTime) - { - base.Run(uid, weather, weatherProto, frameTime); - - var ent = _playerManager.LocalEntity; - - if (ent == null) - return; - - var mapUid = Transform(uid).MapUid; - var entXform = Transform(ent.Value); - - // Maybe have the viewports manage this? - if (mapUid == null || entXform.MapUid != mapUid) - { - weather.Stream = _audio.Stop(weather.Stream); - return; - } - - if (!Timing.IsFirstTimePredicted || weatherProto.Sound == null) - return; - - weather.Stream ??= _audio.PlayGlobal(weatherProto.Sound, Filter.Local(), true)?.Entity; - - if (!TryComp(weather.Stream, out AudioComponent? comp)) - return; - - var occlusion = 0f; - - // Work out tiles nearby to determine volume. - if (TryComp(entXform.GridUid, out var grid)) - { - TryComp(entXform.GridUid, out RoofComponent? roofComp); - var gridId = entXform.GridUid.Value; - // FloodFill to the nearest tile and use that for audio. - var seed = _mapSystem.GetTileRef(gridId, grid, entXform.Coordinates); - var frontier = new Queue(); - frontier.Enqueue(seed); - // If we don't have a nearest node don't play any sound. - EntityCoordinates? nearestNode = null; - var visited = new HashSet(); - - while (frontier.TryDequeue(out var node)) - { - if (!visited.Add(node.GridIndices)) - continue; - - if (!CanWeatherAffect(entXform.GridUid.Value, grid, node, roofComp)) - { - // Add neighbors - // TODO: Ideally we pick some deterministically random direction and use that - // We can't just do that naively here because it will flicker between nearby tiles. - for (var x = -1; x <= 1; x++) - { - for (var y = -1; y <= 1; y++) - { - if (Math.Abs(x) == 1 && Math.Abs(y) == 1 || - x == 0 && y == 0 || - (new Vector2(x, y) + node.GridIndices - seed.GridIndices).Length() > 3) - { - continue; - } - - frontier.Enqueue(_mapSystem.GetTileRef(gridId, grid, new Vector2i(x, y) + node.GridIndices)); - } - } - - continue; - } - - nearestNode = new EntityCoordinates(entXform.GridUid.Value, - node.GridIndices + grid.TileSizeHalfVector); - break; - } - - // Get occlusion to the targeted node if it exists, otherwise set a default occlusion. - if (nearestNode != null) - { - var entPos = _transform.GetMapCoordinates(entXform); - var nodePosition = _transform.ToMapCoordinates(nearestNode.Value).Position; - var delta = nodePosition - entPos.Position; - var distance = delta.Length(); - occlusion = _audio.GetOcclusion(entPos, delta, distance); - } - else - { - occlusion = 3f; - } - } - - var alpha = GetPercent(weather, uid); - alpha *= SharedAudioSystem.VolumeToGain(weatherProto.Sound.Params.Volume); - _audio.SetGain(weather.Stream, alpha, comp); - comp.Occlusion = occlusion; - } - - protected override bool SetState(EntityUid uid, WeatherState state, WeatherComponent comp, WeatherData weather, WeatherPrototype weatherProto) - { - if (!base.SetState(uid, state, comp, weather, weatherProto)) - return false; - - if (!Timing.IsFirstTimePredicted) - return true; - - // TODO: Fades (properly) - weather.Stream = _audio.Stop(weather.Stream); - weather.Stream = _audio.PlayGlobal(weatherProto.Sound, Filter.Local(), true)?.Entity; - return true; - } - - private void OnWeatherHandleState(EntityUid uid, WeatherComponent component, ref ComponentHandleState args) - { - if (args.Current is not WeatherComponentState state) - return; - - foreach (var (proto, weather) in component.Weather) - { - // End existing one - if (!state.Weather.TryGetValue(proto, out var stateData)) - { - EndWeather(uid, component, proto); - continue; - } - - // Data update? - weather.StartTime = stateData.StartTime; - weather.EndTime = stateData.EndTime; - weather.State = stateData.State; - } - - foreach (var (proto, weather) in state.Weather) - { - if (component.Weather.ContainsKey(proto)) - continue; - - // New weather - StartWeather(uid, component, ProtoMan.Index(proto), weather.EndTime); - } - } -} diff --git a/Content.Server/Weather/Commands/WeatherAddCommand.cs b/Content.Server/Weather/Commands/WeatherAddCommand.cs new file mode 100644 index 00000000000..6054012db0c --- /dev/null +++ b/Content.Server/Weather/Commands/WeatherAddCommand.cs @@ -0,0 +1,89 @@ +using Content.Server.Administration; +using Content.Shared.Administration; +using Content.Shared.Prototypes; +using Content.Shared.Weather; +using Robust.Shared.Console; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; + +namespace Content.Server.Weather.Commands; + +/// +/// Add specific weather to map. +/// +[AdminCommand(AdminFlags.Fun)] +public sealed class WeatherAddCommand : LocalizedEntityCommands +{ + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly SharedMapSystem _map = default!; + [Dependency] private readonly WeatherSystem _weather = default!; + [Dependency] private readonly IComponentFactory _compFactory = default!; + + public override string Command => "weatheradd"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length < 2) + { + shell.WriteError(Loc.GetString("cmd-weather-error-no-arguments")); + return; + } + + //MapId parse + if (!int.TryParse(args[0], out var mapInt)) + return; + + var mapId = new MapId(mapInt); + + if (!_map.MapExists(mapId)) + { + shell.WriteError(Loc.GetString("cmd-weather-error-wrong-map", ("id", mapId.ToString()))); + return; + } + + //Weather proto parse + EntProtoId weatherProto = args[1]; + if (!_proto.TryIndex(weatherProto, out _)) + { + shell.WriteError(Loc.GetString("cmd-weather-error-unknown-proto")); + return; + } + + //Time parsing + TimeSpan? duration = null; + if (args.Length == 3) + { + if (int.TryParse(args[2], out var durationInt)) + duration = TimeSpan.FromSeconds(durationInt); + else + shell.WriteError(Loc.GetString("cmd-weather-error-wrong-time")); + } + + _weather.TryAddWeather(mapId, weatherProto, out _, duration); + } + + + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + if (args.Length == 1) + return CompletionResult.FromHintOptions(CompletionHelper.MapIds(EntityManager), Loc.GetString("cmd-weather-hint-map-id")); + + if (args.Length == 2) + { + var opts = new List(); + foreach (var proto in _proto.EnumeratePrototypes()) + { + if (!proto.HasComponent(_compFactory)) + continue; + + opts.Add(new CompletionOption(proto.ID, proto.Name)); + } + return CompletionResult.FromHintOptions(opts, Loc.GetString("cmd-weather-hint-prototype")); + } + + if (args.Length == 3) + return CompletionResult.FromHint(Loc.GetString("cmd-weather-hint-time")); + + return CompletionResult.Empty; + } +} diff --git a/Content.Server/Weather/Commands/WeatherRemoveCommand.cs b/Content.Server/Weather/Commands/WeatherRemoveCommand.cs new file mode 100644 index 00000000000..df8f91003f0 --- /dev/null +++ b/Content.Server/Weather/Commands/WeatherRemoveCommand.cs @@ -0,0 +1,82 @@ +using Content.Server.Administration; +using Content.Shared.Administration; +using Content.Shared.Prototypes; +using Content.Shared.Weather; +using Robust.Shared.Console; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; + +namespace Content.Server.Weather.Commands; + +/// +/// Remove specific weather from map. +/// +[AdminCommand(AdminFlags.Fun)] +public sealed class WeatherRemoveCommand : LocalizedEntityCommands +{ + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly SharedMapSystem _map = default!; + [Dependency] private readonly WeatherSystem _weather = default!; + [Dependency] private readonly IComponentFactory _compFactory = default!; + + public override string Command => "weatherremove"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length < 2) + { + shell.WriteError(Loc.GetString("cmd-weather-error-no-arguments")); + return; + } + + //MapId parse + if (!int.TryParse(args[0], out var mapInt)) + return; + + var mapId = new MapId(mapInt); + + if (!_map.MapExists(mapId)) + { + shell.WriteError(Loc.GetString("cmd-weather-error-wrong-map", ("id", mapId.ToString()))); + return; + } + + //Weather proto parse + EntProtoId weatherProto = args[1]; + if (!_proto.TryIndex(weatherProto, out _)) + { + shell.WriteError(Loc.GetString("cmd-weather-error-unknown-proto")); + return; + } + + if (!_weather.HasWeather(mapId, weatherProto)) + { + shell.WriteError(Loc.GetString("cmd-weather-error-no-weather")); + return; + } + + _weather.TryRemoveWeather(mapId, weatherProto); + } + + + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + if (args.Length == 1) + return CompletionResult.FromHintOptions(CompletionHelper.MapIds(EntityManager), Loc.GetString("cmd-weather-hint-map-id")); + + if (args.Length == 2) //TODO: dont show ALL weathers here, only weathers applied to selected map + { + var opts = new List(); + foreach (var proto in _proto.EnumeratePrototypes()) + { + if (!proto.HasComponent(_compFactory)) + continue; + + opts.Add(new CompletionOption(proto.ID, proto.Name)); + } + return CompletionResult.FromHintOptions(opts, Loc.GetString("cmd-weather-hint-prototype")); + } + + return CompletionResult.Empty; + } +} diff --git a/Content.Server/Weather/Commands/WeatherSetCommand.cs b/Content.Server/Weather/Commands/WeatherSetCommand.cs new file mode 100644 index 00000000000..77e8ed786d7 --- /dev/null +++ b/Content.Server/Weather/Commands/WeatherSetCommand.cs @@ -0,0 +1,91 @@ +using Content.Server.Administration; +using Content.Shared.Administration; +using Content.Shared.Prototypes; +using Content.Shared.Weather; +using Robust.Shared.Console; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; + +namespace Content.Server.Weather.Commands; + +/// +/// Removes all weather except the specified one. If the specified weather does not exist on the map, it adds it. +/// +[AdminCommand(AdminFlags.Fun)] +public sealed class WeatherSetCommand : LocalizedEntityCommands +{ + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly SharedMapSystem _map = default!; + [Dependency] private readonly WeatherSystem _weather = default!; + [Dependency] private readonly IComponentFactory _compFactory = default!; + + public override string Command => "weatherset"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length < 2) + { + shell.WriteError(Loc.GetString("cmd-weather-error-no-arguments")); + return; + } + + //MapId parse + if (!int.TryParse(args[0], out var mapInt)) + return; + + var mapId = new MapId(mapInt); + + if (!_map.MapExists(mapId)) + { + shell.WriteError(Loc.GetString("cmd-weather-error-wrong-map", ("id", mapId.ToString()))); + return; + } + + //Weather proto parse + EntProtoId? weatherProto = args[1]; + if (args[1] == "null") + weatherProto = null; + else if (!_proto.TryIndex(weatherProto, out _)) + { + shell.WriteError(Loc.GetString("cmd-weather-error-unknown-proto")); + return; + } + + //Time parsing + TimeSpan? duration = null; + if (args.Length == 3) + { + if (int.TryParse(args[2], out var durationInt)) + duration = TimeSpan.FromSeconds(durationInt); + else + shell.WriteError(Loc.GetString("cmd-weather-error-wrong-time")); + } + + _weather.TrySetWeather(mapId, weatherProto, out _, duration); + } + + + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + if (args.Length == 1) + return CompletionResult.FromHintOptions(CompletionHelper.MapIds(EntityManager), Loc.GetString("cmd-weather-hint-map-id")); + + if (args.Length == 2) + { + var opts = new List(); + foreach (var proto in _proto.EnumeratePrototypes()) + { + if (!proto.HasComponent(_compFactory)) + continue; + + opts.Add(new CompletionOption(proto.ID, proto.Name)); + } + return CompletionResult.FromHintOptions(opts, Loc.GetString("cmd-weather-hint-prototype")); + } + + if (args.Length == 3) + return CompletionResult.FromHint(Loc.GetString("cmd-weather-hint-time")); + + return CompletionResult.Empty; + } +} diff --git a/Content.Server/Weather/WeatherSystem.cs b/Content.Server/Weather/WeatherSystem.cs index ec377809133..1ce4c56d797 100644 --- a/Content.Server/Weather/WeatherSystem.cs +++ b/Content.Server/Weather/WeatherSystem.cs @@ -2,7 +2,7 @@ using Content.Shared.Administration; using Content.Shared.Weather; using Robust.Shared.Console; -using Robust.Shared.GameStates; +using Robust.Server.GameStates; using Robust.Shared.Map; using System.Linq; @@ -16,6 +16,7 @@ public sealed class WeatherSystem : SharedWeatherSystem public override void Initialize() { base.Initialize(); + SubscribeLocalEvent(OnWeatherGetState); _console.RegisterCommand("weather", Loc.GetString("cmd-weather-desc"), @@ -43,7 +44,7 @@ private void WeatherTwo(IConsoleShell shell, string argStr, string[] args) var mapId = new MapId(mapInt); - if (!MapManager.MapExists(mapId)) + if (!_mapSystem.MapExists(mapId)) return; if (!_mapSystem.TryGetMap(mapId, out var mapUid)) @@ -51,7 +52,6 @@ private void WeatherTwo(IConsoleShell shell, string argStr, string[] args) var weatherComp = EnsureComp(mapUid.Value); - //Weather Proto parsing WeatherPrototype? weather = null; if (!args[1].Equals("null")) { @@ -62,7 +62,6 @@ private void WeatherTwo(IConsoleShell shell, string argStr, string[] args) } } - //Time parsing TimeSpan? endTime = null; if (args.Length == 3) { diff --git a/Content.Shared/Salvage/Expeditions/Modifiers/SalvageWeatherMod.cs b/Content.Shared/Salvage/Expeditions/Modifiers/SalvageWeatherMod.cs index 1f3b13daee6..432dd279e9a 100644 --- a/Content.Shared/Salvage/Expeditions/Modifiers/SalvageWeatherMod.cs +++ b/Content.Shared/Salvage/Expeditions/Modifiers/SalvageWeatherMod.cs @@ -21,8 +21,8 @@ public sealed partial class SalvageWeatherMod : IPrototype, IBiomeSpecificMod public List? Biomes { get; private set; } = null; /// - /// Weather prototype to use on the planet. + /// Weather status effect prototype to use on the planet. /// - [DataField("weather", required: true, customTypeSerializer:typeof(PrototypeIdSerializer))] - public string WeatherPrototype = string.Empty; + [DataField("weather", required: true)] + public EntProtoId WeatherPrototype = string.Empty; } diff --git a/Content.Shared/StatusEffectNew/Components/StatusEffectComponent.cs b/Content.Shared/StatusEffectNew/Components/StatusEffectComponent.cs new file mode 100644 index 00000000000..28becccee21 --- /dev/null +++ b/Content.Shared/StatusEffectNew/Components/StatusEffectComponent.cs @@ -0,0 +1,59 @@ +using Content.Shared.Whitelist; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.StatusEffectNew.Components; + +/// +/// Marker component for all status effects - every status effect entity should have it. +/// Provides a link between the effect and the affected entity, and some data common to all status effects. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true), AutoGenerateComponentPause] +[Access(typeof(StatusEffectsSystem))] +[EntityCategory("StatusEffects")] +public sealed partial class StatusEffectComponent : Component +{ + /// + /// The entity that this status effect is applied to. + /// + [DataField, AutoNetworkedField] + public EntityUid? AppliedTo; + + /// + /// When this effect will start. Set to Timespan.Zero to start the effect immediately. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField, AutoNetworkedField] + public TimeSpan StartEffectTime; + + /// + /// When this effect will end. If Null, the effect lasts indefinitely. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField, AutoNetworkedField] + public TimeSpan? EndEffectTime; + + /// + /// If true, this status effect has been applied. Used to ensure that only fires once. + /// + /// We actually don't want to network this, that way client can apply an effect it's receiving properly! + [DataField] + public bool Applied; + + /// + /// Whitelist, by which it is determined whether this status effect can be imposed on a particular entity. + /// + [DataField] + public EntityWhitelist? Whitelist; + + /// + /// Blacklist, by which it is determined whether this status effect can be imposed on a particular entity. + /// + [DataField] + public EntityWhitelist? Blacklist; + + /// + /// QoL function, returns total duration of this status effect. + /// + [ViewVariables] + public TimeSpan Duration => EndEffectTime == null ? TimeSpan.MaxValue : EndEffectTime.Value - StartEffectTime; +} diff --git a/Content.Shared/StatusEffectNew/StatusEffectsSystem.cs b/Content.Shared/StatusEffectNew/StatusEffectsSystem.cs new file mode 100644 index 00000000000..512285eaf34 --- /dev/null +++ b/Content.Shared/StatusEffectNew/StatusEffectsSystem.cs @@ -0,0 +1,366 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Shared.Rejuvenate; +using Content.Shared.StatusEffectNew.Components; +using Content.Shared.Whitelist; +using Robust.Shared.Containers; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Shared.StatusEffectNew; + +/// +/// This system controls status effects, their lifetime, and provides an API for adding them to entities, +/// removing them from entities, or getting information about current effects on entities. +/// +public sealed partial class StatusEffectsSystem : EntitySystem +{ + [Dependency] private readonly IComponentFactory _factory = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelist = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + + private EntityQuery _containerQuery; + private EntityQuery _effectQuery; + + public readonly HashSet StatusEffectPrototypes = []; + + public override void Initialize() + { + base.Initialize(); + + InitializeRelay(); + + SubscribeLocalEvent(OnStatusContainerInit); + SubscribeLocalEvent(OnStatusContainerShutdown); + SubscribeLocalEvent(OnEntityInserted); + SubscribeLocalEvent(OnEntityRemoved); + + SubscribeLocalEvent>(OnRejuvenate); + + SubscribeLocalEvent(OnPrototypesReloaded); + + _containerQuery = GetEntityQuery(); + _effectQuery = GetEntityQuery(); + + ReloadStatusEffectsCache(); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var ent, out var effect)) + { + TryApplyStatusEffect((ent, effect)); + + if (effect.EndEffectTime is null) + continue; + + if (_timing.CurTime < effect.EndEffectTime) + continue; + + if (effect.AppliedTo is null) + continue; + + PredictedQueueDel(ent); + } + } + + private void OnPrototypesReloaded(PrototypesReloadedEventArgs args) + { + if (!args.WasModified()) + return; + + ReloadStatusEffectsCache(); + } + + private void ReloadStatusEffectsCache() + { + StatusEffectPrototypes.Clear(); + + foreach (var ent in _proto.EnumeratePrototypes()) + { + if (ent.TryGetComponent(out _, _factory)) + StatusEffectPrototypes.Add(ent.ID); + } + } + + private void OnStatusContainerInit(Entity ent, ref ComponentInit args) + { + ent.Comp.ActiveStatusEffects = + _container.EnsureContainer(ent, StatusEffectContainerComponent.ContainerId); + // We show the contents of the container to allow status effects to have visible sprites. + ent.Comp.ActiveStatusEffects.ShowContents = true; + ent.Comp.ActiveStatusEffects.OccludesLight = false; + } + + private void OnStatusContainerShutdown(Entity ent, ref ComponentShutdown args) + { + if (ent.Comp.ActiveStatusEffects is { } container) + _container.ShutdownContainer(container); + } + + private void OnEntityInserted(Entity ent, ref EntInsertedIntoContainerMessage args) + { + if (args.Container.ID != StatusEffectContainerComponent.ContainerId) + return; + + if (!_effectQuery.TryComp(args.Entity, out var statusComp)) + return; + + // Make sure AppliedTo is set correctly so events can rely on it + if (statusComp.AppliedTo != ent) + { + statusComp.AppliedTo = ent; + DirtyField(args.Entity, statusComp, nameof(StatusEffectComponent.AppliedTo)); + } + } + + private void OnEntityRemoved(Entity ent, ref EntRemovedFromContainerMessage args) + { + if (args.Container.ID != StatusEffectContainerComponent.ContainerId) + return; + + if (!_effectQuery.TryComp(args.Entity, out var statusComp)) + return; + + var ev = new StatusEffectRemovedEvent(ent); + RaiseLocalEvent(args.Entity, ref ev); + + // Clear AppliedTo after events are handled so event handlers can use it. + if (statusComp.AppliedTo == null) + return; + + // Why not just delete it? Well, that might end up being best, but this + // could theoretically allow for moving status effects from one entity + // to another. That might be good to have for polymorphs or something. + statusComp.AppliedTo = null; + Dirty(args.Entity, statusComp); + } + + private void OnRejuvenate(Entity ent, + ref StatusEffectRelayedEvent args) + { + PredictedQueueDel(ent.Owner); + } + + /// + /// Applies the status effect, i.e. starts it after it has been added. Ensures delayed start times trigger when they should. + /// + /// The status effect entity. + /// Returns true if the effect is applied. + private bool TryApplyStatusEffect(Entity statusEffectEnt) + { + if (statusEffectEnt.Comp.Applied || + statusEffectEnt.Comp.AppliedTo == null || + _timing.CurTime < statusEffectEnt.Comp.StartEffectTime) + return false; + + var ev = new StatusEffectAppliedEvent(statusEffectEnt.Comp.AppliedTo.Value); + RaiseLocalEvent(statusEffectEnt, ref ev); + + statusEffectEnt.Comp.Applied = true; + + return true; + } + + public bool CanAddStatusEffect(EntityUid uid, EntProtoId effectProto) + { + if (!_proto.Resolve(effectProto, out var effectProtoData)) + return false; + + if (!effectProtoData.TryGetComponent(out var effectProtoComp, Factory)) + return false; + + if (!_whitelist.CheckBoth(uid, effectProtoComp.Blacklist, effectProtoComp.Whitelist)) + return false; + + var ev = new BeforeStatusEffectAddedEvent(effectProto); + RaiseLocalEvent(uid, ref ev); + + if (ev.Cancelled) + return false; + + return true; + } + + /// + /// Attempts to add a status effect to the specified entity. Returns True if the effect is added, does not check if one + /// already exists as it's intended to be called after a check for an existing effect has already failed. + /// + /// The target entity to which the effect should be added. + /// ProtoId of the status effect entity. Make sure it has StatusEffectComponent on it. + /// Duration of status effect. Leave null and the effect will be permanent until it is removed using TryRemoveStatusEffect. + /// The delay of the effect. Leave null and the effect will be immediate. + /// The EntityUid of the status effect we have just created or null if we couldn't create one. + private bool TryAddStatusEffect( + EntityUid target, + EntProtoId effectProto, + [NotNullWhen(true)] out EntityUid? statusEffect, + TimeSpan? duration = null, + TimeSpan? delay = null + ) + { + statusEffect = null; + + if (duration <= TimeSpan.Zero) + return false; + + if (!CanAddStatusEffect(target, effectProto)) + return false; + + EnsureComp(target); + + // And only if all checks passed we spawn the effect + if (!PredictedTrySpawnInContainer(effectProto, + target, + StatusEffectContainerComponent.ContainerId, + out var effect)) + return false; + + if (!_effectQuery.TryComp(effect, out var effectComp)) + return false; + + statusEffect = effect; + + var endTime = delay == null ? _timing.CurTime + duration : _timing.CurTime + delay + duration; + SetStatusEffectEndTime((effect.Value, effectComp), endTime); + var startTime = delay == null ? _timing.CurTime : _timing.CurTime + delay.Value; + SetStatusEffectStartTime(effect.Value, startTime); + + TryApplyStatusEffect((statusEffect.Value, effectComp)); + + return true; + } + + private void UpdateStatusEffectTime(Entity effect, TimeSpan? duration) + { + if (!_effectQuery.Resolve(effect, ref effect.Comp)) + return; + + // It's already infinitely long + if (effect.Comp.EndEffectTime is null) + return; + + TimeSpan? newEndTime = null; + + if (duration is not null) + { + // Don't update time to a smaller timespan... + newEndTime = _timing.CurTime + duration; + if (effect.Comp.EndEffectTime >= newEndTime) + return; + } + + SetStatusEffectEndTime(effect, newEndTime); + } + + private void UpdateStatusEffectDelay(Entity effect, TimeSpan? delay) + { + if (!_effectQuery.Resolve(effect, ref effect.Comp)) + return; + + // It's already started! + if (_timing.CurTime >= effect.Comp.StartEffectTime) + return; + + var newStartTime = TimeSpan.Zero; + + if (delay is not null) + { + // Don't update time to a smaller timespan... + newStartTime = _timing.CurTime + delay.Value; + if (effect.Comp.StartEffectTime < newStartTime) + return; + } + + SetStatusEffectStartTime(effect, newStartTime); + } + + private void AddStatusEffectTime(Entity effect, TimeSpan delta) + { + if (!_effectQuery.Resolve(effect, ref effect.Comp)) + return; + + // It's already infinitely long can't add or subtract from infinity... + if (effect.Comp.EndEffectTime is null) + return; + + // Add to the current end effect time, if we're here we should have one set already, and if it's null it's probably infinite. + SetStatusEffectEndTime((effect, effect.Comp), effect.Comp.EndEffectTime.Value + delta); + } + + private void SetStatusEffectEndTime(Entity ent, TimeSpan? endTime) + { + if (!_effectQuery.Resolve(ent, ref ent.Comp)) + return; + + if (ent.Comp.EndEffectTime == endTime) + return; + + ent.Comp.EndEffectTime = endTime; + + if (ent.Comp.AppliedTo is not { } appliedTo) + return; // Not much we can do! + + var ev = new StatusEffectEndTimeUpdatedEvent(appliedTo, endTime); + RaiseLocalEvent(ent, ref ev); + + DirtyField(ent, ent.Comp, nameof(StatusEffectComponent.EndEffectTime)); + } + + private void SetStatusEffectStartTime(Entity ent, TimeSpan startTime) + { + if (!_effectQuery.Resolve(ent, ref ent.Comp)) + return; + + if (ent.Comp.StartEffectTime == startTime) + return; + + ent.Comp.StartEffectTime = startTime; + + if (ent.Comp.AppliedTo is not { } appliedTo) + return; // Not much we can do! + + var ev = new StatusEffectStartTimeUpdatedEvent(appliedTo, startTime); + RaiseLocalEvent(ent, ref ev); + + DirtyField(ent, ent.Comp, nameof(StatusEffectComponent.StartEffectTime)); + } +} + +/// +/// Calls on effect entity, when a status effect is applied. +/// +[ByRefEvent] +public readonly record struct StatusEffectAppliedEvent(EntityUid Target); + +/// +/// Calls on effect entity, when a status effect is removed. +/// +[ByRefEvent] +public readonly record struct StatusEffectRemovedEvent(EntityUid Target); + +/// +/// Raised on an entity before a status effect is added to determine if adding it should be cancelled. +/// +[ByRefEvent] +public record struct BeforeStatusEffectAddedEvent(EntProtoId Effect, bool Cancelled = false); + +/// +/// Raised on an effect entity when its is updated in any way. +/// +/// The entity the effect is attached to. +/// The new end time of the status effect, included for convenience. +[ByRefEvent] +public record struct StatusEffectEndTimeUpdatedEvent(EntityUid Target, TimeSpan? EndTime); + +/// +/// Raised on an effect entity when its is updated in any way. +/// +/// The entity the effect is attached to. +/// The new start time of the status effect, included for convenience. +[ByRefEvent] +public record struct StatusEffectStartTimeUpdatedEvent(EntityUid Target, TimeSpan? StartTime); diff --git a/Content.Shared/Trigger/Components/Effects/WeatherOnTriggerComponent.cs b/Content.Shared/Trigger/Components/Effects/WeatherOnTriggerComponent.cs new file mode 100644 index 00000000000..68ffeed04e5 --- /dev/null +++ b/Content.Shared/Trigger/Components/Effects/WeatherOnTriggerComponent.cs @@ -0,0 +1,25 @@ +using Content.Shared.Weather; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Trigger.Components.Effects; + +/// +/// Changes the current weather when triggered. +/// If TargetUser is true then it will change the weather at the user's map instead of the entitys map. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class WeatherOnTriggerComponent : BaseXOnTriggerComponent +{ + /// + /// Weather status effect proto. Null to clear the weather. + /// + [DataField, AutoNetworkedField] + public EntProtoId? Weather; + + /// + /// How long the weather should last. Null for forever. + /// + [DataField, AutoNetworkedField] + public TimeSpan? Duration; +} diff --git a/Content.Shared/Trigger/Systems/WeatherTriggerSystem.cs b/Content.Shared/Trigger/Systems/WeatherTriggerSystem.cs new file mode 100644 index 00000000000..c9b7de87d0e --- /dev/null +++ b/Content.Shared/Trigger/Systems/WeatherTriggerSystem.cs @@ -0,0 +1,29 @@ +using Content.Shared.Trigger.Components.Effects; +using Content.Shared.Weather; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Shared.Trigger.Systems; + +public sealed class WeatherTriggerSystem : XOnTriggerSystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly SharedWeatherSystem _weather = default!; + + protected override void OnTrigger(Entity ent, EntityUid target, ref TriggerEvent args) + { + var xform = Transform(target); + + if (ent.Comp.Weather == null) //Clear weather if nothing is set + { + _weather.TrySetWeather(xform.MapID, null, out _); + return; + } + + var endTime = ent.Comp.Duration == null ? null : ent.Comp.Duration + _timing.CurTime; + + if (_prototypeManager.Resolve(ent.Comp.Weather, out var weatherPrototype)) + _weather.TrySetWeather(xform.MapID, weatherPrototype, out _, endTime); + } +} diff --git a/Content.Shared/Weather/SharedWeatherSystem.cs b/Content.Shared/Weather/SharedWeatherSystem.cs index c8337e05b69..8d87eaca8cb 100644 --- a/Content.Shared/Weather/SharedWeatherSystem.cs +++ b/Content.Shared/Weather/SharedWeatherSystem.cs @@ -85,7 +85,6 @@ public float GetPercent(WeatherData component, EntityUid mapUid) return alpha; } - public override void Update(float frameTime) { base.Update(frameTime); @@ -105,7 +104,6 @@ public override void Update(float frameTime) { var endTime = weather.EndTime; - // Ended if (endTime != null && endTime < curTime) { EndWeather(uid, comp, proto); @@ -114,7 +112,6 @@ public override void Update(float frameTime) var remainingTime = endTime - curTime; - // Admin messed up or the likes. if (!ProtoMan.TryIndex(proto, out var weatherProto)) { Log.Error($"Unable to find weather prototype for {comp.Weather}, ending!"); @@ -122,12 +119,10 @@ public override void Update(float frameTime) continue; } - // Shutting down if (endTime != null && remainingTime < WeatherComponent.ShutdownTime) { SetState(uid, WeatherState.Ending, comp, weather, weatherProto); } - // Starting up else { var startTime = weather.StartTime; @@ -139,15 +134,11 @@ public override void Update(float frameTime) } } - // Run whatever code we need. Run(uid, weather, weatherProto, frameTime); } } } - /// - /// Shuts down all existing weather and starts the new one if applicable. - /// public void SetWeather(MapId mapId, WeatherPrototype? proto, TimeSpan? endTime) { if (!_mapSystem.TryGetMap(mapId, out var mapUid)) @@ -157,11 +148,9 @@ public void SetWeather(MapId mapId, WeatherPrototype? proto, TimeSpan? endTime) foreach (var (eProto, weather) in weatherComp.Weather) { - // if we turn off the weather, we don't want endTime = null if (proto == null) endTime ??= Timing.CurTime + WeatherComponent.ShutdownTime; - // Reset cooldown if it's an existing one. if (proto is not null && eProto == proto.ID) { weather.EndTime = endTime; @@ -172,7 +161,6 @@ public void SetWeather(MapId mapId, WeatherPrototype? proto, TimeSpan? endTime) continue; } - // Speedrun var end = Timing.CurTime + WeatherComponent.ShutdownTime; if (weather.EndTime == null || weather.EndTime > end) @@ -186,9 +174,6 @@ public void SetWeather(MapId mapId, WeatherPrototype? proto, TimeSpan? endTime) StartWeather(mapUid.Value, weatherComp, proto, endTime); } - /// - /// Run every tick when the weather is running. - /// protected virtual void Run(EntityUid uid, WeatherData weather, WeatherPrototype weatherProto, float frameTime) { } protected void StartWeather(EntityUid uid, WeatherComponent component, WeatherPrototype weather, TimeSpan? endTime) @@ -238,3 +223,4 @@ public WeatherComponentState(Dictionary, WeatherData> } } } + diff --git a/Content.Shared/Weather/WeatherComponent.cs b/Content.Shared/Weather/WeatherComponent.cs deleted file mode 100644 index eaf901fb424..00000000000 --- a/Content.Shared/Weather/WeatherComponent.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Robust.Shared.GameStates; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; - -namespace Content.Shared.Weather; - -[RegisterComponent, NetworkedComponent] -public sealed partial class WeatherComponent : Component -{ - /// - /// Currently running weathers - /// - [DataField] - public Dictionary, WeatherData> Weather = new(); - - public static readonly TimeSpan StartupTime = TimeSpan.FromSeconds(15); - public static readonly TimeSpan ShutdownTime = TimeSpan.FromSeconds(15); -} - -[DataDefinition, Serializable, NetSerializable] -public sealed partial class WeatherData -{ - // Client audio stream. - [NonSerialized] - public EntityUid? Stream; - - /// - /// When the weather started if relevant. - /// - [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] //TODO: Remove Custom serializer - public TimeSpan StartTime = TimeSpan.Zero; - - /// - /// When the applied weather will end. - /// - [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] //TODO: Remove Custom serializer - public TimeSpan? EndTime; - - [ViewVariables] - public TimeSpan Duration => EndTime == null ? TimeSpan.MaxValue : EndTime.Value - StartTime; - - [DataField] - public WeatherState State = WeatherState.Invalid; -} - -public enum WeatherState : byte -{ - Invalid = 0, - Starting, - Running, - Ending, -} diff --git a/Content.Shared/Weather/WeatherPrototype.cs b/Content.Shared/Weather/WeatherPrototype.cs deleted file mode 100644 index 246e929dcef..00000000000 --- a/Content.Shared/Weather/WeatherPrototype.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Robust.Shared.Audio; -using Robust.Shared.Prototypes; -using Robust.Shared.Utility; - -namespace Content.Shared.Weather; - -[Prototype] -public sealed partial class WeatherPrototype : IPrototype -{ - [IdDataField] public string ID { get; private set; } = default!; - - [ViewVariables(VVAccess.ReadWrite), DataField("sprite", required: true)] - public SpriteSpecifier Sprite = default!; - - [ViewVariables(VVAccess.ReadWrite), DataField("color")] - public Color? Color; - - /// - /// Sound to play on the affected areas. - /// - [ViewVariables(VVAccess.ReadWrite), DataField("sound")] - public SoundSpecifier? Sound; -} diff --git a/Content.Shared/Weather/WeatherStatusEffectComponent.cs b/Content.Shared/Weather/WeatherStatusEffectComponent.cs new file mode 100644 index 00000000000..e82c728515d --- /dev/null +++ b/Content.Shared/Weather/WeatherStatusEffectComponent.cs @@ -0,0 +1,46 @@ +using System.Numerics; +using Content.Shared.StatusEffectNew.Components; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Utility; + +namespace Content.Shared.Weather; + +/// +/// Used only in conjure with for status effects applied to map entities. +/// Contains basic information about all types of weather effects. +/// +[RegisterComponent, NetworkedComponent, Access(typeof(SharedWeatherSystem))] +public sealed partial class WeatherStatusEffectComponent : Component +{ + /// + /// A texture that will tile and render as a weather effect across the entire map. + /// + [DataField(required: true)] + public SpriteSpecifier Sprite = default!; + + /// + /// Tint that will be applied to the weather texture. + /// + [DataField] + public Color? Color; + + /// + /// Weather scrolling speed. + /// + [DataField] + public Vector2? Scrolling; + + /// + /// Sound to play on the affected areas. + /// + [DataField] + public SoundSpecifier? Sound; + + /// + /// Client audio stream. + /// Not used on the server. + /// + [ViewVariables] + public EntityUid? Stream; +} diff --git a/Resources/Locale/en-US/weather/weather.ftl b/Resources/Locale/en-US/weather/weather.ftl index 0c67b6f66bf..146364136fe 100644 --- a/Resources/Locale/en-US/weather/weather.ftl +++ b/Resources/Locale/en-US/weather/weather.ftl @@ -1,8 +1,17 @@ -cmd-weather-desc = Sets the weather for the current map. -cmd-weather-help = weather -cmd-weather-hint = Weather prototype -cmd-weather-null = Clears the weather +cmd-weatherremove-desc = Remove specific weather from map. +cmd-weatherset-desc = Removes all weather except the specified one. If the specified weather does not exist on the map, it adds it. +cmd-weatheradd-desc = Add specific weather to map. + +cmd-weatherremove-help = weatherremove +cmd-weatherset-help = weatherset +cmd-weatheradd-help = weatheradd cmd-weather-error-no-arguments = Not enough arguments! cmd-weather-error-unknown-proto = Unknown Weather prototype! -cmd-weather-error-wrong-time = Time is in the wrong format! \ No newline at end of file +cmd-weather-error-wrong-time = Time is in the wrong format! +cmd-weather-error-wrong-map = Map with MapId {$id} doesn't exist! +cmd-weather-error-no-weather = This weather does not exist on the selected map! + +cmd-weather-hint-map-id = Map Id +cmd-weather-hint-prototype = Weather entity prototype +cmd-weather-hint-time = Duration in seconds (leave empty for infinite duration) diff --git a/Resources/Prototypes/Entities/StatusEffects/weather.yml b/Resources/Prototypes/Entities/StatusEffects/weather.yml new file mode 100644 index 00000000000..5c75565fb05 --- /dev/null +++ b/Resources/Prototypes/Entities/StatusEffects/weather.yml @@ -0,0 +1,178 @@ +- type: entity + parent: StatusEffectBase + id: WeatherBase + abstract: true + components: + - type: WeatherStatusEffect + - type: StatusEffect + whitelist: + components: + - Map + +- type: entity + parent: WeatherBase + id: WeatherRain + components: + - type: WeatherStatusEffect + sprite: + sprite: /Textures/Effects/weather.rsi + state: rain + sound: + collection: Rain + params: + loop: true + volume: -6 + +- type: entity + parent: WeatherBase + id: WeatherAshfall + components: + - type: WeatherStatusEffect + sprite: + sprite: /Textures/Effects/weather.rsi + state: ashfall + sound: + path: /Audio/Effects/Weather/snowstorm_weak.ogg + params: + loop: true + volume: -6 + +- type: entity + parent: WeatherBase + id: WeatherAshfallLight + components: + - type: WeatherStatusEffect + sprite: + sprite: /Textures/Effects/weather.rsi + state: ashfall_light + sound: + path: /Audio/Effects/Weather/snowstorm_weak.ogg + params: + loop: true + volume: -6 + +- type: entity + parent: WeatherBase + id: WeatherAshfallHeavy + components: + - type: WeatherStatusEffect + sprite: + sprite: /Textures/Effects/weather.rsi + state: ashfall_heavy + sound: + path: /Audio/Effects/Weather/snowstorm.ogg + params: + loop: true + volume: -6 + +- type: entity + parent: WeatherBase + id: WeatherFallout + components: + - type: WeatherStatusEffect + sprite: + sprite: /Textures/Effects/weather.rsi + state: fallout + sound: + path: /Audio/Effects/Weather/snowstorm_weak.ogg + params: + loop: true + volume: -6 + +- type: entity + parent: WeatherBase + id: WeatherHail + components: + - type: WeatherStatusEffect + sprite: + sprite: /Textures/Effects/weather.rsi + state: hail + sound: + path: /Audio/Effects/Weather/rain.ogg + params: + loop: true + volume: -6 + +- type: entity + parent: WeatherBase + id: WeatherSandstorm + components: + - type: WeatherStatusEffect + sprite: + sprite: /Textures/Effects/weather.rsi + state: sandstorm + sound: + path: /Audio/Effects/Weather/snowstorm_weak.ogg + params: + loop: true + volume: -6 + +- type: entity + parent: WeatherBase + id: WeatherSandstormHeavy + components: + - type: WeatherStatusEffect + sprite: + sprite: /Textures/Effects/weather.rsi + state: sandstorm_heavy + sound: + path: /Audio/Effects/Weather/snowstorm.ogg + params: + loop: true + volume: -6 + +- type: entity + parent: WeatherBase + id: WeatherSnowfallLight + components: + - type: WeatherStatusEffect + sprite: + sprite: /Textures/Effects/weather.rsi + state: snowfall_light + sound: + path: /Audio/Effects/Weather/snowstorm_weak.ogg + params: + loop: true + volume: -6 + +- type: entity + parent: WeatherBase + id: WeatherSnowfallMedium + components: + - type: WeatherStatusEffect + sprite: + sprite: /Textures/Effects/weather.rsi + state: snowfall_med + sound: + path: /Audio/Effects/Weather/snowstorm_weak.ogg + params: + loop: true + volume: -6 + +- type: entity + parent: WeatherBase + id: WeatherSnowfallHeavy + components: + - type: WeatherStatusEffect + sprite: + sprite: /Textures/Effects/weather.rsi + state: snowfall_heavy + sound: + path: /Audio/Effects/Weather/snowstorm.ogg + params: + loop: true + volume: -6 + +- type: entity + parent: WeatherBase + id: WeatherStorm + components: + - type: WeatherStatusEffect + sprite: + sprite: /Textures/Effects/weather.rsi + state: storm + sound: + path: /Audio/Effects/Weather/rain_heavy.ogg + params: + loop: true + volume: -6 diff --git a/Resources/Prototypes/SoundCollections/weather.yml b/Resources/Prototypes/SoundCollections/weather.yml new file mode 100644 index 00000000000..64efea8c613 --- /dev/null +++ b/Resources/Prototypes/SoundCollections/weather.yml @@ -0,0 +1,5 @@ +- type: soundCollection + id: Rain + files: + - /Audio/Effects/Weather/rain.ogg + - /Audio/Effects/Weather/rain2.ogg diff --git a/Resources/Prototypes/weather.yml b/Resources/Prototypes/weather.yml deleted file mode 100644 index a71e59354af..00000000000 --- a/Resources/Prototypes/weather.yml +++ /dev/null @@ -1,138 +0,0 @@ -- type: weather - id: Ashfall - sprite: - sprite: /Textures/Effects/weather.rsi - state: ashfall - sound: - path: /Audio/Effects/Weather/snowstorm_weak.ogg - params: - loop: true - volume: -6 - -- type: weather - id: AshfallLight - sprite: - sprite: /Textures/Effects/weather.rsi - state: ashfall_light - sound: - path: /Audio/Effects/Weather/snowstorm_weak.ogg - params: - loop: true - volume: -6 - -- type: weather - id: AshfallHeavy - sprite: - sprite: /Textures/Effects/weather.rsi - state: ashfall_heavy - sound: - path: /Audio/Effects/Weather/snowstorm.ogg - params: - loop: true - volume: -6 - -- type: weather - id: Fallout - sprite: - sprite: /Textures/Effects/weather.rsi - state: fallout - sound: - path: /Audio/Effects/Weather/snowstorm_weak.ogg - params: - loop: true - volume: -6 - -- type: weather - id: Hail - sprite: - sprite: /Textures/Effects/weather.rsi - state: hail - sound: - path: - /Audio/Effects/Weather/rain.ogg - params: - loop: true - volume: -6 - -- type: weather - id: Rain - sprite: - sprite: /Textures/Effects/weather.rsi - state: rain - sound: - collection: Rain - params: - loop: true - volume: -6 - -- type: soundCollection - id: Rain - files: - - /Audio/Effects/Weather/rain.ogg - - /Audio/Effects/Weather/rain2.ogg - -- type: weather - id: Sandstorm - sprite: - sprite: /Textures/Effects/weather.rsi - state: sandstorm - sound: - path: /Audio/Effects/Weather/snowstorm_weak.ogg - params: - loop: true - volume: -6 - -- type: weather - id: SandstormHeavy - sprite: - sprite: /Textures/Effects/weather.rsi - state: sandstorm_heavy - sound: - path: /Audio/Effects/Weather/snowstorm.ogg - params: - loop: true - volume: -6 - -- type: weather - id: SnowfallLight - sprite: - sprite: /Textures/Effects/weather.rsi - state: snowfall_light - sound: - path: /Audio/Effects/Weather/snowstorm_weak.ogg - params: - loop: true - volume: -6 - -- type: weather - id: SnowfallMedium - sprite: - sprite: /Textures/Effects/weather.rsi - state: snowfall_med - sound: - path: /Audio/Effects/Weather/snowstorm_weak.ogg - params: - loop: true - volume: -6 - -- type: weather - id: SnowfallHeavy - sprite: - sprite: /Textures/Effects/weather.rsi - state: snowfall_heavy - sound: - path: /Audio/Effects/Weather/snowstorm.ogg - params: - loop: true - volume: -6 - -- type: weather - id: Storm - sprite: - sprite: /Textures/Effects/weather.rsi - state: storm - sound: - path: /Audio/Effects/Weather/rain_heavy.ogg - params: - loop: true - volume: -6 From 615107c4d93f0b2729378a9044237f6ed8a4b220 Mon Sep 17 00:00:00 2001 From: fenndragon Date: Tue, 28 Apr 2026 22:02:34 -0600 Subject: [PATCH 08/49] fix --- .../Overlays/StencilOverlay.Weather.cs | 28 +- Content.Client/Overlays/StencilOverlay.cs | 20 +- .../Weather/Commands/WeatherAddCommand.cs | 89 -- .../Weather/Commands/WeatherRemoveCommand.cs | 82 -- .../Weather/Commands/WeatherSetCommand.cs | 91 -- Content.Server/Weather/WeatherSystem.cs | 81 +- Content.Shared/Weather/SharedWeatherSystem.cs | 783 +++++++++++++++--- .../Weather/WeatherStatusEffectComponent.cs | 46 - 8 files changed, 711 insertions(+), 509 deletions(-) delete mode 100644 Content.Server/Weather/Commands/WeatherAddCommand.cs delete mode 100644 Content.Server/Weather/Commands/WeatherRemoveCommand.cs delete mode 100644 Content.Server/Weather/Commands/WeatherSetCommand.cs delete mode 100644 Content.Shared/Weather/WeatherStatusEffectComponent.cs diff --git a/Content.Client/Overlays/StencilOverlay.Weather.cs b/Content.Client/Overlays/StencilOverlay.Weather.cs index 9e133fb3c06..013ec72878e 100644 --- a/Content.Client/Overlays/StencilOverlay.Weather.cs +++ b/Content.Client/Overlays/StencilOverlay.Weather.cs @@ -1,9 +1,9 @@ using System.Numerics; using Content.Shared.Light.Components; +using Content.Shared.StatusEffectNew.Components; using Content.Shared.Weather; using Robust.Client.Graphics; using Robust.Shared.Map.Components; -using Robust.Shared.Physics.Components; namespace Content.Client.Overlays; @@ -14,8 +14,7 @@ public sealed partial class StencilOverlay private void DrawWeather( in OverlayDrawArgs args, CachedResources res, - WeatherPrototype weatherProto, - float alpha, + HashSet> weathers, Matrix3x2 invMatrix) { var worldHandle = args.WorldHandle; @@ -46,10 +45,8 @@ private void DrawWeather( foreach (var tile in _map.GetTilesIntersecting(grid.Owner, grid, worldAABB)) { // Ignored tiles for stencil - if (_weather.CanWeatherAffect(grid.Owner, grid, tile, roofComp)) - { + if (_weather.CanWeatherAffect((grid.Owner, grid, roofComp), tile)) continue; - } var gridTile = new Box2(tile.GridIndices * grid.Comp.TileSize, (tile.GridIndices + Vector2i.One) * grid.Comp.TileSize); @@ -64,11 +61,22 @@ private void DrawWeather( worldHandle.UseShader(_protoManager.Index(StencilMaskId).Instance()); worldHandle.DrawTextureRect(res.Blep!.Texture, worldBounds); var curTime = _timing.RealTime; - var sprite = _sprite.GetFrame(weatherProto.Sprite, curTime); - // Draw the rain - worldHandle.UseShader(_protoManager.Index(StencilDrawId).Instance()); - _parallax.DrawParallax(worldHandle, worldAABB, sprite, curTime, position, Vector2.Zero, modulate: (weatherProto.Color ?? Color.White).WithAlpha(alpha)); + foreach (var (uid, weather, status) in weathers) + { + var alpha = _weather.GetWeatherPercent((uid, status)); + var sprite = _sprite.GetFrame(weather.Sprite, curTime); + + // Draw the rain + worldHandle.UseShader(_protoManager.Index(StencilDrawId).Instance()); + _parallax.DrawParallax(worldHandle, + worldAABB, + sprite, + curTime, + position, + weather.Scrolling ?? Vector2.Zero, + modulate: (weather.Color ?? Color.White).WithAlpha(alpha)); + } worldHandle.SetTransform(Matrix3x2.Identity); worldHandle.UseShader(null); diff --git a/Content.Client/Overlays/StencilOverlay.cs b/Content.Client/Overlays/StencilOverlay.cs index 95123417b2a..c9d8ad094de 100644 --- a/Content.Client/Overlays/StencilOverlay.cs +++ b/Content.Client/Overlays/StencilOverlay.cs @@ -3,6 +3,8 @@ using Content.Client.Parallax; using Content.Client.Weather; using Content.Shared.Salvage; +using Content.Shared.StatusEffectNew; +using Content.Shared.StatusEffectNew.Components; using Content.Shared.Weather; using Robust.Client.GameObjects; using Robust.Client.Graphics; @@ -28,6 +30,8 @@ public sealed partial class StencilOverlay : Overlay private readonly SharedMapSystem _map; private readonly SpriteSystem _sprite; private readonly WeatherSystem _weather; + private readonly StatusEffectsSystem _statusEffects; + private HashSet>? _weatherSet = new(); public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV; @@ -38,7 +42,7 @@ public sealed partial class StencilOverlay : Overlay private static readonly ProtoId StencilMaskId = "StencilMask"; private static readonly ProtoId StencilDrawId = "StencilDraw"; - public StencilOverlay(ParallaxSystem parallax, SharedTransformSystem transform, SharedMapSystem map, SpriteSystem sprite, WeatherSystem weather) + public StencilOverlay(ParallaxSystem parallax, SharedTransformSystem transform, SharedMapSystem map, SpriteSystem sprite, WeatherSystem weather, StatusEffectsSystem statusEffects) { ZIndex = ParallaxSystem.ParallaxZIndex + 1; _parallax = parallax; @@ -46,6 +50,7 @@ public StencilOverlay(ParallaxSystem parallax, SharedTransformSystem transform, _map = map; _sprite = sprite; _weather = weather; + _statusEffects = statusEffects; IoCManager.InjectDependencies(this); _shader = _protoManager.Index(WorldGradientCircleId).InstanceUnique(); } @@ -63,17 +68,8 @@ protected override void Draw(in OverlayDrawArgs args) res.Blep = _clyde.CreateRenderTarget(args.Viewport.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "weather-stencil"); } - if (_entManager.TryGetComponent(mapUid, out var comp)) - { - foreach (var (proto, weather) in comp.Weather) - { - if (!_protoManager.TryIndex(proto, out var weatherProto)) - continue; - - var alpha = _weather.GetPercent(weather, mapUid); - DrawWeather(args, res, weatherProto, alpha, invMatrix); - } - } + if (_statusEffects.TryEffectsWithComp(mapUid, out _weatherSet)) + DrawWeather(args, res, _weatherSet, invMatrix); if (_entManager.TryGetComponent(mapUid, out var restrictedRangeComponent)) DrawRestrictedRange(args, res, restrictedRangeComponent, invMatrix); diff --git a/Content.Server/Weather/Commands/WeatherAddCommand.cs b/Content.Server/Weather/Commands/WeatherAddCommand.cs deleted file mode 100644 index 6054012db0c..00000000000 --- a/Content.Server/Weather/Commands/WeatherAddCommand.cs +++ /dev/null @@ -1,89 +0,0 @@ -using Content.Server.Administration; -using Content.Shared.Administration; -using Content.Shared.Prototypes; -using Content.Shared.Weather; -using Robust.Shared.Console; -using Robust.Shared.Map; -using Robust.Shared.Prototypes; - -namespace Content.Server.Weather.Commands; - -/// -/// Add specific weather to map. -/// -[AdminCommand(AdminFlags.Fun)] -public sealed class WeatherAddCommand : LocalizedEntityCommands -{ - [Dependency] private readonly IPrototypeManager _proto = default!; - [Dependency] private readonly SharedMapSystem _map = default!; - [Dependency] private readonly WeatherSystem _weather = default!; - [Dependency] private readonly IComponentFactory _compFactory = default!; - - public override string Command => "weatheradd"; - - public override void Execute(IConsoleShell shell, string argStr, string[] args) - { - if (args.Length < 2) - { - shell.WriteError(Loc.GetString("cmd-weather-error-no-arguments")); - return; - } - - //MapId parse - if (!int.TryParse(args[0], out var mapInt)) - return; - - var mapId = new MapId(mapInt); - - if (!_map.MapExists(mapId)) - { - shell.WriteError(Loc.GetString("cmd-weather-error-wrong-map", ("id", mapId.ToString()))); - return; - } - - //Weather proto parse - EntProtoId weatherProto = args[1]; - if (!_proto.TryIndex(weatherProto, out _)) - { - shell.WriteError(Loc.GetString("cmd-weather-error-unknown-proto")); - return; - } - - //Time parsing - TimeSpan? duration = null; - if (args.Length == 3) - { - if (int.TryParse(args[2], out var durationInt)) - duration = TimeSpan.FromSeconds(durationInt); - else - shell.WriteError(Loc.GetString("cmd-weather-error-wrong-time")); - } - - _weather.TryAddWeather(mapId, weatherProto, out _, duration); - } - - - public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) - { - if (args.Length == 1) - return CompletionResult.FromHintOptions(CompletionHelper.MapIds(EntityManager), Loc.GetString("cmd-weather-hint-map-id")); - - if (args.Length == 2) - { - var opts = new List(); - foreach (var proto in _proto.EnumeratePrototypes()) - { - if (!proto.HasComponent(_compFactory)) - continue; - - opts.Add(new CompletionOption(proto.ID, proto.Name)); - } - return CompletionResult.FromHintOptions(opts, Loc.GetString("cmd-weather-hint-prototype")); - } - - if (args.Length == 3) - return CompletionResult.FromHint(Loc.GetString("cmd-weather-hint-time")); - - return CompletionResult.Empty; - } -} diff --git a/Content.Server/Weather/Commands/WeatherRemoveCommand.cs b/Content.Server/Weather/Commands/WeatherRemoveCommand.cs deleted file mode 100644 index df8f91003f0..00000000000 --- a/Content.Server/Weather/Commands/WeatherRemoveCommand.cs +++ /dev/null @@ -1,82 +0,0 @@ -using Content.Server.Administration; -using Content.Shared.Administration; -using Content.Shared.Prototypes; -using Content.Shared.Weather; -using Robust.Shared.Console; -using Robust.Shared.Map; -using Robust.Shared.Prototypes; - -namespace Content.Server.Weather.Commands; - -/// -/// Remove specific weather from map. -/// -[AdminCommand(AdminFlags.Fun)] -public sealed class WeatherRemoveCommand : LocalizedEntityCommands -{ - [Dependency] private readonly IPrototypeManager _proto = default!; - [Dependency] private readonly SharedMapSystem _map = default!; - [Dependency] private readonly WeatherSystem _weather = default!; - [Dependency] private readonly IComponentFactory _compFactory = default!; - - public override string Command => "weatherremove"; - - public override void Execute(IConsoleShell shell, string argStr, string[] args) - { - if (args.Length < 2) - { - shell.WriteError(Loc.GetString("cmd-weather-error-no-arguments")); - return; - } - - //MapId parse - if (!int.TryParse(args[0], out var mapInt)) - return; - - var mapId = new MapId(mapInt); - - if (!_map.MapExists(mapId)) - { - shell.WriteError(Loc.GetString("cmd-weather-error-wrong-map", ("id", mapId.ToString()))); - return; - } - - //Weather proto parse - EntProtoId weatherProto = args[1]; - if (!_proto.TryIndex(weatherProto, out _)) - { - shell.WriteError(Loc.GetString("cmd-weather-error-unknown-proto")); - return; - } - - if (!_weather.HasWeather(mapId, weatherProto)) - { - shell.WriteError(Loc.GetString("cmd-weather-error-no-weather")); - return; - } - - _weather.TryRemoveWeather(mapId, weatherProto); - } - - - public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) - { - if (args.Length == 1) - return CompletionResult.FromHintOptions(CompletionHelper.MapIds(EntityManager), Loc.GetString("cmd-weather-hint-map-id")); - - if (args.Length == 2) //TODO: dont show ALL weathers here, only weathers applied to selected map - { - var opts = new List(); - foreach (var proto in _proto.EnumeratePrototypes()) - { - if (!proto.HasComponent(_compFactory)) - continue; - - opts.Add(new CompletionOption(proto.ID, proto.Name)); - } - return CompletionResult.FromHintOptions(opts, Loc.GetString("cmd-weather-hint-prototype")); - } - - return CompletionResult.Empty; - } -} diff --git a/Content.Server/Weather/Commands/WeatherSetCommand.cs b/Content.Server/Weather/Commands/WeatherSetCommand.cs deleted file mode 100644 index 77e8ed786d7..00000000000 --- a/Content.Server/Weather/Commands/WeatherSetCommand.cs +++ /dev/null @@ -1,91 +0,0 @@ -using Content.Server.Administration; -using Content.Shared.Administration; -using Content.Shared.Prototypes; -using Content.Shared.Weather; -using Robust.Shared.Console; -using Robust.Shared.Map; -using Robust.Shared.Prototypes; - -namespace Content.Server.Weather.Commands; - -/// -/// Removes all weather except the specified one. If the specified weather does not exist on the map, it adds it. -/// -[AdminCommand(AdminFlags.Fun)] -public sealed class WeatherSetCommand : LocalizedEntityCommands -{ - [Dependency] private readonly IPrototypeManager _proto = default!; - [Dependency] private readonly SharedMapSystem _map = default!; - [Dependency] private readonly WeatherSystem _weather = default!; - [Dependency] private readonly IComponentFactory _compFactory = default!; - - public override string Command => "weatherset"; - - public override void Execute(IConsoleShell shell, string argStr, string[] args) - { - if (args.Length < 2) - { - shell.WriteError(Loc.GetString("cmd-weather-error-no-arguments")); - return; - } - - //MapId parse - if (!int.TryParse(args[0], out var mapInt)) - return; - - var mapId = new MapId(mapInt); - - if (!_map.MapExists(mapId)) - { - shell.WriteError(Loc.GetString("cmd-weather-error-wrong-map", ("id", mapId.ToString()))); - return; - } - - //Weather proto parse - EntProtoId? weatherProto = args[1]; - if (args[1] == "null") - weatherProto = null; - else if (!_proto.TryIndex(weatherProto, out _)) - { - shell.WriteError(Loc.GetString("cmd-weather-error-unknown-proto")); - return; - } - - //Time parsing - TimeSpan? duration = null; - if (args.Length == 3) - { - if (int.TryParse(args[2], out var durationInt)) - duration = TimeSpan.FromSeconds(durationInt); - else - shell.WriteError(Loc.GetString("cmd-weather-error-wrong-time")); - } - - _weather.TrySetWeather(mapId, weatherProto, out _, duration); - } - - - public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) - { - if (args.Length == 1) - return CompletionResult.FromHintOptions(CompletionHelper.MapIds(EntityManager), Loc.GetString("cmd-weather-hint-map-id")); - - if (args.Length == 2) - { - var opts = new List(); - foreach (var proto in _proto.EnumeratePrototypes()) - { - if (!proto.HasComponent(_compFactory)) - continue; - - opts.Add(new CompletionOption(proto.ID, proto.Name)); - } - return CompletionResult.FromHintOptions(opts, Loc.GetString("cmd-weather-hint-prototype")); - } - - if (args.Length == 3) - return CompletionResult.FromHint(Loc.GetString("cmd-weather-hint-time")); - - return CompletionResult.Empty; - } -} diff --git a/Content.Server/Weather/WeatherSystem.cs b/Content.Server/Weather/WeatherSystem.cs index 1ce4c56d797..58e1eb2b988 100644 --- a/Content.Server/Weather/WeatherSystem.cs +++ b/Content.Server/Weather/WeatherSystem.cs @@ -1,91 +1,30 @@ -using Content.Server.Administration; -using Content.Shared.Administration; using Content.Shared.Weather; -using Robust.Shared.Console; using Robust.Server.GameStates; -using Robust.Shared.Map; -using System.Linq; namespace Content.Server.Weather; public sealed class WeatherSystem : SharedWeatherSystem { - [Dependency] private readonly IConsoleHost _console = default!; - [Dependency] private readonly SharedMapSystem _mapSystem = default!; + //I dont really like to PVS override weather entities, but map status effect containers dont PVS-ing out of the box + [Dependency] private readonly PvsOverrideSystem _pvs = default!; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnWeatherGetState); - _console.RegisterCommand("weather", - Loc.GetString("cmd-weather-desc"), - Loc.GetString("cmd-weather-help"), - WeatherTwo, - WeatherCompletion); + SubscribeLocalEvent(OnCompInit); + SubscribeLocalEvent(OnCompShutdown); } - private void OnWeatherGetState(EntityUid uid, WeatherComponent component, ref ComponentGetState args) + private void OnCompInit(Entity ent, ref ComponentInit args) { - args.State = new WeatherComponentState(component.Weather); + // The map entitiy itself is networked by PVS if the player is on that map but not anything inside a container, + // So we need to add an overridce to make sure the client sees it. + _pvs.AddGlobalOverride(ent); } - [AdminCommand(AdminFlags.Fun)] - private void WeatherTwo(IConsoleShell shell, string argStr, string[] args) + private void OnCompShutdown(Entity ent, ref ComponentShutdown args) { - if (args.Length < 2) - { - shell.WriteError(Loc.GetString("cmd-weather-error-no-arguments")); - return; - } - - if (!int.TryParse(args[0], out var mapInt)) - return; - - var mapId = new MapId(mapInt); - - if (!_mapSystem.MapExists(mapId)) - return; - - if (!_mapSystem.TryGetMap(mapId, out var mapUid)) - return; - - var weatherComp = EnsureComp(mapUid.Value); - - WeatherPrototype? weather = null; - if (!args[1].Equals("null")) - { - if (!ProtoMan.TryIndex(args[1], out weather)) - { - shell.WriteError(Loc.GetString("cmd-weather-error-unknown-proto")); - return; - } - } - - TimeSpan? endTime = null; - if (args.Length == 3) - { - var curTime = Timing.CurTime; - if (int.TryParse(args[2], out var durationInt)) - { - endTime = curTime + TimeSpan.FromSeconds(durationInt); - } - else - { - shell.WriteError(Loc.GetString("cmd-weather-error-wrong-time")); - } - } - - SetWeather(mapId, weather, endTime); - } - - private CompletionResult WeatherCompletion(IConsoleShell shell, string[] args) - { - if (args.Length == 1) - return CompletionResult.FromHintOptions(CompletionHelper.MapIds(EntityManager), "Map Id"); - - var a = CompletionHelper.PrototypeIDs(true, ProtoMan); - var b = a.Concat(new[] { new CompletionOption("null", Loc.GetString("cmd-weather-null")) }); - return CompletionResult.FromHintOptions(b, Loc.GetString("cmd-weather-hint")); + _pvs.RemoveGlobalOverride(ent); } } diff --git a/Content.Shared/Weather/SharedWeatherSystem.cs b/Content.Shared/Weather/SharedWeatherSystem.cs index 8d87eaca8cb..5cfefb79665 100644 --- a/Content.Shared/Weather/SharedWeatherSystem.cs +++ b/Content.Shared/Weather/SharedWeatherSystem.cs @@ -1,10 +1,13 @@ +using System.Diagnostics.CodeAnalysis; using Content.Shared.Light.Components; using Content.Shared.Light.EntitySystems; +using Content.Shared.Maps; +using Content.Shared.StatusEffectNew; +using Content.Shared.StatusEffectNew.Components; using Robust.Shared.Audio.Systems; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization; using Robust.Shared.Timing; namespace Content.Shared.Weather; @@ -13,76 +16,223 @@ public abstract class SharedWeatherSystem : EntitySystem { [Dependency] protected readonly IGameTiming Timing = default!; [Dependency] protected readonly IPrototypeManager ProtoMan = default!; - [Dependency] private readonly MetaDataSystem _metadata = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] protected readonly SharedAudioSystem Audio = default!; + [Dependency] private readonly ITileDefinitionManager _tileDefManager = default!; [Dependency] private readonly SharedMapSystem _mapSystem = default!; [Dependency] private readonly SharedRoofSystem _roof = default!; + [Dependency] private readonly StatusEffectsSystem _statusEffects = default!; private EntityQuery _blockQuery; + private EntityQuery _weatherQuery; + + public static readonly TimeSpan StartupTime = TimeSpan.FromSeconds(15); + public static readonly TimeSpan ShutdownTime = TimeSpan.FromSeconds(15); public override void Initialize() { base.Initialize(); - _blockQuery = GetEntityQuery(); - SubscribeLocalEvent(OnWeatherUnpaused); - } - - private void OnWeatherUnpaused(EntityUid uid, WeatherComponent component, ref EntityUnpausedEvent args) - { - foreach (var weather in component.Weather.Values) - { - weather.StartTime += args.PausedTime; - if (weather.EndTime != null) - weather.EndTime = weather.EndTime.Value + args.PausedTime; - } + _blockQuery = GetEntityQuery(); + _weatherQuery = GetEntityQuery(); } - public bool CanWeatherAffect(EntityUid uid, MapGridComponent grid, TileRef tileRef, RoofComponent? roofComp = null) + public bool CanWeatherAffect(Entity ent, TileRef tileRef) { if (tileRef.Tile.IsEmpty) return true; - if (Resolve(uid, ref roofComp, false) && _roof.IsWeatherOccluding((uid, grid, roofComp), tileRef.GridIndices)) + if (!Resolve(ent, ref ent.Comp1)) return false; - if (HasComp(uid)) + if (Resolve(ent, ref ent.Comp2, false) && _roof.IsRooved((ent, ent.Comp1, ent.Comp2), tileRef.GridIndices)) return false; - var anchoredEntities = _mapSystem.GetAnchoredEntitiesEnumerator(uid, grid, tileRef.GridIndices); + var tileDef = (ContentTileDefinition)_tileDefManager[tileRef.Tile.TypeId]; + + if (!tileDef.Weather) + return false; - while (anchoredEntities.MoveNext(out var ent)) + var anchoredEntities = _mapSystem.GetAnchoredEntitiesEnumerator(ent, ent.Comp1, tileRef.GridIndices); + + while (anchoredEntities.MoveNext(out var anchored)) { - if (_blockQuery.HasComponent(ent.Value)) + if (_blockQuery.HasComponent(anchored.Value)) return false; } return true; - } - public float GetPercent(WeatherData component, EntityUid mapUid) + /// + /// Calculates the current “strength” of the specified weather based on the duration of the status effect. + /// Between 0 and 1. + /// + public float GetWeatherPercent(Entity ent) { - var pauseTime = _metadata.GetPauseTime(mapUid); - var elapsed = Timing.CurTime - (component.StartTime + pauseTime); - var duration = component.Duration; + var elapsed = Timing.CurTime - ent.Comp.StartEffectTime; + var duration = ent.Comp.Duration; var remaining = duration - elapsed; - float alpha; - if (remaining < WeatherComponent.ShutdownTime) - { - alpha = (float) (remaining / WeatherComponent.ShutdownTime); - } - else if (elapsed < WeatherComponent.StartupTime) + if (remaining < ShutdownTime) + return (float)(remaining / ShutdownTime); + else if (elapsed < StartupTime) + return (float)(elapsed / StartupTime); + else + return 1f; + } + + public bool TryAddWeather(MapId mapId, EntProtoId weatherProto, [NotNullWhen(true)] out EntityUid? weatherEnt, TimeSpan? duration = null) + { + weatherEnt = null; + + if (!_mapSystem.TryGetMap(mapId, out var mapUid)) + return false; + + return TryAddWeather(mapUid.Value, weatherProto, out weatherEnt, duration); + } + + public bool TryAddWeather(EntityUid mapUid, EntProtoId weatherProto, [NotNullWhen(true)] out EntityUid? weatherEnt, TimeSpan? duration = null) + { + return _statusEffects.TrySetStatusEffectDuration(mapUid, weatherProto, out weatherEnt, duration); + } + + public bool HasWeather(MapId mapId, EntProtoId weatherProto) + { + if (!_mapSystem.TryGetMap(mapId, out var mapUid)) + return false; + + return _statusEffects.TryGetStatusEffect(mapUid.Value, weatherProto, out _); + } + + public bool TryRemoveWeather(MapId mapId, EntProtoId weatherProto) + { + if (!_mapSystem.TryGetMap(mapId, out var mapUid)) + return false; + + return TryRemoveWeather(mapUid.Value, weatherProto); + } + + public bool TryRemoveWeather(EntityUid mapUid, EntProtoId weatherProto) + { + if (!_statusEffects.TryGetStatusEffect(mapUid, weatherProto, out var weatherEnt)) + return false; + + if (!_weatherQuery.HasComp(weatherEnt)) + return false; + + return _statusEffects.TrySetStatusEffectDuration(mapUid, weatherProto, ShutdownTime); + } + + public bool TrySetWeather(MapId mapId, EntProtoId? weatherProto, out EntityUid? weatherEnt, TimeSpan? duration = null) + { + weatherEnt = null; + if (!_mapSystem.TryGetMap(mapId, out var mapUid)) + return false; + + if (_statusEffects.TryEffectsWithComp(mapUid, out var effects)) { - alpha = (float) (elapsed / WeatherComponent.StartupTime); + foreach (var effect in effects) + { + var effectProto = Prototype(effect); + if (effectProto is null) + continue; + + if (effectProto != weatherProto) + { + TryRemoveWeather(mapUid.Value, effectProto); + } + else + { + weatherEnt = effect; + } + } } - else + + if (weatherProto is null) + return true; + + if (weatherEnt != null) { - alpha = 1f; + TryAddWeather(mapUid.Value, weatherProto.Value, out weatherEnt, duration); + return true; } - return alpha; + return TryAddWeather(mapUid.Value, weatherProto.Value, out weatherEnt, duration); + } +} + +*** Add File: f:\Floofdev\HardLight\Content.Client\Overlays\StencilOverlaySystem.cs +using Content.Client.Parallax; +using Content.Client.Weather; +using Content.Shared.StatusEffectNew; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; + +namespace Content.Client.Overlays; + +public sealed class StencilOverlaySystem : EntitySystem +{ + [Dependency] private readonly IOverlayManager _overlay = default!; + [Dependency] private readonly ParallaxSystem _parallax = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly SharedMapSystem _map = default!; + [Dependency] private readonly SpriteSystem _sprite = default!; + [Dependency] private readonly WeatherSystem _weather = default!; + [Dependency] private readonly StatusEffectsSystem _status = default!; + + public override void Initialize() + { + base.Initialize(); + _overlay.AddOverlay(new StencilOverlay(_parallax, _transform, _map, _sprite, _weather, _status)); + } + + public override void Shutdown() + { + base.Shutdown(); + _overlay.RemoveOverlay(); + } +} +*** Add File: f:\Floofdev\HardLight\Content.Client\Weather\WeatherSystem.cs +using System.Numerics; +using Content.Shared.Light.Components; +using Content.Shared.StatusEffectNew.Components; +using Content.Shared.Weather; +using Robust.Client.Audio; +using Robust.Client.GameObjects; +using Robust.Client.Player; +using Robust.Shared.Audio.Components; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Player; + +namespace Content.Client.Weather; + +public sealed class WeatherSystem : SharedWeatherSystem +{ + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly AudioSystem _audio = default!; + [Dependency] private readonly MapSystem _mapSystem = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + + private EntityQuery _audioQuery; + private EntityQuery _gridQuery; + private EntityQuery _roofQuery; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnComponentShutdown); + + _audioQuery = GetEntityQuery(); + _gridQuery = GetEntityQuery(); + _roofQuery = GetEntityQuery(); + } + + private void OnComponentShutdown(Entity ent, ref ComponentShutdown args) + { + ent.Comp.Stream = _audio.Stop(ent.Comp.Stream); } public override void Update(float frameTime) @@ -92,135 +242,552 @@ public override void Update(float frameTime) if (!Timing.IsFirstTimePredicted) return; - var curTime = Timing.CurTime; + var player = _playerManager.LocalEntity; - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var comp)) - { - if (comp.Weather.Count == 0) - continue; + if (player == null) + return; + + var playerXform = Transform(player.Value); - foreach (var (proto, weather) in comp.Weather) + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var weather, out var status)) + { + if (weather.Sound == null || status.AppliedTo != playerXform.MapUid) { - var endTime = weather.EndTime; + weather.Stream = _audio.Stop(weather.Stream); + return; + } - if (endTime != null && endTime < curTime) - { - EndWeather(uid, comp, proto); - continue; - } + weather.Stream ??= _audio.PlayGlobal(weather.Sound, Filter.Local(), true)?.Entity; - var remainingTime = endTime - curTime; + if (!_audioQuery.TryComp(weather.Stream, out var audio)) + return; - if (!ProtoMan.TryIndex(proto, out var weatherProto)) + var occlusion = 0f; + + if (_gridQuery.TryComp(playerXform.GridUid, out var grid)) + { + _roofQuery.TryComp(playerXform.GridUid, out var roofComp); + var gridId = playerXform.GridUid.Value; + var seed = _mapSystem.GetTileRef(gridId, grid, playerXform.Coordinates); + var frontier = new Queue(); + frontier.Enqueue(seed); + EntityCoordinates? nearestNode = null; + var visited = new HashSet(); + + while (frontier.TryDequeue(out var node)) { - Log.Error($"Unable to find weather prototype for {comp.Weather}, ending!"); - EndWeather(uid, comp, proto); - continue; + if (!visited.Add(node.GridIndices)) + continue; + + if (!CanWeatherAffect((playerXform.GridUid.Value, grid, roofComp), node)) + { + for (var x = -1; x <= 1; x++) + { + for (var y = -1; y <= 1; y++) + { + if (Math.Abs(x) == 1 && Math.Abs(y) == 1 || + x == 0 && y == 0 || + (new Vector2(x, y) + node.GridIndices - seed.GridIndices).Length() > 3) + { + continue; + } + + frontier.Enqueue(_mapSystem.GetTileRef(gridId, grid, new Vector2i(x, y) + node.GridIndices)); + } + } + + continue; + } + + nearestNode = new EntityCoordinates(playerXform.GridUid.Value, + node.GridIndices + grid.TileSizeHalfVector); + break; } - if (endTime != null && remainingTime < WeatherComponent.ShutdownTime) + if (nearestNode != null) { - SetState(uid, WeatherState.Ending, comp, weather, weatherProto); + var entPos = _transform.GetMapCoordinates(playerXform); + var nodePosition = _transform.ToMapCoordinates(nearestNode.Value).Position; + var delta = nodePosition - entPos.Position; + var distance = delta.Length(); + occlusion = _audio.GetOcclusion(entPos, delta, distance); } else { - var startTime = weather.StartTime; - var elapsed = Timing.CurTime - startTime; - - if (elapsed < WeatherComponent.StartupTime) - { - SetState(uid, WeatherState.Starting, comp, weather, weatherProto); - } + occlusion = 3f; } - - Run(uid, weather, weatherProto, frameTime); } + + var alpha = GetWeatherPercent((uid, status)); + alpha *= SharedAudioSystem.VolumeToGain(weather.Sound.Params.Volume); + _audio.SetGain(weather.Stream, alpha, audio); + audio.Occlusion = occlusion; } } +} +*** Add File: f:\Floofdev\HardLight\Content.Server\Weather\Commands\WeatherAddCommand.cs +using Content.Server.Administration; +using Content.Shared.Administration; +using Content.Shared.Prototypes; +using Content.Shared.Weather; +using Robust.Shared.Console; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; + +namespace Content.Server.Weather.Commands; + +/// +/// Add specific weather to map. +/// +[AdminCommand(AdminFlags.Fun)] +public sealed class WeatherAddCommand : LocalizedEntityCommands +{ + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly SharedMapSystem _map = default!; + [Dependency] private readonly WeatherSystem _weather = default!; + [Dependency] private readonly IComponentFactory _compFactory = default!; + + public override string Command => "weatheradd"; - public void SetWeather(MapId mapId, WeatherPrototype? proto, TimeSpan? endTime) + public override void Execute(IConsoleShell shell, string argStr, string[] args) { - if (!_mapSystem.TryGetMap(mapId, out var mapUid)) + if (args.Length < 2) + { + shell.WriteError(Loc.GetString("cmd-weather-error-no-arguments")); + return; + } + + if (!int.TryParse(args[0], out var mapInt)) return; - var weatherComp = EnsureComp(mapUid.Value); + var mapId = new MapId(mapInt); - foreach (var (eProto, weather) in weatherComp.Weather) + if (!_map.MapExists(mapId)) { - if (proto == null) - endTime ??= Timing.CurTime + WeatherComponent.ShutdownTime; + shell.WriteError(Loc.GetString("cmd-weather-error-wrong-map", ("id", mapId.ToString()))); + return; + } - if (proto is not null && eProto == proto.ID) + EntProtoId weatherProto = args[1]; + if (!_proto.TryIndex(weatherProto, out _)) + { + shell.WriteError(Loc.GetString("cmd-weather-error-unknown-proto")); + return; + } + + TimeSpan? duration = null; + if (args.Length == 3) + { + if (int.TryParse(args[2], out var durationInt)) + duration = TimeSpan.FromSeconds(durationInt); + else + shell.WriteError(Loc.GetString("cmd-weather-error-wrong-time")); + } + + _weather.TryAddWeather(mapId, weatherProto, out _, duration); + } + + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + if (args.Length == 1) + return CompletionResult.FromHintOptions(CompletionHelper.MapIds(EntityManager), Loc.GetString("cmd-weather-hint-map-id")); + + if (args.Length == 2) + { + var opts = new List(); + foreach (var proto in _proto.EnumeratePrototypes()) { - weather.EndTime = endTime; - if (weather.State == WeatherState.Ending) - weather.State = WeatherState.Running; + if (!proto.HasComponent(_compFactory)) + continue; - Dirty(mapUid.Value, weatherComp); - continue; + opts.Add(new CompletionOption(proto.ID, proto.Name)); } + return CompletionResult.FromHintOptions(opts, Loc.GetString("cmd-weather-hint-prototype")); + } + + if (args.Length == 3) + return CompletionResult.FromHint(Loc.GetString("cmd-weather-hint-time")); + + return CompletionResult.Empty; + } +} +*** Add File: f:\Floofdev\HardLight\Content.Server\Weather\Commands\WeatherRemoveCommand.cs +using Content.Server.Administration; +using Content.Shared.Administration; +using Content.Shared.Prototypes; +using Content.Shared.Weather; +using Robust.Shared.Console; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; + +namespace Content.Server.Weather.Commands; + +/// +/// Remove specific weather from map. +/// +[AdminCommand(AdminFlags.Fun)] +public sealed class WeatherRemoveCommand : LocalizedEntityCommands +{ + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly SharedMapSystem _map = default!; + [Dependency] private readonly WeatherSystem _weather = default!; + [Dependency] private readonly IComponentFactory _compFactory = default!; + + public override string Command => "weatherremove"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length < 2) + { + shell.WriteError(Loc.GetString("cmd-weather-error-no-arguments")); + return; + } + + if (!int.TryParse(args[0], out var mapInt)) + return; + + var mapId = new MapId(mapInt); + + if (!_map.MapExists(mapId)) + { + shell.WriteError(Loc.GetString("cmd-weather-error-wrong-map", ("id", mapId.ToString()))); + return; + } + + EntProtoId weatherProto = args[1]; + if (!_proto.TryIndex(weatherProto, out _)) + { + shell.WriteError(Loc.GetString("cmd-weather-error-unknown-proto")); + return; + } + + if (!_weather.HasWeather(mapId, weatherProto)) + { + shell.WriteError(Loc.GetString("cmd-weather-error-no-weather")); + return; + } - var end = Timing.CurTime + WeatherComponent.ShutdownTime; + _weather.TryRemoveWeather(mapId, weatherProto); + } - if (weather.EndTime == null || weather.EndTime > end) + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + if (args.Length == 1) + return CompletionResult.FromHintOptions(CompletionHelper.MapIds(EntityManager), Loc.GetString("cmd-weather-hint-map-id")); + + if (args.Length == 2) + { + var opts = new List(); + foreach (var proto in _proto.EnumeratePrototypes()) { - weather.EndTime = end; - Dirty(mapUid.Value, weatherComp); + if (!proto.HasComponent(_compFactory)) + continue; + + opts.Add(new CompletionOption(proto.ID, proto.Name)); } + return CompletionResult.FromHintOptions(opts, Loc.GetString("cmd-weather-hint-prototype")); } - if (proto != null) - StartWeather(mapUid.Value, weatherComp, proto, endTime); + return CompletionResult.Empty; } +} +*** Add File: f:\Floofdev\HardLight\Content.Server\Weather\Commands\WeatherSetCommand.cs +using Content.Server.Administration; +using Content.Shared.Administration; +using Content.Shared.Prototypes; +using Content.Shared.Weather; +using Robust.Shared.Console; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; - protected virtual void Run(EntityUid uid, WeatherData weather, WeatherPrototype weatherProto, float frameTime) { } +namespace Content.Server.Weather.Commands; - protected void StartWeather(EntityUid uid, WeatherComponent component, WeatherPrototype weather, TimeSpan? endTime) +/// +/// Removes all weather except the specified one. If the specified weather does not exist on the map, it adds it. +/// +[AdminCommand(AdminFlags.Fun)] +public sealed class WeatherSetCommand : LocalizedEntityCommands +{ + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly SharedMapSystem _map = default!; + [Dependency] private readonly WeatherSystem _weather = default!; + [Dependency] private readonly IComponentFactory _compFactory = default!; + + public override string Command => "weatherset"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) { - if (component.Weather.ContainsKey(weather.ID)) + if (args.Length < 2) + { + shell.WriteError(Loc.GetString("cmd-weather-error-no-arguments")); return; + } + + if (!int.TryParse(args[0], out var mapInt)) + return; + + var mapId = new MapId(mapInt); + + if (!_map.MapExists(mapId)) + { + shell.WriteError(Loc.GetString("cmd-weather-error-wrong-map", ("id", mapId.ToString()))); + return; + } - var data = new WeatherData() + EntProtoId? weatherProto = args[1]; + if (args[1] == "null") + weatherProto = null; + else if (!_proto.TryIndex(weatherProto, out _)) { - StartTime = Timing.CurTime, - EndTime = endTime, - }; + shell.WriteError(Loc.GetString("cmd-weather-error-unknown-proto")); + return; + } - component.Weather.Add(weather.ID, data); - Dirty(uid, component); + TimeSpan? duration = null; + if (args.Length == 3) + { + if (int.TryParse(args[2], out var durationInt)) + duration = TimeSpan.FromSeconds(durationInt); + else + shell.WriteError(Loc.GetString("cmd-weather-error-wrong-time")); + } + + _weather.TrySetWeather(mapId, weatherProto, out _, duration); } - protected virtual void EndWeather(EntityUid uid, WeatherComponent component, string proto) + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) { - if (!component.Weather.TryGetValue(proto, out var data)) - return; + if (args.Length == 1) + return CompletionResult.FromHintOptions(CompletionHelper.MapIds(EntityManager), Loc.GetString("cmd-weather-hint-map-id")); + + if (args.Length == 2) + { + var opts = new List(); + foreach (var proto in _proto.EnumeratePrototypes()) + { + if (!proto.HasComponent(_compFactory)) + continue; + + opts.Add(new CompletionOption(proto.ID, proto.Name)); + } + return CompletionResult.FromHintOptions(opts, Loc.GetString("cmd-weather-hint-prototype")); + } + + if (args.Length == 3) + return CompletionResult.FromHint(Loc.GetString("cmd-weather-hint-time")); - _audio.Stop(data.Stream); - data.Stream = null; - component.Weather.Remove(proto); - Dirty(uid, component); + return CompletionResult.Empty; } +} +*** Add File: f:\Floofdev\HardLight\Content.Shared\StatusEffectNew\Components\StatusEffectComponent.cs +using Robust.Shared.GameStates; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.StatusEffectNew.Components; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true), AutoGenerateComponentPause] +public sealed partial class StatusEffectComponent : Component +{ + [DataField, AutoNetworkedField] + public EntityUid? AppliedTo; + + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField, AutoNetworkedField] + public TimeSpan StartEffectTime; + + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField, AutoNetworkedField] + public TimeSpan? EndEffectTime; + + [ViewVariables] + public TimeSpan Duration => EndEffectTime == null ? TimeSpan.MaxValue : EndEffectTime.Value - StartEffectTime; +} +*** Add File: f:\Floofdev\HardLight\Content.Shared\StatusEffectNew\Components\StatusEffectContainerComponent.cs +using Robust.Shared.Containers; + +namespace Content.Shared.StatusEffectNew.Components; + +[RegisterComponent] +public sealed partial class StatusEffectContainerComponent : Component +{ + public const string ContainerId = "status-effects"; + + [ViewVariables] + public Container? ActiveStatusEffects; +} +*** Add File: f:\Floofdev\HardLight\Content.Shared\StatusEffectNew\StatusEffectsSystem.cs +using System.Diagnostics.CodeAnalysis; +using Content.Shared.StatusEffectNew.Components; +using Robust.Shared.Containers; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Shared.StatusEffectNew; - protected virtual bool SetState(EntityUid uid, WeatherState state, WeatherComponent component, WeatherData weather, WeatherPrototype weatherProto) +public sealed class StatusEffectsSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + + private EntityQuery _containerQuery; + private EntityQuery _statusQuery; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnContainerInit); + SubscribeLocalEvent(OnContainerShutdown); + + _containerQuery = GetEntityQuery(); + _statusQuery = GetEntityQuery(); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var status)) + { + if (status.EndEffectTime is not { } endTime) + continue; + + if (_timing.CurTime < endTime) + continue; + + PredictedQueueDel(uid); + } + } + + private void OnContainerInit(Entity ent, ref ComponentInit args) + { + ent.Comp.ActiveStatusEffects = _container.EnsureContainer(ent, StatusEffectContainerComponent.ContainerId); + ent.Comp.ActiveStatusEffects.ShowContents = true; + ent.Comp.ActiveStatusEffects.OccludesLight = false; + } + + private void OnContainerShutdown(Entity ent, ref ComponentShutdown args) { - if (weather.State.Equals(state)) + if (ent.Comp.ActiveStatusEffects is { } container) + _container.ShutdownContainer(container); + } + + public bool TryGetStatusEffect(EntityUid target, EntProtoId effectProto, [NotNullWhen(true)] out EntityUid? statusEffect) + { + statusEffect = null; + + if (!_containerQuery.TryComp(target, out var containerComp) || containerComp.ActiveStatusEffects == null) return false; - weather.State = state; - Dirty(uid, component); + foreach (var contained in containerComp.ActiveStatusEffects.ContainedEntities) + { + if (!_statusQuery.TryComp(contained, out var status) || status.AppliedTo != target) + continue; + + if (Prototype(contained) != effectProto) + continue; + + statusEffect = contained; + return true; + } + + return false; + } + + public bool TrySetStatusEffectDuration(EntityUid target, EntProtoId effectProto, TimeSpan? duration) + { + return TrySetStatusEffectDuration(target, effectProto, out _, duration); + } + + public bool TrySetStatusEffectDuration(EntityUid target, EntProtoId effectProto, [NotNullWhen(true)] out EntityUid? statusEffect, TimeSpan? duration = null) + { + statusEffect = null; + + if (TryGetStatusEffect(target, effectProto, out statusEffect)) + { + if (!_statusQuery.TryComp(statusEffect.Value, out var existing)) + return false; + + existing.AppliedTo = target; + existing.StartEffectTime = _timing.CurTime; + existing.EndEffectTime = duration == null ? null : _timing.CurTime + duration.Value; + Dirty(statusEffect.Value, existing); + return true; + } + + EnsureComp(target); + + if (!PredictedTrySpawnInContainer(effectProto, target, StatusEffectContainerComponent.ContainerId, out var spawned)) + return false; + + if (!_statusQuery.TryComp(spawned, out var status)) + return false; + + status.AppliedTo = target; + status.StartEffectTime = _timing.CurTime; + status.EndEffectTime = duration == null ? null : _timing.CurTime + duration.Value; + Dirty(spawned.Value, status); + + statusEffect = spawned; return true; } - [Serializable, NetSerializable] - protected sealed class WeatherComponentState : ComponentState + public bool TryEffectsWithComp(EntityUid target, [NotNullWhen(true)] out HashSet>? effects) + where T : IComponent { - public Dictionary, WeatherData> Weather; + effects = null; - public WeatherComponentState(Dictionary, WeatherData> weather) + if (!_containerQuery.TryComp(target, out var containerComp) || containerComp.ActiveStatusEffects == null) + return false; + + var set = new HashSet>(); + foreach (var contained in containerComp.ActiveStatusEffects.ContainedEntities) { - Weather = weather; + if (!TryComp(contained, out var comp) || !_statusQuery.TryComp(contained, out var status)) + continue; + + if (status.AppliedTo != target) + continue; + + set.Add((contained, comp, status)); } + + if (set.Count == 0) + return false; + + effects = set; + return true; } } +*** Add File: f:\Floofdev\HardLight\Content.Shared\Weather\WeatherStatusEffectComponent.cs +using System.Numerics; +using Content.Shared.StatusEffectNew.Components; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Utility; + +namespace Content.Shared.Weather; + +/// +/// Used only in conjure with for status effects applied to map entities. +/// Contains basic information about all types of weather effects. +/// +[RegisterComponent, NetworkedComponent, Access(typeof(SharedWeatherSystem))] +public sealed partial class WeatherStatusEffectComponent : Component +{ + [DataField(required: true)] + public SpriteSpecifier Sprite = default!; + + [DataField] + public Color? Color; + + [DataField] + public Vector2? Scrolling; + + [DataField] + public SoundSpecifier? Sound; + + [ViewVariables] + public EntityUid? Stream; +} diff --git a/Content.Shared/Weather/WeatherStatusEffectComponent.cs b/Content.Shared/Weather/WeatherStatusEffectComponent.cs deleted file mode 100644 index e82c728515d..00000000000 --- a/Content.Shared/Weather/WeatherStatusEffectComponent.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Numerics; -using Content.Shared.StatusEffectNew.Components; -using Robust.Shared.Audio; -using Robust.Shared.GameStates; -using Robust.Shared.Utility; - -namespace Content.Shared.Weather; - -/// -/// Used only in conjure with for status effects applied to map entities. -/// Contains basic information about all types of weather effects. -/// -[RegisterComponent, NetworkedComponent, Access(typeof(SharedWeatherSystem))] -public sealed partial class WeatherStatusEffectComponent : Component -{ - /// - /// A texture that will tile and render as a weather effect across the entire map. - /// - [DataField(required: true)] - public SpriteSpecifier Sprite = default!; - - /// - /// Tint that will be applied to the weather texture. - /// - [DataField] - public Color? Color; - - /// - /// Weather scrolling speed. - /// - [DataField] - public Vector2? Scrolling; - - /// - /// Sound to play on the affected areas. - /// - [DataField] - public SoundSpecifier? Sound; - - /// - /// Client audio stream. - /// Not used on the server. - /// - [ViewVariables] - public EntityUid? Stream; -} From f60f9b199cbf008927e3d8c7cd765bd0a88124a1 Mon Sep 17 00:00:00 2001 From: fenndragon Date: Tue, 28 Apr 2026 22:07:16 -0600 Subject: [PATCH 09/49] fixes --- .../Overlays/StencilOverlaySystem.cs | 30 + Content.Client/Weather/WeatherSystem.cs | 133 ++++ .../Components/StatusEffectComponent.cs | 39 -- .../StatusEffectContainerComponent.cs | 12 + .../StatusEffectNew/StatusEffectsSystem.cs | 338 ++------- Content.Shared/Weather/SharedWeatherSystem.cs | 655 +----------------- .../Weather/WeatherStatusEffectComponent.cs | 26 + 7 files changed, 267 insertions(+), 966 deletions(-) create mode 100644 Content.Client/Overlays/StencilOverlaySystem.cs create mode 100644 Content.Client/Weather/WeatherSystem.cs create mode 100644 Content.Shared/StatusEffectNew/Components/StatusEffectContainerComponent.cs create mode 100644 Content.Shared/Weather/WeatherStatusEffectComponent.cs diff --git a/Content.Client/Overlays/StencilOverlaySystem.cs b/Content.Client/Overlays/StencilOverlaySystem.cs new file mode 100644 index 00000000000..95243fe6f99 --- /dev/null +++ b/Content.Client/Overlays/StencilOverlaySystem.cs @@ -0,0 +1,30 @@ +using Content.Client.Parallax; +using Content.Client.Weather; +using Content.Shared.StatusEffectNew; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; + +namespace Content.Client.Overlays; + +public sealed class StencilOverlaySystem : EntitySystem +{ + [Dependency] private readonly IOverlayManager _overlay = default!; + [Dependency] private readonly ParallaxSystem _parallax = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly SharedMapSystem _map = default!; + [Dependency] private readonly SpriteSystem _sprite = default!; + [Dependency] private readonly WeatherSystem _weather = default!; + [Dependency] private readonly StatusEffectsSystem _status = default!; + + public override void Initialize() + { + base.Initialize(); + _overlay.AddOverlay(new StencilOverlay(_parallax, _transform, _map, _sprite, _weather, _status)); + } + + public override void Shutdown() + { + base.Shutdown(); + _overlay.RemoveOverlay(); + } +} diff --git a/Content.Client/Weather/WeatherSystem.cs b/Content.Client/Weather/WeatherSystem.cs new file mode 100644 index 00000000000..c1ccd2a546f --- /dev/null +++ b/Content.Client/Weather/WeatherSystem.cs @@ -0,0 +1,133 @@ +using System.Numerics; +using Content.Shared.Light.Components; +using Content.Shared.StatusEffectNew.Components; +using Content.Shared.Weather; +using Robust.Client.Audio; +using Robust.Client.GameObjects; +using Robust.Client.Player; +using Robust.Shared.Audio.Components; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Player; + +namespace Content.Client.Weather; + +public sealed class WeatherSystem : SharedWeatherSystem +{ + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly AudioSystem _audio = default!; + [Dependency] private readonly MapSystem _mapSystem = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + + private EntityQuery _audioQuery; + private EntityQuery _gridQuery; + private EntityQuery _roofQuery; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnComponentShutdown); + + _audioQuery = GetEntityQuery(); + _gridQuery = GetEntityQuery(); + _roofQuery = GetEntityQuery(); + } + + private void OnComponentShutdown(Entity ent, ref ComponentShutdown args) + { + ent.Comp.Stream = _audio.Stop(ent.Comp.Stream); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + if (!Timing.IsFirstTimePredicted) + return; + + var player = _playerManager.LocalEntity; + + if (player == null) + return; + + var playerXform = Transform(player.Value); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var weather, out var status)) + { + if (weather.Sound == null || status.AppliedTo != playerXform.MapUid) + { + weather.Stream = _audio.Stop(weather.Stream); + return; + } + + weather.Stream ??= _audio.PlayGlobal(weather.Sound, Filter.Local(), true)?.Entity; + + if (!_audioQuery.TryComp(weather.Stream, out var audio)) + return; + + var occlusion = 0f; + + if (_gridQuery.TryComp(playerXform.GridUid, out var grid)) + { + _roofQuery.TryComp(playerXform.GridUid, out var roofComp); + var gridId = playerXform.GridUid.Value; + var seed = _mapSystem.GetTileRef(gridId, grid, playerXform.Coordinates); + var frontier = new Queue(); + frontier.Enqueue(seed); + EntityCoordinates? nearestNode = null; + var visited = new HashSet(); + + while (frontier.TryDequeue(out var node)) + { + if (!visited.Add(node.GridIndices)) + continue; + + if (!CanWeatherAffect((playerXform.GridUid.Value, grid, roofComp), node)) + { + for (var x = -1; x <= 1; x++) + { + for (var y = -1; y <= 1; y++) + { + if (Math.Abs(x) == 1 && Math.Abs(y) == 1 || + x == 0 && y == 0 || + (new Vector2(x, y) + node.GridIndices - seed.GridIndices).Length() > 3) + { + continue; + } + + frontier.Enqueue(_mapSystem.GetTileRef(gridId, grid, new Vector2i(x, y) + node.GridIndices)); + } + } + + continue; + } + + nearestNode = new EntityCoordinates(playerXform.GridUid.Value, + node.GridIndices + grid.TileSizeHalfVector); + break; + } + + if (nearestNode != null) + { + var entPos = _transform.GetMapCoordinates(playerXform); + var nodePosition = _transform.ToMapCoordinates(nearestNode.Value).Position; + var delta = nodePosition - entPos.Position; + var distance = delta.Length(); + occlusion = _audio.GetOcclusion(entPos, delta, distance); + } + else + { + occlusion = 3f; + } + } + + var alpha = GetWeatherPercent((uid, status)); + alpha *= SharedAudioSystem.VolumeToGain(weather.Sound.Params.Volume); + _audio.SetGain(weather.Stream, alpha, audio); + audio.Occlusion = occlusion; + } + } +} diff --git a/Content.Shared/StatusEffectNew/Components/StatusEffectComponent.cs b/Content.Shared/StatusEffectNew/Components/StatusEffectComponent.cs index 28becccee21..6cb0975b557 100644 --- a/Content.Shared/StatusEffectNew/Components/StatusEffectComponent.cs +++ b/Content.Shared/StatusEffectNew/Components/StatusEffectComponent.cs @@ -1,59 +1,20 @@ -using Content.Shared.Whitelist; using Robust.Shared.GameStates; -using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; namespace Content.Shared.StatusEffectNew.Components; -/// -/// Marker component for all status effects - every status effect entity should have it. -/// Provides a link between the effect and the affected entity, and some data common to all status effects. -/// [RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true), AutoGenerateComponentPause] -[Access(typeof(StatusEffectsSystem))] -[EntityCategory("StatusEffects")] public sealed partial class StatusEffectComponent : Component { - /// - /// The entity that this status effect is applied to. - /// [DataField, AutoNetworkedField] public EntityUid? AppliedTo; - /// - /// When this effect will start. Set to Timespan.Zero to start the effect immediately. - /// [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField, AutoNetworkedField] public TimeSpan StartEffectTime; - /// - /// When this effect will end. If Null, the effect lasts indefinitely. - /// [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField, AutoNetworkedField] public TimeSpan? EndEffectTime; - /// - /// If true, this status effect has been applied. Used to ensure that only fires once. - /// - /// We actually don't want to network this, that way client can apply an effect it's receiving properly! - [DataField] - public bool Applied; - - /// - /// Whitelist, by which it is determined whether this status effect can be imposed on a particular entity. - /// - [DataField] - public EntityWhitelist? Whitelist; - - /// - /// Blacklist, by which it is determined whether this status effect can be imposed on a particular entity. - /// - [DataField] - public EntityWhitelist? Blacklist; - - /// - /// QoL function, returns total duration of this status effect. - /// [ViewVariables] public TimeSpan Duration => EndEffectTime == null ? TimeSpan.MaxValue : EndEffectTime.Value - StartEffectTime; } diff --git a/Content.Shared/StatusEffectNew/Components/StatusEffectContainerComponent.cs b/Content.Shared/StatusEffectNew/Components/StatusEffectContainerComponent.cs new file mode 100644 index 00000000000..e1feba1eefa --- /dev/null +++ b/Content.Shared/StatusEffectNew/Components/StatusEffectContainerComponent.cs @@ -0,0 +1,12 @@ +using Robust.Shared.Containers; + +namespace Content.Shared.StatusEffectNew.Components; + +[RegisterComponent] +public sealed partial class StatusEffectContainerComponent : Component +{ + public const string ContainerId = "status-effects"; + + [ViewVariables] + public Container? ActiveStatusEffects; +} diff --git a/Content.Shared/StatusEffectNew/StatusEffectsSystem.cs b/Content.Shared/StatusEffectNew/StatusEffectsSystem.cs index 512285eaf34..e3528f48701 100644 --- a/Content.Shared/StatusEffectNew/StatusEffectsSystem.cs +++ b/Content.Shared/StatusEffectNew/StatusEffectsSystem.cs @@ -1,49 +1,28 @@ using System.Diagnostics.CodeAnalysis; -using Content.Shared.Rejuvenate; using Content.Shared.StatusEffectNew.Components; -using Content.Shared.Whitelist; using Robust.Shared.Containers; using Robust.Shared.Prototypes; using Robust.Shared.Timing; namespace Content.Shared.StatusEffectNew; -/// -/// This system controls status effects, their lifetime, and provides an API for adding them to entities, -/// removing them from entities, or getting information about current effects on entities. -/// -public sealed partial class StatusEffectsSystem : EntitySystem +public sealed class StatusEffectsSystem : EntitySystem { - [Dependency] private readonly IComponentFactory _factory = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly SharedContainerSystem _container = default!; - [Dependency] private readonly EntityWhitelistSystem _whitelist = default!; - [Dependency] private readonly IPrototypeManager _proto = default!; private EntityQuery _containerQuery; - private EntityQuery _effectQuery; - - public readonly HashSet StatusEffectPrototypes = []; + private EntityQuery _statusQuery; public override void Initialize() { base.Initialize(); - InitializeRelay(); - SubscribeLocalEvent(OnStatusContainerInit); SubscribeLocalEvent(OnStatusContainerShutdown); - SubscribeLocalEvent(OnEntityInserted); - SubscribeLocalEvent(OnEntityRemoved); - - SubscribeLocalEvent>(OnRejuvenate); - - SubscribeLocalEvent(OnPrototypesReloaded); _containerQuery = GetEntityQuery(); - _effectQuery = GetEntityQuery(); - - ReloadStatusEffectsCache(); + _statusQuery = GetEntityQuery(); } public override void Update(float frameTime) @@ -51,47 +30,21 @@ public override void Update(float frameTime) base.Update(frameTime); var query = EntityQueryEnumerator(); - while (query.MoveNext(out var ent, out var effect)) + while (query.MoveNext(out var uid, out var status)) { - TryApplyStatusEffect((ent, effect)); - - if (effect.EndEffectTime is null) + if (status.EndEffectTime is not { } endTime) continue; - if (_timing.CurTime < effect.EndEffectTime) + if (_timing.CurTime < endTime) continue; - if (effect.AppliedTo is null) - continue; - - PredictedQueueDel(ent); - } - } - - private void OnPrototypesReloaded(PrototypesReloadedEventArgs args) - { - if (!args.WasModified()) - return; - - ReloadStatusEffectsCache(); - } - - private void ReloadStatusEffectsCache() - { - StatusEffectPrototypes.Clear(); - - foreach (var ent in _proto.EnumeratePrototypes()) - { - if (ent.TryGetComponent(out _, _factory)) - StatusEffectPrototypes.Add(ent.ID); + PredictedQueueDel(uid); } } private void OnStatusContainerInit(Entity ent, ref ComponentInit args) { - ent.Comp.ActiveStatusEffects = - _container.EnsureContainer(ent, StatusEffectContainerComponent.ContainerId); - // We show the contents of the container to allow status effects to have visible sprites. + ent.Comp.ActiveStatusEffects = _container.EnsureContainer(ent, StatusEffectContainerComponent.ContainerId); ent.Comp.ActiveStatusEffects.ShowContents = true; ent.Comp.ActiveStatusEffects.OccludesLight = false; } @@ -102,265 +55,90 @@ private void OnStatusContainerShutdown(Entity en _container.ShutdownContainer(container); } - private void OnEntityInserted(Entity ent, ref EntInsertedIntoContainerMessage args) + public bool TryGetStatusEffect(EntityUid target, EntProtoId effectProto, [NotNullWhen(true)] out EntityUid? statusEffect) { - if (args.Container.ID != StatusEffectContainerComponent.ContainerId) - return; + statusEffect = null; - if (!_effectQuery.TryComp(args.Entity, out var statusComp)) - return; + if (!_containerQuery.TryComp(target, out var containerComp) || containerComp.ActiveStatusEffects == null) + return false; - // Make sure AppliedTo is set correctly so events can rely on it - if (statusComp.AppliedTo != ent) + foreach (var contained in containerComp.ActiveStatusEffects.ContainedEntities) { - statusComp.AppliedTo = ent; - DirtyField(args.Entity, statusComp, nameof(StatusEffectComponent.AppliedTo)); - } - } - - private void OnEntityRemoved(Entity ent, ref EntRemovedFromContainerMessage args) - { - if (args.Container.ID != StatusEffectContainerComponent.ContainerId) - return; - - if (!_effectQuery.TryComp(args.Entity, out var statusComp)) - return; - - var ev = new StatusEffectRemovedEvent(ent); - RaiseLocalEvent(args.Entity, ref ev); - - // Clear AppliedTo after events are handled so event handlers can use it. - if (statusComp.AppliedTo == null) - return; - - // Why not just delete it? Well, that might end up being best, but this - // could theoretically allow for moving status effects from one entity - // to another. That might be good to have for polymorphs or something. - statusComp.AppliedTo = null; - Dirty(args.Entity, statusComp); - } - - private void OnRejuvenate(Entity ent, - ref StatusEffectRelayedEvent args) - { - PredictedQueueDel(ent.Owner); - } - - /// - /// Applies the status effect, i.e. starts it after it has been added. Ensures delayed start times trigger when they should. - /// - /// The status effect entity. - /// Returns true if the effect is applied. - private bool TryApplyStatusEffect(Entity statusEffectEnt) - { - if (statusEffectEnt.Comp.Applied || - statusEffectEnt.Comp.AppliedTo == null || - _timing.CurTime < statusEffectEnt.Comp.StartEffectTime) - return false; + if (!_statusQuery.TryComp(contained, out var status) || status.AppliedTo != target) + continue; - var ev = new StatusEffectAppliedEvent(statusEffectEnt.Comp.AppliedTo.Value); - RaiseLocalEvent(statusEffectEnt, ref ev); + if (Prototype(contained) != effectProto) + continue; - statusEffectEnt.Comp.Applied = true; + statusEffect = contained; + return true; + } - return true; + return false; } - public bool CanAddStatusEffect(EntityUid uid, EntProtoId effectProto) + public bool TrySetStatusEffectDuration(EntityUid target, EntProtoId effectProto, TimeSpan? duration) { - if (!_proto.Resolve(effectProto, out var effectProtoData)) - return false; - - if (!effectProtoData.TryGetComponent(out var effectProtoComp, Factory)) - return false; - - if (!_whitelist.CheckBoth(uid, effectProtoComp.Blacklist, effectProtoComp.Whitelist)) - return false; - - var ev = new BeforeStatusEffectAddedEvent(effectProto); - RaiseLocalEvent(uid, ref ev); - - if (ev.Cancelled) - return false; - - return true; + return TrySetStatusEffectDuration(target, effectProto, out _, duration); } - /// - /// Attempts to add a status effect to the specified entity. Returns True if the effect is added, does not check if one - /// already exists as it's intended to be called after a check for an existing effect has already failed. - /// - /// The target entity to which the effect should be added. - /// ProtoId of the status effect entity. Make sure it has StatusEffectComponent on it. - /// Duration of status effect. Leave null and the effect will be permanent until it is removed using TryRemoveStatusEffect. - /// The delay of the effect. Leave null and the effect will be immediate. - /// The EntityUid of the status effect we have just created or null if we couldn't create one. - private bool TryAddStatusEffect( - EntityUid target, - EntProtoId effectProto, - [NotNullWhen(true)] out EntityUid? statusEffect, - TimeSpan? duration = null, - TimeSpan? delay = null - ) + public bool TrySetStatusEffectDuration(EntityUid target, EntProtoId effectProto, [NotNullWhen(true)] out EntityUid? statusEffect, TimeSpan? duration = null) { statusEffect = null; - if (duration <= TimeSpan.Zero) - return false; - - if (!CanAddStatusEffect(target, effectProto)) - return false; + if (TryGetStatusEffect(target, effectProto, out statusEffect)) + { + if (!_statusQuery.TryComp(statusEffect.Value, out var existing)) + return false; + + existing.AppliedTo = target; + existing.StartEffectTime = _timing.CurTime; + existing.EndEffectTime = duration == null ? null : _timing.CurTime + duration.Value; + Dirty(statusEffect.Value, existing); + return true; + } EnsureComp(target); - // And only if all checks passed we spawn the effect - if (!PredictedTrySpawnInContainer(effectProto, - target, - StatusEffectContainerComponent.ContainerId, - out var effect)) + if (!PredictedTrySpawnInContainer(effectProto, target, StatusEffectContainerComponent.ContainerId, out var spawned)) return false; - if (!_effectQuery.TryComp(effect, out var effectComp)) + if (!_statusQuery.TryComp(spawned, out var status)) return false; - statusEffect = effect; - - var endTime = delay == null ? _timing.CurTime + duration : _timing.CurTime + delay + duration; - SetStatusEffectEndTime((effect.Value, effectComp), endTime); - var startTime = delay == null ? _timing.CurTime : _timing.CurTime + delay.Value; - SetStatusEffectStartTime(effect.Value, startTime); - - TryApplyStatusEffect((statusEffect.Value, effectComp)); + status.AppliedTo = target; + status.StartEffectTime = _timing.CurTime; + status.EndEffectTime = duration == null ? null : _timing.CurTime + duration.Value; + Dirty(spawned.Value, status); + statusEffect = spawned; return true; } - private void UpdateStatusEffectTime(Entity effect, TimeSpan? duration) + public bool TryEffectsWithComp(EntityUid target, [NotNullWhen(true)] out HashSet>? effects) + where T : IComponent { - if (!_effectQuery.Resolve(effect, ref effect.Comp)) - return; - - // It's already infinitely long - if (effect.Comp.EndEffectTime is null) - return; + effects = null; - TimeSpan? newEndTime = null; + if (!_containerQuery.TryComp(target, out var containerComp) || containerComp.ActiveStatusEffects == null) + return false; - if (duration is not null) + var set = new HashSet>(); + foreach (var contained in containerComp.ActiveStatusEffects.ContainedEntities) { - // Don't update time to a smaller timespan... - newEndTime = _timing.CurTime + duration; - if (effect.Comp.EndEffectTime >= newEndTime) - return; - } - - SetStatusEffectEndTime(effect, newEndTime); - } - - private void UpdateStatusEffectDelay(Entity effect, TimeSpan? delay) - { - if (!_effectQuery.Resolve(effect, ref effect.Comp)) - return; - - // It's already started! - if (_timing.CurTime >= effect.Comp.StartEffectTime) - return; + if (!TryComp(contained, out var comp) || !_statusQuery.TryComp(contained, out var status)) + continue; - var newStartTime = TimeSpan.Zero; + if (status.AppliedTo != target) + continue; - if (delay is not null) - { - // Don't update time to a smaller timespan... - newStartTime = _timing.CurTime + delay.Value; - if (effect.Comp.StartEffectTime < newStartTime) - return; + set.Add((contained, comp, status)); } - SetStatusEffectStartTime(effect, newStartTime); - } - - private void AddStatusEffectTime(Entity effect, TimeSpan delta) - { - if (!_effectQuery.Resolve(effect, ref effect.Comp)) - return; - - // It's already infinitely long can't add or subtract from infinity... - if (effect.Comp.EndEffectTime is null) - return; - - // Add to the current end effect time, if we're here we should have one set already, and if it's null it's probably infinite. - SetStatusEffectEndTime((effect, effect.Comp), effect.Comp.EndEffectTime.Value + delta); - } - - private void SetStatusEffectEndTime(Entity ent, TimeSpan? endTime) - { - if (!_effectQuery.Resolve(ent, ref ent.Comp)) - return; - - if (ent.Comp.EndEffectTime == endTime) - return; - - ent.Comp.EndEffectTime = endTime; - - if (ent.Comp.AppliedTo is not { } appliedTo) - return; // Not much we can do! - - var ev = new StatusEffectEndTimeUpdatedEvent(appliedTo, endTime); - RaiseLocalEvent(ent, ref ev); - - DirtyField(ent, ent.Comp, nameof(StatusEffectComponent.EndEffectTime)); - } - - private void SetStatusEffectStartTime(Entity ent, TimeSpan startTime) - { - if (!_effectQuery.Resolve(ent, ref ent.Comp)) - return; - - if (ent.Comp.StartEffectTime == startTime) - return; - - ent.Comp.StartEffectTime = startTime; - - if (ent.Comp.AppliedTo is not { } appliedTo) - return; // Not much we can do! - - var ev = new StatusEffectStartTimeUpdatedEvent(appliedTo, startTime); - RaiseLocalEvent(ent, ref ev); + if (set.Count == 0) + return false; - DirtyField(ent, ent.Comp, nameof(StatusEffectComponent.StartEffectTime)); + effects = set; + return true; } } - -/// -/// Calls on effect entity, when a status effect is applied. -/// -[ByRefEvent] -public readonly record struct StatusEffectAppliedEvent(EntityUid Target); - -/// -/// Calls on effect entity, when a status effect is removed. -/// -[ByRefEvent] -public readonly record struct StatusEffectRemovedEvent(EntityUid Target); - -/// -/// Raised on an entity before a status effect is added to determine if adding it should be cancelled. -/// -[ByRefEvent] -public record struct BeforeStatusEffectAddedEvent(EntProtoId Effect, bool Cancelled = false); - -/// -/// Raised on an effect entity when its is updated in any way. -/// -/// The entity the effect is attached to. -/// The new end time of the status effect, included for convenience. -[ByRefEvent] -public record struct StatusEffectEndTimeUpdatedEvent(EntityUid Target, TimeSpan? EndTime); - -/// -/// Raised on an effect entity when its is updated in any way. -/// -/// The entity the effect is attached to. -/// The new start time of the status effect, included for convenience. -[ByRefEvent] -public record struct StatusEffectStartTimeUpdatedEvent(EntityUid Target, TimeSpan? StartTime); diff --git a/Content.Shared/Weather/SharedWeatherSystem.cs b/Content.Shared/Weather/SharedWeatherSystem.cs index 5cfefb79665..cba54e6c073 100644 --- a/Content.Shared/Weather/SharedWeatherSystem.cs +++ b/Content.Shared/Weather/SharedWeatherSystem.cs @@ -47,7 +47,7 @@ public bool CanWeatherAffect(Entity ent, Tile if (Resolve(ent, ref ent.Comp2, false) && _roof.IsRooved((ent, ent.Comp1, ent.Comp2), tileRef.GridIndices)) return false; - var tileDef = (ContentTileDefinition)_tileDefManager[tileRef.Tile.TypeId]; + var tileDef = (ContentTileDefinition) _tileDefManager[tileRef.Tile.TypeId]; if (!tileDef.Weather) return false; @@ -63,10 +63,6 @@ public bool CanWeatherAffect(Entity ent, Tile return true; } - /// - /// Calculates the current “strength” of the specified weather based on the duration of the status effect. - /// Between 0 and 1. - /// public float GetWeatherPercent(Entity ent) { var elapsed = Timing.CurTime - ent.Comp.StartEffectTime; @@ -74,11 +70,11 @@ public float GetWeatherPercent(Entity ent) var remaining = duration - elapsed; if (remaining < ShutdownTime) - return (float)(remaining / ShutdownTime); - else if (elapsed < StartupTime) - return (float)(elapsed / StartupTime); - else - return 1f; + return (float) (remaining / ShutdownTime); + if (elapsed < StartupTime) + return (float) (elapsed / StartupTime); + + return 1f; } public bool TryAddWeather(MapId mapId, EntProtoId weatherProto, [NotNullWhen(true)] out EntityUid? weatherEnt, TimeSpan? duration = null) @@ -134,21 +130,17 @@ public bool TrySetWeather(MapId mapId, EntProtoId? weatherProto, out EntityUid? foreach (var effect in effects) { var effectProto = Prototype(effect); - if (effectProto is null) + if (effectProto == null) continue; if (effectProto != weatherProto) - { TryRemoveWeather(mapUid.Value, effectProto); - } else - { weatherEnt = effect; - } } } - if (weatherProto is null) + if (weatherProto == null) return true; if (weatherEnt != null) @@ -160,634 +152,3 @@ public bool TrySetWeather(MapId mapId, EntProtoId? weatherProto, out EntityUid? return TryAddWeather(mapUid.Value, weatherProto.Value, out weatherEnt, duration); } } - -*** Add File: f:\Floofdev\HardLight\Content.Client\Overlays\StencilOverlaySystem.cs -using Content.Client.Parallax; -using Content.Client.Weather; -using Content.Shared.StatusEffectNew; -using Robust.Client.GameObjects; -using Robust.Client.Graphics; - -namespace Content.Client.Overlays; - -public sealed class StencilOverlaySystem : EntitySystem -{ - [Dependency] private readonly IOverlayManager _overlay = default!; - [Dependency] private readonly ParallaxSystem _parallax = default!; - [Dependency] private readonly SharedTransformSystem _transform = default!; - [Dependency] private readonly SharedMapSystem _map = default!; - [Dependency] private readonly SpriteSystem _sprite = default!; - [Dependency] private readonly WeatherSystem _weather = default!; - [Dependency] private readonly StatusEffectsSystem _status = default!; - - public override void Initialize() - { - base.Initialize(); - _overlay.AddOverlay(new StencilOverlay(_parallax, _transform, _map, _sprite, _weather, _status)); - } - - public override void Shutdown() - { - base.Shutdown(); - _overlay.RemoveOverlay(); - } -} -*** Add File: f:\Floofdev\HardLight\Content.Client\Weather\WeatherSystem.cs -using System.Numerics; -using Content.Shared.Light.Components; -using Content.Shared.StatusEffectNew.Components; -using Content.Shared.Weather; -using Robust.Client.Audio; -using Robust.Client.GameObjects; -using Robust.Client.Player; -using Robust.Shared.Audio.Components; -using Robust.Shared.Audio.Systems; -using Robust.Shared.Map; -using Robust.Shared.Map.Components; -using Robust.Shared.Player; - -namespace Content.Client.Weather; - -public sealed class WeatherSystem : SharedWeatherSystem -{ - [Dependency] private readonly IPlayerManager _playerManager = default!; - [Dependency] private readonly AudioSystem _audio = default!; - [Dependency] private readonly MapSystem _mapSystem = default!; - [Dependency] private readonly SharedTransformSystem _transform = default!; - - private EntityQuery _audioQuery; - private EntityQuery _gridQuery; - private EntityQuery _roofQuery; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnComponentShutdown); - - _audioQuery = GetEntityQuery(); - _gridQuery = GetEntityQuery(); - _roofQuery = GetEntityQuery(); - } - - private void OnComponentShutdown(Entity ent, ref ComponentShutdown args) - { - ent.Comp.Stream = _audio.Stop(ent.Comp.Stream); - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - - if (!Timing.IsFirstTimePredicted) - return; - - var player = _playerManager.LocalEntity; - - if (player == null) - return; - - var playerXform = Transform(player.Value); - - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var weather, out var status)) - { - if (weather.Sound == null || status.AppliedTo != playerXform.MapUid) - { - weather.Stream = _audio.Stop(weather.Stream); - return; - } - - weather.Stream ??= _audio.PlayGlobal(weather.Sound, Filter.Local(), true)?.Entity; - - if (!_audioQuery.TryComp(weather.Stream, out var audio)) - return; - - var occlusion = 0f; - - if (_gridQuery.TryComp(playerXform.GridUid, out var grid)) - { - _roofQuery.TryComp(playerXform.GridUid, out var roofComp); - var gridId = playerXform.GridUid.Value; - var seed = _mapSystem.GetTileRef(gridId, grid, playerXform.Coordinates); - var frontier = new Queue(); - frontier.Enqueue(seed); - EntityCoordinates? nearestNode = null; - var visited = new HashSet(); - - while (frontier.TryDequeue(out var node)) - { - if (!visited.Add(node.GridIndices)) - continue; - - if (!CanWeatherAffect((playerXform.GridUid.Value, grid, roofComp), node)) - { - for (var x = -1; x <= 1; x++) - { - for (var y = -1; y <= 1; y++) - { - if (Math.Abs(x) == 1 && Math.Abs(y) == 1 || - x == 0 && y == 0 || - (new Vector2(x, y) + node.GridIndices - seed.GridIndices).Length() > 3) - { - continue; - } - - frontier.Enqueue(_mapSystem.GetTileRef(gridId, grid, new Vector2i(x, y) + node.GridIndices)); - } - } - - continue; - } - - nearestNode = new EntityCoordinates(playerXform.GridUid.Value, - node.GridIndices + grid.TileSizeHalfVector); - break; - } - - if (nearestNode != null) - { - var entPos = _transform.GetMapCoordinates(playerXform); - var nodePosition = _transform.ToMapCoordinates(nearestNode.Value).Position; - var delta = nodePosition - entPos.Position; - var distance = delta.Length(); - occlusion = _audio.GetOcclusion(entPos, delta, distance); - } - else - { - occlusion = 3f; - } - } - - var alpha = GetWeatherPercent((uid, status)); - alpha *= SharedAudioSystem.VolumeToGain(weather.Sound.Params.Volume); - _audio.SetGain(weather.Stream, alpha, audio); - audio.Occlusion = occlusion; - } - } -} -*** Add File: f:\Floofdev\HardLight\Content.Server\Weather\Commands\WeatherAddCommand.cs -using Content.Server.Administration; -using Content.Shared.Administration; -using Content.Shared.Prototypes; -using Content.Shared.Weather; -using Robust.Shared.Console; -using Robust.Shared.Map; -using Robust.Shared.Prototypes; - -namespace Content.Server.Weather.Commands; - -/// -/// Add specific weather to map. -/// -[AdminCommand(AdminFlags.Fun)] -public sealed class WeatherAddCommand : LocalizedEntityCommands -{ - [Dependency] private readonly IPrototypeManager _proto = default!; - [Dependency] private readonly SharedMapSystem _map = default!; - [Dependency] private readonly WeatherSystem _weather = default!; - [Dependency] private readonly IComponentFactory _compFactory = default!; - - public override string Command => "weatheradd"; - - public override void Execute(IConsoleShell shell, string argStr, string[] args) - { - if (args.Length < 2) - { - shell.WriteError(Loc.GetString("cmd-weather-error-no-arguments")); - return; - } - - if (!int.TryParse(args[0], out var mapInt)) - return; - - var mapId = new MapId(mapInt); - - if (!_map.MapExists(mapId)) - { - shell.WriteError(Loc.GetString("cmd-weather-error-wrong-map", ("id", mapId.ToString()))); - return; - } - - EntProtoId weatherProto = args[1]; - if (!_proto.TryIndex(weatherProto, out _)) - { - shell.WriteError(Loc.GetString("cmd-weather-error-unknown-proto")); - return; - } - - TimeSpan? duration = null; - if (args.Length == 3) - { - if (int.TryParse(args[2], out var durationInt)) - duration = TimeSpan.FromSeconds(durationInt); - else - shell.WriteError(Loc.GetString("cmd-weather-error-wrong-time")); - } - - _weather.TryAddWeather(mapId, weatherProto, out _, duration); - } - - public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) - { - if (args.Length == 1) - return CompletionResult.FromHintOptions(CompletionHelper.MapIds(EntityManager), Loc.GetString("cmd-weather-hint-map-id")); - - if (args.Length == 2) - { - var opts = new List(); - foreach (var proto in _proto.EnumeratePrototypes()) - { - if (!proto.HasComponent(_compFactory)) - continue; - - opts.Add(new CompletionOption(proto.ID, proto.Name)); - } - return CompletionResult.FromHintOptions(opts, Loc.GetString("cmd-weather-hint-prototype")); - } - - if (args.Length == 3) - return CompletionResult.FromHint(Loc.GetString("cmd-weather-hint-time")); - - return CompletionResult.Empty; - } -} -*** Add File: f:\Floofdev\HardLight\Content.Server\Weather\Commands\WeatherRemoveCommand.cs -using Content.Server.Administration; -using Content.Shared.Administration; -using Content.Shared.Prototypes; -using Content.Shared.Weather; -using Robust.Shared.Console; -using Robust.Shared.Map; -using Robust.Shared.Prototypes; - -namespace Content.Server.Weather.Commands; - -/// -/// Remove specific weather from map. -/// -[AdminCommand(AdminFlags.Fun)] -public sealed class WeatherRemoveCommand : LocalizedEntityCommands -{ - [Dependency] private readonly IPrototypeManager _proto = default!; - [Dependency] private readonly SharedMapSystem _map = default!; - [Dependency] private readonly WeatherSystem _weather = default!; - [Dependency] private readonly IComponentFactory _compFactory = default!; - - public override string Command => "weatherremove"; - - public override void Execute(IConsoleShell shell, string argStr, string[] args) - { - if (args.Length < 2) - { - shell.WriteError(Loc.GetString("cmd-weather-error-no-arguments")); - return; - } - - if (!int.TryParse(args[0], out var mapInt)) - return; - - var mapId = new MapId(mapInt); - - if (!_map.MapExists(mapId)) - { - shell.WriteError(Loc.GetString("cmd-weather-error-wrong-map", ("id", mapId.ToString()))); - return; - } - - EntProtoId weatherProto = args[1]; - if (!_proto.TryIndex(weatherProto, out _)) - { - shell.WriteError(Loc.GetString("cmd-weather-error-unknown-proto")); - return; - } - - if (!_weather.HasWeather(mapId, weatherProto)) - { - shell.WriteError(Loc.GetString("cmd-weather-error-no-weather")); - return; - } - - _weather.TryRemoveWeather(mapId, weatherProto); - } - - public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) - { - if (args.Length == 1) - return CompletionResult.FromHintOptions(CompletionHelper.MapIds(EntityManager), Loc.GetString("cmd-weather-hint-map-id")); - - if (args.Length == 2) - { - var opts = new List(); - foreach (var proto in _proto.EnumeratePrototypes()) - { - if (!proto.HasComponent(_compFactory)) - continue; - - opts.Add(new CompletionOption(proto.ID, proto.Name)); - } - return CompletionResult.FromHintOptions(opts, Loc.GetString("cmd-weather-hint-prototype")); - } - - return CompletionResult.Empty; - } -} -*** Add File: f:\Floofdev\HardLight\Content.Server\Weather\Commands\WeatherSetCommand.cs -using Content.Server.Administration; -using Content.Shared.Administration; -using Content.Shared.Prototypes; -using Content.Shared.Weather; -using Robust.Shared.Console; -using Robust.Shared.Map; -using Robust.Shared.Prototypes; - -namespace Content.Server.Weather.Commands; - -/// -/// Removes all weather except the specified one. If the specified weather does not exist on the map, it adds it. -/// -[AdminCommand(AdminFlags.Fun)] -public sealed class WeatherSetCommand : LocalizedEntityCommands -{ - [Dependency] private readonly IPrototypeManager _proto = default!; - [Dependency] private readonly SharedMapSystem _map = default!; - [Dependency] private readonly WeatherSystem _weather = default!; - [Dependency] private readonly IComponentFactory _compFactory = default!; - - public override string Command => "weatherset"; - - public override void Execute(IConsoleShell shell, string argStr, string[] args) - { - if (args.Length < 2) - { - shell.WriteError(Loc.GetString("cmd-weather-error-no-arguments")); - return; - } - - if (!int.TryParse(args[0], out var mapInt)) - return; - - var mapId = new MapId(mapInt); - - if (!_map.MapExists(mapId)) - { - shell.WriteError(Loc.GetString("cmd-weather-error-wrong-map", ("id", mapId.ToString()))); - return; - } - - EntProtoId? weatherProto = args[1]; - if (args[1] == "null") - weatherProto = null; - else if (!_proto.TryIndex(weatherProto, out _)) - { - shell.WriteError(Loc.GetString("cmd-weather-error-unknown-proto")); - return; - } - - TimeSpan? duration = null; - if (args.Length == 3) - { - if (int.TryParse(args[2], out var durationInt)) - duration = TimeSpan.FromSeconds(durationInt); - else - shell.WriteError(Loc.GetString("cmd-weather-error-wrong-time")); - } - - _weather.TrySetWeather(mapId, weatherProto, out _, duration); - } - - public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) - { - if (args.Length == 1) - return CompletionResult.FromHintOptions(CompletionHelper.MapIds(EntityManager), Loc.GetString("cmd-weather-hint-map-id")); - - if (args.Length == 2) - { - var opts = new List(); - foreach (var proto in _proto.EnumeratePrototypes()) - { - if (!proto.HasComponent(_compFactory)) - continue; - - opts.Add(new CompletionOption(proto.ID, proto.Name)); - } - return CompletionResult.FromHintOptions(opts, Loc.GetString("cmd-weather-hint-prototype")); - } - - if (args.Length == 3) - return CompletionResult.FromHint(Loc.GetString("cmd-weather-hint-time")); - - return CompletionResult.Empty; - } -} -*** Add File: f:\Floofdev\HardLight\Content.Shared\StatusEffectNew\Components\StatusEffectComponent.cs -using Robust.Shared.GameStates; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; - -namespace Content.Shared.StatusEffectNew.Components; - -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true), AutoGenerateComponentPause] -public sealed partial class StatusEffectComponent : Component -{ - [DataField, AutoNetworkedField] - public EntityUid? AppliedTo; - - [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField, AutoNetworkedField] - public TimeSpan StartEffectTime; - - [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField, AutoNetworkedField] - public TimeSpan? EndEffectTime; - - [ViewVariables] - public TimeSpan Duration => EndEffectTime == null ? TimeSpan.MaxValue : EndEffectTime.Value - StartEffectTime; -} -*** Add File: f:\Floofdev\HardLight\Content.Shared\StatusEffectNew\Components\StatusEffectContainerComponent.cs -using Robust.Shared.Containers; - -namespace Content.Shared.StatusEffectNew.Components; - -[RegisterComponent] -public sealed partial class StatusEffectContainerComponent : Component -{ - public const string ContainerId = "status-effects"; - - [ViewVariables] - public Container? ActiveStatusEffects; -} -*** Add File: f:\Floofdev\HardLight\Content.Shared\StatusEffectNew\StatusEffectsSystem.cs -using System.Diagnostics.CodeAnalysis; -using Content.Shared.StatusEffectNew.Components; -using Robust.Shared.Containers; -using Robust.Shared.Prototypes; -using Robust.Shared.Timing; - -namespace Content.Shared.StatusEffectNew; - -public sealed class StatusEffectsSystem : EntitySystem -{ - [Dependency] private readonly IGameTiming _timing = default!; - [Dependency] private readonly SharedContainerSystem _container = default!; - - private EntityQuery _containerQuery; - private EntityQuery _statusQuery; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnContainerInit); - SubscribeLocalEvent(OnContainerShutdown); - - _containerQuery = GetEntityQuery(); - _statusQuery = GetEntityQuery(); - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var status)) - { - if (status.EndEffectTime is not { } endTime) - continue; - - if (_timing.CurTime < endTime) - continue; - - PredictedQueueDel(uid); - } - } - - private void OnContainerInit(Entity ent, ref ComponentInit args) - { - ent.Comp.ActiveStatusEffects = _container.EnsureContainer(ent, StatusEffectContainerComponent.ContainerId); - ent.Comp.ActiveStatusEffects.ShowContents = true; - ent.Comp.ActiveStatusEffects.OccludesLight = false; - } - - private void OnContainerShutdown(Entity ent, ref ComponentShutdown args) - { - if (ent.Comp.ActiveStatusEffects is { } container) - _container.ShutdownContainer(container); - } - - public bool TryGetStatusEffect(EntityUid target, EntProtoId effectProto, [NotNullWhen(true)] out EntityUid? statusEffect) - { - statusEffect = null; - - if (!_containerQuery.TryComp(target, out var containerComp) || containerComp.ActiveStatusEffects == null) - return false; - - foreach (var contained in containerComp.ActiveStatusEffects.ContainedEntities) - { - if (!_statusQuery.TryComp(contained, out var status) || status.AppliedTo != target) - continue; - - if (Prototype(contained) != effectProto) - continue; - - statusEffect = contained; - return true; - } - - return false; - } - - public bool TrySetStatusEffectDuration(EntityUid target, EntProtoId effectProto, TimeSpan? duration) - { - return TrySetStatusEffectDuration(target, effectProto, out _, duration); - } - - public bool TrySetStatusEffectDuration(EntityUid target, EntProtoId effectProto, [NotNullWhen(true)] out EntityUid? statusEffect, TimeSpan? duration = null) - { - statusEffect = null; - - if (TryGetStatusEffect(target, effectProto, out statusEffect)) - { - if (!_statusQuery.TryComp(statusEffect.Value, out var existing)) - return false; - - existing.AppliedTo = target; - existing.StartEffectTime = _timing.CurTime; - existing.EndEffectTime = duration == null ? null : _timing.CurTime + duration.Value; - Dirty(statusEffect.Value, existing); - return true; - } - - EnsureComp(target); - - if (!PredictedTrySpawnInContainer(effectProto, target, StatusEffectContainerComponent.ContainerId, out var spawned)) - return false; - - if (!_statusQuery.TryComp(spawned, out var status)) - return false; - - status.AppliedTo = target; - status.StartEffectTime = _timing.CurTime; - status.EndEffectTime = duration == null ? null : _timing.CurTime + duration.Value; - Dirty(spawned.Value, status); - - statusEffect = spawned; - return true; - } - - public bool TryEffectsWithComp(EntityUid target, [NotNullWhen(true)] out HashSet>? effects) - where T : IComponent - { - effects = null; - - if (!_containerQuery.TryComp(target, out var containerComp) || containerComp.ActiveStatusEffects == null) - return false; - - var set = new HashSet>(); - foreach (var contained in containerComp.ActiveStatusEffects.ContainedEntities) - { - if (!TryComp(contained, out var comp) || !_statusQuery.TryComp(contained, out var status)) - continue; - - if (status.AppliedTo != target) - continue; - - set.Add((contained, comp, status)); - } - - if (set.Count == 0) - return false; - - effects = set; - return true; - } -} -*** Add File: f:\Floofdev\HardLight\Content.Shared\Weather\WeatherStatusEffectComponent.cs -using System.Numerics; -using Content.Shared.StatusEffectNew.Components; -using Robust.Shared.Audio; -using Robust.Shared.GameStates; -using Robust.Shared.Utility; - -namespace Content.Shared.Weather; - -/// -/// Used only in conjure with for status effects applied to map entities. -/// Contains basic information about all types of weather effects. -/// -[RegisterComponent, NetworkedComponent, Access(typeof(SharedWeatherSystem))] -public sealed partial class WeatherStatusEffectComponent : Component -{ - [DataField(required: true)] - public SpriteSpecifier Sprite = default!; - - [DataField] - public Color? Color; - - [DataField] - public Vector2? Scrolling; - - [DataField] - public SoundSpecifier? Sound; - - [ViewVariables] - public EntityUid? Stream; -} - diff --git a/Content.Shared/Weather/WeatherStatusEffectComponent.cs b/Content.Shared/Weather/WeatherStatusEffectComponent.cs new file mode 100644 index 00000000000..177766dace8 --- /dev/null +++ b/Content.Shared/Weather/WeatherStatusEffectComponent.cs @@ -0,0 +1,26 @@ +using System.Numerics; +using Content.Shared.StatusEffectNew.Components; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Utility; + +namespace Content.Shared.Weather; + +[RegisterComponent, NetworkedComponent, Access(typeof(SharedWeatherSystem))] +public sealed partial class WeatherStatusEffectComponent : Component +{ + [DataField(required: true)] + public SpriteSpecifier Sprite = default!; + + [DataField] + public Color? Color; + + [DataField] + public Vector2? Scrolling; + + [DataField] + public SoundSpecifier? Sound; + + [ViewVariables] + public EntityUid? Stream; +} From 03d7e41fa5a755875ae527bd28606d67fc41f030 Mon Sep 17 00:00:00 2001 From: fenndragon Date: Tue, 28 Apr 2026 22:33:33 -0600 Subject: [PATCH 10/49] fix --- .../StatusEffectNew/StatusEffectsSystem.cs | 3 +- .../Effects/WeatherOnTriggerComponent.cs | 25 ---------------- .../Trigger/Systems/WeatherTriggerSystem.cs | 29 ------------------- Content.Shared/Weather/SharedWeatherSystem.cs | 4 +-- 4 files changed, 4 insertions(+), 57 deletions(-) delete mode 100644 Content.Shared/Trigger/Components/Effects/WeatherOnTriggerComponent.cs delete mode 100644 Content.Shared/Trigger/Systems/WeatherTriggerSystem.cs diff --git a/Content.Shared/StatusEffectNew/StatusEffectsSystem.cs b/Content.Shared/StatusEffectNew/StatusEffectsSystem.cs index e3528f48701..c5bb8ebc06d 100644 --- a/Content.Shared/StatusEffectNew/StatusEffectsSystem.cs +++ b/Content.Shared/StatusEffectNew/StatusEffectsSystem.cs @@ -67,7 +67,8 @@ public bool TryGetStatusEffect(EntityUid target, EntProtoId effectProto, [NotNul if (!_statusQuery.TryComp(contained, out var status) || status.AppliedTo != target) continue; - if (Prototype(contained) != effectProto) + var containedProto = Prototype(contained); + if (containedProto == null || containedProto != effectProto) continue; statusEffect = contained; diff --git a/Content.Shared/Trigger/Components/Effects/WeatherOnTriggerComponent.cs b/Content.Shared/Trigger/Components/Effects/WeatherOnTriggerComponent.cs deleted file mode 100644 index 68ffeed04e5..00000000000 --- a/Content.Shared/Trigger/Components/Effects/WeatherOnTriggerComponent.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Content.Shared.Weather; -using Robust.Shared.GameStates; -using Robust.Shared.Prototypes; - -namespace Content.Shared.Trigger.Components.Effects; - -/// -/// Changes the current weather when triggered. -/// If TargetUser is true then it will change the weather at the user's map instead of the entitys map. -/// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] -public sealed partial class WeatherOnTriggerComponent : BaseXOnTriggerComponent -{ - /// - /// Weather status effect proto. Null to clear the weather. - /// - [DataField, AutoNetworkedField] - public EntProtoId? Weather; - - /// - /// How long the weather should last. Null for forever. - /// - [DataField, AutoNetworkedField] - public TimeSpan? Duration; -} diff --git a/Content.Shared/Trigger/Systems/WeatherTriggerSystem.cs b/Content.Shared/Trigger/Systems/WeatherTriggerSystem.cs deleted file mode 100644 index c9b7de87d0e..00000000000 --- a/Content.Shared/Trigger/Systems/WeatherTriggerSystem.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Content.Shared.Trigger.Components.Effects; -using Content.Shared.Weather; -using Robust.Shared.Prototypes; -using Robust.Shared.Timing; - -namespace Content.Shared.Trigger.Systems; - -public sealed class WeatherTriggerSystem : XOnTriggerSystem -{ - [Dependency] private readonly IGameTiming _timing = default!; - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly SharedWeatherSystem _weather = default!; - - protected override void OnTrigger(Entity ent, EntityUid target, ref TriggerEvent args) - { - var xform = Transform(target); - - if (ent.Comp.Weather == null) //Clear weather if nothing is set - { - _weather.TrySetWeather(xform.MapID, null, out _); - return; - } - - var endTime = ent.Comp.Duration == null ? null : ent.Comp.Duration + _timing.CurTime; - - if (_prototypeManager.Resolve(ent.Comp.Weather, out var weatherPrototype)) - _weather.TrySetWeather(xform.MapID, weatherPrototype, out _, endTime); - } -} diff --git a/Content.Shared/Weather/SharedWeatherSystem.cs b/Content.Shared/Weather/SharedWeatherSystem.cs index cba54e6c073..6a7452bb7af 100644 --- a/Content.Shared/Weather/SharedWeatherSystem.cs +++ b/Content.Shared/Weather/SharedWeatherSystem.cs @@ -49,7 +49,7 @@ public bool CanWeatherAffect(Entity ent, Tile var tileDef = (ContentTileDefinition) _tileDefManager[tileRef.Tile.TypeId]; - if (!tileDef.Weather) + if (!tileDef.MapAtmosphere) return false; var anchoredEntities = _mapSystem.GetAnchoredEntitiesEnumerator(ent, ent.Comp1, tileRef.GridIndices); @@ -125,7 +125,7 @@ public bool TrySetWeather(MapId mapId, EntProtoId? weatherProto, out EntityUid? if (!_mapSystem.TryGetMap(mapId, out var mapUid)) return false; - if (_statusEffects.TryEffectsWithComp(mapUid, out var effects)) + if (_statusEffects.TryEffectsWithComp(mapUid.Value, out var effects)) { foreach (var effect in effects) { From 9c0b9a9e6980ff68e9877ed5bcdbaf7bca1adcb0 Mon Sep 17 00:00:00 2001 From: fenndragon Date: Tue, 28 Apr 2026 22:56:25 -0600 Subject: [PATCH 11/49] fix --- .../Worldgen/Systems/SectorChunkCarverSystem.cs | 12 ++++++++---- Content.Server/Worldgen/Systems/SectorWorldSystem.cs | 3 +-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs index b15df3a3e22..6a90ee7b5c7 100644 --- a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs +++ b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs @@ -377,7 +377,8 @@ private void ClearChunkMaterialEntitiesAtTile(Entity if (entity == gridUid) continue; - if (!TryComp(entity, out var meta) || meta.EntityPrototype == null) + var meta = MetaData(entity); + if (meta.EntityPrototype == null) continue; if (!IsChunkMaterialPrototype(meta.EntityPrototype.ID)) @@ -401,7 +402,8 @@ private void SpawnTrackedTileEntity(Entity ent, Enti if (before.Contains(entity)) continue; - if (!TryComp(entity, out var meta) || meta.EntityPrototype == null) + var meta = MetaData(entity); + if (meta.EntityPrototype == null) continue; if (IsTransientChunkSpawnerPrototype(meta.EntityPrototype.ID)) @@ -418,7 +420,8 @@ private void SpawnTrackedTileEntity(Entity ent, Enti if (!ent.Comp.GeneratedEntities.Contains(spawned) && Exists(spawned)) { - if (TryComp(spawned, out var spawnedMeta) && spawnedMeta.EntityPrototype != null) + var spawnedMeta = MetaData(spawned); + if (spawnedMeta.EntityPrototype != null) { if (IsTransientChunkSpawnerPrototype(spawnedMeta.EntityPrototype.ID)) { @@ -440,7 +443,8 @@ private HashSet GetTileEntities(EntityUid gridUid, MapGridComponent g private void AnchorToGrid(EntityUid entity) { - if (!TryComp(entity, out var xform) || xform.Anchored) + var xform = Transform(entity); + if (xform.Anchored) return; _transform.AnchorEntity(entity, xform); diff --git a/Content.Server/Worldgen/Systems/SectorWorldSystem.cs b/Content.Server/Worldgen/Systems/SectorWorldSystem.cs index f061cd227c1..cce8c842a41 100644 --- a/Content.Server/Worldgen/Systems/SectorWorldSystem.cs +++ b/Content.Server/Worldgen/Systems/SectorWorldSystem.cs @@ -471,10 +471,9 @@ private EntityUid CreateLayerMap( EnsureComp(mapUid); if (!string.IsNullOrWhiteSpace(weatherPrototype) && - _proto.TryIndex(weatherPrototype, out var weather) && TryComp(mapUid, out var mapComp)) { - _weather.SetWeather(mapComp.MapId, weather, null); + _weather.TrySetWeather(mapComp.MapId, weatherPrototype, out _); } return mapUid; From a016657e3c936f2a83b8e76ce35256f65f125e22 Mon Sep 17 00:00:00 2001 From: fenndragon Date: Tue, 28 Apr 2026 23:06:02 -0600 Subject: [PATCH 12/49] Update weather.yml --- Resources/Prototypes/Entities/StatusEffects/weather.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Resources/Prototypes/Entities/StatusEffects/weather.yml b/Resources/Prototypes/Entities/StatusEffects/weather.yml index 5c75565fb05..c0442203af7 100644 --- a/Resources/Prototypes/Entities/StatusEffects/weather.yml +++ b/Resources/Prototypes/Entities/StatusEffects/weather.yml @@ -1,3 +1,7 @@ +- type: entity + id: StatusEffectBase + abstract: true + - type: entity parent: StatusEffectBase id: WeatherBase @@ -5,9 +9,6 @@ components: - type: WeatherStatusEffect - type: StatusEffect - whitelist: - components: - - Map - type: entity parent: WeatherBase From c85e74b936ceb7b5d46b07fa5c3ad86467cfdd16 Mon Sep 17 00:00:00 2001 From: fenndragon Date: Tue, 28 Apr 2026 23:30:35 -0600 Subject: [PATCH 13/49] Update PersistenceSaveCommand.cs --- .../Administration/Commands/PersistenceSaveCommand.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Content.Server/Administration/Commands/PersistenceSaveCommand.cs b/Content.Server/Administration/Commands/PersistenceSaveCommand.cs index 9c9c0a88905..eb6a2009417 100644 --- a/Content.Server/Administration/Commands/PersistenceSaveCommand.cs +++ b/Content.Server/Administration/Commands/PersistenceSaveCommand.cs @@ -12,8 +12,7 @@ namespace Content.Server.Administration.Commands; public sealed class PersistenceSave : IConsoleCommand { [Dependency] private readonly IConfigurationManager _config = default!; - [Dependency] private readonly SharedMapSystem _map = default!; - [Dependency] private readonly MapLoaderSystem _mapLoader = default!; + [Dependency] private readonly IEntityManager _entManager = default!; public string Command => "persistencesave"; public string Description => "Saves server data to a persistence file to be loaded later."; @@ -21,6 +20,9 @@ public sealed class PersistenceSave : IConsoleCommand public void Execute(IConsoleShell shell, string argStr, string[] args) { + var mapSystem = _entManager.System(); + var mapLoader = _entManager.System(); + if (args.Length < 1 || args.Length > 2) { shell.WriteError(Loc.GetString("shell-wrong-arguments-number")); @@ -34,7 +36,7 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) } var mapId = new MapId(intMapId); - if (!_map.MapExists(mapId)) + if (!mapSystem.MapExists(mapId)) { shell.WriteError(Loc.GetString("cmd-savemap-not-exist")); return; @@ -47,7 +49,7 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) return; } - _mapLoader.TrySaveMap(mapId, new ResPath(saveFilePath)); + mapLoader.TrySaveMap(mapId, new ResPath(saveFilePath)); shell.WriteLine(Loc.GetString("cmd-savemap-success")); } } From 351a553f2e30d6095bbaeed2913504d3d1d4cdbe Mon Sep 17 00:00:00 2001 From: fenndragon Date: Wed, 29 Apr 2026 12:16:08 -0600 Subject: [PATCH 14/49] Update SectorWorldSystem.cs --- .../Worldgen/Systems/SectorWorldSystem.cs | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/Content.Server/Worldgen/Systems/SectorWorldSystem.cs b/Content.Server/Worldgen/Systems/SectorWorldSystem.cs index cce8c842a41..25afdc6323e 100644 --- a/Content.Server/Worldgen/Systems/SectorWorldSystem.cs +++ b/Content.Server/Worldgen/Systems/SectorWorldSystem.cs @@ -470,10 +470,12 @@ private EntityUid CreateLayerMap( EnsureComp(mapUid); EnsureComp(mapUid); - if (!string.IsNullOrWhiteSpace(weatherPrototype) && + var resolvedWeatherPrototype = ResolveWeatherPrototype(weatherPrototype); + + if (!string.IsNullOrWhiteSpace(resolvedWeatherPrototype) && TryComp(mapUid, out var mapComp)) { - _weather.TrySetWeather(mapComp.MapId, weatherPrototype, out _); + _weather.TrySetWeather(mapComp.MapId, resolvedWeatherPrototype, out _); } return mapUid; @@ -506,4 +508,19 @@ private static Color GetAmbientLightForTimeOfDay(string? timeOfDay) _ => Color.FromHex("#D8B059"), }; } + + private string? ResolveWeatherPrototype(string? weatherPrototype) + { + if (string.IsNullOrWhiteSpace(weatherPrototype)) + return null; + + if (_proto.HasIndex(weatherPrototype)) + return weatherPrototype; + + var weatherEntityId = $"Weather{weatherPrototype}"; + if (_proto.HasIndex(weatherEntityId)) + return weatherEntityId; + + return weatherPrototype; + } } \ No newline at end of file From 564cb3fe3648c5c659f1c567dd16532d60a465b5 Mon Sep 17 00:00:00 2001 From: fenndragon Date: Wed, 29 Apr 2026 13:32:07 -0600 Subject: [PATCH 15/49] fixes --- .../Lobby/UI/HumanoidProfileEditor.xaml | 4 +- .../Gateway/Systems/GatewayGeneratorSystem.cs | 46 ++++++++----------- .../Gateway/Systems/GatewaySystem.cs | 10 ++-- 3 files changed, 28 insertions(+), 32 deletions(-) diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml index ba34d5b976d..93dbf2cad84 100644 --- a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml +++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml @@ -115,14 +115,14 @@ - public const int ChunkSize = 256; + public const int ChunkSize = 128; /// /// Converts world coordinates to chunk coordinates. diff --git a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml index 1078cdb17bf..08facf50fad 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml @@ -134,7 +134,7 @@ type: WiresBoundUserInterface - type: RadarConsole - type: WorldLoader - radius: 384 # Mono + radius: 64 # Mono - type: PointLight radius: 1.5 energy: 1.6 @@ -241,7 +241,7 @@ - Syndicate - type: RadarConsole - type: WorldLoader - radius: 1536 + radius: 64 - type: PointLight radius: 1.5 energy: 1.6 diff --git a/Resources/Prototypes/_Crescent/NPCs/Shuttle/control.yml b/Resources/Prototypes/_Crescent/NPCs/Shuttle/control.yml index e9996dc91f5..7d32fe1ef6c 100644 --- a/Resources/Prototypes/_Crescent/NPCs/Shuttle/control.yml +++ b/Resources/Prototypes/_Crescent/NPCs/Shuttle/control.yml @@ -59,7 +59,7 @@ rootTask: task: DroneCompound - type: WorldLoader - radius: 256 + radius: 64 - type: DeviceNetwork deviceNetId: Wireless receiveFrequencyId: DroneControl @@ -94,7 +94,7 @@ type: DroneConsoleBoundUserInterface - type: RadarConsole - type: WorldLoader - radius: 256 + radius: 64 - type: DeviceNetwork deviceNetId: Wireless transmitFrequencyId: DroneControl diff --git a/Resources/Prototypes/_Mono/Entities/Structures/Machines/FireControl/gunnery.yml b/Resources/Prototypes/_Mono/Entities/Structures/Machines/FireControl/gunnery.yml index 8bf43a2e3a7..3b970b49994 100644 --- a/Resources/Prototypes/_Mono/Entities/Structures/Machines/FireControl/gunnery.yml +++ b/Resources/Prototypes/_Mono/Entities/Structures/Machines/FireControl/gunnery.yml @@ -227,7 +227,7 @@ - type: RadarConsole maxRange: 512 - type: WorldLoader - radius: 256 # Mono + radius: 64 # Mono - type: Computer board: GunneryControlComputerCircuitboard - type: PointLight From 5f95eeef1731eb30ef27be48b513fd150fe843f5 Mon Sep 17 00:00:00 2001 From: fenndragon Date: Thu, 30 Apr 2026 15:41:17 -0600 Subject: [PATCH 25/49] Potential fix for pull request finding 'Dereferenced variable may be null' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- Content.Server/Gateway/Systems/GatewayGeneratorSystem.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Content.Server/Gateway/Systems/GatewayGeneratorSystem.cs b/Content.Server/Gateway/Systems/GatewayGeneratorSystem.cs index 561a217261c..5fba14aa5de 100644 --- a/Content.Server/Gateway/Systems/GatewayGeneratorSystem.cs +++ b/Content.Server/Gateway/Systems/GatewayGeneratorSystem.cs @@ -101,6 +101,11 @@ private void GenerateDestination(EntityUid uid, GatewayGeneratorComponent? gener if (!Resolve(uid, ref generator)) return; + if (generator == null) + return; + + var generatorComp = generator; + var tileDef = _tileDefManager["FloorSteel"]; var tiles = new List<(Vector2i Index, Tile Tile)>(); var seed = _random.Next(); @@ -115,7 +120,7 @@ private void GenerateDestination(EntityUid uid, GatewayGeneratorComponent? gener var hostGridUid = hostMapUid; var grid = EnsureComp(hostGridUid); - var gatewayUid = EntityManager.SpawnEntity(generator.Proto, MapCoordinates.Nullspace); + var gatewayUid = EntityManager.SpawnEntity(generatorComp.Proto, MapCoordinates.Nullspace); if (!_sectorWorld.TryReserveExpeditionSite(seed, gatewayUid, hostPlanet.PlanetTypeId, out var placement)) { @@ -163,7 +168,7 @@ private void GenerateDestination(EntityUid uid, GatewayGeneratorComponent? gener var gatewayComp = Comp(gatewayUid); _gateway.SetDestinationName(gatewayUid, FormattedMessage.FromMarkupOrThrow($"[color=#D381C996]{gatewayName}[/color]"), gatewayComp); _gateway.SetEnabled(gatewayUid, true, gatewayComp); - generator.Generated.Add(gatewayUid); + generatorComp.Generated.Add(gatewayUid); } private void OnGeneratorAttemptOpen(Entity ent, ref AttemptGatewayOpenEvent args) From 0e97dde0ca11f3fe6212af682fece8817777fbc7 Mon Sep 17 00:00:00 2001 From: fenndragon Date: Thu, 30 Apr 2026 15:41:45 -0600 Subject: [PATCH 26/49] Potential fix for pull request finding 'Dereferenced variable may be null' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- Content.Server/Parallax/BiomeSystem.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Content.Server/Parallax/BiomeSystem.cs b/Content.Server/Parallax/BiomeSystem.cs index 62cf1c2db2d..fe3f1fdcbf9 100644 --- a/Content.Server/Parallax/BiomeSystem.cs +++ b/Content.Server/Parallax/BiomeSystem.cs @@ -187,7 +187,10 @@ public void SetEnabled(Entity ent, bool enabled = true) public bool IsChunkLoaded(EntityUid uid, Vector2i chunkOrigin, BiomeComponent? component = null) { - return Resolve(uid, ref component, false) && component.LoadedChunks.Contains(chunkOrigin); + if (!Resolve(uid, ref component, false) || component == null) + return false; + + return component.LoadedChunks.Contains(chunkOrigin); } public void SetSeed(EntityUid uid, BiomeComponent component, int seed, bool dirty = true) From ee74c7723f7873380757ca1e90fbe31e18f6339d Mon Sep 17 00:00:00 2001 From: fenndragon Date: Thu, 30 Apr 2026 15:42:10 -0600 Subject: [PATCH 27/49] Potential fix for pull request finding 'Dereferenced variable may be null' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- Content.Server/Worldgen/Systems/SectorWorldSystem.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Content.Server/Worldgen/Systems/SectorWorldSystem.cs b/Content.Server/Worldgen/Systems/SectorWorldSystem.cs index f1e4e3ff61f..c14d33587a7 100644 --- a/Content.Server/Worldgen/Systems/SectorWorldSystem.cs +++ b/Content.Server/Worldgen/Systems/SectorWorldSystem.cs @@ -503,6 +503,9 @@ public bool TryGetPlanetAtPosition(EntityUid sectorMap, Vector2 worldPos, out Se if (!Resolve(sectorMap, ref sector, false)) return false; + if (sector == null) + return false; + EnsureInitialized((sectorMap, sector)); foreach (var candidate in sector.Planets) From 048ab7a67f7c3b27c200f8f8f15be8eeceb8e70a Mon Sep 17 00:00:00 2001 From: fenndragon Date: Thu, 30 Apr 2026 15:42:27 -0600 Subject: [PATCH 28/49] Potential fix for pull request finding 'Dereferenced variable may be null' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- Content.Server/Worldgen/Systems/SectorWorldSystem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.Server/Worldgen/Systems/SectorWorldSystem.cs b/Content.Server/Worldgen/Systems/SectorWorldSystem.cs index c14d33587a7..4f7748f8262 100644 --- a/Content.Server/Worldgen/Systems/SectorWorldSystem.cs +++ b/Content.Server/Worldgen/Systems/SectorWorldSystem.cs @@ -529,7 +529,7 @@ public bool TryGetSectorGrid(EntityUid sectorMap, out EntityUid gridUid, SectorW EnsureInitialized((sectorMap, sector)); - if (sector.SectorGrid is not { } resolvedGrid || !Exists(resolvedGrid)) + if (sector == null || sector.SectorGrid is not { } resolvedGrid || !Exists(resolvedGrid)) return false; gridUid = resolvedGrid; From 14b7f3e0effaee4037207d339bfadd0c69b56700 Mon Sep 17 00:00:00 2001 From: fenndragon Date: Thu, 30 Apr 2026 15:42:46 -0600 Subject: [PATCH 29/49] Potential fix for pull request finding 'Dereferenced variable may be null' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- Content.Shared/StatusEffectNew/StatusEffectsSystem.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Content.Shared/StatusEffectNew/StatusEffectsSystem.cs b/Content.Shared/StatusEffectNew/StatusEffectsSystem.cs index c5bb8ebc06d..20f91fdb60c 100644 --- a/Content.Shared/StatusEffectNew/StatusEffectsSystem.cs +++ b/Content.Shared/StatusEffectNew/StatusEffectsSystem.cs @@ -89,13 +89,17 @@ public bool TrySetStatusEffectDuration(EntityUid target, EntProtoId effectProto, if (TryGetStatusEffect(target, effectProto, out statusEffect)) { - if (!_statusQuery.TryComp(statusEffect.Value, out var existing)) + if (statusEffect == null) + return false; + + var existingUid = statusEffect.Value; + if (!_statusQuery.TryComp(existingUid, out var existing)) return false; existing.AppliedTo = target; existing.StartEffectTime = _timing.CurTime; existing.EndEffectTime = duration == null ? null : _timing.CurTime + duration.Value; - Dirty(statusEffect.Value, existing); + Dirty(existingUid, existing); return true; } From 896ec084bc1d1f736701d29203d7c7215a2866ca Mon Sep 17 00:00:00 2001 From: fenndragon Date: Thu, 30 Apr 2026 15:43:07 -0600 Subject: [PATCH 30/49] Potential fix for pull request finding 'Generic catch clause' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs index be5b294ae41..4096f0d8344 100644 --- a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs +++ b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs @@ -68,7 +68,11 @@ public override void Shutdown() if (!string.IsNullOrWhiteSpace(_cacheDirectory) && Directory.Exists(_cacheDirectory)) Directory.Delete(_cacheDirectory, true); } - catch + catch (IOException) + { + // Temp cache cleanup is best-effort. + } + catch (UnauthorizedAccessException) { // Temp cache cleanup is best-effort. } From 19b5456d0b051cf8b62f924e5cd0ef4bdadaf55d Mon Sep 17 00:00:00 2001 From: fenndragon Date: Thu, 30 Apr 2026 15:43:23 -0600 Subject: [PATCH 31/49] Potential fix for pull request finding 'Call to 'System.IO.Path.Combine' may silently drop its earlier arguments' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs index 4096f0d8344..9e0fe076087 100644 --- a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs +++ b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs @@ -51,7 +51,8 @@ public sealed class SectorChunkCarverSystem : EntitySystem public override void Initialize() { - _cacheDirectory = Path.Combine(Path.GetTempPath(), "HardLight", "sector-chunk-cache", Guid.NewGuid().ToString("N")); + var cacheRunId = Path.GetFileName(Guid.NewGuid().ToString("N")); + _cacheDirectory = Path.Combine(Path.GetTempPath(), "HardLight", "sector-chunk-cache", cacheRunId); Directory.CreateDirectory(_cacheDirectory); SubscribeLocalEvent(OnChunkLoaded); From 81420a0adc142e4103cf8be222f4af21d1138f44 Mon Sep 17 00:00:00 2001 From: fenndragon Date: Thu, 30 Apr 2026 15:43:39 -0600 Subject: [PATCH 32/49] Potential fix for pull request finding 'Call to 'System.IO.Path.Combine' may silently drop its earlier arguments' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs index 9e0fe076087..449462e60a2 100644 --- a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs +++ b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs @@ -333,7 +333,7 @@ private void RestoreCachedTile(string xText, string yText, string tileId, List<( private string GetCachePath(EntityUid chunkUid, WorldChunkComponent chunk) { - return Path.Combine(_cacheDirectory, $"chunk_{chunkUid}_{chunk.Coordinates.X}_{chunk.Coordinates.Y}.cache"); + return Path.Join(_cacheDirectory, $"chunk_{chunkUid}_{chunk.Coordinates.X}_{chunk.Coordinates.Y}.cache"); } private SectorAsteroidBiomePrototype? GetChunkBiome(SectorChunkCarverComponent carver, SectorWorldComponent sector, Vector2i chunkCoords) From 10f279ba6aec4ca3ee783be35ac416e4072e7be5 Mon Sep 17 00:00:00 2001 From: fenndragon Date: Thu, 30 Apr 2026 15:43:56 -0600 Subject: [PATCH 33/49] Potential fix for pull request finding 'Missed opportunity to use Select' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- .../Worldgen/Systems/SectorChunkCarverSystem.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs index 449462e60a2..6fa8749784b 100644 --- a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs +++ b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs @@ -545,14 +545,7 @@ private bool IsBlockedByOtherGrid(Vector2 worldPos, MapId mapId, List !_mapSystem.GetTileRef(grid.Owner, grid.Comp, coords).Tile.IsEmpty); } private bool TryGetPlanetWallPrototype(EntityUid gridUid, MapGridComponent grid, Vector2i indices, out string prototype) From 8012721d7cbe69b973453c00e418675e3a518d93 Mon Sep 17 00:00:00 2001 From: fenndragon Date: Thu, 30 Apr 2026 16:53:39 -0600 Subject: [PATCH 34/49] fixes --- .../Commands/WeatherCommands.cs | 24 ++- .../GameTicking/GameTicker.RoundFlow.cs | 23 ++- .../Systems/SectorChunkCarverSystem.cs | 51 ++++++- .../Worldgen/Systems/SectorWorldSystem.cs | 144 +++++++++++++++++- 4 files changed, 227 insertions(+), 15 deletions(-) diff --git a/Content.Server/Administration/Commands/WeatherCommands.cs b/Content.Server/Administration/Commands/WeatherCommands.cs index 9f19126c95a..232820dcda9 100644 --- a/Content.Server/Administration/Commands/WeatherCommands.cs +++ b/Content.Server/Administration/Commands/WeatherCommands.cs @@ -1,5 +1,6 @@ using System.Linq; using Content.Server.Administration; +using Content.Server.Shuttles.Systems; using Content.Server.Weather; using Content.Server.Worldgen.Components; using Content.Server.Worldgen.Systems; @@ -150,7 +151,7 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) } var sectorWorld = _entManager.System(); - if (!WeatherCommandHelpers.TryResolvePersistentMap(sectorWorld, args[0], out var mapUid, out var targetLabel, out var targetError)) + if (!WeatherCommandHelpers.TryResolvePersistentMap(_entManager, sectorWorld, args[0], out var mapUid, out var targetLabel, out var targetError)) { shell.WriteError(targetError); return; @@ -221,7 +222,7 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) if (sector.FtlMap is { } ftlMap) shell.WriteLine($"ftl: map {GetMapIdText(ftlMap)}"); - if (sector.ColCommMap is { } colCommMap) + if (WeatherCommandHelpers.TryGetColcommMap(_entManager, out var colCommMap)) shell.WriteLine($"colcomm: map {GetMapIdText(colCommMap)}"); foreach (var planetType in sector.PlanetTypes) @@ -272,7 +273,7 @@ public static bool TryResolveWeather(IPrototypeManager proto, string input, out return false; } - public static bool TryResolvePersistentMap(SectorWorldSystem sectorWorld, string target, out EntityUid mapUid, out string targetLabel, out string error) + public static bool TryResolvePersistentMap(IEntityManager entManager, SectorWorldSystem sectorWorld, string target, out EntityUid mapUid, out string targetLabel, out string error) { mapUid = EntityUid.Invalid; targetLabel = target; @@ -298,7 +299,7 @@ public static bool TryResolvePersistentMap(SectorWorldSystem sectorWorld, string mapUid = ftlMap; targetLabel = "ftl"; return true; - case "colcomm" when sector.ColCommMap is { } colCommMap: + case "colcomm" when TryGetColcommMap(entManager, out var colCommMap): mapUid = colCommMap; targetLabel = "colcomm"; return true; @@ -327,7 +328,7 @@ public static List GetPersistentMapTargets(IEntityManager entM if (sector.FtlMap is { } ftlMap) options.Add(new CompletionOption("ftl", DescribeMap(entManager, ftlMap))); - if (sector.ColCommMap is { } colCommMap) + if (TryGetColcommMap(entManager, out var colCommMap)) options.Add(new CompletionOption("colcomm", DescribeMap(entManager, colCommMap))); foreach (var planetType in sector.PlanetTypes) @@ -347,4 +348,17 @@ private static string DescribeMap(IEntityManager entManager, EntityUid mapUid) ? $"Map {mapComp.MapId}" : "Map unknown"; } + + public static bool TryGetColcommMap(IEntityManager entManager, out EntityUid mapUid) + { + mapUid = EntityUid.Invalid; + + var emergencyShuttle = entManager.System(); + var colcommMaps = emergencyShuttle.GetColcommMaps(); + if (colcommMaps.Count == 0) + return false; + + mapUid = colcommMaps.First(); + return true; + } } \ No newline at end of file diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs index 4cc35a74774..ffd1bf1344d 100644 --- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs +++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs @@ -13,6 +13,7 @@ using Content.Server.Shuttles.Systems; using Content.Server.Station.Components; using Content.Server.Station.Systems; // Add this if missing +using Content.Server.Worldgen.Systems; using Content.Shared._NF.Shipyard.Components; using Content.Shared.CCVar; using Content.Shared.Database; @@ -45,6 +46,7 @@ public sealed partial class GameTicker [Dependency] private readonly ITaskManager _taskManager = default!; [Dependency] private readonly ArrivalsSystem _arrivalsSystem = default!; [Dependency] private readonly ShuttleSystem _shuttleSystem = default!; + [Dependency] private readonly SectorWorldSystem _sectorWorld = default!; private static readonly Counter RoundNumberMetric = Metrics.CreateCounter( "ss14_round_number", @@ -615,23 +617,38 @@ void QueueShuttle(EntityUid shuttleUid, ShuttleComponent shuttle, TransformCompo // HardLight end // --- End Corrected Colcomm logic --- - // Aggressively delete the default map after a 30 second delay + // Aggressively delete the default map and any generated layer maps after a 30 second delay. var defaultMapEntityUid = _mapManager.GetMapEntityId(DefaultMap); + var roundEndCleanupMaps = _sectorWorld.GetRoundEndCleanupMapIds(); if (DefaultMap != null) { Timer.Spawn(TimeSpan.FromSeconds(30), () => { - // Send all players on the default map to the lobby before deleting the map + // Send all players on round-end cleanup maps to the lobby before deleting those maps. foreach (var session in _playerManager.Sessions) { var attachedEntity = session.AttachedEntity; - if (attachedEntity != null && Transform(attachedEntity.Value).MapID == DefaultMap) + if (attachedEntity == null) + continue; + + var playerMap = Transform(attachedEntity.Value).MapID; + if (playerMap == DefaultMap || roundEndCleanupMaps.Contains(playerMap)) { PlayerJoinLobby(session); } } QueueDel(defaultMapEntityUid); + + foreach (var mapId in roundEndCleanupMaps) + { + if (!_map.MapExists(mapId)) + continue; + + var mapUid = _map.GetMapOrInvalid(mapId); + if (mapUid.IsValid()) + QueueDel(mapUid); + } }); } diff --git a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs index 6fa8749784b..456df09f3c4 100644 --- a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs +++ b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs @@ -3,8 +3,10 @@ using System.Linq; using System.Text; using Robust.Server.GameObjects; +using Content.Server._NF.RoundNotifications.Events; using Content.Server.Worldgen.Components; using Content.Server.Worldgen.Tools; +using Content.Shared.GameTicking; using Content.Shared.Maps; using Content.Shared.Storage; using Content.Shared.Worldgen.Prototypes; @@ -48,22 +50,59 @@ public sealed class SectorChunkCarverSystem : EntitySystem private string _cacheDirectory = string.Empty; private readonly Dictionary> _biomeCaches = new(); + private bool _roundRestartCleanupActive; public override void Initialize() { - var cacheRunId = Path.GetFileName(Guid.NewGuid().ToString("N")); - _cacheDirectory = Path.Combine(Path.GetTempPath(), "HardLight", "sector-chunk-cache", cacheRunId); - Directory.CreateDirectory(_cacheDirectory); + ResetCacheDirectory(); SubscribeLocalEvent(OnChunkLoaded); SubscribeLocalEvent(OnChunkUnloaded); SubscribeLocalEvent(OnChunkShutdown); + SubscribeLocalEvent(OnRoundRestartCleanup); + SubscribeLocalEvent(OnRoundStarted); } public override void Shutdown() { base.Shutdown(); + DeleteCacheDirectory(); + } + + private void OnRoundRestartCleanup(RoundRestartCleanupEvent ev) + { + _roundRestartCleanupActive = true; + ResetAllChunkCachePaths(); + ResetCacheDirectory(); + } + + private void OnRoundStarted(RoundStartedEvent ev) + { + _roundRestartCleanupActive = false; + } + + private void ResetAllChunkCachePaths() + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out _, out var carver)) + { + carver.CacheFilePath = null; + } + } + + private void ResetCacheDirectory() + { + DeleteCacheDirectory(); + + var cacheRunId = Path.GetFileName(Guid.NewGuid().ToString("N")); + _cacheDirectory = Path.Combine(Path.GetTempPath(), "HardLight", "sector-chunk-cache", cacheRunId); + Directory.CreateDirectory(_cacheDirectory); + } + + private void DeleteCacheDirectory() + { + try { if (!string.IsNullOrWhiteSpace(_cacheDirectory) && Directory.Exists(_cacheDirectory)) @@ -147,6 +186,9 @@ private void OnChunkLoaded(Entity ent, ref WorldChun private void OnChunkUnloaded(Entity ent, ref WorldChunkUnloadedEvent args) { + if (_roundRestartCleanupActive) + return; + if (!ent.Comp.Materialized || ent.Comp.GeneratedTiles.Count == 0) return; @@ -188,6 +230,9 @@ private void OnChunkUnloaded(Entity ent, ref WorldCh private void OnChunkShutdown(Entity ent, ref ComponentShutdown args) { + if (_roundRestartCleanupActive) + return; + if (!ent.Comp.Materialized || ent.Comp.GeneratedTiles.Count == 0) return; diff --git a/Content.Server/Worldgen/Systems/SectorWorldSystem.cs b/Content.Server/Worldgen/Systems/SectorWorldSystem.cs index 4f7748f8262..20c0f12b773 100644 --- a/Content.Server/Worldgen/Systems/SectorWorldSystem.cs +++ b/Content.Server/Worldgen/Systems/SectorWorldSystem.cs @@ -1,12 +1,14 @@ using System.Numerics; using System.Linq; using Content.Server._Mono.Cleanup; +using Content.Server._NF.RoundNotifications.Events; using Content.Server._NF.Shuttles.Components; using Content.Server.Atmos.EntitySystems; using Content.Server.GameTicking; using Content.Server.Parallax; using Content.Server.Weather; using Content.Server.Worldgen.Components; +using Content.Shared.GameTicking; using Content.Shared.Atmos; using Content.Shared.Gravity; using Content.Shared.Light.Components; @@ -14,6 +16,7 @@ using Content.Shared.Parallax.Biomes; using Content.Shared.Shuttles.Components; using Content.Shared.Weather; +using Robust.Shared.ContentPack; using Robust.Shared.EntitySerialization; using Robust.Shared.EntitySerialization.Systems; using Robust.Server.GameObjects; @@ -44,16 +47,32 @@ public sealed class SectorWorldSystem : EntitySystem [Dependency] private readonly MetaDataSystem _metaData = default!; [Dependency] private readonly SharedMapSystem _mapSystem = default!; [Dependency] private readonly PhysicsSystem _physics = default!; + [Dependency] private readonly IResourceManager _resources = default!; [Dependency] private readonly ITileDefinitionManager _tileDefs = default!; [Dependency] private readonly WeatherSystem _weather = default!; private static readonly string[] TimeOfDayStates = ["Dawn", "Day", "Dusk", "Night"]; private readonly string _siteCacheSession = Guid.NewGuid().ToString("N"); + private bool _roundRestartCleanupActive; public override void Initialize() { SubscribeLocalEvent(OnSectorStartup); SubscribeLocalEvent(OnExpeditionSiteShutdown); + SubscribeLocalEvent(OnRoundRestartCleanup); + SubscribeLocalEvent(OnRoundStarted); + } + + private void OnRoundRestartCleanup(RoundRestartCleanupEvent ev) + { + _roundRestartCleanupActive = true; + CleanupHostedSiteCachesForRoundRestart(); + CleanupLayerMapsForRoundRestart(); + } + + private void OnRoundStarted(RoundStartedEvent ev) + { + _roundRestartCleanupActive = false; } private void OnSectorStartup(Entity ent, ref ComponentStartup args) @@ -131,6 +150,9 @@ public void CaptureHostedSiteGeneratedEntities(Entity(); while (query.MoveNext(out var uid, out var site)) @@ -205,6 +227,68 @@ public void CleanupHostedSite(EntityUid uid, SectorExpeditionSiteComponent compo CleanupHostedSite((uid, component)); } + private void CleanupHostedSiteCachesForRoundRestart() + { + var siteQuery = EntityQueryEnumerator(); + while (siteQuery.MoveNext(out var uid, out var site)) + { + CleanupHostedSite((uid, site)); + } + + var cacheRoot = GetHostedSiteCacheRoot(); + if (_resources.UserData.Exists(cacheRoot)) + _resources.UserData.Delete(cacheRoot); + } + + private void CleanupLayerMapsForRoundRestart() + { + var defaultMapUid = _mapSystem.GetMapOrInvalid(_gameTicker.DefaultMap); + var sectorQuery = EntityQueryEnumerator(); + + while (sectorQuery.MoveNext(out var sectorUid, out var sector)) + { + foreach (var loader in sector.StartupLoaders.ToArray()) + { + if (Exists(loader)) + QueueDel(loader); + } + + sector.StartupLoaders.Clear(); + sector.Reservations.Clear(); + + foreach (var mapUid in sector.PlanetTypeMaps.Values.Distinct().ToArray()) + { + DeleteLayerMapForRoundRestart(sectorUid, defaultMapUid, mapUid); + } + + sector.PlanetTypeMaps.Clear(); + + if (sector.FtlMap is { } ftlMap) + DeleteLayerMapForRoundRestart(sectorUid, defaultMapUid, ftlMap); + + if (sector.ColCommMap is { } colCommMap) + DeleteLayerMapForRoundRestart(sectorUid, defaultMapUid, colCommMap); + + sector.FtlMap = null; + sector.ColCommMap = null; + sector.SpaceMap = sectorUid; + } + } + + private void DeleteLayerMapForRoundRestart(EntityUid sectorUid, EntityUid defaultMapUid, EntityUid mapUid) + { + if (!Exists(mapUid) || mapUid == sectorUid || mapUid == defaultMapUid) + return; + + if (!TryComp(mapUid, out var mapComp)) + return; + + if (mapComp.MapId == _gameTicker.DefaultMap || !_mapSystem.MapExists(mapComp.MapId)) + return; + + _mapSystem.DeleteMap(mapComp.MapId); + } + private void SaveHostedChunkContent(Entity ent, EntityUid gridUid, MapGridComponent grid, Vector2i chunkOrigin, int chunkSize) { var cachedTiles = new Dictionary(); @@ -431,6 +515,38 @@ private ResPath GetHostedChunkEntityCachePath(EntityUid siteUid, Vector2i chunkO return new ResPath($"/HardLight/hosted-site-cache/{_siteCacheSession}/{siteUid.Id.ToString()}/{chunkOrigin.X}_{chunkOrigin.Y}.yml"); } + private ResPath GetHostedSiteCacheRoot() + { + return new ResPath($"/HardLight/hosted-site-cache/{_siteCacheSession}"); + } + + public HashSet GetRoundEndCleanupMapIds() + { + var mapIds = new HashSet(); + + var sectorQuery = EntityQueryEnumerator(); + while (sectorQuery.MoveNext(out _, out var sector)) + { + foreach (var mapUid in sector.PlanetTypeMaps.Values) + { + TryAddRoundEndCleanupMapId(mapIds, mapUid); + } + } + + return mapIds; + } + + private void TryAddRoundEndCleanupMapId(HashSet mapIds, EntityUid mapUid) + { + if (!Exists(mapUid) || !TryComp(mapUid, out var mapComp)) + return; + + if (mapComp.MapId == _gameTicker.DefaultMap) + return; + + mapIds.Add(mapComp.MapId); + } + public bool TryGetDefaultSectorMap(out EntityUid sectorMap, out SectorWorldComponent sector) { sectorMap = EntityUid.Invalid; @@ -762,8 +878,7 @@ private void EnsurePersistentLayerMaps(Entity ent) if (ent.Comp.FtlMap is { } ftlMap && !Exists(ftlMap)) ent.Comp.FtlMap = null; - if (ent.Comp.ColCommMap is { } colCommMap && !Exists(colCommMap)) - ent.Comp.ColCommMap = null; + CleanupLegacyColCommLayerMap(ent); var invalidPlanetMaps = ent.Comp.PlanetTypeMaps .Where(pair => !Exists(pair.Value)) @@ -776,10 +891,8 @@ private void EnsurePersistentLayerMaps(Entity ent) } ent.Comp.FtlMap ??= CreateLayerMap($"{MetaData(ent.Owner).EntityName} FTL", space: true, gravity: false); - ent.Comp.ColCommMap ??= CreateLayerMap($"{MetaData(ent.Owner).EntityName} ColComm", space: false, gravity: true, mixture: CreateStandardAirMixture(), timeOfDay: "Day"); EnsurePersistentWorldGrid(ent.Comp.FtlMap.Value); - EnsurePersistentWorldGrid(ent.Comp.ColCommMap.Value); foreach (var planet in ent.Comp.Planets) { @@ -803,6 +916,29 @@ private void EnsurePersistentLayerMaps(Entity ent) } } + private void CleanupLegacyColCommLayerMap(Entity ent) + { + if (ent.Comp.ColCommMap is not { } colCommMap) + return; + + ent.Comp.ColCommMap = null; + + if (!Exists(colCommMap) || colCommMap == ent.Owner) + return; + + var defaultMapUid = _mapSystem.GetMapOrInvalid(_gameTicker.DefaultMap); + if (colCommMap == defaultMapUid) + return; + + if (TryComp(colCommMap, out var mapComp)) + { + _mapSystem.DeleteMap(mapComp.MapId); + return; + } + + QueueDel(colCommMap); + } + private void EnsurePersistentWorldGrid(EntityUid mapOrGridUid) { if (!Exists(mapOrGridUid)) From 04add606563e787637182dc003575ea6725a0fe7 Mon Sep 17 00:00:00 2001 From: fenndragon Date: Thu, 30 Apr 2026 17:05:27 -0600 Subject: [PATCH 35/49] Potential fix for pull request finding 'Dereferenced variable may be null' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- Content.Server/Worldgen/Systems/WorldControllerSystem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.Server/Worldgen/Systems/WorldControllerSystem.cs b/Content.Server/Worldgen/Systems/WorldControllerSystem.cs index 5a7d6188c16..2d0edfe0f0b 100644 --- a/Content.Server/Worldgen/Systems/WorldControllerSystem.cs +++ b/Content.Server/Worldgen/Systems/WorldControllerSystem.cs @@ -36,7 +36,7 @@ public override void Initialize() public void SetLoaderRadius(EntityUid uid, int radius, WorldLoaderComponent? loader = null) { - if (!Resolve(uid, ref loader, false)) + if (!Resolve(uid, ref loader, false) || loader == null) return; loader.Radius = radius; From 57410847b8e483b381fdb852fc87750b71cb80c1 Mon Sep 17 00:00:00 2001 From: fenndragon Date: Thu, 30 Apr 2026 17:05:44 -0600 Subject: [PATCH 36/49] Potential fix for pull request finding 'Call to 'System.IO.Path.Combine' may silently drop its earlier arguments' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs index 6fa8749784b..9881708bec3 100644 --- a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs +++ b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs @@ -52,7 +52,12 @@ public sealed class SectorChunkCarverSystem : EntitySystem public override void Initialize() { var cacheRunId = Path.GetFileName(Guid.NewGuid().ToString("N")); - _cacheDirectory = Path.Combine(Path.GetTempPath(), "HardLight", "sector-chunk-cache", cacheRunId); + var cacheSegments = new[] { "HardLight", "sector-chunk-cache", cacheRunId }; + + if (cacheSegments.Any(Path.IsPathRooted)) + throw new InvalidOperationException("Cache directory segments must be relative paths."); + + _cacheDirectory = Path.Combine(Path.GetTempPath(), Path.Combine(cacheSegments)); Directory.CreateDirectory(_cacheDirectory); SubscribeLocalEvent(OnChunkLoaded); From 7b4d9ae6646d5d3e806db9e0abb77aef60dab394 Mon Sep 17 00:00:00 2001 From: fenndragon Date: Thu, 30 Apr 2026 17:05:59 -0600 Subject: [PATCH 37/49] Potential fix for pull request finding 'Missed opportunity to use Where' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- Content.Server/Administration/Commands/WeatherCommands.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Content.Server/Administration/Commands/WeatherCommands.cs b/Content.Server/Administration/Commands/WeatherCommands.cs index 9f19126c95a..78924b60744 100644 --- a/Content.Server/Administration/Commands/WeatherCommands.cs +++ b/Content.Server/Administration/Commands/WeatherCommands.cs @@ -330,11 +330,9 @@ public static List GetPersistentMapTargets(IEntityManager entM if (sector.ColCommMap is { } colCommMap) options.Add(new CompletionOption("colcomm", DescribeMap(entManager, colCommMap))); - foreach (var planetType in sector.PlanetTypes) + foreach (var planetType in sector.PlanetTypes.Where(pt => sector.PlanetTypeMaps.ContainsKey(pt.Id))) { - if (!sector.PlanetTypeMaps.TryGetValue(planetType.Id, out var mapUid)) - continue; - + sector.PlanetTypeMaps.TryGetValue(planetType.Id, out var mapUid); options.Add(new CompletionOption(planetType.Id, $"{planetType.Name} - {DescribeMap(entManager, mapUid)}")); } From 96a4bcc9ac6d1f360e256a4d41c89d1d5a6b130b Mon Sep 17 00:00:00 2001 From: fenndragon Date: Thu, 30 Apr 2026 17:06:19 -0600 Subject: [PATCH 38/49] Potential fix for pull request finding 'Missed opportunity to use Where' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- Content.Server/Parallax/BiomeSystem.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Content.Server/Parallax/BiomeSystem.cs b/Content.Server/Parallax/BiomeSystem.cs index fe3f1fdcbf9..71509f203f5 100644 --- a/Content.Server/Parallax/BiomeSystem.cs +++ b/Content.Server/Parallax/BiomeSystem.cs @@ -967,11 +967,8 @@ private void FlushLoadedChunks(BiomeComponent component, EntityUid gridUid, MapG var tiles = new List<(Vector2i, Tile)>(ChunkSize * ChunkSize); var chunks = component.LoadedChunks.ToArray(); - foreach (var chunk in chunks) + foreach (var chunk in chunks.Where(component.LoadedChunks.Contains)) { - if (!component.LoadedChunks.Contains(chunk)) - continue; - UnloadChunk(component, gridUid, grid, chunk, seed, tiles); } } From a6690b0bdf865f3853f9ae0eccc67bfd36ad4280 Mon Sep 17 00:00:00 2001 From: fenndragon Date: Thu, 30 Apr 2026 17:06:35 -0600 Subject: [PATCH 39/49] Potential fix for pull request finding 'Missed opportunity to use Where' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- Content.Server/Procedural/DungeonJob/DungeonJob.PostGen.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Content.Server/Procedural/DungeonJob/DungeonJob.PostGen.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGen.cs index fa49255fd57..3a251395c5f 100644 --- a/Content.Server/Procedural/DungeonJob/DungeonJob.PostGen.cs +++ b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGen.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Numerics; using Content.Shared.Procedural; using Content.Shared.Tag; @@ -53,11 +54,8 @@ private void AnchorNewStructuralTileEntities(Vector2i tile, HashSet b { var after = GetTileEntities(tile); - foreach (var entity in after) + foreach (var entity in after.Where(entity => !before.Contains(entity) && ShouldAnchorDungeonStructure(entity))) { - if (before.Contains(entity) || !ShouldAnchorDungeonStructure(entity)) - continue; - var xform = _xformQuery.Comp(entity); if (!xform.Anchored) _transform.AnchorEntity((entity, xform), (_gridUid, _grid), tile); From 07836fbf230ffc9ff0816abe8eeec56315c45c4a Mon Sep 17 00:00:00 2001 From: fenndragon Date: Thu, 30 Apr 2026 17:06:55 -0600 Subject: [PATCH 40/49] Potential fix for pull request finding 'Missed opportunity to use Where' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- Content.Server/Salvage/SalvageSystem.Expeditions.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Content.Server/Salvage/SalvageSystem.Expeditions.cs b/Content.Server/Salvage/SalvageSystem.Expeditions.cs index 6b27eaf8c9e..877d459cde0 100644 --- a/Content.Server/Salvage/SalvageSystem.Expeditions.cs +++ b/Content.Server/Salvage/SalvageSystem.Expeditions.cs @@ -173,10 +173,9 @@ private void OnExpeditionShutdown(EntityUid uid, SalvageExpeditionComponent comp private void CleanupHostedExpeditionContent(SalvageExpeditionComponent component) { - foreach (var generated in component.GeneratedEntities) + foreach (var generated in component.GeneratedEntities.Where(generated => Exists(generated))) { - if (Exists(generated)) - QueueDel(generated); + QueueDel(generated); } component.GeneratedEntities.Clear(); From e3b38a165d803d782e7d4a62028482722295a24d Mon Sep 17 00:00:00 2001 From: fenndragon Date: Thu, 30 Apr 2026 17:07:17 -0600 Subject: [PATCH 41/49] Potential fix for pull request finding 'Missed opportunity to use Where' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs index 9881708bec3..6a8fdb8c747 100644 --- a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs +++ b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs @@ -269,11 +269,8 @@ private bool TryRestoreChunkFromCache(Entity ent, Wo var tilePlacements = new List<(Vector2i, Tile)>(); var entityPlacements = new List<(Vector2i Indices, string PrototypeId)>(); - foreach (var line in File.ReadLines(cachePath)) + foreach (var line in File.ReadLines(cachePath).Where(line => !string.IsNullOrWhiteSpace(line) && line != "v1" && line != "v2")) { - if (string.IsNullOrWhiteSpace(line) || line == "v1" || line == "v2") - continue; - var parts = line.Split(',', 4); if (parts.Length == 3) From 9e38a74c853f2098cd7a91c905dbac7531c1de1a Mon Sep 17 00:00:00 2001 From: fenndragon Date: Thu, 30 Apr 2026 17:07:38 -0600 Subject: [PATCH 42/49] Potential fix for pull request finding 'Missed opportunity to use Where' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs index 6a8fdb8c747..c87377f5f33 100644 --- a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs +++ b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs @@ -426,11 +426,8 @@ private void SpawnTrackedTileEntity(Entity ent, Enti var spawned = Spawn(prototypeId, new EntityCoordinates(gridUid, indices + new Vector2(0.5f, 0.5f))); var after = GetTileEntities(gridUid, grid, indices); - foreach (var entity in after) + foreach (var entity in after.Where(entity => !before.Contains(entity))) { - if (before.Contains(entity)) - continue; - var meta = MetaData(entity); if (meta.EntityPrototype == null) continue; From e45999dcecb161b5c577047b14f8ae5d4d5f9e4e Mon Sep 17 00:00:00 2001 From: fenndragon Date: Thu, 30 Apr 2026 17:07:52 -0600 Subject: [PATCH 43/49] Potential fix for pull request finding 'Missed opportunity to use Where' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs index c87377f5f33..588f96b1650 100644 --- a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs +++ b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs @@ -378,11 +378,8 @@ private void SpawnChunkEntities(Entity ent, EntityUi spawns.Clear(); cache.GetSpawns(_random, ref spawns); - foreach (var prototype in spawns) + foreach (var prototype in spawns.Where(p => p != null && _proto.HasIndex(p))) { - if (prototype == null || !_proto.HasIndex(prototype)) - continue; - SpawnTrackedTileEntity(ent, gridUid, grid, indices, prototype); } } From 2fee5fed8145880a6b1761baab606006ce726d7d Mon Sep 17 00:00:00 2001 From: fenndragon Date: Thu, 30 Apr 2026 17:08:09 -0600 Subject: [PATCH 44/49] Potential fix for pull request finding 'Missed opportunity to use Where' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs index 588f96b1650..2c06086a055 100644 --- a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs +++ b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs @@ -303,11 +303,8 @@ private bool TryRestoreChunkFromCache(Entity ent, Wo if (entityPlacements.Count > 0) { - foreach (var entityPlacement in entityPlacements) + foreach (var entityPlacement in entityPlacements.Where(ep => _proto.HasIndex(ep.PrototypeId))) { - if (!_proto.HasIndex(entityPlacement.PrototypeId)) - continue; - ClearChunkMaterialEntitiesAtTile((ent.Owner, ent.Comp), gridUid, grid, entityPlacement.Indices); SpawnTrackedTileEntity((ent.Owner, ent.Comp), gridUid, grid, entityPlacement.Indices, entityPlacement.PrototypeId); } From 954fd325b51020cd081df9fab9f45643202aac88 Mon Sep 17 00:00:00 2001 From: fenndragon Date: Thu, 30 Apr 2026 17:24:11 -0600 Subject: [PATCH 45/49] fixes --- Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs index 04e58ebb168..f3878ac5d65 100644 --- a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs +++ b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs @@ -415,9 +415,12 @@ private void SpawnChunkEntities(Entity ent, EntityUi spawns.Clear(); cache.GetSpawns(_random, ref spawns); - foreach (var prototype in spawns.Where(p => p != null && _proto.HasIndex(p))) + foreach (var prototype in spawns) { - SpawnTrackedTileEntity(ent, gridUid, grid, indices, prototype); + if (prototype is not { } prototypeId || !_proto.HasIndex(prototypeId)) + continue; + + SpawnTrackedTileEntity(ent, gridUid, grid, indices, prototypeId); } } From 6b4fb84f699c5f957880440c73d0fa74d19beaf9 Mon Sep 17 00:00:00 2001 From: fenndragon Date: Fri, 1 May 2026 13:57:34 -0600 Subject: [PATCH 46/49] Update SectorWorldSystem.cs --- .../Gateway/Systems/GatewaySystem.cs | 2 +- .../GridSplit/OrphanedGridCleanupSystem.cs | 7 +++++ .../Worldgen/Systems/SectorWorldSystem.cs | 31 +++++++++++++++++-- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/Content.Server/Gateway/Systems/GatewaySystem.cs b/Content.Server/Gateway/Systems/GatewaySystem.cs index 5d4afbdaaf6..fc4a4696cf8 100644 --- a/Content.Server/Gateway/Systems/GatewaySystem.cs +++ b/Content.Server/Gateway/Systems/GatewaySystem.cs @@ -286,7 +286,7 @@ private void OpenPortal(EntityUid uid, GatewayComponent comp, EntityUid dest, Ga if (ev.Cancelled) return; - _linkedEntity.OneWayLink(uid, dest); + _linkedEntity.Link(uid, dest); var sourcePortal = EnsureComp(uid); var targetPortal = EnsureComp(dest); diff --git a/Content.Server/GridSplit/OrphanedGridCleanupSystem.cs b/Content.Server/GridSplit/OrphanedGridCleanupSystem.cs index 6ed0c00058c..0ca82f794ba 100644 --- a/Content.Server/GridSplit/OrphanedGridCleanupSystem.cs +++ b/Content.Server/GridSplit/OrphanedGridCleanupSystem.cs @@ -2,6 +2,7 @@ using Content.Server.Power.Components; using Content.Server.Procedural; using Content.Server._Mono.Cleanup; +using Content.Server.Salvage.Expeditions; using Content.Server.Worldgen.Components; using Content.Server.Station.Components; using Content.Shared.CCVar; @@ -307,6 +308,12 @@ private bool ShouldPreserveGrid(EntityUid gridUid) if (HasComp(gridUid)) return true; + if (HasComp(gridUid)) + return true; + + if (HasComp(gridUid)) + return true; + if (HasComp(gridUid)) return true; diff --git a/Content.Server/Worldgen/Systems/SectorWorldSystem.cs b/Content.Server/Worldgen/Systems/SectorWorldSystem.cs index 20c0f12b773..fb1b1f69f39 100644 --- a/Content.Server/Worldgen/Systems/SectorWorldSystem.cs +++ b/Content.Server/Worldgen/Systems/SectorWorldSystem.cs @@ -49,6 +49,7 @@ public sealed class SectorWorldSystem : EntitySystem [Dependency] private readonly PhysicsSystem _physics = default!; [Dependency] private readonly IResourceManager _resources = default!; [Dependency] private readonly ITileDefinitionManager _tileDefs = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly WeatherSystem _weather = default!; private static readonly string[] TimeOfDayStates = ["Dawn", "Day", "Dusk", "Night"]; @@ -374,11 +375,37 @@ private void RestoreHostedChunkContent(Entity ent foreach (var entity in result.Entities) { - if (entity != ent.Owner && entity != ent.Comp.HostGridUid) - ent.Comp.GeneratedEntities.Add(entity); + if (entity == ent.Owner || entity == ent.Comp.HostGridUid) + continue; + + ent.Comp.GeneratedEntities.Add(entity); + + var meta = MetaData(entity); + if (meta.EntityPrototype == null || !ShouldAnchorHostedCachedEntity(meta.EntityPrototype.ID)) + continue; + + var xform = Transform(entity); + if (xform.GridUid != gridUid || xform.Anchored) + continue; + + _transform.AnchorEntity(entity, xform); } } + private static bool ShouldAnchorHostedCachedEntity(string prototypeId) + { + return prototypeId.StartsWith("Wall", StringComparison.OrdinalIgnoreCase) + || prototypeId.StartsWith("NFWall", StringComparison.OrdinalIgnoreCase) + || prototypeId.Contains("Door", StringComparison.OrdinalIgnoreCase) + || prototypeId.Contains("Airlock", StringComparison.OrdinalIgnoreCase) + || prototypeId.Contains("Windoor", StringComparison.OrdinalIgnoreCase) + || prototypeId.Contains("Mineral", StringComparison.OrdinalIgnoreCase) + || (prototypeId.Contains("Cable", StringComparison.OrdinalIgnoreCase) + && !prototypeId.Contains("Stack", StringComparison.OrdinalIgnoreCase) + && !prototypeId.Contains("Placer", StringComparison.OrdinalIgnoreCase)) + || string.Equals(prototypeId, "Grille", StringComparison.OrdinalIgnoreCase); + } + private HashSet CollectHostedSiteEntities(SectorExpeditionSiteComponent site) { var entities = new HashSet(); From 4922f1de4365aa519b87f120f92ecfde43d2507f Mon Sep 17 00:00:00 2001 From: fenndragon Date: Sat, 2 May 2026 00:05:09 -0600 Subject: [PATCH 47/49] Update GatewaySystem.cs --- Content.Server/Gateway/Systems/GatewaySystem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.Server/Gateway/Systems/GatewaySystem.cs b/Content.Server/Gateway/Systems/GatewaySystem.cs index fc4a4696cf8..91724d71e6a 100644 --- a/Content.Server/Gateway/Systems/GatewaySystem.cs +++ b/Content.Server/Gateway/Systems/GatewaySystem.cs @@ -286,7 +286,7 @@ private void OpenPortal(EntityUid uid, GatewayComponent comp, EntityUid dest, Ga if (ev.Cancelled) return; - _linkedEntity.Link(uid, dest); + _linkedEntity.TryLink(uid, dest); var sourcePortal = EnsureComp(uid); var targetPortal = EnsureComp(dest); From 01010e703dffa7e2a876fecbbd69c37f73aa57c6 Mon Sep 17 00:00:00 2001 From: fenndragon Date: Sat, 2 May 2026 13:43:21 -0600 Subject: [PATCH 48/49] fix --- .../Gateway/Systems/GatewayGeneratorSystem.cs | 6 +- .../Gateway/Systems/GatewaySystem.cs | 1 + .../Salvage/SalvageSystem.Expeditions.cs | 48 +++++- .../Salvage/SalvageSystem.Runner.cs | 25 ++- .../Salvage/SpawnSalvageMissionJob.cs | 6 + .../Components/ChunkEntityMutationRecord.cs | 26 +++ .../SectorExpeditionSiteComponent.cs | 2 +- .../Systems/SectorChunkCarverSystem.cs | 133 +++++++++++----- .../Worldgen/Systems/SectorWorldSystem.cs | 148 ++++++------------ .../Worldgen/Systems/WorldControllerSystem.cs | 9 ++ 10 files changed, 254 insertions(+), 150 deletions(-) create mode 100644 Content.Server/Worldgen/Components/ChunkEntityMutationRecord.cs diff --git a/Content.Server/Gateway/Systems/GatewayGeneratorSystem.cs b/Content.Server/Gateway/Systems/GatewayGeneratorSystem.cs index 5fba14aa5de..f8ee2bda4bf 100644 --- a/Content.Server/Gateway/Systems/GatewayGeneratorSystem.cs +++ b/Content.Server/Gateway/Systems/GatewayGeneratorSystem.cs @@ -231,7 +231,7 @@ private async Task FinishGeneratorOpenAsync( // TODO: Dungeon mobs + loot. // Do markers on the map. - if (TryComp(ent.Owner, out BiomeComponent? biomeComp) && generatorComp != null) + if (TryComp(gridUid, out BiomeComponent? biomeComp) && generatorComp != null) { var lootLayers = generatorComp.LootLayers.ToList(); @@ -241,7 +241,7 @@ private async Task FinishGeneratorOpenAsync( var layer = lootLayers[layerIdx]; lootLayers.RemoveSwap(layerIdx); - _biome.AddMarkerLayer(ent.Owner, biomeComp, layer.Id); + _biome.AddMarkerLayer(gridUid, biomeComp, layer.Id); } var mobLayers = generatorComp.MobLayers.ToList(); @@ -252,7 +252,7 @@ private async Task FinishGeneratorOpenAsync( var layer = mobLayers[layerIdx]; mobLayers.RemoveSwap(layerIdx); - _biome.AddMarkerLayer(ent.Owner, biomeComp, layer.Id); + _biome.AddMarkerLayer(gridUid, biomeComp, layer.Id); } if (TryComp(ent.Owner, out siteComp)) diff --git a/Content.Server/Gateway/Systems/GatewaySystem.cs b/Content.Server/Gateway/Systems/GatewaySystem.cs index 91724d71e6a..ff9e825b183 100644 --- a/Content.Server/Gateway/Systems/GatewaySystem.cs +++ b/Content.Server/Gateway/Systems/GatewaySystem.cs @@ -203,6 +203,7 @@ private void OnOpenPortal(EntityUid uid, GatewayComponent comp, GatewayOpenPorta // TODO: admin log??? ClosePortal(uid, comp, false); + ClosePortal(desto, dest, false); OpenPortal(uid, comp, desto, dest); } diff --git a/Content.Server/Salvage/SalvageSystem.Expeditions.cs b/Content.Server/Salvage/SalvageSystem.Expeditions.cs index 877d459cde0..2d182cd44a2 100644 --- a/Content.Server/Salvage/SalvageSystem.Expeditions.cs +++ b/Content.Server/Salvage/SalvageSystem.Expeditions.cs @@ -7,6 +7,7 @@ using Content.Shared.Examine; using Content.Shared.Random.Helpers; using Content.Shared.Salvage.Expeditions; +using Content.Shared.Salvage.Expeditions.Modifiers; using Robust.Shared.Audio; using Robust.Shared.CPUJob.JobQueues; using Robust.Shared.CPUJob.JobQueues.Queues; @@ -400,14 +401,8 @@ private void GenerateMissions(SalvageExpeditionDataComponent component) var missionIndex = 0; for (var i = 0; i < MissionLimit; i++) { - var mission = new SalvageMissionParams - { - Index = (ushort)missionIndex, - // Pick a valid mission type; Max is a sentinel and must be excluded. - MissionType = (SalvageMissionType)_random.NextByte((byte)SalvageMissionType.Max), - Seed = _random.Next(), - Difficulty = difficulties[i].id, - }; + if (!TryGenerateSectorMission(difficulties[i].id, (ushort) missionIndex, out var mission)) + continue; component.Missions[(ushort)missionIndex] = mission; missionIndex++; @@ -426,6 +421,43 @@ private void GenerateMissions(SalvageExpeditionDataComponent component) } } + private bool TryGenerateSectorMission(string difficultyId, ushort missionIndex, out SalvageMissionParams mission) + { + const int maxAttempts = 32; + mission = default!; + + if (!_prototypeManager.TryIndex(difficultyId, out var difficultyProto)) + return false; + + for (var attempt = 0; attempt < maxAttempts; attempt++) + { + var seed = _random.Next(); + var missionType = (SalvageMissionType) _random.NextByte((byte) SalvageMissionType.Max); + var generatedMission = GetMission(missionType, difficultyProto, seed); + var biomeProto = _prototypeManager.Index(generatedMission.Biome); + + if (!_sectorWorld.TryResolvePlanetTypeForBiome(biomeProto.BiomePrototype, out var planetTypeId) || + string.IsNullOrWhiteSpace(planetTypeId) || + !_sectorWorld.TryGetPersistentMap(planetTypeId, out _, out _)) + { + continue; + } + + mission = new SalvageMissionParams + { + Index = missionIndex, + Seed = seed, + Difficulty = difficultyId, + MissionType = missionType, + }; + + return true; + } + + Log.Warning($"Failed to generate sector-valid salvage mission for difficulty {difficultyId} after {maxAttempts} attempts."); + return false; + } + // HARDLIGHT: Public method for round persistence system to properly regenerate missions public void ForceGenerateMissions(SalvageExpeditionDataComponent component) { diff --git a/Content.Server/Salvage/SalvageSystem.Runner.cs b/Content.Server/Salvage/SalvageSystem.Runner.cs index 1c535f008f1..12089a25390 100644 --- a/Content.Server/Salvage/SalvageSystem.Runner.cs +++ b/Content.Server/Salvage/SalvageSystem.Runner.cs @@ -16,6 +16,8 @@ using Content.Server.GameTicking; // Frontier using Content.Server._NF.Salvage.Expeditions.Structure; // Frontier using Content.Server._NF.Salvage.Expeditions; +using Content.Shared.Ghost; +using Content.Shared.Mind.Components; using Content.Shared.Salvage; // Frontier using RobustTimer = Robust.Shared.Timing.Timer; // HardLight @@ -227,7 +229,7 @@ private void OnFTLStarted(ref FTLStartedEvent ev) if (HasComp(ev.Entity)) RemComp(ev.Entity); - if (HasExpeditionParticipantShuttlesOnMap(expeditionMapUid)) + if (HasExpeditionParticipantShuttlesOnMap(expeditionMapUid) || HasActivePlayersOnExpedition(expeditionMapUid)) return; // Last shuttle has left so finish the mission. @@ -570,6 +572,23 @@ private bool HasExpeditionParticipantShuttlesOnMap(EntityUid expeditionMapUid) return false; } + private bool HasActivePlayersOnExpedition(EntityUid expeditionMapUid) + { + var ghostQuery = GetEntityQuery(); + var mindQuery = EntityQueryEnumerator(); + + while (mindQuery.MoveNext(out var uid, out var mind, out var xform)) + { + if (!mind.HasMind || ghostQuery.HasComponent(uid)) + continue; + + if (IsEntityOnExpedition(uid, expeditionMapUid, xform)) + return true; + } + + return false; + } + /// /// HardLight: FTL all shuttles currently on an expedition map back to the home map. /// @@ -608,11 +627,11 @@ private void QueueExpeditionDeletionWhenEmpty(EntityUid expeditionMapUid, int at if (!Exists(expeditionMapUid) || !TryComp(expeditionMapUid, out _)) return; - if (HasExpeditionParticipantShuttlesOnMap(expeditionMapUid)) + if (HasExpeditionParticipantShuttlesOnMap(expeditionMapUid) || HasActivePlayersOnExpedition(expeditionMapUid)) { if (attempt >= 24) { - Log.Warning($"Expedition {expeditionMapUid} still has expedition participant shuttles after cleanup retries; skipping forced map deletion to avoid deleting active players."); + Log.Warning($"Expedition {expeditionMapUid} still has expedition occupants after cleanup retries; skipping forced map deletion to avoid deleting active players."); return; } diff --git a/Content.Server/Salvage/SpawnSalvageMissionJob.cs b/Content.Server/Salvage/SpawnSalvageMissionJob.cs index 21fa148c60f..e5b9ffb7b74 100644 --- a/Content.Server/Salvage/SpawnSalvageMissionJob.cs +++ b/Content.Server/Salvage/SpawnSalvageMissionJob.cs @@ -40,6 +40,7 @@ using Content.Server.Station.Systems; // Frontier using Content.Server.Shuttles.Systems; using Content.Server._NF.Salvage.Expeditions.Structure; // Frontier +using Content.Server.Worldgen; using Content.Server.Worldgen.Components; using Content.Server.Worldgen.Systems; using Robust.Shared.Audio.Systems; @@ -268,6 +269,11 @@ private async Task InternalProcess() // Frontier: make process an internal expedition.HostGridUid = hostGridUid; var captureRadius = placement.ReservationRadius + 32f; + _entManager.EnsureComponent(mapUid); + var worldController = _entManager.System(); + worldController.SetLoaderRadius(mapUid, (int) MathF.Ceiling(captureRadius + WorldGen.ChunkSize)); + worldController.SetLoaderEnabled(mapUid, true); + _sectorWorld.CaptureHostedSiteBaseline((mapUid, site), hostGridUid, grid, placement.Center, captureRadius); CaptureOriginalTiles(expedition, hostGridUid, grid, placement.Center, captureRadius); var existingEntities = CaptureNearbyEntities(hostMapUid, placement.Center, captureRadius); diff --git a/Content.Server/Worldgen/Components/ChunkEntityMutationRecord.cs b/Content.Server/Worldgen/Components/ChunkEntityMutationRecord.cs new file mode 100644 index 00000000000..7080d2a205c --- /dev/null +++ b/Content.Server/Worldgen/Components/ChunkEntityMutationRecord.cs @@ -0,0 +1,26 @@ +using System.Numerics; + +namespace Content.Server.Worldgen.Components; + +public sealed record ChunkEntityMutationRecord( + Vector2 LocalPosition, + string PrototypeId, + double Rotation, + bool Anchored); + +public static class ChunkEntityMutationRules +{ + public static bool ShouldAnchor(string prototypeId) + { + return prototypeId.StartsWith("Wall", StringComparison.OrdinalIgnoreCase) + || prototypeId.StartsWith("NFWall", StringComparison.OrdinalIgnoreCase) + || prototypeId.Contains("Door", StringComparison.OrdinalIgnoreCase) + || prototypeId.Contains("Airlock", StringComparison.OrdinalIgnoreCase) + || prototypeId.Contains("Window", StringComparison.OrdinalIgnoreCase) + || prototypeId.Contains("Mineral", StringComparison.OrdinalIgnoreCase) + || (prototypeId.Contains("Cable", StringComparison.OrdinalIgnoreCase) + && !prototypeId.Contains("Stack", StringComparison.OrdinalIgnoreCase) + && !prototypeId.Contains("Placer", StringComparison.OrdinalIgnoreCase)) + || string.Equals(prototypeId, "Grille", StringComparison.OrdinalIgnoreCase); + } +} \ No newline at end of file diff --git a/Content.Server/Worldgen/Components/SectorExpeditionSiteComponent.cs b/Content.Server/Worldgen/Components/SectorExpeditionSiteComponent.cs index a4a684f29be..7096a2ada11 100644 --- a/Content.Server/Worldgen/Components/SectorExpeditionSiteComponent.cs +++ b/Content.Server/Worldgen/Components/SectorExpeditionSiteComponent.cs @@ -34,5 +34,5 @@ public sealed partial class SectorExpeditionSiteComponent : Component public Dictionary> CachedChunkTiles = new(); - public Dictionary CachedChunkEntityFiles = new(); + public Dictionary> CachedChunkEntities = new(); } \ No newline at end of file diff --git a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs index f3878ac5d65..a283eab5286 100644 --- a/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs +++ b/Content.Server/Worldgen/Systems/SectorChunkCarverSystem.cs @@ -2,6 +2,7 @@ using System.IO; using System.Linq; using System.Text; +using System.Globalization; using Robust.Server.GameObjects; using Content.Server._NF.RoundNotifications.Events; using Content.Server.Worldgen.Components; @@ -253,7 +254,7 @@ private void SaveChunkToCache(Entity ent, EntityUid Directory.CreateDirectory(Path.GetDirectoryName(cachePath)!); var builder = new StringBuilder(); - builder.AppendLine("v2"); + builder.AppendLine("v3"); foreach (var indices in ent.Comp.GeneratedTiles) { @@ -285,14 +286,17 @@ private void SaveChunkToCache(Entity ent, EntityUid if (xform.GridUid != gridUid) continue; - var indices = _mapSystem.TileIndicesFor(gridUid, grid, xform.Coordinates); builder.Append('e') .Append(',') - .Append(indices.X) + .Append(xform.Coordinates.Position.X.ToString(CultureInfo.InvariantCulture)) .Append(',') - .Append(indices.Y) + .Append(xform.Coordinates.Position.Y.ToString(CultureInfo.InvariantCulture)) .Append(',') .Append(meta.EntityPrototype.ID) + .Append(',') + .Append(xform.LocalRotation.Theta.ToString(CultureInfo.InvariantCulture)) + .Append(',') + .Append(xform.Anchored) .AppendLine(); } @@ -308,48 +312,89 @@ private bool TryRestoreChunkFromCache(Entity ent, Wo return false; var tilePlacements = new List<(Vector2i, Tile)>(); - var entityPlacements = new List<(Vector2i Indices, string PrototypeId)>(); - foreach (var line in File.ReadLines(cachePath).Where(line => !string.IsNullOrWhiteSpace(line) && line != "v1" && line != "v2")) + var entityPlacements = new List(); + var cacheVersion = 1; + + foreach (var line in File.ReadLines(cachePath)) { - var parts = line.Split(',', 4); + if (string.IsNullOrWhiteSpace(line)) + continue; - if (parts.Length == 3) + if (line is "v1" or "v2" or "v3") { - RestoreCachedTile(parts[0], parts[1], parts[2], tilePlacements, ent.Comp.GeneratedTiles); + cacheVersion = line[1] - '0'; continue; } - if (parts.Length != 4) + var parts = line.Split(','); + + if (parts.Length == 3) + { + RestoreCachedTile(parts[0], parts[1], parts[2], tilePlacements, ent.Comp.GeneratedTiles); continue; + } switch (parts[0]) { case "t": + if (parts.Length != 4) + continue; + RestoreCachedTile(parts[1], parts[2], parts[3], tilePlacements, ent.Comp.GeneratedTiles); break; case "e": - if (!int.TryParse(parts[1], out var entityX) || !int.TryParse(parts[2], out var entityY)) + if (cacheVersion >= 3) + { + if (parts.Length != 6 + || !float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var entityPosX) + || !float.TryParse(parts[2], NumberStyles.Float, CultureInfo.InvariantCulture, out var entityPosY) + || !float.TryParse(parts[4], NumberStyles.Float, CultureInfo.InvariantCulture, out var entityRotation) + || !bool.TryParse(parts[5], out var entityAnchored)) + { + continue; + } + + entityPlacements.Add(new ChunkEntityMutationRecord( + new Vector2(entityPosX, entityPosY), + parts[3], + entityRotation, + entityAnchored)); + continue; + } + + if (parts.Length != 4 + || !int.TryParse(parts[1], out var entityX) + || !int.TryParse(parts[2], out var entityY)) + { continue; + } - entityPlacements.Add((new Vector2i(entityX, entityY), parts[3])); + entityPlacements.Add(new ChunkEntityMutationRecord( + new Vector2(entityX + 0.5f, entityY + 0.5f), + parts[3], + 0f, + ChunkEntityMutationRules.ShouldAnchor(parts[3]))); break; } } - if (tilePlacements.Count == 0) + var authoritativeSnapshot = cacheVersion >= 3; + if (!authoritativeSnapshot && tilePlacements.Count == 0) return false; - _mapSystem.SetTiles(gridUid, grid, tilePlacements); + if (tilePlacements.Count > 0) + _mapSystem.SetTiles(gridUid, grid, tilePlacements); if (entityPlacements.Count > 0) { foreach (var entityPlacement in entityPlacements.Where(ep => _proto.HasIndex(ep.PrototypeId))) { - ClearChunkMaterialEntitiesAtTile((ent.Owner, ent.Comp), gridUid, grid, entityPlacement.Indices); - SpawnTrackedTileEntity((ent.Owner, ent.Comp), gridUid, grid, entityPlacement.Indices, entityPlacement.PrototypeId); + var indices = _mapSystem.TileIndicesFor(gridUid, grid, new EntityCoordinates(gridUid, entityPlacement.LocalPosition)); + ClearChunkMaterialEntitiesAtTile((ent.Owner, ent.Comp), gridUid, grid, indices); + SpawnTrackedChunkEntity((ent.Owner, ent.Comp), gridUid, grid, entityPlacement.LocalPosition, entityPlacement.PrototypeId, new Angle(entityPlacement.Rotation), entityPlacement.Anchored); } } - else + else if (!authoritativeSnapshot) { SpawnChunkEntities((ent.Owner, ent.Comp), gridUid, grid, chunkBiome); } @@ -459,8 +504,26 @@ private void ClearChunkMaterialEntitiesAtTile(Entity private void SpawnTrackedTileEntity(Entity ent, EntityUid gridUid, MapGridComponent grid, Vector2i indices, string prototypeId) { + SpawnTrackedChunkEntity(ent, gridUid, grid, indices + new Vector2(0.5f, 0.5f), prototypeId); + } + + private void SpawnTrackedChunkEntity( + Entity ent, + EntityUid gridUid, + MapGridComponent grid, + Vector2 localPosition, + string prototypeId, + Angle? rotation = null, + bool? anchored = null) + { + var coordinates = new EntityCoordinates(gridUid, localPosition); + var indices = _mapSystem.TileIndicesFor(gridUid, grid, coordinates); var before = GetTileEntities(gridUid, grid, indices); - var spawned = Spawn(prototypeId, new EntityCoordinates(gridUid, indices + new Vector2(0.5f, 0.5f))); + var spawned = Spawn(prototypeId, _transform.ToMapCoordinates(coordinates)); + _transform.SetCoordinates(spawned, coordinates); + if (rotation != null) + _transform.SetLocalRotation(spawned, rotation.Value); + var after = GetTileEntities(gridUid, grid, indices); foreach (var entity in after.Where(entity => !before.Contains(entity))) @@ -477,8 +540,7 @@ private void SpawnTrackedTileEntity(Entity ent, Enti continue; } - if (ShouldAnchorChunkEntity(meta.EntityPrototype.ID)) - AnchorToGrid(entity); + ApplyChunkEntityAnchoring(entity, meta.EntityPrototype.ID, anchored); ent.Comp.GeneratedEntities.Add(entity); } @@ -494,8 +556,7 @@ private void SpawnTrackedTileEntity(Entity ent, Enti return; } - if (ShouldAnchorChunkEntity(spawnedMeta.EntityPrototype.ID)) - AnchorToGrid(spawned); + ApplyChunkEntityAnchoring(spawned, spawnedMeta.EntityPrototype.ID, anchored); } ent.Comp.GeneratedEntities.Add(spawned); @@ -508,13 +569,21 @@ private HashSet GetTileEntities(EntityUid gridUid, MapGridComponent g return _lookup.GetLocalEntitiesIntersecting(tileRef, flags: LookupFlags.Dynamic | LookupFlags.Static | LookupFlags.StaticSundries | LookupFlags.Sundries | LookupFlags.Approximate).ToHashSet(); } - private void AnchorToGrid(EntityUid entity) + private void ApplyChunkEntityAnchoring(EntityUid entity, string prototypeId, bool? anchored) { var xform = Transform(entity); - if (xform.Anchored) + var shouldAnchor = anchored ?? ChunkEntityMutationRules.ShouldAnchor(prototypeId); + + if (shouldAnchor) + { + if (!xform.Anchored) + _transform.AnchorEntity(entity, xform); + return; + } - _transform.AnchorEntity(entity, xform); + if (xform.Anchored) + _transform.Unanchor(entity, xform); } private static bool IsTransientChunkSpawnerPrototype(string prototypeId) @@ -534,20 +603,6 @@ private static bool IsChunkMaterialPrototype(string prototypeId) || string.Equals(prototypeId, "Grille", StringComparison.OrdinalIgnoreCase); } - private static bool ShouldAnchorChunkEntity(string prototypeId) - { - return prototypeId.StartsWith("Wall", StringComparison.OrdinalIgnoreCase) - || prototypeId.StartsWith("NFWall", StringComparison.OrdinalIgnoreCase) - || prototypeId.Contains("Door", StringComparison.OrdinalIgnoreCase) - || prototypeId.Contains("Airlock", StringComparison.OrdinalIgnoreCase) - || prototypeId.Contains("Windoor", StringComparison.OrdinalIgnoreCase) - || prototypeId.Contains("Mineral", StringComparison.OrdinalIgnoreCase) - || (prototypeId.Contains("Cable", StringComparison.OrdinalIgnoreCase) - && !prototypeId.Contains("Stack", StringComparison.OrdinalIgnoreCase) - && !prototypeId.Contains("Placer", StringComparison.OrdinalIgnoreCase)) - || string.Equals(prototypeId, "Grille", StringComparison.OrdinalIgnoreCase); - } - private Dictionary? GetBiomeCache(SectorAsteroidBiomePrototype? biome) { if (biome == null) diff --git a/Content.Server/Worldgen/Systems/SectorWorldSystem.cs b/Content.Server/Worldgen/Systems/SectorWorldSystem.cs index fb1b1f69f39..d732a253d4b 100644 --- a/Content.Server/Worldgen/Systems/SectorWorldSystem.cs +++ b/Content.Server/Worldgen/Systems/SectorWorldSystem.cs @@ -16,9 +16,6 @@ using Content.Shared.Parallax.Biomes; using Content.Shared.Shuttles.Components; using Content.Shared.Weather; -using Robust.Shared.ContentPack; -using Robust.Shared.EntitySerialization; -using Robust.Shared.EntitySerialization.Systems; using Robust.Server.GameObjects; using Robust.Shared.Map; using Robust.Shared.Map.Components; @@ -43,17 +40,14 @@ public sealed class SectorWorldSystem : EntitySystem [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly AtmosphereSystem _atmosphere = default!; [Dependency] private readonly BiomeSystem _biome = default!; - [Dependency] private readonly MapLoaderSystem _loader = default!; [Dependency] private readonly MetaDataSystem _metaData = default!; [Dependency] private readonly SharedMapSystem _mapSystem = default!; [Dependency] private readonly PhysicsSystem _physics = default!; - [Dependency] private readonly IResourceManager _resources = default!; [Dependency] private readonly ITileDefinitionManager _tileDefs = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly WeatherSystem _weather = default!; private static readonly string[] TimeOfDayStates = ["Dawn", "Day", "Dusk", "Night"]; - private readonly string _siteCacheSession = Guid.NewGuid().ToString("N"); private bool _roundRestartCleanupActive; public override void Initialize() @@ -99,7 +93,7 @@ public void CaptureHostedSiteBaseline(Entity ent, ent.Comp.OriginalEntities.Clear(); ent.Comp.GeneratedEntities.Clear(); ent.Comp.CachedChunkTiles.Clear(); - ent.Comp.CachedChunkEntityFiles.Clear(); + ent.Comp.CachedChunkEntities.Clear(); foreach (var tile in _mapSystem.GetTilesIntersecting(hostGridUid, grid, new Circle(center, radius), false)) { @@ -177,7 +171,7 @@ public void RestoreHostedChunkContent(EntityUid gridUid, MapGridComponent grid, if (site.HostGridUid != gridUid) continue; - if (!site.CachedChunkTiles.ContainsKey(chunkOrigin) && !site.CachedChunkEntityFiles.ContainsKey(chunkOrigin)) + if (!site.CachedChunkTiles.ContainsKey(chunkOrigin) && !site.CachedChunkEntities.ContainsKey(chunkOrigin)) continue; if (!DoesSiteIntersectChunk(site, chunkOrigin, chunkSize)) @@ -220,7 +214,7 @@ public void CleanupHostedSite(Entity ent) ent.Comp.OriginalTiles.Clear(); ent.Comp.OriginalEntities.Clear(); ent.Comp.CachedChunkTiles.Clear(); - ent.Comp.CachedChunkEntityFiles.Clear(); + ent.Comp.CachedChunkEntities.Clear(); } public void CleanupHostedSite(EntityUid uid, SectorExpeditionSiteComponent component) @@ -235,10 +229,6 @@ private void CleanupHostedSiteCachesForRoundRestart() { CleanupHostedSite((uid, site)); } - - var cacheRoot = GetHostedSiteCacheRoot(); - if (_resources.UserData.Exists(cacheRoot)) - _resources.UserData.Delete(cacheRoot); } private void CleanupLayerMapsForRoundRestart() @@ -310,28 +300,45 @@ private void SaveHostedChunkContent(Entity ent, E } } - if (cachedTiles.Count > 0) - ent.Comp.CachedChunkTiles[chunkOrigin] = cachedTiles; + ent.Comp.CachedChunkTiles[chunkOrigin] = cachedTiles; if (TryGetHostedSiteMapUid(gridUid, out var mapUid)) { var generated = CollectHostedChunkEntities(ent.Comp, mapUid, gridUid, grid, chunkOrigin, chunkSize); + var cachedEntities = new List(generated.Count); - if (generated.Count > 0) + foreach (var entity in generated) { - var cachePath = GetHostedChunkEntityCachePath(ent.Owner, chunkOrigin); - if (_loader.TrySaveGeneric(generated, cachePath, out _)) - { - ent.Comp.CachedChunkEntityFiles[chunkOrigin] = cachePath.ToString(); - } + if (!Exists(entity)) + continue; - foreach (var entity in generated) - { - ent.Comp.GeneratedEntities.Remove(entity); - if (Exists(entity)) - QueueDel(entity); - } + var meta = MetaData(entity); + if (meta.EntityPrototype == null || meta.EntityLifeStage >= EntityLifeStage.Terminating) + continue; + + var xform = Transform(entity); + if (xform.GridUid != gridUid) + continue; + + cachedEntities.Add(new ChunkEntityMutationRecord( + xform.Coordinates.Position, + meta.EntityPrototype.ID, + xform.LocalRotation.Theta, + xform.Anchored)); } + + ent.Comp.CachedChunkEntities[chunkOrigin] = cachedEntities; + + foreach (var entity in generated) + { + ent.Comp.GeneratedEntities.Remove(entity); + if (Exists(entity)) + QueueDel(entity); + } + } + else + { + ent.Comp.CachedChunkEntities[chunkOrigin] = new List(); } if (cachedTiles.Count == 0) @@ -359,51 +366,27 @@ private void RestoreHostedChunkContent(Entity ent _mapSystem.SetTiles(gridUid, grid, restoreTiles); } - if (!ent.Comp.CachedChunkEntityFiles.Remove(chunkOrigin, out var cachePathString)) + if (!ent.Comp.CachedChunkEntities.Remove(chunkOrigin, out var cachedEntities)) return; - if (!TryGetHostedSiteMapId(gridUid, out var mapId)) - return; - - var loadOptions = new MapLoadOptions - { - MergeMap = mapId, - }; - - if (!_loader.TryLoadGeneric(new ResPath(cachePathString), out var result, loadOptions) || result == null) - return; - - foreach (var entity in result.Entities) + foreach (var record in cachedEntities) { - if (entity == ent.Owner || entity == ent.Comp.HostGridUid) - continue; - - ent.Comp.GeneratedEntities.Add(entity); - - var meta = MetaData(entity); - if (meta.EntityPrototype == null || !ShouldAnchorHostedCachedEntity(meta.EntityPrototype.ID)) + if (!_proto.HasIndex(record.PrototypeId)) continue; + var coordinates = new EntityCoordinates(gridUid, record.LocalPosition); + var entity = Spawn(record.PrototypeId, _transform.ToMapCoordinates(coordinates)); + _transform.SetCoordinates(entity, coordinates); var xform = Transform(entity); - if (xform.GridUid != gridUid || xform.Anchored) - continue; + _transform.SetLocalRotation(entity, new Angle(record.Rotation), xform); - _transform.AnchorEntity(entity, xform); - } - } + if (record.Anchored) + _transform.AnchorEntity(entity, xform); + else if (xform.Anchored) + _transform.Unanchor(entity, xform); - private static bool ShouldAnchorHostedCachedEntity(string prototypeId) - { - return prototypeId.StartsWith("Wall", StringComparison.OrdinalIgnoreCase) - || prototypeId.StartsWith("NFWall", StringComparison.OrdinalIgnoreCase) - || prototypeId.Contains("Door", StringComparison.OrdinalIgnoreCase) - || prototypeId.Contains("Airlock", StringComparison.OrdinalIgnoreCase) - || prototypeId.Contains("Windoor", StringComparison.OrdinalIgnoreCase) - || prototypeId.Contains("Mineral", StringComparison.OrdinalIgnoreCase) - || (prototypeId.Contains("Cable", StringComparison.OrdinalIgnoreCase) - && !prototypeId.Contains("Stack", StringComparison.OrdinalIgnoreCase) - && !prototypeId.Contains("Placer", StringComparison.OrdinalIgnoreCase)) - || string.Equals(prototypeId, "Grille", StringComparison.OrdinalIgnoreCase); + ent.Comp.GeneratedEntities.Add(entity); + } } private HashSet CollectHostedSiteEntities(SectorExpeditionSiteComponent site) @@ -443,14 +426,15 @@ private HashSet CollectHostedChunkEntities( int chunkSize) { var entities = new HashSet(); - var query = AllEntityQuery(); - while (query.MoveNext(out var uid, out var xform)) + foreach (var uid in site.GeneratedEntities.ToArray()) { - if (uid == gridUid || uid == mapUid || site.OriginalEntities.Contains(uid)) + if (!Exists(uid)) continue; - if (xform.MapUid != mapUid) + var xform = Transform(uid); + + if (xform.MapUid != mapUid || xform.GridUid != gridUid) continue; var tile = _mapSystem.LocalToTile(gridUid, grid, xform.Coordinates); @@ -511,24 +495,6 @@ private bool TryGetHostedSiteMapUid(EntityUid gridUid, out EntityUid mapUid) return true; } - private bool TryGetHostedSiteMapId(EntityUid gridUid, out MapId mapId) - { - mapId = MapId.Nullspace; - - if (TryComp(gridUid, out var mapComp)) - { - mapId = mapComp.MapId; - return true; - } - - var xform = Transform(gridUid); - if (xform.MapUid is not { } mapUid || !TryComp(mapUid, out mapComp)) - return false; - - mapId = mapComp.MapId; - return true; - } - private static bool IsChunkTile(Vector2i tile, Vector2i chunkOrigin, int chunkSize) { return tile.X >= chunkOrigin.X @@ -537,16 +503,6 @@ private static bool IsChunkTile(Vector2i tile, Vector2i chunkOrigin, int chunkSi && tile.Y < chunkOrigin.Y + chunkSize; } - private ResPath GetHostedChunkEntityCachePath(EntityUid siteUid, Vector2i chunkOrigin) - { - return new ResPath($"/HardLight/hosted-site-cache/{_siteCacheSession}/{siteUid.Id.ToString()}/{chunkOrigin.X}_{chunkOrigin.Y}.yml"); - } - - private ResPath GetHostedSiteCacheRoot() - { - return new ResPath($"/HardLight/hosted-site-cache/{_siteCacheSession}"); - } - public HashSet GetRoundEndCleanupMapIds() { var mapIds = new HashSet(); diff --git a/Content.Server/Worldgen/Systems/WorldControllerSystem.cs b/Content.Server/Worldgen/Systems/WorldControllerSystem.cs index 2d0edfe0f0b..0c7131220ed 100644 --- a/Content.Server/Worldgen/Systems/WorldControllerSystem.cs +++ b/Content.Server/Worldgen/Systems/WorldControllerSystem.cs @@ -43,6 +43,15 @@ public void SetLoaderRadius(EntityUid uid, int radius, WorldLoaderComponent? loa Dirty(uid, loader); } + public void SetLoaderEnabled(EntityUid uid, bool enabled, WorldLoaderComponent? loader = null) + { + if (!Resolve(uid, ref loader, false) || loader == null) + return; + + loader.Disabled = !enabled; + Dirty(uid, loader); + } + /// /// Handles deleting chunks properly. /// From ca49ec22aa1dc4c866d8ae91823e999d521ad8ac Mon Sep 17 00:00:00 2001 From: fenndragon Date: Sat, 2 May 2026 17:20:10 -0600 Subject: [PATCH 49/49] fixes --- .../Shuttles/UI/BaseShuttleControl.xaml.cs | 55 ++++++- Content.Client/Weather/WeatherSystem.cs | 4 +- .../Gateway/Systems/GatewayGeneratorSystem.cs | 90 +++++++---- .../Salvage/SpawnSalvageMissionJob.cs | 151 ++++++++++++++++-- .../Components/WorldLoaderComponent.cs | 2 +- .../Worldgen/Systems/SectorWorldSystem.cs | 68 +++++++- .../Worldgen/Systems/WorldControllerSystem.cs | 25 +++ .../Components/RadarTileMaskComponent.cs | 10 ++ 8 files changed, 355 insertions(+), 50 deletions(-) create mode 100644 Content.Shared/Shuttles/Components/RadarTileMaskComponent.cs diff --git a/Content.Client/Shuttles/UI/BaseShuttleControl.xaml.cs b/Content.Client/Shuttles/UI/BaseShuttleControl.xaml.cs index dc66f02522e..3cea308710b 100644 --- a/Content.Client/Shuttles/UI/BaseShuttleControl.xaml.cs +++ b/Content.Client/Shuttles/UI/BaseShuttleControl.xaml.cs @@ -12,6 +12,8 @@ using Content.Shared._Mono.GridEdgeMarker; // Mono using Content.Shared.Maps; // Mono using Content.Shared.Shuttles.Components; +using Content.Shared.StepTrigger.Components; +using Content.Shared.Light.Components; using Robust.Client.AutoGenerated; using Robust.Client.Graphics; using Robust.Client.ResourceManagement; @@ -55,6 +57,9 @@ public partial class BaseShuttleControl : MapGridControl private Vector2 CenterVec = new Vector2(0.5f, 0.5f); // Mono private EntityQuery _xformQuery; // Mono + private EntityQuery _radarMaskQuery; + private EntityQuery _stepTriggerQuery; + private EntityQuery _tileEmissionQuery; private Vector2[] _allVertices = Array.Empty(); @@ -70,6 +75,9 @@ public BaseShuttleControl(float minRange, float maxRange, float range) : base(mi Maps = EntManager.System(); _lookup = EntManager.System(); // Mono _xformQuery = EntManager.GetEntityQuery(); // Mono + _radarMaskQuery = EntManager.GetEntityQuery(); + _stepTriggerQuery = EntManager.GetEntityQuery(); + _tileEmissionQuery = EntManager.GetEntityQuery(); Font = new VectorFont(IoCManager.Resolve().GetResource("/Fonts/NotoSans/NotoSans-Regular.ttf"), 12); _drawJob = new GridDrawJob() @@ -160,8 +168,14 @@ protected void DrawGrid(DrawingHandleScreen handle, Matrix3x2 gridToView, Entity if (gridData.LastBuild < grid.Comp.LastTileModifiedTick) { gridData.Vertices.Clear(); + gridData.HazardVertices.Clear(); _gridTileList.Clear(); _gridNeighborSet.Clear(); + var maskedGrid = _radarMaskQuery.TryGetComponent(grid.Owner, out var radarMask); + HashSet? hiddenTiles = null; + + if (maskedGrid && radarMask != null) + hiddenTiles = radarMask.HiddenTileIds.Count > 0 ? new HashSet(radarMask.HiddenTileIds) : null; // Okay so there's 2 steps to this // 1. Is that get we get a set of all tiles. This is used to decompose into triangle-strips @@ -172,6 +186,12 @@ protected void DrawGrid(DrawingHandleScreen handle, Matrix3x2 gridToView, Entity // Mono - drawing logic rewritten var def = (ContentTileDefinition)_tileDef[tileRef.Value.Tile.TypeId]; + var hiddenTile = hiddenTiles != null && hiddenTiles.Contains(def.ID); + var hazardTile = hiddenTile && IsRadarHazardTile(grid, index); + + if (hiddenTile && !hazardTile) + continue; + _gridTileList.Add((index, def)); // since our shape has to be convex, just draw it by taking our first vertex as origin @@ -181,9 +201,10 @@ protected void DrawGrid(DrawingHandleScreen handle, Matrix3x2 gridToView, Entity for (var i = 2; i < def.Vertices.Count; i++) { var vert = bl + def.Vertices[i] * tileSize; - gridData.Vertices.Add(origin); - gridData.Vertices.Add(prev); - gridData.Vertices.Add(vert); + var verts = hazardTile ? gridData.HazardVertices : gridData.Vertices; + verts.Add(origin); + verts.Add(prev); + verts.Add(vert); prev = vert; } @@ -339,9 +360,36 @@ protected void DrawGrid(DrawingHandleScreen handle, Matrix3x2 gridToView, Entity handle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, new Span(_allVertices, start, count), color.WithAlpha(alpha)); } + if (gridData.HazardVertices.Count > 0) + { + var hazardVerts = new Vector2[gridData.HazardVertices.Count]; + var hazardJob = new GridDrawJob + { + Matrix = gridToView, + Vertices = gridData.HazardVertices, + ScaledVertices = hazardVerts, + }; + + _parallel.ProcessNow(hazardJob, hazardVerts.Length); + handle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, hazardVerts, Color.Red.WithAlpha(0.2f)); + } + handle.DrawPrimitives(DrawPrimitiveTopology.LineList, new Span(_allVertices, gridData.EdgeIndex, edgeCount), color); } + private bool IsRadarHazardTile(Entity grid, Vector2i indices) + { + var anchored = Maps.GetAnchoredEntitiesEnumerator(grid, grid.Comp, indices); + + while (anchored.MoveNext(out var entity)) + { + if (_stepTriggerQuery.HasComp(entity.Value) || _tileEmissionQuery.HasComp(entity.Value)) + return true; + } + + return false; + } + private record struct GridDrawJob : IParallelRobustJob { public int BatchSize => 64; @@ -365,6 +413,7 @@ public sealed class GridDrawData */ public List Vertices = new(); + public List HazardVertices = new(); /// /// Vertices index from when edges start. diff --git a/Content.Client/Weather/WeatherSystem.cs b/Content.Client/Weather/WeatherSystem.cs index c1ccd2a546f..7e65b048fcc 100644 --- a/Content.Client/Weather/WeatherSystem.cs +++ b/Content.Client/Weather/WeatherSystem.cs @@ -60,13 +60,13 @@ public override void Update(float frameTime) if (weather.Sound == null || status.AppliedTo != playerXform.MapUid) { weather.Stream = _audio.Stop(weather.Stream); - return; + continue; } weather.Stream ??= _audio.PlayGlobal(weather.Sound, Filter.Local(), true)?.Entity; if (!_audioQuery.TryComp(weather.Stream, out var audio)) - return; + continue; var occlusion = 0f; diff --git a/Content.Server/Gateway/Systems/GatewayGeneratorSystem.cs b/Content.Server/Gateway/Systems/GatewayGeneratorSystem.cs index f8ee2bda4bf..897afd02419 100644 --- a/Content.Server/Gateway/Systems/GatewayGeneratorSystem.cs +++ b/Content.Server/Gateway/Systems/GatewayGeneratorSystem.cs @@ -3,14 +3,14 @@ using Content.Server.Gateway.Components; using Content.Server.Parallax; using Content.Server.Procedural; +using Content.Server.Shuttles.Components; +using Content.Server.Worldgen; using Content.Server.Worldgen.Components; using Content.Server.Worldgen.Systems; using Content.Shared.CCVar; -using Content.Shared.Dataset; using Content.Shared.Maps; using Content.Shared.Parallax.Biomes; using Content.Shared.Procedural; -using Content.Shared.Salvage; using Robust.Shared.Configuration; using Robust.Shared.Map; using Robust.Shared.Map.Components; @@ -37,13 +37,11 @@ public sealed class GatewayGeneratorSystem : EntitySystem [Dependency] private readonly GatewaySystem _gateway = default!; [Dependency] private readonly MetaDataSystem _metadata = default!; [Dependency] private readonly SharedMapSystem _maps = default!; - [Dependency] private readonly SharedSalvageSystem _salvage = default!; [Dependency] private readonly TileSystem _tile = default!; [Dependency] private readonly SectorWorldSystem _sectorWorld = default!; [Dependency] private readonly SharedTransformSystem _xform = default!; + [Dependency] private readonly WorldControllerSystem _worldController = default!; - private static readonly ProtoId PlanetNamesId = "NamesBorer"; - private static readonly ProtoId ContinentalId = "Continental"; private static readonly ProtoId ExperimentDungeonId = "Experiment"; // TODO: @@ -106,15 +104,20 @@ private void GenerateDestination(EntityUid uid, GatewayGeneratorComponent? gener var generatorComp = generator; - var tileDef = _tileDefManager["FloorSteel"]; - var tiles = new List<(Vector2i Index, Tile Tile)>(); var seed = _random.Next(); var random = new Random(seed); if (!_sectorWorld.TryGetDefaultSectorMap(out _, out var sector) || sector.Planets.Count == 0) return; - var hostPlanet = sector.Planets[random.Next(sector.Planets.Count)]; + var availablePlanets = sector.Planets + .Where(planet => _sectorWorld.TryGetPersistentMap(planet.PlanetTypeId, out _, out _, sector)) + .ToList(); + + if (availablePlanets.Count == 0) + return; + + var hostPlanet = availablePlanets[random.Next(availablePlanets.Count)]; if (!_sectorWorld.TryGetPersistentMap(hostPlanet.PlanetTypeId, out var hostMapUid, out _)) return; @@ -128,7 +131,7 @@ private void GenerateDestination(EntityUid uid, GatewayGeneratorComponent? gener return; } - var gatewayName = _salvage.GetFTLName(_protoManager.Index(PlanetNamesId), seed); + var gatewayName = placement.Planet.Name; _metadata.SetEntityName(gatewayUid, gatewayName); _xform.SetCoordinates(gatewayUid, new EntityCoordinates(hostGridUid, placement.Center)); @@ -138,28 +141,11 @@ private void GenerateDestination(EntityUid uid, GatewayGeneratorComponent? gener site.Center = placement.Center; site.Radius = placement.ReservationRadius; - _sectorWorld.CaptureHostedSiteBaseline((gatewayUid, site), hostGridUid, grid, placement.Center, placement.ReservationRadius + 32f); - - var biome = EnsureComp(hostGridUid); - var biomeTemplate = string.IsNullOrWhiteSpace(hostPlanet.BiomeTemplate) - ? ContinentalId - : new ProtoId(hostPlanet.BiomeTemplate); - _biome.SetTemplate(hostGridUid, biome, _protoManager.Index(biomeTemplate)); - _biome.SetSeed(hostGridUid, biome, seed); + EnsureComp(gatewayUid); + _worldController.SetLoaderEnabled(gatewayUid, false); var origin = placement.Center.Floored(); - for (var x = -2; x <= 2; x++) - { - for (var y = -2; y <= 2; y++) - { - tiles.Add((new Vector2i(x, y) + origin, new Tile(tileDef.TileId, variant: _tile.PickVariant((ContentTileDefinition) tileDef, random)))); - } - } - - // Clear area nearby as a sort of landing pad. - _maps.SetTiles(hostGridUid, grid, tiles); - var genDest = AddComp(gatewayUid); genDest.Origin = origin; genDest.Seed = seed; @@ -199,9 +185,17 @@ private void OnGeneratorOpen(Entity ent, r } var xform = Transform(ent.Owner); - if (xform.GridUid is not { } gridUid || !TryComp(gridUid, out MapGridComponent? grid)) + var gridUid = xform.GridUid ?? xform.MapUid; + if (gridUid is not { } resolvedGridUid || !TryComp(resolvedGridUid, out MapGridComponent? grid)) return; + if (TryComp(ent.Owner, out var siteComp)) + { + PrepareDestinationSite(ent, (ent.Owner, siteComp), xform.MapUid ?? resolvedGridUid, resolvedGridUid, grid); + var loadRadius = (int) MathF.Ceiling((siteComp.ContentRadius > 0f ? siteComp.ContentRadius : siteComp.Radius) + WorldGen.ChunkSize); + _worldController.EnsureChunksLoaded(xform.MapUid ?? resolvedGridUid, siteComp.Center, loadRadius, ent.Owner); + } + ent.Comp.Locked = false; ent.Comp.Loaded = true; @@ -209,7 +203,7 @@ private void OnGeneratorOpen(Entity ent, r var seed = ent.Comp.Seed; var origin = ent.Comp.Origin; var dungeonPosition = origin; - _ = FinishGeneratorOpenAsync(ent, gridUid, grid, xform.MapUid ?? gridUid, dungeonPosition, seed, generatorComp); + _ = FinishGeneratorOpenAsync(ent, resolvedGridUid, grid, xform.MapUid ?? resolvedGridUid, dungeonPosition, seed, generatorComp); } private async Task FinishGeneratorOpenAsync( @@ -259,4 +253,40 @@ private async Task FinishGeneratorOpenAsync( _sectorWorld.CaptureHostedSiteGeneratedEntities((ent.Owner, siteComp), hostMapUid, siteComp.Center, siteComp.ContentRadius > 0f ? siteComp.ContentRadius : siteComp.Radius); } } + + private void PrepareDestinationSite( + Entity ent, + Entity site, + EntityUid hostMapUid, + EntityUid hostGridUid, + MapGridComponent grid) + { + if (site.Comp.HostGridUid != EntityUid.Invalid) + return; + + var captureRadius = site.Comp.Radius + 32f; + var loadRadius = (int) MathF.Ceiling(captureRadius + WorldGen.ChunkSize); + _worldController.SetLoaderRadius(ent.Owner, loadRadius); + _worldController.SetLoaderEnabled(ent.Owner, true); + _worldController.EnsureChunksLoaded(hostMapUid, site.Comp.Center, loadRadius, ent.Owner); + _sectorWorld.CaptureHostedSiteBaseline(site, hostGridUid, grid, site.Comp.Center, captureRadius); + StampLandingPad(hostGridUid, grid, ent.Comp.Origin, ent.Comp.Seed); + } + + private void StampLandingPad(EntityUid hostGridUid, MapGridComponent grid, Vector2i origin, int seed) + { + var tileDef = _tileDefManager["FloorSteel"]; + var random = new Random(seed); + var tiles = new List<(Vector2i Index, Tile Tile)>(25); + + for (var x = -2; x <= 2; x++) + { + for (var y = -2; y <= 2; y++) + { + tiles.Add((new Vector2i(x, y) + origin, new Tile(tileDef.TileId, variant: _tile.PickVariant((ContentTileDefinition) tileDef, random)))); + } + } + + _maps.SetTiles(hostGridUid, grid, tiles); + } } diff --git a/Content.Server/Salvage/SpawnSalvageMissionJob.cs b/Content.Server/Salvage/SpawnSalvageMissionJob.cs index e5b9ffb7b74..9422b22fd60 100644 --- a/Content.Server/Salvage/SpawnSalvageMissionJob.cs +++ b/Content.Server/Salvage/SpawnSalvageMissionJob.cs @@ -18,6 +18,7 @@ using Content.Shared.Dataset; using Content.Shared.Gravity; using Content.Shared.Parallax.Biomes; +using Content.Shared.Parallax.Biomes.Markers; using Content.Shared.Physics; using Content.Shared.Procedural; using Content.Shared.Procedural.Loot; @@ -30,6 +31,7 @@ using Robust.Shared.Collections; using Robust.Shared.Map; using Robust.Shared.Map.Components; +using Robust.Shared.Maths; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Timing; @@ -271,8 +273,10 @@ private async Task InternalProcess() // Frontier: make process an internal var captureRadius = placement.ReservationRadius + 32f; _entManager.EnsureComponent(mapUid); var worldController = _entManager.System(); - worldController.SetLoaderRadius(mapUid, (int) MathF.Ceiling(captureRadius + WorldGen.ChunkSize)); + var loadRadius = (int) MathF.Ceiling(captureRadius + WorldGen.ChunkSize); + worldController.SetLoaderRadius(mapUid, loadRadius); worldController.SetLoaderEnabled(mapUid, true); + worldController.EnsureChunksLoaded(hostMapUid, placement.Center, loadRadius, mapUid); _sectorWorld.CaptureHostedSiteBaseline((mapUid, site), hostGridUid, grid, placement.Center, captureRadius); CaptureOriginalTiles(expedition, hostGridUid, grid, placement.Center, captureRadius); @@ -398,7 +402,7 @@ private async Task InternalProcess() // Frontier: make process an internal try { - await SpawnDungeonLoot(lootProto, hostGridUid); + await SpawnDungeonLoot(lootProto, (hostGridUid, grid), dungeon, random); } catch (Exception e) { @@ -592,7 +596,7 @@ private async Task SpawnRandomEntry(Entity grid, IBudgetEntry // oh noooooooooooo } - private async Task SpawnDungeonLoot(SalvageLootPrototype loot, EntityUid gridUid) + private async Task SpawnDungeonLoot(SalvageLootPrototype loot, Entity grid, Dungeon dungeon, Random random) { for (var i = 0; i < loot.LootRules.Count; i++) { @@ -602,17 +606,14 @@ private async Task SpawnDungeonLoot(SalvageLootPrototype loot, EntityUid gridUid { case BiomeMarkerLoot biomeLoot: { - if (_entManager.TryGetComponent(gridUid, out var biome)) - { - _biome.AddMarkerLayer(gridUid, biome, biomeLoot.Prototype); - } + await SpawnDungeonMarkerLoot(grid, dungeon, biomeLoot.Prototype, new Random(_missionParams.Seed + i)); } break; case BiomeTemplateLoot biomeLoot: { - if (_entManager.TryGetComponent(gridUid, out var biome)) + if (_entManager.TryGetComponent(grid.Owner, out var biome)) { - _biome.AddTemplate(gridUid, biome, "Loot", _prototypeManager.Index(biomeLoot.Prototype), i); + _biome.AddTemplate(grid.Owner, biome, "Loot", _prototypeManager.Index(biomeLoot.Prototype), i); } } break; @@ -620,6 +621,138 @@ private async Task SpawnDungeonLoot(SalvageLootPrototype loot, EntityUid gridUid } } + private async Task SpawnDungeonMarkerLoot(Entity grid, Dungeon dungeon, string markerId, Random random) + { + if (!_prototypeManager.TryIndex(markerId, out var markerTemplate)) + return; + + Box2i? bounds = null; + foreach (var tile in dungeon.RoomTiles) + { + bounds = bounds == null ? new Box2i(tile, tile + Vector2i.One) : bounds.Value.UnionTile(tile); + } + + if (bounds == null) + return; + + var availableTiles = new List(); + var replaceEntities = new Dictionary(); + + for (var x = bounds.Value.Left; x < bounds.Value.Right; x++) + { + for (var y = bounds.Value.Bottom; y < bounds.Value.Top; y++) + { + var tile = new Vector2i(x, y); + + if (!_map.TryGetTileRef(grid.Owner, grid.Comp, tile, out var tileRef) || tileRef.Tile.IsEmpty) + continue; + + var enumerator = _map.GetAnchoredEntitiesEnumerator(grid.Owner, grid.Comp, tile); + + if (markerTemplate.EntityMask.Count > 0) + { + var found = false; + var blocked = false; + + while (enumerator.MoveNext(out var uid)) + { + var prototype = _entManager.GetComponent(uid.Value).EntityPrototype?.ID; + if (prototype == null) + continue; + + if (markerTemplate.EntityMask.ContainsKey(prototype)) + { + if (!found) + replaceEntities[tile] = uid.Value; + + found = true; + continue; + } + + blocked = true; + break; + } + + if (!found || blocked) + continue; + } + else + { + if (!_anchorable.TileFree(grid, tile, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask)) + continue; + + if (enumerator.MoveNext(out _)) + continue; + } + + availableTiles.Add(tile); + } + } + + if (availableTiles.Count == 0) + return; + + var area = Math.Max(bounds.Value.Area, 1); + var count = Math.Min(markerTemplate.MaxCount, Math.Max(1, (int) (area / Math.Max(markerTemplate.Radius * markerTemplate.Radius, 1f)))); + var frontier = new ValueList(32); + + for (var i = 0; i < count; i++) + { + await SuspendIfOutOfTime(); + + var groupSize = random.Next(markerTemplate.MinGroupSize, markerTemplate.MaxGroupSize + 1); + + while (groupSize > 0 && availableTiles.Count > 0) + { + var startNode = availableTiles.RemoveSwap(random.Next(availableTiles.Count)); + frontier.Clear(); + frontier.Add(startNode); + + while (frontier.Count > 0 && groupSize > 0) + { + var frontierIndex = random.Next(frontier.Count); + var node = frontier[frontierIndex]; + frontier.RemoveSwap(frontierIndex); + availableTiles.Remove(node); + + for (var x = -1; x <= 1; x++) + { + for (var y = -1; y <= 1; y++) + { + var neighbor = new Vector2i(node.X + x, node.Y + y); + + if (frontier.Contains(neighbor) || !availableTiles.Contains(neighbor)) + continue; + + frontier.Add(neighbor); + } + } + + string? prototype = markerTemplate.Prototype; + + if (replaceEntities.TryGetValue(node, out var existingEnt)) + { + var existingProto = _entManager.GetComponent(existingEnt).EntityPrototype?.ID; + _entManager.DeleteEntity(existingEnt); + + if (existingProto != null && markerTemplate.EntityMask.TryGetValue(existingProto, out var remapped)) + prototype = remapped; + } + + if (!string.IsNullOrEmpty(prototype)) + { + var spawned = _entManager.SpawnAtPosition(prototype, _map.GridTileToLocal(grid.Owner, grid.Comp, node)); + var xform = _entManager.GetComponent(spawned); + if (!xform.Anchored) + _entManager.System().AnchorEntity(spawned, xform); + } + + groupSize--; + } + } + } + } + // Frontier: mission-specific setup functions private async Task SetupStructure( SalvageMission mission, diff --git a/Content.Server/Worldgen/Components/WorldLoaderComponent.cs b/Content.Server/Worldgen/Components/WorldLoaderComponent.cs index 95f76866956..348e92b0d53 100644 --- a/Content.Server/Worldgen/Components/WorldLoaderComponent.cs +++ b/Content.Server/Worldgen/Components/WorldLoaderComponent.cs @@ -14,7 +14,7 @@ public sealed partial class WorldLoaderComponent : Component /// [Access(typeof(WorldControllerSystem), typeof(SectorWorldSystem))] [ViewVariables(VVAccess.ReadWrite)] [DataField("radius")] - public int Radius = 64; + public int Radius = 24; /// /// Frontier: if true, this loader is disabled, and will not be used diff --git a/Content.Server/Worldgen/Systems/SectorWorldSystem.cs b/Content.Server/Worldgen/Systems/SectorWorldSystem.cs index d732a253d4b..7fd8ecfee4e 100644 --- a/Content.Server/Worldgen/Systems/SectorWorldSystem.cs +++ b/Content.Server/Worldgen/Systems/SectorWorldSystem.cs @@ -6,6 +6,8 @@ using Content.Server.Atmos.EntitySystems; using Content.Server.GameTicking; using Content.Server.Parallax; +using Content.Server.Shuttles.Components; +using Content.Server.Shuttles.Systems; using Content.Server.Weather; using Content.Server.Worldgen.Components; using Content.Shared.GameTicking; @@ -25,6 +27,7 @@ using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Utility; +using Content.Server.Worldgen; namespace Content.Server.Worldgen.Systems; @@ -43,9 +46,11 @@ public sealed class SectorWorldSystem : EntitySystem [Dependency] private readonly MetaDataSystem _metaData = default!; [Dependency] private readonly SharedMapSystem _mapSystem = default!; [Dependency] private readonly PhysicsSystem _physics = default!; + [Dependency] private readonly ShuttleSystem _shuttle = default!; [Dependency] private readonly ITileDefinitionManager _tileDefs = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly WeatherSystem _weather = default!; + [Dependency] private readonly WorldControllerSystem _worldController = default!; private static readonly string[] TimeOfDayStates = ["Dawn", "Day", "Dusk", "Night"]; private bool _roundRestartCleanupActive; @@ -844,16 +849,48 @@ private void EnsureInitialized(Entity ent) private void EnsureStartupPlanetLoaders(Entity ent) { - if (ent.Comp.StartupLoaders.Count == 0) - return; + var desiredMaps = new HashSet(); + + if (ent.Comp.FtlMap is { } ftlMap && Exists(ftlMap)) + desiredMaps.Add(ftlMap); + + foreach (var mapUid in ent.Comp.PlanetTypeMaps.Values) + { + if (Exists(mapUid)) + desiredMaps.Add(mapUid); + } + + var retainedLoaders = new List(desiredMaps.Count); foreach (var loader in ent.Comp.StartupLoaders) { - if (Exists(loader)) + if (!Exists(loader)) + continue; + + var xform = Transform(loader); + if (xform.MapUid is not { } mapUid || !desiredMaps.Remove(mapUid)) + { QueueDel(loader); + continue; + } + + _worldController.SetLoaderRadius(loader, WorldGen.ChunkSize * 2); + _worldController.SetLoaderEnabled(loader, true); + retainedLoaders.Add(loader); + } + + foreach (var mapUid in desiredMaps) + { + var loader = Spawn(null, new MapCoordinates(Vector2.Zero, Comp(mapUid).MapId)); + EnsureComp(loader); + EnsureComp(loader); + _worldController.SetLoaderRadius(loader, WorldGen.ChunkSize * 2); + _worldController.SetLoaderEnabled(loader, true); + retainedLoaders.Add(loader); } ent.Comp.StartupLoaders.Clear(); + ent.Comp.StartupLoaders.AddRange(retainedLoaders); } private void EnsurePersistentLayerMaps(Entity ent) @@ -885,15 +922,19 @@ private void EnsurePersistentLayerMaps(Entity ent) continue; } + var planetType = ent.Comp.PlanetTypes.FirstOrDefault(type => type.Id == planet.PlanetTypeId); + ent.Comp.PlanetTypeMaps[planet.PlanetTypeId] = CreateLayerMap( $"{planet.Name} Surface", space: false, gravity: true, + createFtlDestination: true, mixture: CreatePlanetMixture(planet), timeOfDay: planet.TimeOfDay, weatherPrototype: planet.WeatherPrototype, biomeTemplateId: planet.BiomeTemplate, - biomeSeed: planet.Seed); + biomeSeed: planet.Seed, + hiddenSurfaceTileIds: planetType?.SurfaceTiles); EnsurePersistentWorldGrid(ent.Comp.PlanetTypeMaps[planet.PlanetTypeId]); } @@ -945,15 +986,18 @@ private EntityUid CreateLayerMap( string name, bool space, bool gravity, + bool createFtlDestination = false, GasMixture? mixture = null, string? timeOfDay = null, string? weatherPrototype = null, string? biomeTemplateId = null, - int? biomeSeed = null) + int? biomeSeed = null, + IReadOnlyList? hiddenSurfaceTileIds = null) { var mapUid = _mapSystem.CreateMap(out _); EnsureComp(mapUid); EnsureComp(mapUid); + EnsureComp(mapUid); _metaData.SetEntityName(mapUid, name); if (!space && !string.IsNullOrWhiteSpace(biomeTemplateId) && _proto.TryIndex(biomeTemplateId, out var biomeTemplate)) @@ -977,6 +1021,20 @@ private EntityUid CreateLayerMap( EnsureComp(mapUid); EnsureComp(mapUid); + if (hiddenSurfaceTileIds is { Count: > 0 }) + { + var mask = EnsureComp(mapUid); + mask.HiddenTileIds.Clear(); + mask.HiddenTileIds.AddRange(hiddenSurfaceTileIds); + Dirty(mapUid, mask); + } + + if (createFtlDestination && TryComp(mapUid, out var ftlMapComp)) + { + _shuttle.TryAddFTLDestination(ftlMapComp.MapId, true, requireDisk: false, beaconsOnly: false, out _); + EnsureComp(mapUid); + } + var resolvedWeatherPrototype = ResolveWeatherPrototype(weatherPrototype); if (!string.IsNullOrWhiteSpace(resolvedWeatherPrototype) && diff --git a/Content.Server/Worldgen/Systems/WorldControllerSystem.cs b/Content.Server/Worldgen/Systems/WorldControllerSystem.cs index 0c7131220ed..fbb0bb73932 100644 --- a/Content.Server/Worldgen/Systems/WorldControllerSystem.cs +++ b/Content.Server/Worldgen/Systems/WorldControllerSystem.cs @@ -1,4 +1,5 @@ using System.Linq; +using System.Numerics; using Content.Server.Worldgen.Components; using Content.Shared.Ghost; using Content.Shared.Mind.Components; @@ -52,6 +53,30 @@ public void SetLoaderEnabled(EntityUid uid, bool enabled, WorldLoaderComponent? Dirty(uid, loader); } + public void EnsureChunksLoaded(EntityUid mapUid, Vector2 worldPos, int radius, EntityUid? loaderUid = null, WorldControllerComponent? controller = null) + { + if (!Resolve(mapUid, ref controller)) + return; + + var loadedQuery = GetEntityQuery(); + var coords = WorldGen.WorldToChunkCoords(worldPos); + var chunkRadius = (int) Math.Ceiling(radius / (float) WorldGen.ChunkSize) + 1; + var chunks = new GridPointsNearEnumerator(coords.Floored(), chunkRadius); + + while (chunks.MoveNext(out var chunk)) + { + var ent = GetOrCreateChunk(chunk.Value, mapUid, controller); + if (ent is not { } chunkUid) + continue; + + if (!loadedQuery.TryGetComponent(chunkUid, out var loaded)) + loaded = AddComp(chunkUid); + + if (loaderUid is { } loader && Exists(loader)) + loaded.Loaders = [loader]; + } + } + /// /// Handles deleting chunks properly. /// diff --git a/Content.Shared/Shuttles/Components/RadarTileMaskComponent.cs b/Content.Shared/Shuttles/Components/RadarTileMaskComponent.cs new file mode 100644 index 00000000000..d3baeb12280 --- /dev/null +++ b/Content.Shared/Shuttles/Components/RadarTileMaskComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Shuttles.Components; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class RadarTileMaskComponent : Component +{ + [DataField, AutoNetworkedField] + public List HiddenTileIds = new(); +} \ No newline at end of file