SharedDeviceLinkSystem.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. using Content.Shared.Administration.Logs;
  2. using Content.Shared.Database;
  3. using Content.Shared.DeviceLinking.Events;
  4. using Content.Shared.DeviceNetwork;
  5. using Content.Shared.Popups;
  6. using Robust.Shared.Prototypes;
  7. using Robust.Shared.Utility;
  8. namespace Content.Shared.DeviceLinking;
  9. public abstract class SharedDeviceLinkSystem : EntitySystem
  10. {
  11. [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
  12. [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
  13. [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
  14. [Dependency] private readonly SharedTransformSystem _transform = default!;
  15. public const string InvokedPort = "link_port";
  16. /// <inheritdoc/>
  17. public override void Initialize()
  18. {
  19. SubscribeLocalEvent<DeviceLinkSourceComponent, ComponentStartup>(OnSourceStartup);
  20. SubscribeLocalEvent<DeviceLinkSourceComponent, ComponentRemove>(OnSourceRemoved);
  21. SubscribeLocalEvent<DeviceLinkSinkComponent, ComponentRemove>(OnSinkRemoved);
  22. }
  23. #region Link Validation
  24. /// <summary>
  25. /// Removes invalid links where the saved sink doesn't exist/have a sink component for example
  26. /// </summary>
  27. private void OnSourceStartup(Entity<DeviceLinkSourceComponent> source, ref ComponentStartup args)
  28. {
  29. List<EntityUid> invalidSinks = new();
  30. List<(string, string)> invalidLinks = new();
  31. foreach (var (sink, links) in source.Comp.LinkedPorts)
  32. {
  33. if (!TryComp(sink, out DeviceLinkSinkComponent? sinkComponent))
  34. {
  35. invalidSinks.Add(sink);
  36. continue;
  37. }
  38. foreach (var link in links)
  39. {
  40. if (sinkComponent.Ports.Contains(link.Sink) && source.Comp.Ports.Contains(link.Source))
  41. source.Comp.Outputs.GetOrNew(link.Source).Add(sink);
  42. else
  43. invalidLinks.Add(link);
  44. }
  45. foreach (var link in invalidLinks)
  46. {
  47. Log.Warning($"Device source {ToPrettyString(source)} contains invalid links to entity {ToPrettyString(sink)}: {link.Item1}->{link.Item2}");
  48. links.Remove(link);
  49. }
  50. if (links.Count == 0)
  51. {
  52. invalidSinks.Add(sink);
  53. continue;
  54. }
  55. invalidLinks.Clear();
  56. sinkComponent.LinkedSources.Add(source.Owner);
  57. }
  58. foreach (var sink in invalidSinks)
  59. {
  60. source.Comp.LinkedPorts.Remove(sink);
  61. Log.Warning($"Device source {ToPrettyString(source)} contains invalid sink: {ToPrettyString(sink)}");
  62. }
  63. }
  64. #endregion
  65. /// <summary>
  66. /// Ensures that its links get deleted when a source gets removed
  67. /// </summary>
  68. private void OnSourceRemoved(Entity<DeviceLinkSourceComponent> source, ref ComponentRemove args)
  69. {
  70. var query = GetEntityQuery<DeviceLinkSinkComponent>();
  71. foreach (var sinkUid in source.Comp.LinkedPorts.Keys)
  72. {
  73. if (query.TryGetComponent(sinkUid, out var sink))
  74. RemoveSinkFromSourceInternal(source, sinkUid, source, sink);
  75. else
  76. Log.Error($"Device source {ToPrettyString(source)} links to invalid entity: {ToPrettyString(sinkUid)}");
  77. }
  78. }
  79. /// <summary>
  80. /// Ensures that its links get deleted when a sink gets removed
  81. /// </summary>
  82. private void OnSinkRemoved(Entity<DeviceLinkSinkComponent> sink, ref ComponentRemove args)
  83. {
  84. foreach (var sourceUid in sink.Comp.LinkedSources)
  85. {
  86. if (TryComp(sourceUid, out DeviceLinkSourceComponent? source))
  87. RemoveSinkFromSourceInternal(sourceUid, sink, source, sink);
  88. else
  89. Log.Error($"Device sink {ToPrettyString(sink)} source list contains invalid entity: {ToPrettyString(sourceUid)}");
  90. }
  91. }
  92. #region Ports
  93. /// <summary>
  94. /// Convenience function to add several ports to an entity
  95. /// </summary>
  96. public void EnsureSourcePorts(EntityUid uid, params ProtoId<SourcePortPrototype>[] ports)
  97. {
  98. if (ports.Length == 0)
  99. return;
  100. var comp = EnsureComp<DeviceLinkSourceComponent>(uid);
  101. foreach (var port in ports)
  102. {
  103. if (!_prototypeManager.HasIndex(port))
  104. Log.Error($"Attempted to add invalid port {port} to {ToPrettyString(uid)}");
  105. else
  106. comp.Ports.Add(port);
  107. }
  108. }
  109. /// <summary>
  110. /// Convenience function to add several ports to an entity.
  111. /// </summary>
  112. public void EnsureSinkPorts(EntityUid uid, params ProtoId<SinkPortPrototype>[] ports)
  113. {
  114. if (ports.Length == 0)
  115. return;
  116. var comp = EnsureComp<DeviceLinkSinkComponent>(uid);
  117. foreach (var port in ports)
  118. {
  119. if (!_prototypeManager.HasIndex(port))
  120. Log.Error($"Attempted to add invalid port {port} to {ToPrettyString(uid)}");
  121. else
  122. comp.Ports.Add(port);
  123. }
  124. }
  125. /// <summary>
  126. /// Retrieves the available ports from a source
  127. /// </summary>
  128. /// <returns>A list of source port prototypes</returns>
  129. public List<SourcePortPrototype> GetSourcePorts(EntityUid sourceUid, DeviceLinkSourceComponent? sourceComponent = null)
  130. {
  131. if (!Resolve(sourceUid, ref sourceComponent))
  132. return new List<SourcePortPrototype>();
  133. var sourcePorts = new List<SourcePortPrototype>();
  134. foreach (var port in sourceComponent.Ports)
  135. {
  136. sourcePorts.Add(_prototypeManager.Index(port));
  137. }
  138. return sourcePorts;
  139. }
  140. /// <summary>
  141. /// Retrieves the available ports from a sink
  142. /// </summary>
  143. /// <returns>A list of sink port prototypes</returns>
  144. public List<SinkPortPrototype> GetSinkPorts(EntityUid sinkUid, DeviceLinkSinkComponent? sinkComponent = null)
  145. {
  146. if (!Resolve(sinkUid, ref sinkComponent))
  147. return new List<SinkPortPrototype>();
  148. var sinkPorts = new List<SinkPortPrototype>();
  149. foreach (var port in sinkComponent.Ports)
  150. {
  151. sinkPorts.Add(_prototypeManager.Index(port));
  152. }
  153. return sinkPorts;
  154. }
  155. /// <summary>
  156. /// Convenience function to retrieve the name of a port prototype
  157. /// </summary>
  158. public string PortName<TPort>(string port) where TPort : DevicePortPrototype, IPrototype
  159. {
  160. if (!_prototypeManager.TryIndex<TPort>(port, out var proto))
  161. return port;
  162. return Loc.GetString(proto.Name);
  163. }
  164. #endregion
  165. #region Links
  166. /// <summary>
  167. /// Returns the links of a source
  168. /// </summary>
  169. /// <returns>A list of sink and source port ids that are linked together</returns>
  170. public HashSet<(ProtoId<SourcePortPrototype> source, ProtoId<SinkPortPrototype> sink)> GetLinks(EntityUid sourceUid, EntityUid sinkUid, DeviceLinkSourceComponent? sourceComponent = null)
  171. {
  172. if (!Resolve(sourceUid, ref sourceComponent) || !sourceComponent.LinkedPorts.TryGetValue(sinkUid, out var links))
  173. return new HashSet<(ProtoId<SourcePortPrototype>, ProtoId<SinkPortPrototype>)>();
  174. return links;
  175. }
  176. /// <summary>
  177. /// Returns the default links for the given list of source port prototypes
  178. /// </summary>
  179. /// <param name="sources">The list of source port prototypes to get the default links for</param>
  180. /// <returns>A list of sink and source port ids</returns>
  181. public List<(string source, string sink)> GetDefaults(List<SourcePortPrototype> sources)
  182. {
  183. var defaults = new List<(string, string)>();
  184. foreach (var source in sources)
  185. {
  186. if (source.DefaultLinks == null)
  187. return new List<(string, string)>();
  188. foreach (var defaultLink in source.DefaultLinks)
  189. {
  190. defaults.Add((source.ID, defaultLink));
  191. }
  192. }
  193. return defaults;
  194. }
  195. /// <summary>
  196. /// Links the given source and sink by their default links
  197. /// </summary>
  198. /// <param name="userId">Optinal user uid for displaying popups</param>
  199. /// <param name="sourceUid">The source uid</param>
  200. /// <param name="sinkUid">The sink uid</param>
  201. /// <param name="sourceComponent"></param>
  202. /// <param name="sinkComponent"></param>
  203. public void LinkDefaults(
  204. EntityUid? userId,
  205. EntityUid sourceUid,
  206. EntityUid sinkUid,
  207. DeviceLinkSourceComponent? sourceComponent = null,
  208. DeviceLinkSinkComponent? sinkComponent = null)
  209. {
  210. if (!Resolve(sourceUid, ref sourceComponent) || !Resolve(sinkUid, ref sinkComponent))
  211. return;
  212. if (userId != null)
  213. _adminLogger.Add(LogType.DeviceLinking, LogImpact.Low, $"{ToPrettyString(userId.Value):actor} is linking defaults between {ToPrettyString(sourceUid):source} and {ToPrettyString(sinkUid):sink}");
  214. else
  215. _adminLogger.Add(LogType.DeviceLinking, LogImpact.Low, $"linking defaults between {ToPrettyString(sourceUid):source} and {ToPrettyString(sinkUid):sink}");
  216. var sourcePorts = GetSourcePorts(sourceUid, sourceComponent);
  217. var defaults = GetDefaults(sourcePorts);
  218. SaveLinks(userId, sourceUid, sinkUid, defaults, sourceComponent, sinkComponent);
  219. if (userId != null)
  220. _popupSystem.PopupCursor(Loc.GetString("signal-linking-verb-success", ("machine", sourceUid)), userId.Value);
  221. }
  222. /// <summary>
  223. /// Saves multiple links between a source and a sink device.
  224. /// Ignores links where either the source or sink port aren't present
  225. /// </summary>
  226. /// <param name="userId">Optinal user uid for displaying popups</param>
  227. /// <param name="sourceUid">The source uid</param>
  228. /// <param name="sinkUid">The sink uid</param>
  229. /// <param name="links">List of source and sink ids to link</param>
  230. /// <param name="sourceComponent"></param>
  231. /// <param name="sinkComponent"></param>
  232. public void SaveLinks(
  233. EntityUid? userId,
  234. EntityUid sourceUid,
  235. EntityUid sinkUid,
  236. List<(string source, string sink)> links,
  237. DeviceLinkSourceComponent? sourceComponent = null,
  238. DeviceLinkSinkComponent? sinkComponent = null)
  239. {
  240. if (!Resolve(sourceUid, ref sourceComponent) || !Resolve(sinkUid, ref sinkComponent))
  241. return;
  242. if (!InRange(sourceUid, sinkUid, sourceComponent.Range))
  243. {
  244. if (userId != null)
  245. _popupSystem.PopupCursor(Loc.GetString("signal-linker-component-out-of-range"), userId.Value);
  246. return;
  247. }
  248. RemoveSinkFromSource(sourceUid, sinkUid, sourceComponent);
  249. foreach (var (source, sink) in links)
  250. {
  251. DebugTools.Assert(_prototypeManager.HasIndex<SourcePortPrototype>(source));
  252. DebugTools.Assert(_prototypeManager.HasIndex<SinkPortPrototype>(sink));
  253. if (!sourceComponent.Ports.Contains(source) || !sinkComponent.Ports.Contains(sink))
  254. continue;
  255. if (!CanLink(userId, sourceUid, sinkUid, source, sink, false, sourceComponent))
  256. continue;
  257. sourceComponent.Outputs.GetOrNew(source).Add(sinkUid);
  258. sourceComponent.LinkedPorts.GetOrNew(sinkUid).Add((source, sink));
  259. SendNewLinkEvent(userId, sourceUid, source, sinkUid, sink);
  260. }
  261. if (links.Count > 0)
  262. sinkComponent.LinkedSources.Add(sourceUid);
  263. }
  264. /// <summary>
  265. /// Removes every link from the given sink
  266. /// </summary>
  267. public void RemoveAllFromSink(EntityUid sinkUid, DeviceLinkSinkComponent? sinkComponent = null)
  268. {
  269. if (!Resolve(sinkUid, ref sinkComponent))
  270. return;
  271. foreach (var sourceUid in sinkComponent.LinkedSources)
  272. {
  273. RemoveSinkFromSource(sourceUid, sinkUid, null, sinkComponent);
  274. }
  275. }
  276. /// <summary>
  277. /// Removes all links between a source and a sink
  278. /// </summary>
  279. public void RemoveSinkFromSource(
  280. EntityUid sourceUid,
  281. EntityUid sinkUid,
  282. DeviceLinkSourceComponent? sourceComponent = null,
  283. DeviceLinkSinkComponent? sinkComponent = null)
  284. {
  285. if (Resolve(sourceUid, ref sourceComponent, false) && Resolve(sinkUid, ref sinkComponent, false))
  286. {
  287. RemoveSinkFromSourceInternal(sourceUid, sinkUid, sourceComponent, sinkComponent);
  288. return;
  289. }
  290. if (sourceComponent == null && sinkComponent == null)
  291. {
  292. // Both were deleted?
  293. return;
  294. }
  295. if (sourceComponent == null)
  296. {
  297. Log.Error($"Attempted to remove link between {ToPrettyString(sourceUid)} and {ToPrettyString(sinkUid)}, but the source component was missing.");
  298. sinkComponent!.LinkedSources.Remove(sourceUid);
  299. }
  300. else
  301. {
  302. Log.Error($"Attempted to remove link between {ToPrettyString(sourceUid)} and {ToPrettyString(sinkUid)}, but the sink component was missing.");
  303. sourceComponent.LinkedPorts.Remove(sinkUid);
  304. }
  305. }
  306. private void RemoveSinkFromSourceInternal(
  307. EntityUid sourceUid,
  308. EntityUid sinkUid,
  309. DeviceLinkSourceComponent sourceComponent,
  310. DeviceLinkSinkComponent sinkComponent)
  311. {
  312. // This function gets called on component removal. Beware that TryComp & Resolve may return false.
  313. if (sourceComponent.LinkedPorts.TryGetValue(sinkUid, out var ports))
  314. {
  315. foreach (var (sourcePort, sinkPort) in ports)
  316. {
  317. RaiseLocalEvent(sourceUid, new PortDisconnectedEvent(sourcePort));
  318. RaiseLocalEvent(sinkUid, new PortDisconnectedEvent(sinkPort));
  319. }
  320. }
  321. sinkComponent.LinkedSources.Remove(sourceUid);
  322. sourceComponent.LinkedPorts.Remove(sinkUid);
  323. foreach (var outputList in sourceComponent.Outputs.Values)
  324. {
  325. outputList.Remove(sinkUid);
  326. }
  327. }
  328. /// <summary>
  329. /// Adds or removes a link depending on if it's already present
  330. /// </summary>
  331. /// <returns>True if the link was successfully added or removed</returns>
  332. public bool ToggleLink(
  333. EntityUid? userId,
  334. EntityUid sourceUid,
  335. EntityUid sinkUid,
  336. string source,
  337. string sink,
  338. DeviceLinkSourceComponent? sourceComponent = null,
  339. DeviceLinkSinkComponent? sinkComponent = null)
  340. {
  341. if (!Resolve(sourceUid, ref sourceComponent) || !Resolve(sinkUid, ref sinkComponent))
  342. return false;
  343. var outputs = sourceComponent.Outputs.GetOrNew(source);
  344. var linkedPorts = sourceComponent.LinkedPorts.GetOrNew(sinkUid);
  345. if (linkedPorts.Contains((source, sink)))
  346. {
  347. if (userId != null)
  348. _adminLogger.Add(LogType.DeviceLinking, LogImpact.Low, $"{ToPrettyString(userId.Value):actor} unlinked {ToPrettyString(sourceUid):source} {source} and {ToPrettyString(sinkUid):sink} {sink}");
  349. else
  350. _adminLogger.Add(LogType.DeviceLinking, LogImpact.Low, $"unlinked {ToPrettyString(sourceUid):source} {source} and {ToPrettyString(sinkUid):sink} {sink}");
  351. RaiseLocalEvent(sourceUid, new PortDisconnectedEvent(source));
  352. RaiseLocalEvent(sinkUid, new PortDisconnectedEvent(sink));
  353. outputs.Remove(sinkUid);
  354. linkedPorts.Remove((source, sink));
  355. if (linkedPorts.Count != 0)
  356. return true;
  357. sourceComponent.LinkedPorts.Remove(sinkUid);
  358. sinkComponent.LinkedSources.Remove(sourceUid);
  359. CreateLinkPopup(userId, sourceUid, source, sinkUid, sink, true);
  360. }
  361. else
  362. {
  363. if (!sourceComponent.Ports.Contains(source) || !sinkComponent.Ports.Contains(sink))
  364. return false;
  365. if (!CanLink(userId, sourceUid, sinkUid, source, sink, true, sourceComponent))
  366. return false;
  367. outputs.Add(sinkUid);
  368. linkedPorts.Add((source, sink));
  369. sinkComponent.LinkedSources.Add(sourceUid);
  370. SendNewLinkEvent(userId, sourceUid, source, sinkUid, sink);
  371. CreateLinkPopup(userId, sourceUid, source, sinkUid, sink, false);
  372. }
  373. return true;
  374. }
  375. /// <summary>
  376. /// Checks if a source and a sink can be linked by allowing other systems to veto the link
  377. /// and by optionally checking if they are in range of each other
  378. /// </summary>
  379. /// <returns></returns>
  380. private bool CanLink(
  381. EntityUid? userId,
  382. EntityUid sourceUid,
  383. EntityUid sinkUid,
  384. string source,
  385. string sink,
  386. bool checkRange = true,
  387. DeviceLinkSourceComponent? sourceComponent = null)
  388. {
  389. if (!Resolve(sourceUid, ref sourceComponent))
  390. return false;
  391. if (checkRange && !InRange(sourceUid, sinkUid, sourceComponent.Range))
  392. {
  393. if (userId.HasValue)
  394. _popupSystem.PopupCursor(Loc.GetString("signal-linker-component-out-of-range"), userId.Value);
  395. return false;
  396. }
  397. var linkAttemptEvent = new LinkAttemptEvent(userId, sourceUid, source, sinkUid, sink);
  398. RaiseLocalEvent(sourceUid, linkAttemptEvent, true);
  399. if (linkAttemptEvent.Cancelled && userId.HasValue)
  400. {
  401. _popupSystem.PopupCursor(Loc.GetString("signal-linker-component-connection-refused", ("machine", source)), userId.Value);
  402. return false;
  403. }
  404. RaiseLocalEvent(sinkUid, linkAttemptEvent, true);
  405. if (linkAttemptEvent.Cancelled && userId.HasValue)
  406. {
  407. _popupSystem.PopupCursor(Loc.GetString("signal-linker-component-connection-refused", ("machine", source)), userId.Value);
  408. return false;
  409. }
  410. return !linkAttemptEvent.Cancelled;
  411. }
  412. private bool InRange(EntityUid sourceUid, EntityUid sinkUid, float range)
  413. {
  414. // TODO: This should be using an existing method and also coordinates inrange instead.
  415. return _transform.GetMapCoordinates(sourceUid).InRange(_transform.GetMapCoordinates(sinkUid), range);
  416. }
  417. private void SendNewLinkEvent(EntityUid? user, EntityUid sourceUid, string source, EntityUid sinkUid, string sink)
  418. {
  419. if (user != null)
  420. _adminLogger.Add(LogType.DeviceLinking, LogImpact.Low, $"{ToPrettyString(user.Value):actor} linked {ToPrettyString(sourceUid):source} {source} and {ToPrettyString(sinkUid):sink} {sink}");
  421. else
  422. _adminLogger.Add(LogType.DeviceLinking, LogImpact.Low, $"linked {ToPrettyString(sourceUid):source} {source} and {ToPrettyString(sinkUid):sink} {sink}");
  423. var newLinkEvent = new NewLinkEvent(user, sourceUid, source, sinkUid, sink);
  424. RaiseLocalEvent(sourceUid, newLinkEvent);
  425. RaiseLocalEvent(sinkUid, newLinkEvent);
  426. }
  427. private void CreateLinkPopup(EntityUid? userId, EntityUid sourceUid, string source, EntityUid sinkUid, string sink, bool removed)
  428. {
  429. if (!userId.HasValue)
  430. return;
  431. var locString = removed ? "signal-linker-component-unlinked-port" : "signal-linker-component-linked-port";
  432. _popupSystem.PopupCursor(Loc.GetString(locString, ("machine1", sourceUid), ("port1", PortName<SourcePortPrototype>(source)),
  433. ("machine2", sinkUid), ("port2", PortName<SinkPortPrototype>(sink))), userId.Value, PopupType.Medium);
  434. }
  435. #endregion
  436. #region Sending & Receiving
  437. /// <summary>
  438. /// Sends a network payload directed at the sink entity.
  439. /// Just raises a <see cref="SignalReceivedEvent"/> without data if the source or the sink doesn't have a <see cref="DeviceNetworkComponent"/>
  440. /// </summary>
  441. /// <param name="uid">The source uid that invokes the port</param>
  442. /// <param name="port">The port to invoke</param>
  443. /// <param name="data">Optional data to send along</param>
  444. /// <param name="sourceComponent"></param>
  445. public virtual void InvokePort(EntityUid uid, string port, NetworkPayload? data = null,
  446. DeviceLinkSourceComponent? sourceComponent = null)
  447. {
  448. // NOOP on client for the moment.
  449. }
  450. #endregion
  451. }