SharedAutodocSystem.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  1. // SPDX-FileCopyrightText: 2024 Piras314 <p1r4s@proton.me>
  2. // SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
  3. // SPDX-FileCopyrightText: 2025 JohnOakman <sremy2012@hotmail.fr>
  4. // SPDX-FileCopyrightText: 2025 deltanedas <39013340+deltanedas@users.noreply.github.com>
  5. // SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
  6. //
  7. // SPDX-License-Identifier: AGPL-3.0-or-later
  8. using Content.Shared._Shitmed.Autodoc.Components;
  9. using Content.Shared._Shitmed.Medical.Surgery;
  10. using Content.Shared._Shitmed.Medical.Surgery.Steps;
  11. using Content.Shared.Administration.Logs;
  12. using Content.Shared.Bed.Sleep;
  13. using Content.Shared.Body.Part;
  14. using Content.Shared.Body.Systems;
  15. using Content.Shared.Buckle.Components;
  16. using Content.Shared.Database;
  17. using Content.Shared.DeviceLinking.Events;
  18. using Content.Shared.Hands.Components;
  19. using Content.Shared.Hands.EntitySystems;
  20. using Content.Shared.Labels.EntitySystems;
  21. using Content.Shared.Mobs.Systems;
  22. using Content.Shared.Storage;
  23. using Content.Shared.Storage.EntitySystems;
  24. using Content.Shared.Whitelist;
  25. using Robust.Shared.Prototypes;
  26. using Robust.Shared.Timing;
  27. using System.Linq;
  28. namespace Content.Shared._Shitmed.Autodoc.Systems;
  29. public abstract class SharedAutodocSystem : EntitySystem
  30. {
  31. [Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
  32. [Dependency] protected readonly IGameTiming Timing = default!;
  33. [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
  34. [Dependency] private readonly MobStateSystem _mobState = default!;
  35. [Dependency] private readonly SharedBodySystem _body = default!;
  36. [Dependency] private readonly SharedHandsSystem _hands = default!;
  37. [Dependency] private readonly SharedLabelSystem _label = default!;
  38. [Dependency] private readonly SharedStorageSystem _storage = default!;
  39. [Dependency] private readonly SharedSurgerySystem _surgery = default!;
  40. [Dependency] private readonly SleepingSystem _sleeping = default!;
  41. public override void Initialize()
  42. {
  43. base.Initialize();
  44. SubscribeLocalEvent<AutodocComponent, NewLinkEvent>(OnNewLink);
  45. SubscribeLocalEvent<AutodocComponent, PortDisconnectedEvent>(OnPortDisconnected);
  46. Subs.BuiEvents<AutodocComponent>(AutodocUiKey.Key, s =>
  47. {
  48. s.Event<AutodocCreateProgramMessage>(OnCreateProgram);
  49. s.Event<AutodocToggleProgramSafetyMessage>(OnToggleProgramSafety);
  50. s.Event<AutodocRemoveProgramMessage>(OnRemoveProgram);
  51. s.Event<AutodocAddStepMessage>(OnAddStep);
  52. s.Event<AutodocRemoveStepMessage>(OnRemoveStep);
  53. s.Event<AutodocStartMessage>(OnStart);
  54. s.Event<AutodocStopMessage>(OnStop);
  55. s.Event<AutodocImportProgramMessage>(OnImportProgram);
  56. });
  57. SubscribeLocalEvent<ActiveAutodocComponent, SurgeryStepEvent>(OnSurgeryStep);
  58. SubscribeLocalEvent<ActiveAutodocComponent, SurgeryStepFailedEvent>(OnSurgeryStepFailed);
  59. SubscribeLocalEvent<ActiveAutodocComponent, ComponentShutdown>(OnActiveShutdown);
  60. }
  61. private void OnNewLink(Entity<AutodocComponent> ent, ref NewLinkEvent args)
  62. {
  63. if (args.SinkPort == ent.Comp.OperatingTablePort &&
  64. HasComp<OperatingTableComponent>(args.Source))
  65. {
  66. ent.Comp.OperatingTable = args.Source;
  67. Dirty(ent);
  68. }
  69. }
  70. private void OnPortDisconnected(Entity<AutodocComponent> ent, ref PortDisconnectedEvent args)
  71. {
  72. if (args.Port != ent.Comp.OperatingTablePort)
  73. return;
  74. ent.Comp.OperatingTable = null;
  75. Dirty(ent);
  76. }
  77. #region UI Handling
  78. private void OnCreateProgram(Entity<AutodocComponent> ent, ref AutodocCreateProgramMessage args)
  79. {
  80. CreateProgram(ent, args.Title);
  81. }
  82. private void OnToggleProgramSafety(Entity<AutodocComponent> ent, ref AutodocToggleProgramSafetyMessage args)
  83. {
  84. if (IsActive(ent))
  85. return;
  86. if (args.Program >= ent.Comp.Programs.Count)
  87. return;
  88. var program = ent.Comp.Programs[args.Program];
  89. program.SkipFailed ^= true;
  90. Dirty(ent);
  91. _adminLogger.Add(LogType.InteractActivate, LogImpact.Low, $"{ToPrettyString(args.Actor):user} toggled safety of autodoc program {program.Title}");
  92. }
  93. private void OnRemoveProgram(Entity<AutodocComponent> ent, ref AutodocRemoveProgramMessage args)
  94. {
  95. RemoveProgram(ent, args.Program);
  96. }
  97. private void OnAddStep(Entity<AutodocComponent> ent, ref AutodocAddStepMessage args)
  98. {
  99. if (!args.Step.Validate(ent, this))
  100. {
  101. Log.Warning($"User {ToPrettyString(args.Actor)} tried to add an invalid autodoc step!");
  102. return;
  103. }
  104. AddStep(ent, args.Program, args.Step, args.Index, args.Actor);
  105. }
  106. private void OnRemoveStep(Entity<AutodocComponent> ent, ref AutodocRemoveStepMessage args)
  107. {
  108. RemoveStep(ent, args.Program, args.Step);
  109. }
  110. private void OnStart(Entity<AutodocComponent> ent, ref AutodocStartMessage args)
  111. {
  112. StartProgram(ent, args.Program, args.Actor);
  113. }
  114. private void OnStop(Entity<AutodocComponent> ent, ref AutodocStopMessage args)
  115. {
  116. RemComp<ActiveAutodocComponent>(ent);
  117. }
  118. private void OnImportProgram(Entity<AutodocComponent> ent, ref AutodocImportProgramMessage args)
  119. {
  120. ImportProgram(ent, args.Program, args.Actor);
  121. }
  122. #endregion
  123. private void OnSurgeryStep(Entity<ActiveAutodocComponent> ent, ref SurgeryStepEvent args)
  124. {
  125. if (!TryComp<AutodocComponent>(ent, out var comp))
  126. return;
  127. var repeatable = HasComp<SurgeryRepeatableStepComponent>(args.Step);
  128. if (args.Complete || !repeatable)
  129. {
  130. ent.Comp.Waiting = false; // try the next autodoc or surgery step
  131. return;
  132. }
  133. // for tend wounds dont abort, more wounds need tending
  134. if (HasComp<SurgeryRepeatableStepComponent>(args.Step))
  135. return;
  136. ent.Comp.Waiting = repeatable;
  137. }
  138. private void OnSurgeryStepFailed(Entity<ActiveAutodocComponent> ent, ref SurgeryStepFailedEvent args)
  139. {
  140. if (!TryComp<AutodocComponent>(ent, out var comp))
  141. return;
  142. var program = comp.Programs[ent.Comp.CurrentProgram];
  143. var error = Loc.GetString("autodoc-error-surgery-failed");
  144. if (program.SkipFailed)
  145. {
  146. Say(ent, Loc.GetString("autodoc-error", ("error", error)));
  147. ent.Comp.ProgramStep++;
  148. }
  149. else
  150. {
  151. Say(ent, Loc.GetString("autodoc-fatal-error", ("error", error)));
  152. RemCompDeferred<ActiveAutodocComponent>(ent);
  153. }
  154. }
  155. private void OnActiveShutdown(Entity<ActiveAutodocComponent> ent, ref ComponentShutdown args)
  156. {
  157. if (!TryComp<AutodocComponent>(ent, out var comp))
  158. return;
  159. // wake the patient when program completes or errors out
  160. if (GetPatient((ent.Owner, comp)) is {} patient)
  161. WakePatient(patient);
  162. }
  163. protected virtual void WakePatient(EntityUid patient)
  164. {
  165. _sleeping.TryWaking(patient);
  166. }
  167. #region Step API
  168. public bool IsSurgery(EntProtoId id)
  169. {
  170. // this is O(n) so with a fuck ton of surgeries it could slow down the server
  171. return _surgery.AllSurgeries.Contains(id);
  172. }
  173. public EntityUid? FindItem(EntityUid uid, string name)
  174. {
  175. var storage = Comp<StorageComponent>(uid);
  176. foreach (var item in storage.Container.ContainedEntities)
  177. {
  178. if (Name(item) == name)
  179. return item;
  180. }
  181. return null;
  182. }
  183. public EntityUid? FindItem(EntityUid uid, EntityWhitelist? whitelist)
  184. {
  185. var storage = Comp<StorageComponent>(uid);
  186. foreach (var item in storage.Container.ContainedEntities)
  187. {
  188. if (_whitelist.IsWhitelistPassOrNull(whitelist, item))
  189. return item;
  190. }
  191. return null;
  192. }
  193. public bool GrabItem(Entity<AutodocComponent, HandsComponent> ent, EntityUid item)
  194. {
  195. return _hands.TryPickup(ent, item, ent.Comp1.ItemSlot, animate: false, handsComp: ent.Comp2);
  196. }
  197. public void GrabItemOrThrow(Entity<AutodocComponent, HandsComponent> ent, EntityUid item)
  198. {
  199. if (!GrabItem(ent, item))
  200. throw new AutodocError("hand-full");
  201. }
  202. public void StoreItemOrThrow(Entity<AutodocComponent, HandsComponent> ent)
  203. {
  204. var item = GetHeldOrThrow(ent);
  205. if (!_storage.Insert(ent, item, out _))
  206. throw new AutodocError("storage-full");
  207. }
  208. public EntityUid GetHeldOrThrow(Entity<AutodocComponent, HandsComponent> ent)
  209. {
  210. if (!_hands.TryGetHand(ent, ent.Comp1.ItemSlot, out var hand, ent.Comp2))
  211. throw new AutodocError("item-unavailable");
  212. if (hand.HeldEntity is not {} item)
  213. throw new AutodocError("item-unavailable");
  214. return item;
  215. }
  216. public void LabelItem(EntityUid item, string label)
  217. {
  218. _label.Label(item, label);
  219. }
  220. public void DelayUpdate(EntityUid uid, TimeSpan delay)
  221. {
  222. if (TryComp<ActiveAutodocComponent>(uid, out var active))
  223. active.NextUpdate += delay;
  224. }
  225. public EntityUid? GetPatient(Entity<AutodocComponent> ent)
  226. {
  227. if (!TryComp<StrapComponent>(ent.Comp.OperatingTable, out var strap))
  228. return null;
  229. var buckled = strap.BuckledEntities;
  230. if (buckled.Count == 0)
  231. return null;
  232. var patient = buckled.First();
  233. if (!HasComp<SurgeryTargetComponent>(patient))
  234. return null; // TODO: auto draping anything with a body
  235. return patient;
  236. }
  237. public EntityUid GetPatientOrThrow(Entity<AutodocComponent> ent)
  238. {
  239. if (GetPatient(ent) is not {} patient)
  240. throw new AutodocError("missing-patient");
  241. return patient;
  242. }
  243. public EntityUid? FindPart(EntityUid patient, BodyPartType type, BodyPartSymmetry? symmetry)
  244. {
  245. foreach (var ent in _body.GetBodyChildrenOfType(patient, type, symmetry: symmetry))
  246. {
  247. return ent.Id;
  248. }
  249. return null;
  250. }
  251. /// <summary>
  252. /// Starts doing a surgery, returns true if successful.
  253. /// </summary>
  254. public bool StartSurgery(Entity<AutodocComponent> ent, EntityUid patient, EntityUid part, EntProtoId surgery)
  255. {
  256. if (ent.Comp.RequireSleeping && IsAwake(patient))
  257. throw new AutodocError("patient-unsedated");
  258. if (_surgery.GetSingleton(surgery) is not {} singleton)
  259. return false;
  260. if (_surgery.GetNextStep(patient, part, singleton) is not {} pair)
  261. return false;
  262. var nextSurgery = pair.Item1;
  263. var index = pair.Item2;
  264. var nextStep = nextSurgery.Comp.Steps[index];
  265. if (!_surgery.TryDoSurgeryStep(patient, part, ent, MetaData(nextSurgery).EntityPrototype!.ID, nextStep))
  266. return false;
  267. Comp<ActiveAutodocComponent>(ent).CurrentSurgery = (patient, part, surgery);
  268. return true;
  269. }
  270. public bool IsAwake(EntityUid uid)
  271. {
  272. return _mobState.IsAlive(uid) && !HasComp<SleepingComponent>(uid);
  273. }
  274. /// <summary>
  275. /// Creates a new program and populates it using another AutodocProgram.
  276. /// Will return false on fail. True on success.
  277. /// </summary>
  278. public bool ImportProgram(Entity<AutodocComponent> ent, AutodocProgram program, EntityUid user)
  279. {
  280. var idx = CreateProgram(ent, program.Title);
  281. if (!idx.HasValue)
  282. return false;
  283. for (int key = 0; key < program.Steps.Count; ++key)
  284. {
  285. if (!program.Steps[key].Validate(ent, this))
  286. {
  287. Log.Warning($"User {ToPrettyString(user)} tried to add an invalid autodoc step!");
  288. return false;
  289. }
  290. AddStep(ent, idx.Value, program.Steps[key], key, user);
  291. }
  292. return true;
  293. }
  294. /// <summary>
  295. /// Create a blank program and return the index to it.
  296. /// Programs cannot be created while operating or if there are too many, in which case it will return null.
  297. /// </summary>
  298. public int? CreateProgram(Entity<AutodocComponent> ent, string title)
  299. {
  300. var index = ent.Comp.Programs.Count;
  301. if (IsActive(ent) || index >= ent.Comp.MaxPrograms)
  302. return null;
  303. if (string.IsNullOrEmpty(title) || title.Length > ent.Comp.MaxProgramTitleLength)
  304. return null;
  305. ent.Comp.Programs.Add(new AutodocProgram()
  306. {
  307. Title = title
  308. });
  309. Dirty(ent);
  310. return index;
  311. }
  312. /// <summary>
  313. /// Removes a program at an index, returning true if it succeeded.
  314. /// </summary>
  315. public bool RemoveProgram(Entity<AutodocComponent> ent, int index)
  316. {
  317. if (IsActive(ent) || index >= ent.Comp.Programs.Count)
  318. return false;
  319. ent.Comp.Programs.RemoveAt(index);
  320. Dirty(ent);
  321. return true;
  322. }
  323. /// <summary>
  324. /// Adds a step to a program at an index, returning true if it succeeded.
  325. /// </summary>
  326. public bool AddStep(Entity<AutodocComponent> ent, int programIndex, IAutodocStep step, int index, EntityUid user)
  327. {
  328. if (IsActive(ent) || programIndex >= ent.Comp.Programs.Count)
  329. return false;
  330. var program = ent.Comp.Programs[programIndex];
  331. if (program.Steps.Count >= ent.Comp.MaxProgramSteps || index < 0 || index > program.Steps.Count)
  332. return false;
  333. program.Steps.Insert(index, step);
  334. Dirty(ent);
  335. _adminLogger.Add(LogType.InteractActivate, LogImpact.Low, $"{ToPrettyString(user):user} added step '{step.Title}' to autodoc program '{program.Title}'");
  336. return true;
  337. }
  338. /// <summary>
  339. /// Removes a step from a program, returning true if it succeeded.
  340. /// </summary>
  341. public bool RemoveStep(Entity<AutodocComponent> ent, int programIndex, int step)
  342. {
  343. if (IsActive(ent) || programIndex >= ent.Comp.Programs.Count)
  344. return false;
  345. var program = ent.Comp.Programs[programIndex];
  346. if (step >= program.Steps.Count)
  347. return false;
  348. program.Steps.RemoveAt(step);
  349. Dirty(ent);
  350. return true;
  351. }
  352. public bool IsActive(EntityUid uid)
  353. {
  354. return HasComp<ActiveAutodocComponent>(uid);
  355. }
  356. public AutodocProgram CurrentProgram(Entity<AutodocComponent, ActiveAutodocComponent> ent)
  357. {
  358. // not checking if it exists since Programs isnt allowed to be changed while operating
  359. return ent.Comp1.Programs[ent.Comp2.CurrentProgram];
  360. }
  361. public bool StartProgram(Entity<AutodocComponent> ent, int index, EntityUid user)
  362. {
  363. // no error since UI checks this too
  364. if (IsActive(ent) || index >= ent.Comp.Programs.Count || GetPatient(ent) is not {} patient)
  365. return false;
  366. var active = EnsureComp<ActiveAutodocComponent>(ent);
  367. active.CurrentProgram = index;
  368. active.NextUpdate = Timing.CurTime + ent.Comp.UpdateDelay;
  369. Dirty(ent.Owner, active);
  370. _adminLogger.Add(LogType.InteractActivate, LogImpact.High, $"{ToPrettyString(user):user} started autodoc program '{ent.Comp.Programs[index].Title}' on {ToPrettyString(patient):patient}");
  371. return true;
  372. }
  373. /// <summary>
  374. /// Tries to start the next step, shouting the error if it fails.
  375. /// Returns true if the program is being stopped.
  376. /// </summary>
  377. public bool Proceed(Entity<AutodocComponent, ActiveAutodocComponent> ent)
  378. {
  379. if (ent.Comp2.Waiting)
  380. return false;
  381. // stay on this AutodocSurgeryStep until every step of the surgery (and its dependencies) is complete
  382. // if this was the last step, StartSurgery will fail and the next autodoc step will run
  383. if (ent.Comp2.CurrentSurgery is {} args)
  384. {
  385. var (body, part, surgery) = args;
  386. if (StartSurgery((ent.Owner, ent.Comp1), body, part, surgery))
  387. {
  388. ent.Comp2.Waiting = true;
  389. return false;
  390. }
  391. // done with the surgery onto next step!!!
  392. ent.Comp2.CurrentSurgery = null;
  393. ent.Comp2.ProgramStep++;
  394. }
  395. var program = ent.Comp1.Programs[ent.Comp2.CurrentProgram];
  396. var index = ent.Comp2.ProgramStep;
  397. if (index >= program.Steps.Count)
  398. {
  399. Say(ent, Loc.GetString("autodoc-program-completed"));
  400. return true;
  401. }
  402. try
  403. {
  404. var step = program.Steps[index];
  405. if (step.Run((ent.Owner, ent.Comp1, Comp<HandsComponent>(ent)), this))
  406. ent.Comp2.ProgramStep++;
  407. else
  408. ent.Comp2.Waiting = true;
  409. }
  410. catch (AutodocError e)
  411. {
  412. var error = Loc.GetString("autodoc-error-" + e.Message);
  413. if (program.SkipFailed)
  414. {
  415. Say(ent, Loc.GetString("autodoc-error", ("error", error)));
  416. ent.Comp2.ProgramStep++;
  417. }
  418. else
  419. {
  420. Say(ent, Loc.GetString("autodoc-fatal-error", ("error", error)));
  421. return true;
  422. }
  423. }
  424. Dirty(ent.Owner, ent.Comp1);
  425. return false;
  426. }
  427. #endregion
  428. public virtual void Say(EntityUid uid, string msg)
  429. {
  430. }
  431. public void SetSafety(Entity<AutodocComponent> ent, bool enabled)
  432. {
  433. if (enabled == ent.Comp.RequireSleeping)
  434. return;
  435. ent.Comp.RequireSleeping = enabled;
  436. Dirty(ent);
  437. }
  438. }
  439. /// <summary>
  440. /// Error autodoc steps can use to abort the program execution and shout an error message.
  441. /// </summary>
  442. public sealed class AutodocError : Exception
  443. {
  444. /// <summary>
  445. /// Message has "autodoc-error-" prepended to it, then it gets localized.
  446. /// </summary>
  447. public AutodocError(string message) : base(message)
  448. {
  449. }
  450. }