CartridgeLoaderSystem.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. using System.Diagnostics.CodeAnalysis;
  2. using System.Linq;
  3. using Content.Server.DeviceNetwork.Systems;
  4. using Content.Server.PDA;
  5. using Content.Shared.CartridgeLoader;
  6. using Content.Shared.Interaction;
  7. using Robust.Server.Containers;
  8. using Robust.Server.GameObjects;
  9. using Robust.Shared.Containers;
  10. using Robust.Shared.Map;
  11. using Robust.Shared.Player;
  12. namespace Content.Server.CartridgeLoader;
  13. public sealed class CartridgeLoaderSystem : SharedCartridgeLoaderSystem
  14. {
  15. [Dependency] private readonly ContainerSystem _containerSystem = default!;
  16. [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
  17. [Dependency] private readonly PdaSystem _pda = default!;
  18. public override void Initialize()
  19. {
  20. base.Initialize();
  21. SubscribeLocalEvent<CartridgeLoaderComponent, MapInitEvent>(OnMapInit);
  22. SubscribeLocalEvent<CartridgeLoaderComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
  23. SubscribeLocalEvent<CartridgeLoaderComponent, AfterInteractEvent>(OnUsed);
  24. SubscribeLocalEvent<CartridgeLoaderComponent, CartridgeLoaderUiMessage>(OnLoaderUiMessage);
  25. SubscribeLocalEvent<CartridgeLoaderComponent, CartridgeUiMessage>(OnUiMessage);
  26. }
  27. public IReadOnlyList<EntityUid> GetInstalled(EntityUid uid, ContainerManagerComponent? comp = null)
  28. {
  29. if (_containerSystem.TryGetContainer(uid, InstalledContainerId, out var container, comp))
  30. return container.ContainedEntities;
  31. return Array.Empty<EntityUid>();
  32. }
  33. public bool TryGetProgram<T>(
  34. EntityUid uid,
  35. [NotNullWhen(true)] out EntityUid? programUid,
  36. [NotNullWhen(true)] out T? program,
  37. bool installedOnly = false,
  38. CartridgeLoaderComponent? loader = null,
  39. ContainerManagerComponent? containerManager = null) where T : IComponent
  40. {
  41. program = default;
  42. programUid = null;
  43. if (!_containerSystem.TryGetContainer(uid, InstalledContainerId, out var container, containerManager))
  44. return false;
  45. foreach (var prog in container.ContainedEntities)
  46. {
  47. if (!TryComp(prog, out program))
  48. continue;
  49. programUid = prog;
  50. return true;
  51. }
  52. if (installedOnly)
  53. return false;
  54. if (!Resolve(uid, ref loader) || !TryComp(loader.CartridgeSlot.Item, out program))
  55. return false;
  56. programUid = loader.CartridgeSlot.Item;
  57. return true;
  58. }
  59. public bool TryGetProgram<T>(
  60. EntityUid uid,
  61. [NotNullWhen(true)] out EntityUid? programUid,
  62. bool installedOnly = false,
  63. CartridgeLoaderComponent? loader = null,
  64. ContainerManagerComponent? containerManager = null) where T : IComponent
  65. {
  66. return TryGetProgram<T>(uid, out programUid, out _, installedOnly, loader, containerManager);
  67. }
  68. public bool HasProgram<T>(
  69. EntityUid uid,
  70. bool installedOnly = false,
  71. CartridgeLoaderComponent? loader = null,
  72. ContainerManagerComponent? containerManager = null) where T : IComponent
  73. {
  74. return TryGetProgram<T>(uid, out _, out _, installedOnly, loader, containerManager);
  75. }
  76. /// <summary>
  77. /// Updates the cartridge loaders ui state.
  78. /// </summary>
  79. /// <remarks>
  80. /// Because the cartridge loader integrates with the ui of the entity using it, the entities ui state needs to inherit from <see cref="CartridgeLoaderUiState"/>
  81. /// and use this method to update its state so the cartridge loaders state can be added to it.
  82. /// </remarks>
  83. /// <seealso cref="PDA.PdaSystem.UpdatePdaUserInterface"/>
  84. public void UpdateUiState(EntityUid loaderUid, ICommonSession? session, CartridgeLoaderComponent? loader)
  85. {
  86. if (!Resolve(loaderUid, ref loader))
  87. return;
  88. if (!_userInterfaceSystem.HasUi(loaderUid, loader.UiKey))
  89. return;
  90. var programs = GetAvailablePrograms(loaderUid, loader);
  91. var state = new CartridgeLoaderUiState(programs, GetNetEntity(loader.ActiveProgram));
  92. _userInterfaceSystem.SetUiState(loaderUid, loader.UiKey, state);
  93. }
  94. /// <summary>
  95. /// Updates the programs ui state
  96. /// </summary>
  97. /// <param name="loaderUid">The cartridge loaders entity uid</param>
  98. /// <param name="state">The programs ui state. Programs should use their own ui state class inheriting from <see cref="BoundUserInterfaceState"/></param>
  99. /// <param name="session">The players session</param>
  100. /// <param name="loader">The cartridge loader component</param>
  101. /// <remarks>
  102. /// This method is called "UpdateCartridgeUiState" but cartridges and a programs are the same. A cartridge is just a program as a visible item.
  103. /// </remarks>
  104. /// <seealso cref="Cartridges.NotekeeperCartridgeSystem.UpdateUiState"/>
  105. public void UpdateCartridgeUiState(EntityUid loaderUid, BoundUserInterfaceState state, ICommonSession? session = default!, CartridgeLoaderComponent? loader = default!)
  106. {
  107. if (!Resolve(loaderUid, ref loader))
  108. return;
  109. if (_userInterfaceSystem.HasUi(loaderUid, loader.UiKey))
  110. _userInterfaceSystem.SetUiState(loaderUid, loader.UiKey, state);
  111. }
  112. /// <summary>
  113. /// Returns a list of all installed programs and the inserted cartridge if it isn't already installed
  114. /// </summary>
  115. /// <param name="uid">The cartridge loaders uid</param>
  116. /// <param name="loader">The cartridge loader component</param>
  117. /// <returns>A list of all the available program entity ids</returns>
  118. public List<NetEntity> GetAvailablePrograms(EntityUid uid, CartridgeLoaderComponent? loader = default!)
  119. {
  120. if (!Resolve(uid, ref loader))
  121. return new List<NetEntity>();
  122. var available = GetNetEntityList(GetInstalled(uid));
  123. if (loader.CartridgeSlot.Item is not { } cartridge)
  124. return available;
  125. // TODO exclude duplicate programs. Or something I dunno I CBF fixing this mess.
  126. available.Add(GetNetEntity(cartridge));
  127. return available;
  128. }
  129. /// <summary>
  130. /// Installs a cartridge by spawning an invisible version of the cartridges prototype into the cartridge loaders program container program container
  131. /// </summary>
  132. /// <param name="loaderUid">The cartridge loader uid</param>
  133. /// <param name="cartridgeUid">The uid of the cartridge to be installed</param>
  134. /// <param name="loader">The cartridge loader component</param>
  135. /// <returns>Whether installing the cartridge was successful</returns>
  136. public bool InstallCartridge(EntityUid loaderUid, EntityUid cartridgeUid, CartridgeLoaderComponent? loader = default!)
  137. {
  138. if (!Resolve(loaderUid, ref loader))
  139. return false;
  140. if (!TryComp(cartridgeUid, out CartridgeComponent? loadedCartridge))
  141. return false;
  142. foreach (var program in GetInstalled(loaderUid))
  143. {
  144. if (TryComp(program, out CartridgeComponent? installedCartridge) && installedCartridge.ProgramName == loadedCartridge.ProgramName)
  145. return false;
  146. }
  147. //This will eventually be replaced by serializing and deserializing the cartridge to copy it when something needs
  148. //the data on the cartridge to carry over when installing
  149. // For anyone stumbling onto this: Do not do this or I will cut you.
  150. var prototypeId = Prototype(cartridgeUid)?.ID;
  151. return prototypeId != null && InstallProgram(loaderUid, prototypeId, loader: loader);
  152. }
  153. /// <summary>
  154. /// Installs a program by its prototype
  155. /// </summary>
  156. /// <param name="loaderUid">The cartridge loader uid</param>
  157. /// <param name="prototype">The prototype name</param>
  158. /// <param name="deinstallable">Whether the program can be deinstalled or not</param>
  159. /// <param name="loader">The cartridge loader component</param>
  160. /// <returns>Whether installing the cartridge was successful</returns>
  161. public bool InstallProgram(EntityUid loaderUid, string prototype, bool deinstallable = true, CartridgeLoaderComponent? loader = default!)
  162. {
  163. if (!Resolve(loaderUid, ref loader))
  164. return false;
  165. if (!_containerSystem.TryGetContainer(loaderUid, InstalledContainerId, out var container))
  166. return false;
  167. if (container.Count >= loader.DiskSpace)
  168. return false;
  169. var ev = new ProgramInstallationAttempt(loaderUid, prototype);
  170. RaiseLocalEvent(ref ev);
  171. if (ev.Cancelled)
  172. return false;
  173. var installedProgram = Spawn(prototype, new EntityCoordinates(loaderUid, 0, 0));
  174. if (!TryComp(installedProgram, out CartridgeComponent? cartridge))
  175. return false;
  176. _containerSystem.Insert(installedProgram, container);
  177. UpdateCartridgeInstallationStatus(installedProgram, deinstallable ? InstallationStatus.Installed : InstallationStatus.Readonly, cartridge);
  178. cartridge.LoaderUid = loaderUid;
  179. RaiseLocalEvent(installedProgram, new CartridgeAddedEvent(loaderUid));
  180. UpdateUserInterfaceState(loaderUid, loader);
  181. return true;
  182. }
  183. /// <summary>
  184. /// Uninstalls a program using its uid
  185. /// </summary>
  186. /// <param name="loaderUid">The cartridge loader uid</param>
  187. /// <param name="programUid">The uid of the program to be uninstalled</param>
  188. /// <param name="loader">The cartridge loader component</param>
  189. /// <returns>Whether uninstalling the program was successful</returns>
  190. public bool UninstallProgram(EntityUid loaderUid, EntityUid programUid, CartridgeLoaderComponent? loader = default!)
  191. {
  192. if (!Resolve(loaderUid, ref loader))
  193. return false;
  194. if (!GetInstalled(loaderUid).Contains(programUid))
  195. return false;
  196. if (TryComp(programUid, out CartridgeComponent? cartridge))
  197. cartridge.LoaderUid = null;
  198. if (loader.ActiveProgram == programUid)
  199. loader.ActiveProgram = null;
  200. loader.BackgroundPrograms.Remove(programUid);
  201. QueueDel(programUid);
  202. UpdateUserInterfaceState(loaderUid, loader);
  203. return true;
  204. }
  205. /// <summary>
  206. /// Activates a program or cartridge and displays its ui fragment. Deactivates any previously active program.
  207. /// </summary>
  208. public void ActivateProgram(EntityUid loaderUid, EntityUid programUid, CartridgeLoaderComponent? loader = default!)
  209. {
  210. if (!Resolve(loaderUid, ref loader))
  211. return;
  212. if (!HasProgram(loaderUid, programUid, loader))
  213. return;
  214. if (loader.ActiveProgram.HasValue)
  215. DeactivateProgram(loaderUid, programUid, loader);
  216. if (!loader.BackgroundPrograms.Contains(programUid))
  217. RaiseLocalEvent(programUid, new CartridgeActivatedEvent(loaderUid));
  218. loader.ActiveProgram = programUid;
  219. UpdateUserInterfaceState(loaderUid, loader);
  220. }
  221. /// <summary>
  222. /// Deactivates the currently active program or cartridge.
  223. /// </summary>
  224. public void DeactivateProgram(EntityUid loaderUid, EntityUid programUid, CartridgeLoaderComponent? loader = default!)
  225. {
  226. if (!Resolve(loaderUid, ref loader))
  227. return;
  228. if (!HasProgram(loaderUid, programUid, loader) || loader.ActiveProgram != programUid)
  229. return;
  230. if (!loader.BackgroundPrograms.Contains(programUid))
  231. RaiseLocalEvent(programUid, new CartridgeDeactivatedEvent(programUid));
  232. loader.ActiveProgram = default;
  233. UpdateUserInterfaceState(loaderUid, loader);
  234. }
  235. /// <summary>
  236. /// 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
  237. /// </summary>
  238. /// <remarks>
  239. /// Programs wanting to use this functionality will have to provide a way to register and unregister themselves as background programs through their ui fragment.
  240. /// </remarks>
  241. public void RegisterBackgroundProgram(EntityUid loaderUid, EntityUid cartridgeUid, CartridgeLoaderComponent? loader = default!)
  242. {
  243. if (!Resolve(loaderUid, ref loader))
  244. return;
  245. if (!HasProgram(loaderUid, cartridgeUid, loader))
  246. return;
  247. if (loader.ActiveProgram != cartridgeUid)
  248. RaiseLocalEvent(cartridgeUid, new CartridgeActivatedEvent(loaderUid));
  249. loader.BackgroundPrograms.Add(cartridgeUid);
  250. }
  251. /// <summary>
  252. /// Unregisters the given program as running in the background
  253. /// </summary>
  254. public void UnregisterBackgroundProgram(EntityUid loaderUid, EntityUid cartridgeUid, CartridgeLoaderComponent? loader = default!)
  255. {
  256. if (!Resolve(loaderUid, ref loader))
  257. return;
  258. if (!HasProgram(loaderUid, cartridgeUid, loader))
  259. return;
  260. if (loader.ActiveProgram != cartridgeUid)
  261. RaiseLocalEvent(cartridgeUid, new CartridgeDeactivatedEvent(loaderUid));
  262. loader.BackgroundPrograms.Remove(cartridgeUid);
  263. }
  264. public void SendNotification(EntityUid loaderUid, string header, string message, CartridgeLoaderComponent? loader = default!)
  265. {
  266. if (!Resolve(loaderUid, ref loader))
  267. return;
  268. if (!loader.NotificationsEnabled)
  269. return;
  270. var args = new CartridgeLoaderNotificationSentEvent(header, message);
  271. RaiseLocalEvent(loaderUid, ref args);
  272. }
  273. protected override void OnItemInserted(EntityUid uid, CartridgeLoaderComponent loader, EntInsertedIntoContainerMessage args)
  274. {
  275. if (args.Container.ID != InstalledContainerId && args.Container.ID != loader.CartridgeSlot.ID)
  276. return;
  277. if (TryComp(args.Entity, out CartridgeComponent? cartridge))
  278. cartridge.LoaderUid = uid;
  279. RaiseLocalEvent(args.Entity, new CartridgeAddedEvent(uid));
  280. base.OnItemInserted(uid, loader, args);
  281. }
  282. protected override void OnItemRemoved(EntityUid uid, CartridgeLoaderComponent loader, EntRemovedFromContainerMessage args)
  283. {
  284. if (args.Container.ID != InstalledContainerId && args.Container.ID != loader.CartridgeSlot.ID)
  285. return;
  286. var deactivate = loader.BackgroundPrograms.Remove(args.Entity);
  287. if (loader.ActiveProgram == args.Entity)
  288. {
  289. loader.ActiveProgram = default;
  290. deactivate = true;
  291. }
  292. if (deactivate)
  293. RaiseLocalEvent(args.Entity, new CartridgeDeactivatedEvent(uid));
  294. if (TryComp(args.Entity, out CartridgeComponent? cartridge))
  295. cartridge.LoaderUid = null;
  296. RaiseLocalEvent(args.Entity, new CartridgeRemovedEvent(uid));
  297. base.OnItemRemoved(uid, loader, args);
  298. _pda.UpdatePdaUi(uid);
  299. }
  300. /// <summary>
  301. /// Installs programs from the list of preinstalled programs
  302. /// </summary>
  303. private void OnMapInit(EntityUid uid, CartridgeLoaderComponent component, MapInitEvent args)
  304. {
  305. // TODO remove this and use container fill.
  306. foreach (var prototype in component.PreinstalledPrograms)
  307. {
  308. InstallProgram(uid, prototype, deinstallable: false);
  309. }
  310. }
  311. private void OnUsed(EntityUid uid, CartridgeLoaderComponent component, AfterInteractEvent args)
  312. {
  313. RelayEvent(component, new CartridgeAfterInteractEvent(uid, args));
  314. }
  315. private void OnPacketReceived(EntityUid uid, CartridgeLoaderComponent component, DeviceNetworkPacketEvent args)
  316. {
  317. RelayEvent(component, new CartridgeDeviceNetPacketEvent(uid, args));
  318. }
  319. private void OnLoaderUiMessage(EntityUid loaderUid, CartridgeLoaderComponent component, CartridgeLoaderUiMessage message)
  320. {
  321. var cartridge = GetEntity(message.CartridgeUid);
  322. switch (message.Action)
  323. {
  324. case CartridgeUiMessageAction.Activate:
  325. ActivateProgram(loaderUid, cartridge, component);
  326. break;
  327. case CartridgeUiMessageAction.Deactivate:
  328. DeactivateProgram(loaderUid, cartridge, component);
  329. break;
  330. case CartridgeUiMessageAction.Install:
  331. InstallCartridge(loaderUid, cartridge, component);
  332. break;
  333. case CartridgeUiMessageAction.Uninstall:
  334. UninstallProgram(loaderUid, cartridge, component);
  335. break;
  336. case CartridgeUiMessageAction.UIReady:
  337. if (component.ActiveProgram.HasValue)
  338. RaiseLocalEvent(component.ActiveProgram.Value, new CartridgeUiReadyEvent(loaderUid));
  339. break;
  340. default:
  341. throw new ArgumentOutOfRangeException($"Unrecognized UI action passed from cartridge loader ui {message.Action}.");
  342. }
  343. }
  344. /// <summary>
  345. /// Relays ui messages meant for cartridges to the currently active cartridge
  346. /// </summary>
  347. private void OnUiMessage(EntityUid uid, CartridgeLoaderComponent component, CartridgeUiMessage args)
  348. {
  349. var cartridgeEvent = args.MessageEvent;
  350. cartridgeEvent.User = args.Actor;
  351. cartridgeEvent.LoaderUid = GetNetEntity(uid);
  352. cartridgeEvent.Actor = args.Actor;
  353. RelayEvent(component, cartridgeEvent, true);
  354. }
  355. /// <summary>
  356. /// Relays events to the currently active program and and programs running in the background.
  357. /// Skips background programs if "skipBackgroundPrograms" is set to true
  358. /// </summary>
  359. /// <param name="loader">The cartritge loader component</param>
  360. /// <param name="args">The event to be relayed</param>
  361. /// <param name="skipBackgroundPrograms">Whether to skip relaying the event to programs running in the background</param>
  362. private void RelayEvent<TEvent>(CartridgeLoaderComponent loader, TEvent args, bool skipBackgroundPrograms = false) where TEvent : notnull
  363. {
  364. if (loader.ActiveProgram.HasValue)
  365. RaiseLocalEvent(loader.ActiveProgram.Value, args);
  366. if (skipBackgroundPrograms)
  367. return;
  368. foreach (var program in loader.BackgroundPrograms)
  369. {
  370. //Prevent programs registered as running in the background receiving events twice if they are active
  371. if (loader.ActiveProgram.HasValue && loader.ActiveProgram.Value.Equals(program))
  372. continue;
  373. RaiseLocalEvent(program, args);
  374. }
  375. }
  376. /// <summary>
  377. /// Shortcut for updating the loaders user interface state without passing in a subtype of <see cref="CartridgeLoaderUiState"/>
  378. /// like the <see cref="PDA.PdaSystem"/> does when updating its ui state
  379. /// </summary>
  380. /// <seealso cref="PDA.PdaSystem.UpdatePdaUserInterface"/>
  381. private void UpdateUserInterfaceState(EntityUid loaderUid, CartridgeLoaderComponent loader)
  382. {
  383. UpdateUiState(loaderUid, null, loader);
  384. }
  385. private void UpdateCartridgeInstallationStatus(EntityUid cartridgeUid, InstallationStatus installationStatus, CartridgeComponent cartridgeComponent)
  386. {
  387. cartridgeComponent.InstallationStatus = installationStatus;
  388. Dirty(cartridgeUid, cartridgeComponent);
  389. }
  390. private bool HasProgram(EntityUid loader, EntityUid program, CartridgeLoaderComponent component)
  391. {
  392. return component.CartridgeSlot.Item == program || GetInstalled(loader).Contains(program);
  393. }
  394. }
  395. /// <summary>
  396. /// Gets sent to running programs when the cartridge loader receives a device net package
  397. /// </summary>
  398. /// <seealso cref="DeviceNetworkPacketEvent"/>
  399. public sealed class CartridgeDeviceNetPacketEvent : EntityEventArgs
  400. {
  401. public readonly EntityUid Loader;
  402. public readonly DeviceNetworkPacketEvent PacketEvent;
  403. public CartridgeDeviceNetPacketEvent(EntityUid loader, DeviceNetworkPacketEvent packetEvent)
  404. {
  405. Loader = loader;
  406. PacketEvent = packetEvent;
  407. }
  408. }
  409. /// <summary>
  410. /// Gets sent to running programs when the cartridge loader receives an after interact event
  411. /// </summary>
  412. /// <seealso cref="AfterInteractEvent"/>
  413. public sealed class CartridgeAfterInteractEvent : EntityEventArgs
  414. {
  415. public readonly EntityUid Loader;
  416. public readonly AfterInteractEvent InteractEvent;
  417. public CartridgeAfterInteractEvent(EntityUid loader, AfterInteractEvent interactEvent)
  418. {
  419. Loader = loader;
  420. InteractEvent = interactEvent;
  421. }
  422. }
  423. /// <summary>
  424. /// Raised on an attempt of program installation.
  425. /// </summary>
  426. [ByRefEvent]
  427. public record struct ProgramInstallationAttempt(EntityUid LoaderUid, string Prototype, bool Cancelled = false);