AmeControllerSystem.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. using System.Diagnostics.CodeAnalysis;
  2. using System.Linq;
  3. using Content.Server.Administration.Logs;
  4. using Content.Server.Administration.Managers;
  5. using Content.Server.Ame.Components;
  6. using Content.Server.Chat.Managers;
  7. using Content.Server.NodeContainer;
  8. using Content.Server.Power.Components;
  9. using Content.Shared.Ame.Components;
  10. using Content.Shared.Containers.ItemSlots;
  11. using Content.Shared.Database;
  12. using Content.Shared.Mind.Components;
  13. using Content.Shared.Power;
  14. using Robust.Server.GameObjects;
  15. using Robust.Shared.Audio;
  16. using Robust.Shared.Audio.Systems;
  17. using Robust.Shared.Containers;
  18. using Robust.Shared.Player;
  19. using Robust.Shared.Timing;
  20. namespace Content.Server.Ame.EntitySystems;
  21. public sealed class AmeControllerSystem : EntitySystem
  22. {
  23. [Dependency] private readonly IAdminLogManager _adminLogger = default!;
  24. [Dependency] private readonly IGameTiming _gameTiming = default!;
  25. [Dependency] private readonly AppearanceSystem _appearanceSystem = default!;
  26. [Dependency] private readonly SharedAudioSystem _audioSystem = default!;
  27. [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
  28. [Dependency] private readonly ItemSlotsSystem _itemSlots = default!;
  29. public override void Initialize()
  30. {
  31. base.Initialize();
  32. SubscribeLocalEvent<AmeControllerComponent, ComponentInit>(OnInit);
  33. SubscribeLocalEvent<AmeControllerComponent, ComponentRemove>(OnRemove);
  34. SubscribeLocalEvent<AmeControllerComponent, EntInsertedIntoContainerMessage>(OnItemSlotChanged);
  35. SubscribeLocalEvent<AmeControllerComponent, EntRemovedFromContainerMessage>(OnItemSlotChanged);
  36. SubscribeLocalEvent<AmeControllerComponent, PowerChangedEvent>(OnPowerChanged);
  37. SubscribeLocalEvent<AmeControllerComponent, UiButtonPressedMessage>(OnUiButtonPressed);
  38. }
  39. private void OnInit(EntityUid uid, AmeControllerComponent component, ComponentInit args)
  40. {
  41. _itemSlots.AddItemSlot(uid, SharedAmeControllerComponent.FuelSlotId, component.FuelSlot);
  42. UpdateUi(uid, component);
  43. }
  44. public override void Update(float frameTime)
  45. {
  46. var curTime = _gameTiming.CurTime;
  47. var query = EntityQueryEnumerator<AmeControllerComponent, NodeContainerComponent>();
  48. while (query.MoveNext(out var uid, out var controller, out var nodes))
  49. {
  50. if (controller.NextUpdate <= curTime)
  51. UpdateController(uid, curTime, controller, nodes);
  52. else if (controller.NextUIUpdate <= curTime)
  53. UpdateUi(uid, controller);
  54. }
  55. }
  56. private void OnRemove(EntityUid uid, AmeControllerComponent component, ComponentRemove args)
  57. {
  58. _itemSlots.RemoveItemSlot(uid, component.FuelSlot);
  59. }
  60. private void OnItemSlotChanged(EntityUid uid, AmeControllerComponent component, ContainerModifiedMessage args)
  61. {
  62. if (!component.Initialized)
  63. return;
  64. if (args.Container.ID != component.FuelSlot.ID)
  65. return;
  66. UpdateUi(uid, component);
  67. }
  68. private void UpdateController(EntityUid uid, TimeSpan curTime, AmeControllerComponent? controller = null, NodeContainerComponent? nodes = null)
  69. {
  70. if (!Resolve(uid, ref controller))
  71. return;
  72. controller.LastUpdate = curTime;
  73. controller.NextUpdate = curTime + controller.UpdatePeriod;
  74. // update the UI regardless of other factors to update the power readings
  75. UpdateUi(uid, controller);
  76. if (!controller.Injecting)
  77. return;
  78. if (!TryGetAMENodeGroup(uid, out var group, nodes))
  79. return;
  80. if (TryComp<AmeFuelContainerComponent>(controller.FuelSlot.Item, out var fuelContainer))
  81. {
  82. // if the jar is empty shut down the AME
  83. if (fuelContainer.FuelAmount <= 0)
  84. {
  85. SetInjecting(uid, false, null, controller);
  86. }
  87. else
  88. {
  89. var availableInject = Math.Min(controller.InjectionAmount, fuelContainer.FuelAmount);
  90. var powerOutput = group.InjectFuel(availableInject, out var overloading);
  91. if (TryComp<PowerSupplierComponent>(uid, out var powerOutlet))
  92. powerOutlet.MaxSupply = powerOutput;
  93. fuelContainer.FuelAmount -= availableInject;
  94. // only play audio if we actually had an injection
  95. if (availableInject > 0)
  96. _audioSystem.PlayPvs(controller.InjectSound, uid, AudioParams.Default.WithVolume(overloading ? 10f : 0f));
  97. UpdateUi(uid, controller);
  98. }
  99. }
  100. controller.Stability = group.GetTotalStability();
  101. group.UpdateCoreVisuals();
  102. UpdateDisplay(uid, controller.Stability, controller);
  103. if (controller.Stability <= 0)
  104. group.ExplodeCores();
  105. }
  106. public void UpdateUi(EntityUid uid, AmeControllerComponent? controller = null)
  107. {
  108. if (!Resolve(uid, ref controller))
  109. return;
  110. if (!_userInterfaceSystem.HasUi(uid, AmeControllerUiKey.Key))
  111. return;
  112. var state = GetUiState(uid, controller);
  113. _userInterfaceSystem.SetUiState(uid, AmeControllerUiKey.Key, state);
  114. controller.NextUIUpdate = _gameTiming.CurTime + controller.UpdateUIPeriod;
  115. }
  116. private AmeControllerBoundUserInterfaceState GetUiState(EntityUid uid, AmeControllerComponent controller)
  117. {
  118. var powered = !TryComp<ApcPowerReceiverComponent>(uid, out var powerSource) || powerSource.Powered;
  119. var coreCount = 0;
  120. // how much power can be produced at the current settings, in kW
  121. // we don't use max. here since this is what is set in the Controller, not what the AME is actually producing
  122. float targetedPowerSupply = 0;
  123. if (TryGetAMENodeGroup(uid, out var group))
  124. {
  125. coreCount = group.CoreCount;
  126. targetedPowerSupply = group.CalculatePower(controller.InjectionAmount, group.CoreCount) / 1000;
  127. }
  128. // set current power statistics in kW
  129. float currentPowerSupply = 0;
  130. if (TryComp<PowerSupplierComponent>(uid, out var powerOutlet) && coreCount > 0)
  131. {
  132. currentPowerSupply = powerOutlet.CurrentSupply / 1000;
  133. }
  134. var fuelContainerInSlot = controller.FuelSlot.Item;
  135. var hasFuelContainerInSlot = Exists(fuelContainerInSlot);
  136. if (!hasFuelContainerInSlot || !TryComp<AmeFuelContainerComponent>(fuelContainerInSlot, out var fuelContainer))
  137. return new AmeControllerBoundUserInterfaceState(powered,
  138. IsMasterController(uid),
  139. false,
  140. hasFuelContainerInSlot,
  141. 0,
  142. controller.InjectionAmount,
  143. coreCount,
  144. currentPowerSupply,
  145. targetedPowerSupply);
  146. return new AmeControllerBoundUserInterfaceState(powered,
  147. IsMasterController(uid),
  148. controller.Injecting,
  149. hasFuelContainerInSlot,
  150. fuelContainer.FuelAmount,
  151. controller.InjectionAmount,
  152. coreCount,
  153. currentPowerSupply,
  154. targetedPowerSupply);
  155. }
  156. private bool IsMasterController(EntityUid uid)
  157. {
  158. return TryGetAMENodeGroup(uid, out var group) && group.MasterController == uid;
  159. }
  160. private bool TryGetAMENodeGroup(EntityUid uid, [MaybeNullWhen(false)] out AmeNodeGroup group, NodeContainerComponent? nodes = null)
  161. {
  162. if (!Resolve(uid, ref nodes))
  163. {
  164. group = null;
  165. return false;
  166. }
  167. group = nodes.Nodes.Values
  168. .Select(node => node.NodeGroup)
  169. .OfType<AmeNodeGroup>()
  170. .FirstOrDefault();
  171. return group != null;
  172. }
  173. public void TryEject(EntityUid uid, EntityUid? user = null, AmeControllerComponent? controller = null)
  174. {
  175. if (!Resolve(uid, ref controller))
  176. return;
  177. if (controller.Injecting)
  178. return;
  179. if (!Exists(controller.FuelSlot.Item))
  180. return;
  181. _itemSlots.TryEjectToHands(uid, controller.FuelSlot, user);
  182. UpdateUi(uid, controller);
  183. }
  184. public void SetInjecting(EntityUid uid, bool value, EntityUid? user = null, AmeControllerComponent? controller = null)
  185. {
  186. if (!Resolve(uid, ref controller))
  187. return;
  188. if (controller.Injecting == value)
  189. return;
  190. controller.Injecting = value;
  191. UpdateDisplay(uid, controller.Stability, controller);
  192. if (!value && TryComp<PowerSupplierComponent>(uid, out var powerOut))
  193. powerOut.MaxSupply = 0;
  194. UpdateUi(uid, controller);
  195. _itemSlots.SetLock(uid, controller.FuelSlot, value);
  196. // Logging
  197. if (!HasComp<MindContainerComponent>(user))
  198. return;
  199. var humanReadableState = value ? "Inject" : "Not inject";
  200. _adminLogger.Add(LogType.Action, LogImpact.Extreme, $"{EntityManager.ToPrettyString(user.Value):player} has set the AME to {humanReadableState}");
  201. }
  202. public void ToggleInjecting(EntityUid uid, EntityUid? user = null, AmeControllerComponent? controller = null)
  203. {
  204. if (!Resolve(uid, ref controller))
  205. return;
  206. SetInjecting(uid, !controller.Injecting, user, controller);
  207. }
  208. public void SetInjectionAmount(EntityUid uid, int value, EntityUid? user = null, AmeControllerComponent? controller = null)
  209. {
  210. if (!Resolve(uid, ref controller))
  211. return;
  212. if (controller.InjectionAmount == value)
  213. return;
  214. var oldValue = controller.InjectionAmount;
  215. controller.InjectionAmount = value;
  216. UpdateUi(uid, controller);
  217. // Logging
  218. if (!TryComp<MindContainerComponent>(user, out var mindContainer))
  219. return;
  220. var humanReadableState = controller.Injecting ? "Inject" : "Not inject";
  221. _adminLogger.Add(LogType.Action, LogImpact.Extreme, $"{EntityManager.ToPrettyString(user.Value):player} has set the AME to inject {controller.InjectionAmount} while set to {humanReadableState}");
  222. /* This needs to be information which an admin is very likely to want to be informed about in order to be an admin alert or have a sound notification.
  223. At the time of editing, players regularly "overclock" the AME and those cases require no admin attention.
  224. // Admin alert
  225. var safeLimit = int.MaxValue;
  226. if (TryGetAMENodeGroup(uid, out var group))
  227. safeLimit = group.CoreCount * 4;
  228. if (oldValue <= safeLimit && value > safeLimit)
  229. {
  230. if (_gameTiming.CurTime > controller.EffectCooldown)
  231. {
  232. _chatManager.SendAdminAlert(user.Value, $"increased AME over safe limit to {controller.InjectionAmount}");
  233. _audioSystem.PlayGlobal("/Audio/Misc/adminlarm.ogg",
  234. Filter.Empty().AddPlayers(_adminManager.ActiveAdmins), false, AudioParams.Default.WithVolume(-8f));
  235. controller.EffectCooldown = _gameTiming.CurTime + controller.CooldownDuration;
  236. }
  237. }
  238. */
  239. }
  240. public void AdjustInjectionAmount(EntityUid uid, int delta, EntityUid? user = null, AmeControllerComponent? controller = null)
  241. {
  242. if (!Resolve(uid, ref controller))
  243. return;
  244. var max = GetMaxInjectionAmount((uid, controller));
  245. SetInjectionAmount(uid, MathHelper.Clamp(controller.InjectionAmount + delta, 0, max), user, controller);
  246. }
  247. public int GetMaxInjectionAmount(Entity<AmeControllerComponent> ent)
  248. {
  249. if (!TryGetAMENodeGroup(ent, out var group))
  250. return 0;
  251. return group.CoreCount * 8;
  252. }
  253. private void UpdateDisplay(EntityUid uid, int stability, AmeControllerComponent? controller = null, AppearanceComponent? appearance = null)
  254. {
  255. if (!Resolve(uid, ref controller, ref appearance))
  256. return;
  257. var ameControllerState = stability switch
  258. {
  259. < 10 => AmeControllerState.Fuck,
  260. < 50 => AmeControllerState.Critical,
  261. < 80 => AmeControllerState.Warning,
  262. _ => AmeControllerState.On,
  263. };
  264. if (!controller.Injecting)
  265. ameControllerState = AmeControllerState.Off;
  266. _appearanceSystem.SetData(
  267. uid,
  268. AmeControllerVisuals.DisplayState,
  269. ameControllerState,
  270. appearance
  271. );
  272. }
  273. private void OnPowerChanged(EntityUid uid, AmeControllerComponent comp, ref PowerChangedEvent args)
  274. {
  275. UpdateUi(uid, comp);
  276. }
  277. private void OnUiButtonPressed(EntityUid uid, AmeControllerComponent comp, UiButtonPressedMessage msg)
  278. {
  279. var user = msg.Actor;
  280. if (!Exists(user))
  281. return;
  282. var needsPower = msg.Button switch
  283. {
  284. UiButton.Eject => false,
  285. _ => true,
  286. };
  287. if (!PlayerCanUseController(uid, user, needsPower, comp))
  288. return;
  289. _audioSystem.PlayPvs(comp.ClickSound, uid, AudioParams.Default.WithVolume(-2f));
  290. switch (msg.Button)
  291. {
  292. case UiButton.Eject:
  293. TryEject(uid, user: user, controller: comp);
  294. break;
  295. case UiButton.ToggleInjection:
  296. ToggleInjecting(uid, user: user, controller: comp);
  297. break;
  298. case UiButton.IncreaseFuel:
  299. AdjustInjectionAmount(uid, +2, user: user, controller: comp);
  300. break;
  301. case UiButton.DecreaseFuel:
  302. AdjustInjectionAmount(uid, -2, user: user, controller: comp);
  303. break;
  304. }
  305. if (TryGetAMENodeGroup(uid, out var group))
  306. group.UpdateCoreVisuals();
  307. UpdateUi(uid, comp);
  308. }
  309. /// <summary>
  310. /// Checks whether the player entity is able to use the controller.
  311. /// </summary>
  312. /// <param name="playerEntity">The player entity.</param>
  313. /// <returns>Returns true if the entity can use the controller, and false if it cannot.</returns>
  314. private bool PlayerCanUseController(EntityUid uid, EntityUid playerEntity, bool needsPower = true, AmeControllerComponent? controller = null)
  315. {
  316. if (!Resolve(uid, ref controller))
  317. return false;
  318. //Need player entity to check if they are still able to use the dispenser
  319. if (!Exists(playerEntity))
  320. return false;
  321. //Check if device is powered
  322. if (needsPower && TryComp<ApcPowerReceiverComponent>(uid, out var powerSource) && !powerSource.Powered)
  323. return false;
  324. return true;
  325. }
  326. }