using System.Linq; using System.Numerics; using Content.Server.Administration.Logs; using Content.Server.Chat.Managers; using Content.Server.GameTicking; using Content.Server.Ghost.Components; using Content.Server.Mind; using Content.Server.Roles.Jobs; using Content.Server.Warps; using Content.Shared.Actions; using Content.Shared.Database; using Content.Shared.Examine; using Content.Shared.Eye; using Content.Shared.Follower; using Content.Shared.GameTicking; using Content.Shared.Ghost; using Content.Shared.Mind; using Content.Shared.Mind.Components; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; using Content.Shared.Movement.Events; using Content.Shared.Movement.Systems; using Content.Shared.Storage.Components; using Content.Shared._White; using Content.Shared._White.Administration; using JetBrains.Annotations; using Robust.Server.GameObjects; using Robust.Server.Player; using Content.Shared._White; using Content.Shared._White.Antag; using Content.Shared.Humanoid; using Content.Shared.Roles; using Content.Shared.SSDIndicator; using Robust.Shared.Configuration; using Robust.Shared.Map; using Robust.Shared.Network; using Robust.Shared.Map; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Systems; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.Manager.Exceptions; using Robust.Shared.Timing; using InvisibilityComponent = Content.Shared._White.Administration.InvisibilityComponent; namespace Content.Server.Ghost { public sealed class GhostSystem : SharedGhostSystem { [Dependency] private readonly SharedActionsSystem _actions = default!; [Dependency] private readonly SharedEyeSystem _eye = default!; [Dependency] private readonly FollowerSystem _followerSystem = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly JobSystem _jobs = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly MindSystem _minds = default!; [Dependency] private readonly SharedMindSystem _mindSystem = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly GameTicker _ticker = default!; [Dependency] private readonly TransformSystem _transformSystem = default!; [Dependency] private readonly VisibilitySystem _visibilitySystem = default!; [Dependency] private readonly MetaDataSystem _metaData = default!; [Dependency] private readonly IChatManager _chatManager = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; private EntityQuery _ghostQuery; private EntityQuery _physicsQuery; public override void Initialize() { base.Initialize(); _ghostQuery = GetEntityQuery(); _physicsQuery = GetEntityQuery(); SubscribeLocalEvent(OnGhostStartup); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnGhostShutdown); SubscribeLocalEvent(OnGhostExamine); SubscribeLocalEvent(OnMindRemovedMessage); SubscribeLocalEvent(OnMindUnvisitedMessage); SubscribeLocalEvent(OnPlayerDetached); SubscribeLocalEvent(OnRelayMoveInput); SubscribeNetworkEvent(OnGhostWarpsRequest); SubscribeNetworkEvent(OnGhostReturnToBodyRequest); SubscribeNetworkEvent(OnGhostWarpToTargetRequest); SubscribeNetworkEvent(OnGhostReturnToRoundRequest); SubscribeLocalEvent(OnActionPerform); SubscribeLocalEvent(OnGhostHearingAction); SubscribeLocalEvent(OnEntityStorageInsertAttempt); SubscribeLocalEvent(_ => MakeVisible(true)); SubscribeLocalEvent(ResetDeathTimes); } public readonly Dictionary _deathTime = new(); private void ResetDeathTimes(RoundRestartCleanupEvent ev) { _deathTime.Clear(); } private void OnGhostReturnToRoundRequest(GhostReturnToRoundRequest msg, EntitySessionEventArgs args) { var cfg = IoCManager.Resolve(); var maxPlayers = cfg.GetCVar(WhiteCVars.GhostRespawnMaxPlayers); if (_playerManager.PlayerCount >= maxPlayers) { var message = Loc.GetString("ghost-respawn-max-players", ("players", maxPlayers)); var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message)); _chatManager.ChatMessageToOne(Shared.Chat.ChatChannel.Server, message, wrappedMessage, default, false, args.SenderSession.ConnectedClient, Color.Red); return; } var userId = args.SenderSession.UserId; if (userId == null) return; if (!_deathTime.TryGetValue(userId, out var deathTime)) { var message = Loc.GetString("ghost-respawn-bug"); var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message)); _chatManager.ChatMessageToOne(Shared.Chat.ChatChannel.Server, message, wrappedMessage, default, false, args.SenderSession.ConnectedClient, Color.Red); _deathTime[userId] = _gameTiming.CurTime; return; } var timeUntilRespawn = (double) cfg.GetCVar(WhiteCVars.GhostRespawnTime); var timePast = (_gameTiming.CurTime - deathTime).TotalMinutes; if (timePast >= timeUntilRespawn) { var ticker = Get(); var playerMgr = IoCManager.Resolve(); playerMgr.TryGetSessionById(userId, out var targetPlayer); if (targetPlayer != null) ticker.Respawn(targetPlayer); _deathTime.Remove(userId); _adminLogger.Add(LogType.Mind, LogImpact.Extreme, $"{args.SenderSession.ConnectedClient.UserName} вернулся в лобби посредством гост респавна."); var message = Loc.GetString("ghost-respawn-window-rules-footer"); var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message)); _chatManager.ChatMessageToOne(Shared.Chat.ChatChannel.Server, message, wrappedMessage, default, false, args.SenderSession.ConnectedClient, Color.Red); } else { var message = Loc.GetString("ghost-respawn-time-left", ("time", (int) (timeUntilRespawn - timePast))); var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message)); _chatManager.ChatMessageToOne(Shared.Chat.ChatChannel.Server, message, wrappedMessage, default, false, args.SenderSession.ConnectedClient, Color.Red); } } private void OnGhostHearingAction(EntityUid uid, GhostComponent component, ToggleGhostHearingActionEvent args) { args.Handled = true; if (HasComp(uid)) { RemComp(uid); _actions.SetToggled(component.ToggleGhostHearingActionEntity, true); } else { AddComp(uid); _actions.SetToggled(component.ToggleGhostHearingActionEntity, false); } var str = HasComp(uid) ? Loc.GetString("ghost-gui-toggle-hearing-popup-on") : Loc.GetString("ghost-gui-toggle-hearing-popup-off"); Popup.PopupEntity(str, uid, uid); Dirty(uid, component); } public override void SetVisible(Entity ghost, bool visible) { if (!Resolve(ghost.Owner, ref ghost.Comp)) return; if (ghost.Comp.Visible == visible) return; base.SetVisible(ghost, visible); if (!TryComp(ghost.Owner, out VisibilityComponent? visibility)) return; if (visible) { _visibilitySystem.RemoveLayer((ghost.Owner, visibility), (int)VisibilityFlags.Ghost, false); _visibilitySystem.AddLayer((ghost.Owner, visibility), (int)VisibilityFlags.Normal, false); _visibilitySystem.RefreshVisibility(ghost.Owner, visibility); } else { _visibilitySystem.AddLayer((ghost.Owner, visibility), (int)VisibilityFlags.Ghost, false); _visibilitySystem.RemoveLayer((ghost.Owner, visibility), (int)VisibilityFlags.Normal, false); _visibilitySystem.RefreshVisibility(ghost.Owner, visibility); } } private void OnActionPerform(EntityUid uid, GhostComponent component, BooActionEvent args) { if (args.Handled) return; var entities = _lookup.GetEntitiesInRange(args.Performer, component.BooRadius); var booCounter = 0; foreach (var ent in entities) { var handled = DoGhostBooEvent(ent); if (handled) booCounter++; if (booCounter >= component.BooMaxTargets) break; } args.Handled = true; } private void OnRelayMoveInput(EntityUid uid, GhostOnMoveComponent component, ref MoveInputEvent args) { // If they haven't actually moved then ignore it. if ((args.Component.HeldMoveButtons & (MoveButtons.Down | MoveButtons.Left | MoveButtons.Up | MoveButtons.Right)) == 0x0) { return; } // Let's not ghost if our mind is visiting... if (HasComp(uid)) return; if (!_minds.TryGetMind(uid, out var mindId, out var mind) || mind.IsVisitingEntity) return; if (component.MustBeDead && (_mobState.IsAlive(uid) || _mobState.IsCritical(uid))) return; _ticker.OnGhostAttempt(mindId, component.CanReturn, mind: mind); } private void OnGhostStartup(EntityUid uid, GhostComponent component, ComponentStartup args) { // Allow this entity to be seen by other ghosts. var visibility = EnsureComp(uid); if (_ticker.RunLevel != GameRunLevel.PostRound && !component.Visible) { _visibilitySystem.AddLayer(new Entity(uid, visibility), (int) VisibilityFlags.Ghost, false); _visibilitySystem.RemoveLayer(new Entity(uid, visibility), (int) VisibilityFlags.Normal, false); _visibilitySystem.RefreshVisibility(uid, visibilityComponent: visibility); } SetCanSeeGhosts(uid, true); var time = _gameTiming.CurTime; component.TimeOfDeath = time; } private void OnGhostShutdown(EntityUid uid, GhostComponent component, ComponentShutdown args) { // Perf: If the entity is deleting itself, no reason to change these back. if (Terminating(uid)) return; // Entity can't be seen by ghosts anymore. if (TryComp(uid, out VisibilityComponent? visibility) && !component.Visible) { _visibilitySystem.RemoveLayer(new Entity(uid, visibility), (int) VisibilityFlags.Ghost, false); _visibilitySystem.AddLayer(new Entity(uid, visibility), (int) VisibilityFlags.Normal, false); _visibilitySystem.RefreshVisibility(uid, visibilityComponent: visibility); } // Entity can't see ghosts anymore. SetCanSeeGhosts(uid, false); _actions.RemoveAction(uid, component.BooActionEntity); } private void SetCanSeeGhosts(EntityUid uid, bool canSee, EyeComponent? eyeComponent = null) { if (!Resolve(uid, ref eyeComponent, false)) return; if (canSee) _eye.SetVisibilityMask(uid, eyeComponent.VisibilityMask | (int) VisibilityFlags.Ghost, eyeComponent); else _eye.SetVisibilityMask(uid, eyeComponent.VisibilityMask & ~(int) VisibilityFlags.Ghost, eyeComponent); } private void OnMapInit(EntityUid uid, GhostComponent component, MapInitEvent args) { if (_actions.AddAction(uid, ref component.BooActionEntity, out var act, component.BooAction) && act.UseDelay != null) { var start = _gameTiming.CurTime; var end = start + act.UseDelay.Value; _actions.SetCooldown(component.BooActionEntity.Value, start, end); } _actions.AddAction(uid, ref component.ToggleGhostHearingActionEntity, component.ToggleGhostHearingAction); _actions.AddAction(uid, ref component.ToggleLightingActionEntity, component.ToggleLightingAction); _actions.AddAction(uid, ref component.ToggleFoVActionEntity, component.ToggleFoVAction); _actions.AddAction(uid, ref component.ToggleGhostsActionEntity, component.ToggleGhostsAction); } private void OnGhostExamine(EntityUid uid, GhostComponent component, ExaminedEvent args) { var timeSinceDeath = _gameTiming.RealTime.Subtract(component.TimeOfDeath); var deathTimeInfo = timeSinceDeath.Minutes > 0 ? Loc.GetString("comp-ghost-examine-time-minutes", ("minutes", timeSinceDeath.Minutes)) : Loc.GetString("comp-ghost-examine-time-seconds", ("seconds", timeSinceDeath.Seconds)); args.PushMarkup(deathTimeInfo); } #region Ghost Deletion private void OnMindRemovedMessage(EntityUid uid, GhostComponent component, MindRemovedMessage args) { QueueDel(uid); } private void OnMindUnvisitedMessage(EntityUid uid, GhostComponent component, MindUnvisitedMessage args) { QueueDel(uid); } private void OnPlayerDetached(EntityUid uid, GhostComponent component, PlayerDetachedEvent args) { QueueDel(uid); } #endregion private void OnGhostReturnToBodyRequest(GhostReturnToBodyRequest msg, EntitySessionEventArgs args) { if (args.SenderSession.AttachedEntity is not { Valid: true } attached || !_ghostQuery.TryComp(attached, out var ghost) || !ghost.CanReturnToBody || !TryComp(attached, out ActorComponent? actor)) { Log.Warning($"User {args.SenderSession.Name} sent an invalid {nameof(GhostReturnToBodyRequest)}"); return; } _mindSystem.UnVisit(actor.PlayerSession); } #region Warp private void OnGhostWarpsRequest(GhostWarpsRequestEvent msg, EntitySessionEventArgs args) { if (args.SenderSession.AttachedEntity is not { Valid: true } entity || !_ghostQuery.HasComp(entity)) { Log.Warning( $"User {args.SenderSession.Name} sent a {nameof(GhostWarpsRequestEvent)} without being a ghost."); return; } var response = new GhostWarpsResponseEvent(GetPlayerWarps(), GetLocationWarps(), GetAntagonistWarps()); // WD edit RaiseNetworkEvent(response, args.SenderSession.Channel); } private void OnGhostWarpToTargetRequest(GhostWarpToTargetRequestEvent msg, EntitySessionEventArgs args) { if (args.SenderSession.AttachedEntity is not { Valid: true } attached || !_ghostQuery.HasComp(attached)) { Log.Warning($"User {args.SenderSession.Name} tried to warp to {msg.Target} without being a ghost."); return; } var target = GetEntity(msg.Target); if (!Exists(target)) { Log.Warning($"User {args.SenderSession.Name} tried to warp to an invalid entity id: {msg.Target}"); return; } if ((TryComp(target, out WarpPointComponent? warp) && warp.Follow) || HasComp(target)) { _followerSystem.StartFollowingEntity(attached, target); return; } var xform = Transform(attached); _transformSystem.SetCoordinates(attached, xform, Transform(target).Coordinates); _transformSystem.AttachToGridOrMap(attached, xform); if (TryComp(attached, out PhysicsComponent? physics)) _physics.SetLinearVelocity(attached, Vector2.Zero, body: physics); WarpTo(attached, target); } private void WarpTo(EntityUid uid, EntityUid target) { if ((TryComp(target, out WarpPointComponent? warp) && warp.Follow) || HasComp(target)) { _followerSystem.StartFollowingEntity(uid, target); return; } var xform = Transform(uid); _transformSystem.SetCoordinates(uid, xform, Transform(target).Coordinates); _transformSystem.AttachToGridOrMap(uid, xform); if (_physicsQuery.TryComp(uid, out var physics)) _physics.SetLinearVelocity(uid, Vector2.Zero, body: physics); } private List GetLocationWarps() // WD added { var warps = new List { }; var allQuery = AllEntityQuery(); while (allQuery.MoveNext(out var uid, out var warp)) { var newWarp = new GhostWarpPlace(GetNetEntity(uid), warp.Location ?? Name(uid), warp.Location ?? Description(uid)); warps.Add(newWarp); } return warps; } private List GetPlayerWarps() // WD added { var warps = new List { }; foreach (var mindContainer in EntityQuery()) { var entity = mindContainer.Owner; if (!(HasComp(entity) || HasComp(entity))) continue; if (HasComp(entity)) continue; if (TryComp(entity, out var invisibilityComponent) && invisibilityComponent.Invisible) continue; var playerDepartmentId = _prototypeManager.Index("Specific").ID; var playerJobName = "Неизвестно"; if (_jobs.MindTryGetJob(mindContainer.Mind ?? mindContainer.LastMindStored, out _, out var jobPrototype)) { playerJobName = Loc.GetString(jobPrototype.Name); if (_jobs.TryGetDepartment(jobPrototype.ID, out var departmentPrototype)) { playerDepartmentId = departmentPrototype.ID; } } var hasAnyMind = (mindContainer.Mind ?? mindContainer.LastMindStored) != null; var isDead = _mobState.IsDead(entity); var isLeft = TryComp(entity, out var indicator) && indicator.IsSSD && !isDead && hasAnyMind; var warp = new GhostWarpPlayer( GetNetEntity(entity), Comp(entity).EntityName, playerJobName, playerDepartmentId, HasComp(entity), isLeft, isDead, _mobState.IsAlive(entity) ); warps.Add(warp); } return warps; } private List GetAntagonistWarps() // WD added { var warps = new List { }; foreach (var antagonist in EntityQuery()) { var entity = antagonist.Owner; if (!_mobState.IsAlive(entity)) continue; var prototype = _prototypeManager.Index(antagonist.AntagonistPrototype ?? "globalAntagonistUnknown"); var warp = new GhostWarpGlobalAntagonist( GetNetEntity(entity), Comp(entity).EntityName, prototype.Name, prototype.Description, prototype.ID ); warps.Add(warp); } return warps; } #endregion private void OnEntityStorageInsertAttempt(EntityUid uid, GhostComponent comp, ref InsertIntoEntityStorageAttemptEvent args) { args.Cancelled = true; } /// /// When the round ends, make all players able to see ghosts. /// public void MakeVisible(bool visible) { var entityQuery = EntityQueryEnumerator(); while (entityQuery.MoveNext(out var uid, out var ghost)) { // WD if (EntityManager.TryGetComponent(uid, out InvisibilityComponent? invis) && invis.Invisible) continue; SetVisible((uid, ghost), visible); } } public bool DoGhostBooEvent(EntityUid target) { var ghostBoo = new GhostBooEvent(); RaiseLocalEvent(target, ghostBoo, true); return ghostBoo.Handled; } public EntityUid? SpawnGhost(Entity mind, EntityUid targetEntity, bool canReturn = false) { _transformSystem.TryGetMapOrGridCoordinates(targetEntity, out var spawnPosition); return SpawnGhost(mind, spawnPosition, canReturn); } public EntityUid? SpawnGhost(Entity mind, EntityCoordinates? spawnPosition = null, bool canReturn = false) { if (!Resolve(mind, ref mind.Comp)) return null; // Test if the map is being deleted var mapUid = spawnPosition?.GetMapUid(EntityManager); if (mapUid == null || TerminatingOrDeleted(mapUid.Value)) spawnPosition = null; spawnPosition ??= _ticker.GetObserverSpawnPoint(); if (!spawnPosition.Value.IsValid(EntityManager)) { Log.Warning($"No spawn valid ghost spawn position found for {mind.Comp.CharacterName}" + " \"{ToPrettyString(mind)}\""); _minds.TransferTo(mind.Owner, null, createGhost: false, mind: mind.Comp); return null; } var ghost = SpawnAtPosition(GameTicker.ObserverPrototypeName, spawnPosition.Value); var ghostComponent = Comp(ghost); // Try setting the ghost entity name to either the character name or the player name. // If all else fails, it'll default to the default entity prototype name, "observer". // However, that should rarely happen. if (!string.IsNullOrWhiteSpace(mind.Comp.CharacterName)) _metaData.SetEntityName(ghost, mind.Comp.CharacterName); else if (!string.IsNullOrWhiteSpace(mind.Comp.Session?.Name)) _metaData.SetEntityName(ghost, mind.Comp.Session.Name); if (mind.Comp.TimeOfDeath.HasValue) { SetTimeOfDeath(ghost, mind.Comp.TimeOfDeath!.Value, ghostComponent); } SetCanReturnToBody(ghostComponent, canReturn); if (canReturn) _minds.Visit(mind.Owner, ghost, mind.Comp); else _minds.TransferTo(mind.Owner, ghost, mind: mind.Comp); Log.Debug($"Spawned ghost \"{ToPrettyString(ghost)}\" for {mind.Comp.CharacterName}."); return ghost; } } }