using System.Linq; using Content.Shared.DeviceNetwork.Components; using Content.Shared.DeviceNetwork.Events; using Content.Shared.DeviceNetwork.Systems; using JetBrains.Annotations; using Robust.Shared.Map.Events; namespace Content.Server.DeviceNetwork.Systems; [UsedImplicitly] public sealed class DeviceListSystem : SharedDeviceListSystem { [Dependency] private readonly NetworkConfiguratorSystem _configurator = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnShutdown); SubscribeLocalEvent(OnBeforeBroadcast); SubscribeLocalEvent(OnBeforePacketSent); SubscribeLocalEvent(OnMapSave); } private void OnShutdown(EntityUid uid, DeviceListComponent component, ComponentShutdown args) { foreach (var conf in component.Configurators) { _configurator.OnDeviceListShutdown(conf, (uid, component)); } var query = GetEntityQuery(); foreach (var device in component.Devices) { if (query.TryGetComponent(device, out var comp)) comp.DeviceLists.Remove(uid); } component.Devices.Clear(); } /// /// Gets the given device list as a dictionary /// /// /// If any entity in the device list is pre-map init, it will show the entity UID of the device instead. /// public Dictionary GetDeviceList(EntityUid uid, DeviceListComponent? deviceList = null) { if (!Resolve(uid, ref deviceList)) return new Dictionary(); var devices = new Dictionary(deviceList.Devices.Count); foreach (var deviceUid in deviceList.Devices) { if (!TryComp(deviceUid, out DeviceNetworkComponent? deviceNet)) continue; var address = MetaData(deviceUid).EntityLifeStage == EntityLifeStage.MapInitialized ? deviceNet.Address : $"UID: {deviceUid.ToString()}"; devices.Add(address, deviceUid); } return devices; } /// /// Checks if the given address is present in a device list /// /// The entity uid that has the device list that should be checked for the address /// The address to check for /// The device list component /// True if the address is present. False if not public bool ExistsInDeviceList(EntityUid uid, string address, DeviceListComponent? deviceList = null) { var addresses = GetDeviceList(uid).Keys; return addresses.Contains(address); } /// /// Filters the broadcasts recipient list against the device list as either an allow or deny list depending on the components IsAllowList field /// private void OnBeforeBroadcast(EntityUid uid, DeviceListComponent component, BeforeBroadcastAttemptEvent args) { //Don't filter anything if the device list is empty if (component.Devices.Count == 0) { if (component.IsAllowList) args.Cancel(); return; } HashSet filteredRecipients = new(args.Recipients.Count); foreach (var recipient in args.Recipients) { if (component.Devices.Contains(recipient.Owner) == component.IsAllowList) filteredRecipients.Add(recipient); } args.ModifiedRecipients = filteredRecipients; } /// /// Filters incoming packets if that is enabled /// private void OnBeforePacketSent(EntityUid uid, DeviceListComponent component, BeforePacketSentEvent args) { if (component.HandleIncomingPackets && component.Devices.Contains(args.Sender) != component.IsAllowList) args.Cancel(); } public void OnDeviceShutdown(Entity list, Entity device) { device.Comp.DeviceLists.Remove(list.Owner); if (!Resolve(list.Owner, ref list.Comp)) return; list.Comp.Devices.Remove(device); Dirty(list); } private void OnMapSave(BeforeSerializationEvent ev) { List toRemove = new(); var query = GetEntityQuery(); var enumerator = AllEntityQuery(); while (enumerator.MoveNext(out var uid, out var device, out var xform)) { if (!ev.MapIds.Contains(xform.MapID)) continue; foreach (var ent in device.Devices) { if (!query.TryGetComponent(ent, out var linkedXform)) { // Entity was deleted. // TODO remove these on deletion instead of on-save. toRemove.Add(ent); continue; } // This is assuming that **all** of the map is getting saved. // Which is not necessarily true. // AAAAAAAAAAAAAA if (ev.MapIds.Contains(linkedXform.MapID)) continue; toRemove.Add(ent); // TODO full game saves. // when full saves are supported, this should instead add data to the BeforeSaveEvent informing the // saving system that this map (or null-space entity) also needs to be included in the save. Log.Error( $"Saving a device list ({ToPrettyString(uid)}) that has a reference to an entity on another map ({ToPrettyString(ent)}). Removing entity from list."); } if (toRemove.Count == 0) continue; var old = device.Devices.ToList(); device.Devices.ExceptWith(toRemove); RaiseLocalEvent(uid, new DeviceListUpdateEvent(old, device.Devices.ToList())); Dirty(uid, device); toRemove.Clear(); } } /// /// Updates the device list stored on this entity. /// /// The entity to update. /// The devices to store. /// Whether to merge or replace the devices stored. /// Device list component public DeviceListUpdateResult UpdateDeviceList(EntityUid uid, IEnumerable devices, bool merge = false, DeviceListComponent? deviceList = null) { if (!Resolve(uid, ref deviceList)) return DeviceListUpdateResult.NoComponent; var list = devices.ToList(); var newDevices = new HashSet(list); if (merge) newDevices.UnionWith(deviceList.Devices); if (newDevices.Count > deviceList.DeviceLimit) { return DeviceListUpdateResult.TooManyDevices; } var query = GetEntityQuery(); var oldDevices = deviceList.Devices.ToList(); foreach (var device in oldDevices) { if (newDevices.Contains(device)) continue; deviceList.Devices.Remove(device); if (query.TryGetComponent(device, out var comp)) comp.DeviceLists.Remove(uid); } foreach (var device in newDevices) { if (!query.TryGetComponent(device, out var comp)) continue; if (!deviceList.Devices.Add(device)) continue; comp.DeviceLists.Add(uid); } RaiseLocalEvent(uid, new DeviceListUpdateEvent(oldDevices, list)); Dirty(uid, deviceList); return DeviceListUpdateResult.UpdateOk; } }