ContainmentFieldGeneratorSystem.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. using Content.Server.Administration.Logs;
  2. using Content.Server.Popups;
  3. using Content.Server.Singularity.Events;
  4. using Content.Shared.Construction.Components;
  5. using Content.Shared.Database;
  6. using Content.Shared.Examine;
  7. using Content.Shared.Interaction;
  8. using Content.Shared.Popups;
  9. using Content.Shared.Singularity.Components;
  10. using Content.Shared.Tag;
  11. using Robust.Server.GameObjects;
  12. using Robust.Shared.Physics;
  13. using Robust.Shared.Physics.Components;
  14. using Robust.Shared.Physics.Events;
  15. namespace Content.Server.Singularity.EntitySystems;
  16. public sealed class ContainmentFieldGeneratorSystem : EntitySystem
  17. {
  18. [Dependency] private readonly IAdminLogManager _adminLogger = default!;
  19. [Dependency] private readonly AppearanceSystem _visualizer = default!;
  20. [Dependency] private readonly PhysicsSystem _physics = default!;
  21. [Dependency] private readonly PopupSystem _popupSystem = default!;
  22. [Dependency] private readonly SharedPointLightSystem _light = default!;
  23. [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
  24. [Dependency] private readonly TagSystem _tags = default!;
  25. public override void Initialize()
  26. {
  27. base.Initialize();
  28. SubscribeLocalEvent<ContainmentFieldGeneratorComponent, StartCollideEvent>(HandleGeneratorCollide);
  29. SubscribeLocalEvent<ContainmentFieldGeneratorComponent, ExaminedEvent>(OnExamine);
  30. SubscribeLocalEvent<ContainmentFieldGeneratorComponent, ActivateInWorldEvent>(OnActivate);
  31. SubscribeLocalEvent<ContainmentFieldGeneratorComponent, AnchorStateChangedEvent>(OnAnchorChanged);
  32. SubscribeLocalEvent<ContainmentFieldGeneratorComponent, ReAnchorEvent>(OnReanchorEvent);
  33. SubscribeLocalEvent<ContainmentFieldGeneratorComponent, UnanchorAttemptEvent>(OnUnanchorAttempt);
  34. SubscribeLocalEvent<ContainmentFieldGeneratorComponent, ComponentRemove>(OnComponentRemoved);
  35. SubscribeLocalEvent<ContainmentFieldGeneratorComponent, EventHorizonAttemptConsumeEntityEvent>(PreventBreach);
  36. SubscribeLocalEvent<ContainmentFieldGeneratorComponent, MapInitEvent>(OnMapInit);
  37. }
  38. public override void Update(float frameTime)
  39. {
  40. base.Update(frameTime);
  41. var query = EntityQueryEnumerator<ContainmentFieldGeneratorComponent>();
  42. while (query.MoveNext(out var uid, out var generator))
  43. {
  44. if (generator.PowerBuffer <= 0) //don't drain power if there's no power, or if it's somehow less than 0.
  45. continue;
  46. generator.Accumulator += frameTime;
  47. if (generator.Accumulator >= generator.Threshold)
  48. {
  49. LosePower((uid, generator), generator.PowerLoss);
  50. generator.Accumulator -= generator.Threshold;
  51. }
  52. }
  53. }
  54. #region Events
  55. private void OnMapInit(Entity<ContainmentFieldGeneratorComponent> generator, ref MapInitEvent args)
  56. {
  57. if (generator.Comp.Enabled)
  58. ChangeFieldVisualizer(generator);
  59. }
  60. /// <summary>
  61. /// A generator receives power from a source colliding with it.
  62. /// </summary>
  63. private void HandleGeneratorCollide(Entity<ContainmentFieldGeneratorComponent> generator, ref StartCollideEvent args)
  64. {
  65. if (args.OtherFixtureId == generator.Comp.SourceFixtureId &&
  66. _tags.HasTag(args.OtherEntity, generator.Comp.IDTag))
  67. {
  68. ReceivePower(generator.Comp.PowerReceived, generator);
  69. generator.Comp.Accumulator = 0f;
  70. }
  71. }
  72. private void OnExamine(EntityUid uid, ContainmentFieldGeneratorComponent component, ExaminedEvent args)
  73. {
  74. if (component.Enabled)
  75. args.PushMarkup(Loc.GetString("comp-containment-on"));
  76. else
  77. args.PushMarkup(Loc.GetString("comp-containment-off"));
  78. }
  79. private void OnActivate(Entity<ContainmentFieldGeneratorComponent> generator, ref ActivateInWorldEvent args)
  80. {
  81. if (args.Handled)
  82. return;
  83. if (TryComp(generator, out TransformComponent? transformComp) && transformComp.Anchored)
  84. {
  85. if (!generator.Comp.Enabled)
  86. TurnOn(generator);
  87. else if (generator.Comp.Enabled && generator.Comp.IsConnected)
  88. {
  89. _popupSystem.PopupEntity(Loc.GetString("comp-containment-toggle-warning"), args.User, args.User, PopupType.LargeCaution);
  90. return;
  91. }
  92. else
  93. TurnOff(generator);
  94. }
  95. args.Handled = true;
  96. }
  97. private void OnAnchorChanged(Entity<ContainmentFieldGeneratorComponent> generator, ref AnchorStateChangedEvent args)
  98. {
  99. if (!args.Anchored)
  100. RemoveConnections(generator);
  101. }
  102. private void OnReanchorEvent(Entity<ContainmentFieldGeneratorComponent> generator, ref ReAnchorEvent args)
  103. {
  104. GridCheck(generator);
  105. }
  106. private void OnUnanchorAttempt(EntityUid uid, ContainmentFieldGeneratorComponent component,
  107. UnanchorAttemptEvent args)
  108. {
  109. if (component.Enabled || component.IsConnected)
  110. {
  111. _popupSystem.PopupEntity(Loc.GetString("comp-containment-anchor-warning"), args.User, args.User, PopupType.LargeCaution);
  112. args.Cancel();
  113. }
  114. }
  115. private void TurnOn(Entity<ContainmentFieldGeneratorComponent> generator)
  116. {
  117. generator.Comp.Enabled = true;
  118. ChangeFieldVisualizer(generator);
  119. _popupSystem.PopupEntity(Loc.GetString("comp-containment-turned-on"), generator);
  120. }
  121. private void TurnOff(Entity<ContainmentFieldGeneratorComponent> generator)
  122. {
  123. generator.Comp.Enabled = false;
  124. ChangeFieldVisualizer(generator);
  125. _popupSystem.PopupEntity(Loc.GetString("comp-containment-turned-off"), generator);
  126. }
  127. private void OnComponentRemoved(Entity<ContainmentFieldGeneratorComponent> generator, ref ComponentRemove args)
  128. {
  129. RemoveConnections(generator);
  130. }
  131. /// <summary>
  132. /// Deletes the fields and removes the respective connections for the generators.
  133. /// </summary>
  134. private void RemoveConnections(Entity<ContainmentFieldGeneratorComponent> generator)
  135. {
  136. var (uid, component) = generator;
  137. foreach (var (direction, value) in component.Connections)
  138. {
  139. foreach (var field in value.Item2)
  140. {
  141. QueueDel(field);
  142. }
  143. value.Item1.Comp.Connections.Remove(direction.GetOpposite());
  144. if (value.Item1.Comp.Connections.Count == 0) //Change isconnected only if there's no more connections
  145. {
  146. value.Item1.Comp.IsConnected = false;
  147. ChangeOnLightVisualizer(value.Item1);
  148. }
  149. ChangeFieldVisualizer(value.Item1);
  150. }
  151. component.Connections.Clear();
  152. if (component.IsConnected)
  153. _popupSystem.PopupEntity(Loc.GetString("comp-containment-disconnected"), uid, PopupType.LargeCaution);
  154. component.IsConnected = false;
  155. ChangeOnLightVisualizer(generator);
  156. ChangeFieldVisualizer(generator);
  157. _adminLogger.Add(LogType.FieldGeneration, LogImpact.Medium, $"{ToPrettyString(uid)} lost field connections"); // Ideally LogImpact would depend on if there is a singulo nearby
  158. }
  159. #endregion
  160. #region Connections
  161. /// <summary>
  162. /// Stores power in the generator. If it hits the threshold, it tries to establish a connection.
  163. /// </summary>
  164. /// <param name="power">The power that this generator received from the collision in <see cref="HandleGeneratorCollide"/></param>
  165. public void ReceivePower(int power, Entity<ContainmentFieldGeneratorComponent> generator)
  166. {
  167. var component = generator.Comp;
  168. component.PowerBuffer += power;
  169. var genXForm = Transform(generator);
  170. if (component.PowerBuffer >= component.PowerMinimum)
  171. {
  172. var directions = Enum.GetValues<Direction>().Length;
  173. for (int i = 0; i < directions-1; i+=2)
  174. {
  175. var dir = (Direction)i;
  176. if (component.Connections.ContainsKey(dir))
  177. continue; // This direction already has an active connection
  178. TryGenerateFieldConnection(dir, generator, genXForm);
  179. }
  180. }
  181. ChangePowerVisualizer(power, generator);
  182. }
  183. public void LosePower(Entity<ContainmentFieldGeneratorComponent> generator, int power)
  184. {
  185. var component = generator.Comp;
  186. component.PowerBuffer -= power;
  187. if (component.PowerBuffer < component.PowerMinimum && component.Connections.Count != 0)
  188. {
  189. RemoveConnections(generator);
  190. }
  191. ChangePowerVisualizer(power, generator);
  192. }
  193. /// <summary>
  194. /// This will attempt to establish a connection of fields between two generators.
  195. /// If all the checks pass and fields spawn, it will store this connection on each respective generator.
  196. /// </summary>
  197. /// <param name="dir">The field generator establishes a connection in this direction.</param>
  198. /// <param name="generator">The field generator component</param>
  199. /// <param name="gen1XForm">The transform component for the first generator</param>
  200. /// <returns></returns>
  201. private bool TryGenerateFieldConnection(Direction dir, Entity<ContainmentFieldGeneratorComponent> generator, TransformComponent gen1XForm)
  202. {
  203. var component = generator.Comp;
  204. if (!component.Enabled)
  205. return false;
  206. if (!gen1XForm.Anchored)
  207. return false;
  208. var genWorldPosRot = _transformSystem.GetWorldPositionRotation(gen1XForm);
  209. var dirRad = dir.ToAngle() + genWorldPosRot.WorldRotation; //needs to be like this for the raycast to work properly
  210. var ray = new CollisionRay(genWorldPosRot.WorldPosition, dirRad.ToVec(), component.CollisionMask);
  211. var rayCastResults = _physics.IntersectRay(gen1XForm.MapID, ray, component.MaxLength, generator, false);
  212. var genQuery = GetEntityQuery<ContainmentFieldGeneratorComponent>();
  213. RayCastResults? closestResult = null;
  214. foreach (var result in rayCastResults)
  215. {
  216. if (genQuery.HasComponent(result.HitEntity))
  217. closestResult = result;
  218. break;
  219. }
  220. if (closestResult == null)
  221. return false;
  222. var ent = closestResult.Value.HitEntity;
  223. if (!TryComp<ContainmentFieldGeneratorComponent>(ent, out var otherFieldGeneratorComponent) ||
  224. otherFieldGeneratorComponent == component ||
  225. !TryComp<PhysicsComponent>(ent, out var collidableComponent) ||
  226. collidableComponent.BodyType != BodyType.Static ||
  227. gen1XForm.ParentUid != Transform(ent).ParentUid)
  228. {
  229. return false;
  230. }
  231. var otherFieldGenerator = (ent, otherFieldGeneratorComponent);
  232. var fields = GenerateFieldConnection(generator, otherFieldGenerator);
  233. component.Connections[dir] = (otherFieldGenerator, fields);
  234. otherFieldGeneratorComponent.Connections[dir.GetOpposite()] = (generator, fields);
  235. ChangeFieldVisualizer(otherFieldGenerator);
  236. if (!component.IsConnected)
  237. {
  238. component.IsConnected = true;
  239. ChangeOnLightVisualizer(generator);
  240. }
  241. if (!otherFieldGeneratorComponent.IsConnected)
  242. {
  243. otherFieldGeneratorComponent.IsConnected = true;
  244. ChangeOnLightVisualizer(otherFieldGenerator);
  245. }
  246. ChangeFieldVisualizer(generator);
  247. UpdateConnectionLights(generator);
  248. _popupSystem.PopupEntity(Loc.GetString("comp-containment-connected"), generator);
  249. return true;
  250. }
  251. /// <summary>
  252. /// Spawns fields between two generators if the <see cref="TryGenerateFieldConnection"/> finds two generators to connect.
  253. /// </summary>
  254. /// <param name="firstGen">The source field generator</param>
  255. /// <param name="secondGen">The second generator that the source is connected to</param>
  256. /// <returns></returns>
  257. private List<EntityUid> GenerateFieldConnection(Entity<ContainmentFieldGeneratorComponent> firstGen, Entity<ContainmentFieldGeneratorComponent> secondGen)
  258. {
  259. var fieldList = new List<EntityUid>();
  260. var gen1Coords = Transform(firstGen).Coordinates;
  261. var gen2Coords = Transform(secondGen).Coordinates;
  262. var delta = (gen2Coords - gen1Coords).Position;
  263. var dirVec = delta.Normalized();
  264. var stopDist = delta.Length();
  265. var currentOffset = dirVec;
  266. while (currentOffset.Length() < stopDist)
  267. {
  268. var currentCoords = gen1Coords.Offset(currentOffset);
  269. var newField = Spawn(firstGen.Comp.CreatedField, currentCoords);
  270. var fieldXForm = Transform(newField);
  271. _transformSystem.SetParent(newField, fieldXForm, firstGen);
  272. if (dirVec.GetDir() == Direction.East || dirVec.GetDir() == Direction.West)
  273. {
  274. var angle = fieldXForm.LocalPosition.ToAngle();
  275. var rotateBy90 = angle.Degrees + 90;
  276. var rotatedAngle = Angle.FromDegrees(rotateBy90);
  277. fieldXForm.LocalRotation = rotatedAngle;
  278. }
  279. fieldList.Add(newField);
  280. currentOffset += dirVec;
  281. }
  282. return fieldList;
  283. }
  284. /// <summary>
  285. /// Creates a light component for the spawned fields.
  286. /// </summary>
  287. public void UpdateConnectionLights(Entity<ContainmentFieldGeneratorComponent> generator)
  288. {
  289. if (_light.TryGetLight(generator, out var pointLightComponent))
  290. {
  291. _light.SetEnabled(generator, generator.Comp.Connections.Count > 0, pointLightComponent);
  292. }
  293. }
  294. /// <summary>
  295. /// Checks to see if this or the other gens connected to a new grid. If they did, remove connection.
  296. /// </summary>
  297. public void GridCheck(Entity<ContainmentFieldGeneratorComponent> generator)
  298. {
  299. var xFormQuery = GetEntityQuery<TransformComponent>();
  300. foreach (var (_, generators) in generator.Comp.Connections)
  301. {
  302. var gen1ParentGrid = xFormQuery.GetComponent(generator).ParentUid;
  303. var gent2ParentGrid = xFormQuery.GetComponent(generators.Item1).ParentUid;
  304. if (gen1ParentGrid != gent2ParentGrid)
  305. RemoveConnections(generator);
  306. }
  307. }
  308. #endregion
  309. #region VisualizerHelpers
  310. /// <summary>
  311. /// Check if a fields power falls between certain ranges to update the field gen visual for power.
  312. /// </summary>
  313. /// <param name="power"></param>
  314. /// <param name="generator"></param>
  315. private void ChangePowerVisualizer(int power, Entity<ContainmentFieldGeneratorComponent> generator)
  316. {
  317. var component = generator.Comp;
  318. _visualizer.SetData(generator, ContainmentFieldGeneratorVisuals.PowerLight, component.PowerBuffer switch
  319. {
  320. <= 0 => PowerLevelVisuals.NoPower,
  321. >= 25 => PowerLevelVisuals.HighPower,
  322. _ => (component.PowerBuffer < component.PowerMinimum)
  323. ? PowerLevelVisuals.LowPower
  324. : PowerLevelVisuals.MediumPower
  325. });
  326. }
  327. /// <summary>
  328. /// Check if a field has any or no connections and if it's enabled to toggle the field level light
  329. /// </summary>
  330. /// <param name="generator"></param>
  331. private void ChangeFieldVisualizer(Entity<ContainmentFieldGeneratorComponent> generator)
  332. {
  333. _visualizer.SetData(generator, ContainmentFieldGeneratorVisuals.FieldLight, generator.Comp.Connections.Count switch
  334. {
  335. >1 => FieldLevelVisuals.MultipleFields,
  336. 1 => FieldLevelVisuals.OneField,
  337. _ => generator.Comp.Enabled ? FieldLevelVisuals.On : FieldLevelVisuals.NoLevel
  338. });
  339. }
  340. private void ChangeOnLightVisualizer(Entity<ContainmentFieldGeneratorComponent> generator)
  341. {
  342. _visualizer.SetData(generator, ContainmentFieldGeneratorVisuals.OnLight, generator.Comp.IsConnected);
  343. }
  344. #endregion
  345. /// <summary>
  346. /// Prevents singularities from breaching containment if the containment field generator is connected.
  347. /// </summary>
  348. /// <param name="uid">The entity the singularity is trying to eat.</param>
  349. /// <param name="comp">The containment field generator the singularity is trying to eat.</param>
  350. /// <param name="args">The event arguments.</param>
  351. private void PreventBreach(EntityUid uid, ContainmentFieldGeneratorComponent comp, ref EventHorizonAttemptConsumeEntityEvent args)
  352. {
  353. if (args.Cancelled)
  354. return;
  355. if (comp.IsConnected && !args.EventHorizon.CanBreachContainment)
  356. args.Cancelled = true;
  357. }
  358. }