CatStation/Content.Server/PDA/PdaSystem.cs
Rouden cc2c14f8e4
Device Network refactor part 1: API improvements (#44346)
* Refactor DeviceNetwork

* Singleton

* NetworkPayloads refactor

* Reorganize DeviceNetworkSystem

* Surveillance cameras

* Fix code

* Fix build

* Fix test fail by rewriting entire singleton logic

* Properly predict examine

* Update DeviceNetworkSystem.API.cs

* Update EntityTest.cs

* Update DeviceNetworkSystem.cs

* Alright I'll do this expensive check fine

* Fix disposals debug assert

* Implement device network static events support

* Use frozen dictionaries for performance

* Update SharedDeviceNetworkSystem.Payloads.cs

* Parallel payload handling support

* Fix some spagetti-code

* Optimize NetworkPayload cancellation

* Parallel payloads are now cancellable

* Fix screens

* Fix robotics console

* Allow new methods for shared

* Mail fix

* Turret fix

* Fix network system method usage + signal and borg fix

* finally fix atmos devices OH MY GOOOD

* Remove stupid parallelism from fax

* Fix surveiollance cameras

* Working on a single thing for 2 days straight award

* Fix the test?

* Update units.yml

* Add IDevice interface because why not

* Add more device network suffixes

* Fix camera router failing test

* yaml linter peak

* Improve docs for DeviceNetworkHandlers

* fix build

* Update CartridgeLoaderSystem.Relay.cs

* Remove parallel system and EntitySystem post init

* Add more docs and also events

* Remove unnecessary caching

* Update DevicePayloadComponentSystem.cs

* Back to entity events

* Implement DeviceData

* Remove serializable from payloads

* Update DeviceData.cs

* Add a prototype for a singleton

* Fix tests

* Fix air alarms

* Update DeviceNetworkSystem.cs

* Fix cameras

* Make camera router event-less

* Fix atmos sync payloads, add more docs

* Remove singleton + instant packets

* oops remove public access

* Fix tests?

* Why the hell overload effect makes puddle sparks

* Apparently it was redundant

* Update gates.yml

* Finish documentation

* Review

* Mark relay properties as obsolete

* Struct payloads support (#blazinglyfastmemorysafe)

* Update DeviceNetworkTest.cs

* Make device structs stuff better

* Move address prefix to DeviceNetworkComponent

* Locale fixes

* Fix tests

* Doesn't know boolean logic award

* Update AtmosAlarmableSystem.cs

* Fix address predicted examine

* Partial fix to air alarmables

* Fix deployable turrets UI

* Fix firelocks, maybe

* Apparently Activatable UI was enough lol

* Fix surveillance cameras

* Rename `QueuePacket` into `SendPacket`

* Simplify DeviceData as much as possible

* Update DeviceData.cs

* Update FaxMachineComponent.cs

* Remove DeviceData because its stupid

* Update ScreenSystem.cs

* Fix build (camera map broken still)

* Fix map

* fix usings

* Review changes

* Build fix
2026-08-31 19:42:56 +00:00

350 lines
13 KiB
C#

using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Access.Systems;
using Content.Server.Chat.Managers;
using Content.Server.Instruments;
using Content.Server.PDA.Ringer;
using Content.Server.Station.Systems;
using Content.Server.Store.Systems;
using Content.Shared.Access.Components;
using Content.Shared.AlertLevel;
using Content.Shared.CartridgeLoader;
using Content.Shared.Chat;
using Content.Shared.DeviceNetwork.Components;
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;
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 _prototype = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PdaComponent, LightToggleEvent>(OnLightToggle);
// UI Events:
SubscribeLocalEvent<PdaComponent, BoundUIOpenedEvent>(OnPdaOpen);
SubscribeLocalEvent<PdaComponent, PdaRequestUpdateInterfaceMessage>(OnUiMessage);
SubscribeLocalEvent<PdaComponent, PdaToggleFlashlightMessage>(OnUiMessage);
SubscribeLocalEvent<PdaComponent, PdaShowRingtoneMessage>(OnUiMessage);
SubscribeLocalEvent<PdaComponent, PdaShowMusicMessage>(OnUiMessage);
SubscribeLocalEvent<PdaComponent, PdaShowUplinkMessage>(OnUiMessage);
SubscribeLocalEvent<PdaComponent, PdaLockUplinkMessage>(OnUiMessage);
SubscribeLocalEvent<PdaComponent, CartridgeLoaderNotificationSentEvent>(OnNotification);
SubscribeLocalEvent<StationRenamedEvent>(OnStationRenamed);
SubscribeLocalEvent<EntityRenamedEvent>(OnEntityRenamed, after: new[] { typeof(IdCardSystem) });
SubscribeLocalEvent<AlertLevelChangedEvent>(OnAlertLevelChanged);
SubscribeLocalEvent<PdaComponent, InventoryRelayedEvent<ChameleonControllerOutfitSelectedEvent>>(OnRelayedEventToIdCard);
SubscribeLocalEvent<PdaComponent, InventoryRelayedEvent<VoiceMaskNameUpdatedEvent>>(OnRelayedEventToIdCard);
}
private void OnRelayedEventToIdCard<T>(Entity<PdaComponent> ent, ref InventoryRelayedEvent<T> 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<IdCardComponent>(ev.Uid))
return;
if (_idCard.TryFindIdCard(ev.Uid, out var idCard))
{
var query = EntityQueryEnumerator<PdaComponent>();
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<UserInterfaceComponent>(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<IdCardComponent>(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(ref AlertLevelChangedEvent args)
{
UpdateAllPdaUisOnStation();
}
private void UpdateAllPdaUisOnStation()
{
var query = AllEntityQuery<PdaComponent>();
while (query.MoveNext(out var ent, out var comp))
{
UpdatePdaUi(ent, comp);
}
}
private void OnNotification(Entity<PdaComponent> ent, ref CartridgeLoaderNotificationSentEvent args)
{
_ringer.RingerPlayRingtone(ent.Owner);
if (!_containerSystem.TryGetContainingContainer((ent, null, null), out var container)
|| !TryComp<ActorComponent>(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);
}
/// <summary>
/// Send new UI state to clients, call if you modify something like uplink.
/// </summary>
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<InstrumentComponent>(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 programs = GetNetEntityList(_cartridgeLoader.GetAllPrograms(uid).ToList());
var id = CompOrNull<IdCardComponent>(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
},
pda.StationName,
showUplink,
hasInstrument,
address);
_ui.SetUiState(uid, PdaUiKey.Key, state);
}
private void OnPdaOpen(Entity<PdaComponent> 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<RingerComponent>(uid))
_ringer.TryToggleRingerUi(uid, msg.Actor);
}
private void OnUiMessage(EntityUid uid, PdaComponent pda, PdaShowMusicMessage msg)
{
if (!PdaUiKey.Key.Equals(msg.UiKey))
return;
if (TryComp<InstrumentComponent>(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<RemoteStoreComponent>(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<RingerUplinkComponent>(uid, out var uplink))
{
if (TryComp<RemoteStoreComponent>(uid, out var remoteStore))
remoteStore.Store = null;
_ringer.LockUplink((uid, uplink));
UpdatePdaUi(uid, pda);
}
}
/// <summary>
/// Returns the currently unlocked store, if there is one.
/// </summary>
private bool TryGetUnlockedStore(EntityUid uid, [NotNullWhen(true)] out EntityUid? store)
{
store = null;
if (!TryComp<RingerUplinkComponent>(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))
return;
pda.StationAlertLevel = alertComp.CurrentAlertLevel;
if (_prototype.Resolve(alertComp.CurrentAlertLevel, out var level))
pda.StationAlertColor = level.Color;
}
private string? GetDeviceNetAddress(EntityUid uid)
{
string? address = null;
if (TryComp(uid, out DeviceNetworkComponent? deviceNetworkComponent))
{
address = deviceNetworkComponent.Address;
}
return address;
}
}
}