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.Shuttles.Components; 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; using Content.Server.Shuttles.Components; 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; private readonly SharedTransformSystem _xforms; private readonly SharedMapSystem _map; public readonly EntityUid Station; public readonly EntityUid? CoordinatesDisk; 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, SharedTransformSystem xform, SharedMapSystem map, EntityUid station, EntityUid? coordinatesDisk, SalvageMissionParams missionParams, CancellationToken cancellation = default) : base(maxTime, cancellation) { _entManager = entManager; _timing = timing; _mapManager = mapManager; _prototypeManager = protoManager; _anchorable = anchorable; _biome = biome; _dungeon = dungeon; _metaData = metaData; _xforms = xform; _map = map; Station = station; CoordinatesDisk = coordinatesDisk; _missionParams = missionParams; _sawmill = logManager.GetSawmill("salvage_job"); #if !DEBUG _sawmill.Level = LogLevel.Info; #endif } protected override async Task Process() { _sawmill.Debug("salvage", $"Spawning salvage mission with seed {_missionParams.Seed}"); var mapUid = _map.CreateMap(out var mapId, runMapInit: false); MetaDataComponent? metadata = null; var grid = _entManager.EnsureComponent(mapUid); var random = new Random(_missionParams.Seed); var destComp = _entManager.AddComponent(mapUid); destComp.BeaconsOnly = true; destComp.RequireCoordinateDisk = true; destComp.Enabled = true; _metaData.SetEntityName( mapUid, _entManager.System().GetFTLName(_prototypeManager.Index("NamesBorer"), _missionParams.Seed)); _entManager.AddComponent(mapUid); // Saving the mission mapUid to a CD is made optional, in case one is somehow made in a process without a CD entity if (CoordinatesDisk.HasValue) { var cd = _entManager.EnsureComponent(CoordinatesDisk.Value); cd.Destination = 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. 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(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); // 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); } } _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; 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 dungeons = await WaitAsyncTask(_dungeon.GenerateDungeonAsync(dungeonConfig, mapUid, grid, (Vector2i) dungeonOffset, _missionParams.Seed)); var dungeon = dungeons.First(); // Aborty if (dungeon.Rooms.Count == 0) { return false; } expedition.DungeonLocation = dungeonOffset; List reservedTiles = new(); foreach (var tile in _map.GetTilesIntersecting(mapUid, grid, 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(lootProto, mapUid); } // 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(SalvageLootPrototype loot, EntityUid gridUid) { 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)) { _biome.AddMarkerLayer(gridUid, biome, biomeLoot.Prototype); } } break; case BiomeTemplateLoot biomeLoot: { if (_entManager.TryGetComponent(gridUid, out var biome)) { _biome.AddTemplate(gridUid, biome, "Loot", _prototypeManager.Index(biomeLoot.Prototype), i); } } break; } } } }