using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Server.DeviceNetwork.Systems; using Content.Server.PDA; using Content.Shared.CartridgeLoader; using Content.Shared.DeviceNetwork.Events; using Content.Shared.Interaction; using Robust.Server.Containers; using Robust.Server.GameObjects; using Robust.Shared.Containers; using Robust.Shared.Map; using Robust.Shared.Player; namespace Content.Server.CartridgeLoader; public sealed class CartridgeLoaderSystem : SharedCartridgeLoaderSystem { [Dependency] private readonly ContainerSystem _containerSystem = default!; [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!; [Dependency] private readonly PdaSystem _pda = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnPacketReceived); SubscribeLocalEvent(OnUsed); SubscribeLocalEvent(OnLoaderUiMessage); SubscribeLocalEvent(OnUiMessage); } public IReadOnlyList GetInstalled(EntityUid uid, ContainerManagerComponent? comp = null) { if (_containerSystem.TryGetContainer(uid, InstalledContainerId, out var container, comp)) return container.ContainedEntities; return Array.Empty(); } public bool TryGetProgram( EntityUid uid, [NotNullWhen(true)] out EntityUid? programUid, [NotNullWhen(true)] out T? program, bool installedOnly = false, CartridgeLoaderComponent? loader = null, ContainerManagerComponent? containerManager = null) where T : IComponent { program = default; programUid = null; if (!_containerSystem.TryGetContainer(uid, InstalledContainerId, out var container, containerManager)) return false; foreach (var prog in container.ContainedEntities) { if (!TryComp(prog, out program)) continue; programUid = prog; return true; } if (installedOnly) return false; if (!Resolve(uid, ref loader) || !TryComp(loader.CartridgeSlot.Item, out program)) return false; programUid = loader.CartridgeSlot.Item; return true; } public bool TryGetProgram( EntityUid uid, [NotNullWhen(true)] out EntityUid? programUid, bool installedOnly = false, CartridgeLoaderComponent? loader = null, ContainerManagerComponent? containerManager = null) where T : IComponent { return TryGetProgram(uid, out programUid, out _, installedOnly, loader, containerManager); } public bool HasProgram( EntityUid uid, bool installedOnly = false, CartridgeLoaderComponent? loader = null, ContainerManagerComponent? containerManager = null) where T : IComponent { return TryGetProgram(uid, out _, out _, installedOnly, loader, containerManager); } /// /// Updates the cartridge loaders ui state. /// /// /// Because the cartridge loader integrates with the ui of the entity using it, the entities ui state needs to inherit from /// and use this method to update its state so the cartridge loaders state can be added to it. /// /// public void UpdateUiState(EntityUid loaderUid, ICommonSession? session, CartridgeLoaderComponent? loader) { if (!Resolve(loaderUid, ref loader)) return; if (!_userInterfaceSystem.HasUi(loaderUid, loader.UiKey)) return; var programs = GetAvailablePrograms(loaderUid, loader); var state = new CartridgeLoaderUiState(programs, GetNetEntity(loader.ActiveProgram)); _userInterfaceSystem.SetUiState(loaderUid, loader.UiKey, state); } /// /// Updates the programs ui state /// /// The cartridge loaders entity uid /// The programs ui state. Programs should use their own ui state class inheriting from /// The players session /// The cartridge loader component /// /// This method is called "UpdateCartridgeUiState" but cartridges and a programs are the same. A cartridge is just a program as a visible item. /// /// public void UpdateCartridgeUiState(EntityUid loaderUid, BoundUserInterfaceState state, ICommonSession? session = default!, CartridgeLoaderComponent? loader = default!) { if (!Resolve(loaderUid, ref loader)) return; if (_userInterfaceSystem.HasUi(loaderUid, loader.UiKey)) _userInterfaceSystem.SetUiState(loaderUid, loader.UiKey, state); } /// /// Returns a list of all installed programs and the inserted cartridge if it isn't already installed /// /// The cartridge loaders uid /// The cartridge loader component /// A list of all the available program entity ids public List GetAvailablePrograms(EntityUid uid, CartridgeLoaderComponent? loader = default!) { if (!Resolve(uid, ref loader)) return new List(); var available = GetNetEntityList(GetInstalled(uid)); if (loader.CartridgeSlot.Item is not { } cartridge) return available; // TODO exclude duplicate programs. Or something I dunno I CBF fixing this mess. available.Add(GetNetEntity(cartridge)); return available; } /// /// Installs a cartridge by spawning an invisible version of the cartridges prototype into the cartridge loaders program container program container /// /// The cartridge loader uid /// The uid of the cartridge to be installed /// The cartridge loader component /// Whether installing the cartridge was successful public bool InstallCartridge(EntityUid loaderUid, EntityUid cartridgeUid, CartridgeLoaderComponent? loader = default!) { if (!Resolve(loaderUid, ref loader)) return false; if (!TryComp(cartridgeUid, out CartridgeComponent? loadedCartridge)) return false; foreach (var program in GetInstalled(loaderUid)) { if (TryComp(program, out CartridgeComponent? installedCartridge) && installedCartridge.ProgramName == loadedCartridge.ProgramName) return false; } //This will eventually be replaced by serializing and deserializing the cartridge to copy it when something needs //the data on the cartridge to carry over when installing // For anyone stumbling onto this: Do not do this or I will cut you. var prototypeId = Prototype(cartridgeUid)?.ID; return prototypeId != null && InstallProgram(loaderUid, prototypeId, loader: loader); } /// /// Installs a program by its prototype /// /// The cartridge loader uid /// The prototype name /// Whether the program can be deinstalled or not /// The cartridge loader component /// Whether installing the cartridge was successful public bool InstallProgram(EntityUid loaderUid, string prototype, bool deinstallable = true, CartridgeLoaderComponent? loader = default!) { if (!Resolve(loaderUid, ref loader)) return false; if (!_containerSystem.TryGetContainer(loaderUid, InstalledContainerId, out var container)) return false; if (container.Count >= loader.DiskSpace) return false; var ev = new ProgramInstallationAttempt(loaderUid, prototype); RaiseLocalEvent(ref ev); if (ev.Cancelled) return false; var installedProgram = Spawn(prototype, new EntityCoordinates(loaderUid, 0, 0)); if (!TryComp(installedProgram, out CartridgeComponent? cartridge)) return false; _containerSystem.Insert(installedProgram, container); UpdateCartridgeInstallationStatus(installedProgram, deinstallable ? InstallationStatus.Installed : InstallationStatus.Readonly, cartridge); cartridge.LoaderUid = loaderUid; RaiseLocalEvent(installedProgram, new CartridgeAddedEvent(loaderUid)); UpdateUserInterfaceState(loaderUid, loader); return true; } /// /// Uninstalls a program using its uid /// /// The cartridge loader uid /// The uid of the program to be uninstalled /// The cartridge loader component /// Whether uninstalling the program was successful public bool UninstallProgram(EntityUid loaderUid, EntityUid programUid, CartridgeLoaderComponent? loader = default!) { if (!Resolve(loaderUid, ref loader)) return false; if (!GetInstalled(loaderUid).Contains(programUid)) return false; if (TryComp(programUid, out CartridgeComponent? cartridge)) cartridge.LoaderUid = null; if (loader.ActiveProgram == programUid) loader.ActiveProgram = null; loader.BackgroundPrograms.Remove(programUid); QueueDel(programUid); UpdateUserInterfaceState(loaderUid, loader); return true; } /// /// Activates a program or cartridge and displays its ui fragment. Deactivates any previously active program. /// public void ActivateProgram(EntityUid loaderUid, EntityUid programUid, CartridgeLoaderComponent? loader = default!) { if (!Resolve(loaderUid, ref loader)) return; if (!HasProgram(loaderUid, programUid, loader)) return; if (loader.ActiveProgram.HasValue) DeactivateProgram(loaderUid, programUid, loader); if (!loader.BackgroundPrograms.Contains(programUid)) RaiseLocalEvent(programUid, new CartridgeActivatedEvent(loaderUid)); loader.ActiveProgram = programUid; UpdateUserInterfaceState(loaderUid, loader); } /// /// Deactivates the currently active program or cartridge. /// public void DeactivateProgram(EntityUid loaderUid, EntityUid programUid, CartridgeLoaderComponent? loader = default!) { if (!Resolve(loaderUid, ref loader)) return; if (!HasProgram(loaderUid, programUid, loader) || loader.ActiveProgram != programUid) return; if (!loader.BackgroundPrograms.Contains(programUid)) RaiseLocalEvent(programUid, new CartridgeDeactivatedEvent(programUid)); loader.ActiveProgram = default; UpdateUserInterfaceState(loaderUid, loader); } /// /// Registers the given program as a running in the background. Programs running in the background will receive certain events like device net packets but not ui messages /// /// /// Programs wanting to use this functionality will have to provide a way to register and unregister themselves as background programs through their ui fragment. /// public void RegisterBackgroundProgram(EntityUid loaderUid, EntityUid cartridgeUid, CartridgeLoaderComponent? loader = default!) { if (!Resolve(loaderUid, ref loader)) return; if (!HasProgram(loaderUid, cartridgeUid, loader)) return; if (loader.ActiveProgram != cartridgeUid) RaiseLocalEvent(cartridgeUid, new CartridgeActivatedEvent(loaderUid)); loader.BackgroundPrograms.Add(cartridgeUid); } /// /// Unregisters the given program as running in the background /// public void UnregisterBackgroundProgram(EntityUid loaderUid, EntityUid cartridgeUid, CartridgeLoaderComponent? loader = default!) { if (!Resolve(loaderUid, ref loader)) return; if (!HasProgram(loaderUid, cartridgeUid, loader)) return; if (loader.ActiveProgram != cartridgeUid) RaiseLocalEvent(cartridgeUid, new CartridgeDeactivatedEvent(loaderUid)); loader.BackgroundPrograms.Remove(cartridgeUid); } public void SendNotification(EntityUid loaderUid, string header, string message, CartridgeLoaderComponent? loader = default!) { if (!Resolve(loaderUid, ref loader)) return; if (!loader.NotificationsEnabled) return; var args = new CartridgeLoaderNotificationSentEvent(header, message); RaiseLocalEvent(loaderUid, ref args); } protected override void OnItemInserted(EntityUid uid, CartridgeLoaderComponent loader, EntInsertedIntoContainerMessage args) { if (args.Container.ID != InstalledContainerId && args.Container.ID != loader.CartridgeSlot.ID) return; if (TryComp(args.Entity, out CartridgeComponent? cartridge)) cartridge.LoaderUid = uid; RaiseLocalEvent(args.Entity, new CartridgeAddedEvent(uid)); base.OnItemInserted(uid, loader, args); } protected override void OnItemRemoved(EntityUid uid, CartridgeLoaderComponent loader, EntRemovedFromContainerMessage args) { if (args.Container.ID != InstalledContainerId && args.Container.ID != loader.CartridgeSlot.ID) return; var deactivate = loader.BackgroundPrograms.Remove(args.Entity); if (loader.ActiveProgram == args.Entity) { loader.ActiveProgram = default; deactivate = true; } if (deactivate) RaiseLocalEvent(args.Entity, new CartridgeDeactivatedEvent(uid)); if (TryComp(args.Entity, out CartridgeComponent? cartridge)) cartridge.LoaderUid = null; RaiseLocalEvent(args.Entity, new CartridgeRemovedEvent(uid)); base.OnItemRemoved(uid, loader, args); _pda.UpdatePdaUi(uid); } /// /// Installs programs from the list of preinstalled programs /// private void OnMapInit(EntityUid uid, CartridgeLoaderComponent component, MapInitEvent args) { // TODO remove this and use container fill. foreach (var prototype in component.PreinstalledPrograms) { InstallProgram(uid, prototype, deinstallable: false); } } private void OnUsed(EntityUid uid, CartridgeLoaderComponent component, AfterInteractEvent args) { RelayEvent(component, new CartridgeAfterInteractEvent(uid, args)); } private void OnPacketReceived(EntityUid uid, CartridgeLoaderComponent component, DeviceNetworkPacketEvent args) { RelayEvent(component, new CartridgeDeviceNetPacketEvent(uid, args)); } private void OnLoaderUiMessage(EntityUid loaderUid, CartridgeLoaderComponent component, CartridgeLoaderUiMessage message) { var cartridge = GetEntity(message.CartridgeUid); switch (message.Action) { case CartridgeUiMessageAction.Activate: ActivateProgram(loaderUid, cartridge, component); break; case CartridgeUiMessageAction.Deactivate: DeactivateProgram(loaderUid, cartridge, component); break; case CartridgeUiMessageAction.Install: InstallCartridge(loaderUid, cartridge, component); break; case CartridgeUiMessageAction.Uninstall: UninstallProgram(loaderUid, cartridge, component); break; case CartridgeUiMessageAction.UIReady: if (component.ActiveProgram.HasValue) RaiseLocalEvent(component.ActiveProgram.Value, new CartridgeUiReadyEvent(loaderUid)); break; default: throw new ArgumentOutOfRangeException($"Unrecognized UI action passed from cartridge loader ui {message.Action}."); } } /// /// Relays ui messages meant for cartridges to the currently active cartridge /// private void OnUiMessage(EntityUid uid, CartridgeLoaderComponent component, CartridgeUiMessage args) { var cartridgeEvent = args.MessageEvent; cartridgeEvent.User = args.Actor; cartridgeEvent.LoaderUid = GetNetEntity(uid); cartridgeEvent.Actor = args.Actor; RelayEvent(component, cartridgeEvent, true); } /// /// Relays events to the currently active program and and programs running in the background. /// Skips background programs if "skipBackgroundPrograms" is set to true /// /// The cartritge loader component /// The event to be relayed /// Whether to skip relaying the event to programs running in the background private void RelayEvent(CartridgeLoaderComponent loader, TEvent args, bool skipBackgroundPrograms = false) where TEvent : notnull { if (loader.ActiveProgram.HasValue) RaiseLocalEvent(loader.ActiveProgram.Value, args); if (skipBackgroundPrograms) return; foreach (var program in loader.BackgroundPrograms) { //Prevent programs registered as running in the background receiving events twice if they are active if (loader.ActiveProgram.HasValue && loader.ActiveProgram.Value.Equals(program)) continue; RaiseLocalEvent(program, args); } } /// /// Shortcut for updating the loaders user interface state without passing in a subtype of /// like the does when updating its ui state /// /// private void UpdateUserInterfaceState(EntityUid loaderUid, CartridgeLoaderComponent loader) { UpdateUiState(loaderUid, null, loader); } private void UpdateCartridgeInstallationStatus(EntityUid cartridgeUid, InstallationStatus installationStatus, CartridgeComponent cartridgeComponent) { cartridgeComponent.InstallationStatus = installationStatus; Dirty(cartridgeUid, cartridgeComponent); } private bool HasProgram(EntityUid loader, EntityUid program, CartridgeLoaderComponent component) { return component.CartridgeSlot.Item == program || GetInstalled(loader).Contains(program); } } /// /// Gets sent to running programs when the cartridge loader receives a device net package /// /// public sealed class CartridgeDeviceNetPacketEvent : EntityEventArgs { public readonly EntityUid Loader; public readonly DeviceNetworkPacketEvent PacketEvent; public CartridgeDeviceNetPacketEvent(EntityUid loader, DeviceNetworkPacketEvent packetEvent) { Loader = loader; PacketEvent = packetEvent; } } /// /// Gets sent to running programs when the cartridge loader receives an after interact event /// /// public sealed class CartridgeAfterInteractEvent : EntityEventArgs { public readonly EntityUid Loader; public readonly AfterInteractEvent InteractEvent; public CartridgeAfterInteractEvent(EntityUid loader, AfterInteractEvent interactEvent) { Loader = loader; InteractEvent = interactEvent; } } /// /// Raised on an attempt of program installation. /// [ByRefEvent] public record struct ProgramInstallationAttempt(EntityUid LoaderUid, string Prototype, bool Cancelled = false);