using Content.Server.Administration.Logs; using Content.Server.Atmos.EntitySystems; using Content.Server.Chat.Managers; using Content.Server.GameTicking; using Content.Shared.Database; using Content.Shared.Maps; using Content.Shared.Physics; using Content.Shared.Respawn; using Content.Shared.Station.Components; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Prototypes; using Robust.Shared.Random; namespace Content.Server.Respawn; public sealed class SpecialRespawnSystem : SharedSpecialRespawnSystem { [Dependency] private readonly IAdminLogManager _adminLog = default!; [Dependency] private readonly AtmosphereSystem _atmosphere = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly SharedMapSystem _map = default!; [Dependency] private readonly TurfSystem _turf = default!; [Dependency] private readonly IChatManager _chat = default!; [Dependency] private readonly IPrototypeManager _proto = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnRunLevelChanged); SubscribeLocalEvent(OnSpecialRespawnSetup); SubscribeLocalEvent(OnStartup); SubscribeLocalEvent(OnTermination); } private void OnRunLevelChanged(GameRunLevelChangedEvent ev) { //Try to compensate for restartroundnow command if (ev.Old == GameRunLevel.InRound && ev.New == GameRunLevel.PreRoundLobby) OnRoundEnd(); switch (ev.New) { case GameRunLevel.PostRound: OnRoundEnd(); break; } } private void OnSpecialRespawnSetup(SpecialRespawnSetupEvent ev) { if (!TryComp(ev.Entity, out var comp)) return; var xform = Transform(ev.Entity); if (xform.GridUid != null) comp.StationMap = (xform.MapUid, xform.GridUid); } private void OnRoundEnd() { var specialRespawnQuery = EntityQuery(); //Turn respawning off so the entity doesn't respawn during reset foreach (var entity in specialRespawnQuery) { entity.Respawn = false; } } private void OnStartup(EntityUid uid, SpecialRespawnComponent component, ComponentStartup args) { var ev = new SpecialRespawnSetupEvent(uid); QueueLocalEvent(ev); } private void OnTermination(EntityUid uid, SpecialRespawnComponent component, ref EntityTerminatingEvent args) { var entityMapUid = component.StationMap.Item1; var entityGridUid = component.StationMap.Item2; if (!component.Respawn || !HasComp(entityGridUid) || entityMapUid == null) return; if (!TryComp(entityGridUid, out var grid) || MetaData(entityGridUid.Value).EntityLifeStage >= EntityLifeStage.Terminating) return; //Invalid prototype if (!_proto.HasIndex(component.Prototype)) return; if (TryFindRandomTile(entityGridUid.Value, entityMapUid.Value, 10, out var coords)) Respawn(uid, component.Prototype, coords); //If the above fails, spawn at the center of the grid on the station else { var xform = Transform(entityGridUid.Value); var pos = xform.Coordinates; var mapPos = _transform.GetMapCoordinates(entityGridUid.Value, xform: xform); var circle = new Circle(mapPos.Position, 2); var found = false; foreach (var tile in _map.GetTilesIntersecting(entityGridUid.Value, grid, circle)) { if (_turf.IsSpace(tile) || _turf.IsTileBlocked(tile, CollisionGroup.MobMask) || !_atmosphere.IsTileMixtureProbablySafe(entityGridUid, entityMapUid.Value, _map.TileIndicesFor((entityGridUid.Value, grid), mapPos))) { continue; } pos = _turf.GetTileCenter(tile); found = true; if (found) break; } Respawn(uid, component.Prototype, pos); } } /// /// Respawn the entity and log it. /// /// The entity being deleted /// The prototype being spawned /// The place where it will be spawned private void Respawn(EntityUid oldEntity, string prototype, EntityCoordinates coords) { var entity = Spawn(prototype, coords); _adminLog.Add(LogType.Respawn, LogImpact.Extreme, $"{ToPrettyString(oldEntity)} was deleted and was respawned at {_transform.ToMapCoordinates(coords)} as {ToPrettyString(entity)}"); _chat.SendAdminAlert($"{MetaData(oldEntity).EntityName} was deleted and was respawned as {ToPrettyString(entity)}"); } /// /// Try to find a random safe tile on the supplied grid /// /// The grid that you're looking for a safe tile on /// The map that you're looking for a safe tile on /// The maximum amount of attempts it should try before it gives up /// If successful, the coordinates of the safe tile /// public bool TryFindRandomTile(EntityUid targetGrid, EntityUid targetMap, int maxAttempts, out EntityCoordinates targetCoords) { targetCoords = EntityCoordinates.Invalid; if (!TryComp(targetGrid, out var grid)) return false; var xform = Transform(targetGrid); if (!_map.TryGetTileRef(targetGrid, grid, xform.Coordinates, out var tileRef)) return false; var tile = tileRef.GridIndices; var found = false; var (gridPos, _, gridMatrix) = _transform.GetWorldPositionRotationMatrix(xform); var gridBounds = gridMatrix.TransformBox(grid.LocalAABB); //Obviously don't put anything ridiculous in here for (var i = 0; i < maxAttempts; i++) { var randomX = _random.Next((int)gridBounds.Left, (int)gridBounds.Right); var randomY = _random.Next((int)gridBounds.Bottom, (int)gridBounds.Top); tile = new Vector2i(randomX - (int)gridPos.X, randomY - (int)gridPos.Y); var mapPos = _map.GridTileToWorldPos(targetGrid, grid, tile); var mapTarget = _map.WorldToTile(targetGrid, grid, mapPos); var circle = new Circle(mapPos, 2); foreach (var newTileRef in _map.GetTilesIntersecting(targetGrid, grid, circle)) { if (_turf.IsSpace(newTileRef) || _turf.IsTileBlocked(newTileRef, CollisionGroup.MobMask) || !_atmosphere.IsTileMixtureProbablySafe(targetGrid, targetMap, mapTarget)) continue; found = true; targetCoords = _map.GridTileToLocal(targetGrid, grid, tile); break; } //Found a safe tile, no need to continue if (found) break; } if (!found) return false; return true; } }