using System.Collections; using System.Linq; using System.Numerics; using System.Threading; using System.Threading.Tasks; using Content.Server.Atmos; using Content.Server.Atmos.Components; using Content.Server.Atmos.EntitySystems; using Robust.Shared.CPUJob.JobQueues; using Content.Server.Ghost.Roles.Components; using Content.Server.Parallax; using Content.Server.Procedural; using Content.Server.Salvage.Expeditions; using Content.Server.Salvage.Expeditions.Structure; using Content.Shared.Atmos; using Content.Shared.Construction.EntitySystems; using Content.Shared.Dataset; using Content.Shared.Gravity; using Content.Shared.Parallax.Biomes; using Content.Shared.Physics; using Content.Shared.Procedural; using Content.Shared.Procedural.Loot; using Content.Shared.Random; using Content.Shared.Salvage; using Content.Shared.Salvage.Expeditions; using Content.Shared.Salvage.Expeditions.Modifiers; using Content.Shared.Storage; using Robust.Shared.Collections; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Content.Server.Salvage; public sealed class SpawnSalvageMissionJob : Job { private readonly IEntityManager _entManager; private readonly IGameTiming _timing; private readonly IMapManager _mapManager; private readonly IPrototypeManager _prototypeManager; private readonly AnchorableSystem _anchorable; private readonly BiomeSystem _biome; private readonly DungeonSystem _dungeon; private readonly MetaDataSystem _metaData; public readonly EntityUid Station; private readonly SalvageMissionParams _missionParams; private readonly ISawmill _sawmill; public SpawnSalvageMissionJob( double maxTime, IEntityManager entManager, IGameTiming timing, ILogManager logManager, IMapManager mapManager, IPrototypeManager protoManager, AnchorableSystem anchorable, BiomeSystem biome, DungeonSystem dungeon, MetaDataSystem metaData, EntityUid station, SalvageMissionParams missionParams, CancellationToken cancellation = default) : base(maxTime, cancellation) { _entManager = entManager; _timing = timing; _mapManager = mapManager; _prototypeManager = protoManager; _anchorable = anchorable; _biome = biome; _dungeon = dungeon; _metaData = metaData; Station = station; _missionParams = missionParams; _sawmill = logManager.GetSawmill("salvage_job"); #if !DEBUG _sawmill.Level = LogLevel.Info; #endif } protected override async Task Process() { _sawmill.Debug("salvage", $"Spawning salvage mission with seed {_missionParams.Seed}"); var mapId = _mapManager.CreateMap(); var mapUid = _mapManager.GetMapEntityId(mapId); _mapManager.AddUninitializedMap(mapId); MetaDataComponent? metadata = null; var grid = _entManager.EnsureComponent(mapUid); var random = new Random(_missionParams.Seed); // Setup mission configs // As we go through the config the rating will deplete so we'll go for most important to least important. var difficultyId = "Moderate"; var difficultyProto = _prototypeManager.Index(difficultyId); var mission = _entManager.System() .GetMission(difficultyProto, _missionParams.Seed); var missionBiome = _prototypeManager.Index(mission.Biome); if (missionBiome.BiomePrototype != null) { var biome = _entManager.AddComponent(mapUid); var biomeSystem = _entManager.System(); biomeSystem.SetTemplate(biome, _prototypeManager.Index(missionBiome.BiomePrototype)); biomeSystem.SetSeed(biome, mission.Seed); _entManager.Dirty(biome); // Gravity var gravity = _entManager.EnsureComponent(mapUid); gravity.Enabled = true; _entManager.Dirty(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(2500) { Temperature = mission.Temperature, Moles = moles, }, atmos); if (mission.Color != null) { var lighting = _entManager.EnsureComponent(mapUid); lighting.AmbientLightColor = mission.Color.Value; _entManager.Dirty(mapUid, lighting); } } _mapManager.DoMapInitialize(mapId); _mapManager.SetMapPaused(mapId, true); // Setup expedition var expedition = _entManager.AddComponent(mapUid); expedition.Station = Station; expedition.EndTime = _timing.CurTime + mission.Duration; expedition.MissionParams = _missionParams; // Don't want consoles to have the incorrect name until refreshed. var ftlUid = _entManager.CreateEntityUninitialized("FTLPoint", new EntityCoordinates(mapUid, grid.TileSizeHalfVector)); _metaData.SetEntityName(ftlUid, SharedSalvageSystem.GetFTLName(_prototypeManager.Index("names_borer"), _missionParams.Seed)); _entManager.InitializeAndStartEntity(ftlUid); var landingPadRadius = 24; var minDungeonOffset = landingPadRadius + 4; // We'll use the dungeon rotation as the spawn angle var dungeonRotation = _dungeon.GetDungeonRotation(_missionParams.Seed); var maxDungeonOffset = minDungeonOffset + 12; var dungeonOffsetDistance = minDungeonOffset + (maxDungeonOffset - minDungeonOffset) * random.NextFloat(); var dungeonOffset = new Vector2(0f, dungeonOffsetDistance); dungeonOffset = dungeonRotation.RotateVec(dungeonOffset); var dungeonMod = _prototypeManager.Index(mission.Dungeon); var dungeonConfig = _prototypeManager.Index(dungeonMod.Proto); var dungeon = await WaitAsyncTask(_dungeon.GenerateDungeonAsync(dungeonConfig, mapUid, grid, (Vector2i) dungeonOffset, _missionParams.Seed)); // Aborty if (dungeon.Rooms.Count == 0) { return false; } expedition.DungeonLocation = dungeonOffset; List reservedTiles = new(); foreach (var tile in grid.GetTilesIntersecting(new Circle(Vector2.Zero, landingPadRadius), false)) { if (!_biome.TryGetBiomeTile(mapUid, grid, tile.GridIndices, out _)) continue; reservedTiles.Add(tile.GridIndices); } var budgetEntries = new List(); /* * GUARANTEED LOOT */ // We'll always add this loot if possible // mainly used for ore layers. foreach (var lootProto in _prototypeManager.EnumeratePrototypes()) { if (!lootProto.Guaranteed) continue; await SpawnDungeonLoot(dungeon, missionBiome, lootProto, mapUid, grid, random, reservedTiles); } // Handle boss loot (when relevant). // Handle mob loot. // Handle remaining loot /* * MOB SPAWNS */ var mobBudget = difficultyProto.MobBudget; var faction = _prototypeManager.Index(mission.Faction); var randomSystem = _entManager.System(); foreach (var entry in faction.MobGroups) { budgetEntries.Add(entry); } var probSum = budgetEntries.Sum(x => x.Prob); while (mobBudget > 0f) { var entry = randomSystem.GetBudgetEntry(ref mobBudget, ref probSum, budgetEntries, random); if (entry == null) break; await SpawnRandomEntry(grid, entry, dungeon, random); } var allLoot = _prototypeManager.Index(SharedSalvageSystem.ExpeditionsLootProto); var lootBudget = difficultyProto.LootBudget; foreach (var rule in allLoot.LootRules) { switch (rule) { case RandomSpawnsLoot randomLoot: budgetEntries.Clear(); foreach (var entry in randomLoot.Entries) { budgetEntries.Add(entry); } probSum = budgetEntries.Sum(x => x.Prob); while (lootBudget > 0f) { var entry = randomSystem.GetBudgetEntry(ref lootBudget, ref probSum, budgetEntries, random); if (entry == null) break; _sawmill.Debug($"Spawning dungeon loot {entry.Proto}"); await SpawnRandomEntry(grid, entry, dungeon, random); } break; default: throw new NotImplementedException(); } } return true; } private async Task SpawnRandomEntry(MapGridComponent grid, IBudgetEntry entry, Dungeon dungeon, Random random) { await SuspendIfOutOfTime(); var availableRooms = new ValueList(dungeon.Rooms); var availableTiles = new List(); while (availableRooms.Count > 0) { availableTiles.Clear(); var roomIndex = random.Next(availableRooms.Count); var room = availableRooms.RemoveSwap(roomIndex); availableTiles.AddRange(room.Tiles); while (availableTiles.Count > 0) { var tile = availableTiles.RemoveSwap(random.Next(availableTiles.Count)); if (!_anchorable.TileFree(grid, tile, (int) CollisionGroup.MachineLayer, (int) CollisionGroup.MachineLayer)) { continue; } var uid = _entManager.SpawnAtPosition(entry.Proto, grid.GridTileToLocal(tile)); _entManager.RemoveComponent(uid); _entManager.RemoveComponent(uid); return; } } // oh noooooooooooo } private async Task SpawnDungeonLoot(Dungeon dungeon, SalvageBiomeModPrototype biomeMod, SalvageLootPrototype loot, EntityUid gridUid, MapGridComponent grid, Random random, List reservedTiles) { for (var i = 0; i < loot.LootRules.Count; i++) { var rule = loot.LootRules[i]; switch (rule) { case BiomeMarkerLoot biomeLoot: { if (_entManager.TryGetComponent(gridUid, out var biome) && biomeLoot.Prototype.TryGetValue(biomeMod.ID, out var mod)) { _biome.AddMarkerLayer(biome, mod); } } break; case BiomeTemplateLoot biomeLoot: { if (_entManager.TryGetComponent(gridUid, out var biome)) { _biome.AddTemplate(biome, "Loot", _prototypeManager.Index(biomeLoot.Prototype), i); } } break; } } } }