using System.Diagnostics.CodeAnalysis; using Content.Server.Access.Systems; using Content.Server.AlertLevel; using Content.Server.CartridgeLoader; using Content.Server.Chat.Managers; using Content.Server.Instruments; using Content.Server.PDA.Ringer; using Content.Server.RoundEnd; // WL-Changes: ETA in PDA using Content.Server.Shuttles.Systems; // WL-Changes: ETA in PDA using Content.Server.Station.Systems; using Content.Server.Store.Systems; using Content.Server.Traitor.Uplink; using Content.Shared.Access.Components; using Content.Shared.CartridgeLoader; using Content.Shared.Chat; using Content.Shared.DeviceNetwork.Components; using Content.Shared.GameTicking; // WL-Changes: ETA in PDA using Content.Shared.Implants; using Content.Shared.Inventory; using Content.Shared.Light; using Content.Shared.Light.EntitySystems; using Content.Shared.PDA; using Content.Shared.PDA.Ringer; using Content.Shared.Store.Components; using Content.Shared.VoiceMask; using Robust.Server.Containers; using Robust.Server.GameObjects; using Robust.Shared.Containers; using Robust.Shared.Player; using Robust.Shared.Prototypes; // WL-Changes: Alert Level Rework using Robust.Shared.Utility; namespace Content.Server.PDA { public sealed partial class PdaSystem : SharedPdaSystem { [Dependency] private CartridgeLoaderSystem _cartridgeLoader = default!; [Dependency] private InstrumentSystem _instrument = default!; [Dependency] private RingerSystem _ringer = default!; [Dependency] private StationSystem _station = default!; [Dependency] private StoreSystem _store = default!; [Dependency] private IChatManager _chatManager = default!; [Dependency] private UserInterfaceSystem _ui = default!; [Dependency] private UnpoweredFlashlightSystem _unpoweredFlashlight = default!; [Dependency] private ContainerSystem _containerSystem = default!; [Dependency] private IdCardSystem _idCard = default!; [Dependency] private IPrototypeManager _prototypeManager = default!; // WL-Changes: Alert Level Rework // WL-Changes-start: ETA in PDA [Dependency] private RoundEndSystem _roundEnd = default!; [Access(typeof(EmergencyShuttleSystem), Other = AccessPermissions.None)] public TimeSpan? BeforeETA; [Access(typeof(RoundEndSystem), Other = AccessPermissions.None)] public bool RoundEnd = false; // WL-Changes-end public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnLightToggle); // UI Events: SubscribeLocalEvent(OnPdaOpen); SubscribeLocalEvent(OnUiMessage); SubscribeLocalEvent(OnUiMessage); SubscribeLocalEvent(OnUiMessage); SubscribeLocalEvent(OnUiMessage); SubscribeLocalEvent(OnUiMessage); SubscribeLocalEvent(OnUiMessage); SubscribeLocalEvent(OnNotification); SubscribeLocalEvent(OnStationRenamed); SubscribeLocalEvent(OnEntityRenamed, after: new[] { typeof(IdCardSystem) }); SubscribeLocalEvent(OnAlertLevelChanged); SubscribeLocalEvent>(OnRelayedEventToIdCard); SubscribeLocalEvent>(OnRelayedEventToIdCard); // WL-Changes-start: ETA in PDA SubscribeLocalEvent(_ => UpdateAllPdaUisOnStation()); SubscribeLocalEvent(_ => Reset()); } private void Reset() { BeforeETA = null; RoundEnd = false; } // WL-Changes-end private void OnRelayedEventToIdCard(Entity ent, ref InventoryRelayedEvent args) { // Relay it to your ID so it can update as well. if (ent.Comp.ContainedId != null) RaiseLocalEvent(ent.Comp.ContainedId.Value, args); } private void OnEntityRenamed(ref EntityRenamedEvent ev) { if (HasComp(ev.Uid)) return; if (_idCard.TryFindIdCard(ev.Uid, out var idCard)) { var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var comp)) { if (comp.ContainedId == idCard) { SetOwner(uid, comp, ev.Uid, ev.NewName); } } } } protected override void OnComponentInit(EntityUid uid, PdaComponent pda, ComponentInit args) { base.OnComponentInit(uid, pda, args); if (!HasComp(uid)) return; UpdateAlertLevel(uid, pda); UpdateStationName(uid, pda); } protected override void OnItemInserted(EntityUid uid, PdaComponent pda, EntInsertedIntoContainerMessage args) { base.OnItemInserted(uid, pda, args); var id = CompOrNull(pda.ContainedId); if (id != null) pda.OwnerName = id.FullName; UpdatePdaUi(uid, pda); } protected override void OnItemRemoved(EntityUid uid, PdaComponent pda, EntRemovedFromContainerMessage args) { if (args.Container.ID != pda.IdSlot.ID && args.Container.ID != pda.PenSlot.ID && args.Container.ID != pda.PaiSlot.ID) return; // TODO: This is super cursed just use compstates please. if (MetaData(uid).EntityLifeStage >= EntityLifeStage.Terminating) return; base.OnItemRemoved(uid, pda, args); UpdatePdaUi(uid, pda); } private void OnLightToggle(EntityUid uid, PdaComponent pda, LightToggleEvent args) { pda.FlashlightOn = args.IsOn; UpdatePdaUi(uid, pda); } public void SetOwner(EntityUid uid, PdaComponent pda, EntityUid owner, string ownerName) { pda.OwnerName = ownerName; pda.PdaOwner = owner; UpdatePdaUi(uid, pda); } private void OnStationRenamed(StationRenamedEvent ev) { UpdateAllPdaUisOnStation(); } private void OnAlertLevelChanged(AlertLevelChangedEvent args) { UpdateAllPdaUisOnStation(); } public void UpdateAllPdaUisOnStation() // WL-Changes: ETA in PDA // private -> public { var query = AllEntityQuery(); while (query.MoveNext(out var ent, out var comp)) { UpdatePdaUi(ent, comp); } } private void OnNotification(Entity ent, ref CartridgeLoaderNotificationSentEvent args) { _ringer.RingerPlayRingtone(ent.Owner); if (!_containerSystem.TryGetContainingContainer((ent, null, null), out var container) || !TryComp(container.Owner, out var actor)) return; var message = FormattedMessage.EscapeText(args.Message); var wrappedMessage = Loc.GetString("pda-notification-message", ("header", args.Header), ("message", message)); _chatManager.ChatMessageToOne( ChatChannel.Notifications, message, wrappedMessage, EntityUid.Invalid, false, actor.PlayerSession.Channel); } /// /// Send new UI state to clients, call if you modify something like uplink. /// public override void UpdatePdaUi(EntityUid uid, PdaComponent? pda = null) { if (!Resolve(uid, ref pda, false)) return; if (!_ui.HasUi(uid, PdaUiKey.Key)) return; var address = GetDeviceNetAddress(uid); var hasInstrument = HasComp(uid); var showUplink = TryGetUnlockedStore(uid, out _); UpdateStationName(uid, pda); UpdateAlertLevel(uid, pda); // TODO: Update the level and name of the station with each call to UpdatePdaUi is only needed for latejoin players. // TODO: If someone can implement changing the level and name of the station when changing the PDA grid, this can be removed. // TODO don't make this depend on cartridge loader!?!? if (!TryComp(uid, out CartridgeLoaderComponent? loader)) return; var expectedCountdownEnd = _roundEnd.IsRoundEndRequested() ? _roundEnd.ExpectedCountdownEnd : null; // WL-Changes: ETA in PDA var programs = _cartridgeLoader.GetAvailablePrograms(uid, loader); var id = CompOrNull(pda.ContainedId); var state = new PdaUpdateState( programs, GetNetEntity(loader.ActiveProgram), pda.FlashlightOn, pda.PenSlot.HasItem, pda.PaiSlot.HasItem, new PdaIdInfoText { ActualOwnerName = pda.OwnerName, IdOwner = id?.FullName, JobTitle = id?.LocalizedJobTitle, StationAlertLevel = pda.StationAlertLevel, StationAlertColor = pda.StationAlertColor, StationAlertInstructions = pda.StationAlertInstructions, // WL-Changes: custom alert instructions in PDA StationAlertName = pda.StationAlertName // WL-Changes: Alert Level Rework }, pda.StationName, showUplink, hasInstrument, address, // WL-Changes-start: ETA in PDA expectedCountdownEnd, // сколько до прибытия эвака на станцию BeforeETA, // сколько до отбытия шаттла со станции RoundEnd); // закончился ли раунд - нужен для полного отключения видимости таймера в кпк // WL-Changes-end _ui.SetUiState(uid, PdaUiKey.Key, state); } private void OnPdaOpen(Entity ent, ref BoundUIOpenedEvent args) { if (!PdaUiKey.Key.Equals(args.UiKey)) return; UpdatePdaUi(ent.Owner, ent.Comp); } private void OnUiMessage(EntityUid uid, PdaComponent pda, PdaRequestUpdateInterfaceMessage msg) { if (!PdaUiKey.Key.Equals(msg.UiKey)) return; UpdatePdaUi(uid, pda); } private void OnUiMessage(EntityUid uid, PdaComponent pda, PdaToggleFlashlightMessage msg) { if (!PdaUiKey.Key.Equals(msg.UiKey)) return; // TODO PREDICTION // When moving this to shared, fill in the user field _unpoweredFlashlight.TryToggleLight(uid, user: null); } private void OnUiMessage(EntityUid uid, PdaComponent pda, PdaShowRingtoneMessage msg) { if (!PdaUiKey.Key.Equals(msg.UiKey)) return; if (HasComp(uid)) _ringer.TryToggleRingerUi(uid, msg.Actor); } private void OnUiMessage(EntityUid uid, PdaComponent pda, PdaShowMusicMessage msg) { if (!PdaUiKey.Key.Equals(msg.UiKey)) return; if (TryComp(uid, out var instrument)) _instrument.ToggleInstrumentUi(uid, msg.Actor, instrument); } private void OnUiMessage(EntityUid uid, PdaComponent pda, PdaShowUplinkMessage msg) { if (!PdaUiKey.Key.Equals(msg.UiKey)) return; // check if its locked again to prevent malicious clients opening locked uplinks if (TryGetUnlockedStore(uid, out var store)) { if (store != uid) { if (TryComp(uid, out var remoteStore)) remoteStore.Store = store; _store.ToggleUi(msg.Actor, store.Value, remoteAccess: uid); } else { _store.ToggleUi(msg.Actor, store.Value); } } } private void OnUiMessage(EntityUid uid, PdaComponent pda, PdaLockUplinkMessage msg) { if (!PdaUiKey.Key.Equals(msg.UiKey)) return; if (TryComp(uid, out var uplink)) { if (TryComp(uid, out var remoteStore)) remoteStore.Store = null; _ringer.LockUplink((uid, uplink)); UpdatePdaUi(uid, pda); } } /// /// Returns the currently unlocked store, if there is one. /// private bool TryGetUnlockedStore(EntityUid uid, [NotNullWhen(true)] out EntityUid? store) { store = null; if (!TryComp(uid, out var uplink) || !uplink.Unlocked) return false; store = _store.GetStore(uid); return store != null; } private void UpdateStationName(EntityUid uid, PdaComponent pda) { var station = _station.GetOwningStation(uid); pda.StationName = station is null ? null : Name(station.Value); } private void UpdateAlertLevel(EntityUid uid, PdaComponent pda) { var station = _station.GetOwningStation(uid); if (!TryComp(station, out AlertLevelComponent? alertComp) || alertComp.AlertLevels == null) return; pda.StationAlertLevel = alertComp.CurrentLevel; if (alertComp.AlertLevels.Levels.TryGetValue(alertComp.CurrentLevel, out var details)) { // WL-Changes-start: Alert Level Rework if (_prototypeManager.TryIndex(details, out var index)) { pda.StationAlertColor = index.Color; pda.StationAlertInstructions = index.Instruction; if (!string.IsNullOrEmpty(index.SetName)) pda.StationAlertName = index.SetName; } // WL-Changes-end } } private string? GetDeviceNetAddress(EntityUid uid) { string? address = null; if (TryComp(uid, out DeviceNetworkComponent? deviceNetworkComponent)) { address = deviceNetworkComponent?.Address; } return address; } } }