1
0

LatheSystem.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. using System.Diagnostics.CodeAnalysis;
  2. using System.Linq;
  3. using Content.Server.Administration.Logs;
  4. using Content.Server.Atmos.EntitySystems;
  5. using Content.Server.Fluids.EntitySystems;
  6. using Content.Server.Lathe.Components;
  7. using Content.Server.Materials;
  8. using Content.Server.Popups;
  9. using Content.Server.Power.Components;
  10. using Content.Server.Power.EntitySystems;
  11. using Content.Server.Stack;
  12. using Content.Shared.Atmos;
  13. using Content.Shared.Chemistry.Components;
  14. using Content.Shared.Chemistry.EntitySystems;
  15. using Content.Shared.Chemistry.Reagent;
  16. using Content.Shared.UserInterface;
  17. using Content.Shared.Database;
  18. using Content.Shared.Emag.Components;
  19. using Content.Shared.Emag.Systems;
  20. using Content.Shared.Examine;
  21. using Content.Shared.Lathe;
  22. using Content.Shared.Lathe.Prototypes;
  23. using Content.Shared.Materials;
  24. using Content.Shared.Power;
  25. using Content.Shared.ReagentSpeed;
  26. using Content.Shared.Research.Components;
  27. using Content.Shared.Research.Prototypes;
  28. using JetBrains.Annotations;
  29. using Robust.Server.Containers;
  30. using Robust.Server.GameObjects;
  31. using Robust.Shared.Audio.Systems;
  32. using Robust.Shared.Prototypes;
  33. using Robust.Shared.Timing;
  34. namespace Content.Server.Lathe
  35. {
  36. [UsedImplicitly]
  37. public sealed class LatheSystem : SharedLatheSystem
  38. {
  39. [Dependency] private readonly IGameTiming _timing = default!;
  40. [Dependency] private readonly IPrototypeManager _proto = default!;
  41. [Dependency] private readonly IAdminLogManager _adminLogger = default!;
  42. [Dependency] private readonly AtmosphereSystem _atmosphere = default!;
  43. [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
  44. [Dependency] private readonly SharedAudioSystem _audio = default!;
  45. [Dependency] private readonly ContainerSystem _container = default!;
  46. [Dependency] private readonly EmagSystem _emag = default!;
  47. [Dependency] private readonly UserInterfaceSystem _uiSys = default!;
  48. [Dependency] private readonly MaterialStorageSystem _materialStorage = default!;
  49. [Dependency] private readonly PopupSystem _popup = default!;
  50. [Dependency] private readonly PuddleSystem _puddle = default!;
  51. [Dependency] private readonly ReagentSpeedSystem _reagentSpeed = default!;
  52. [Dependency] private readonly SharedSolutionContainerSystem _solution = default!;
  53. [Dependency] private readonly StackSystem _stack = default!;
  54. [Dependency] private readonly TransformSystem _transform = default!;
  55. /// <summary>
  56. /// Per-tick cache
  57. /// </summary>
  58. private readonly List<GasMixture> _environments = new();
  59. private readonly HashSet<ProtoId<LatheRecipePrototype>> _availableRecipes = new();
  60. public override void Initialize()
  61. {
  62. base.Initialize();
  63. SubscribeLocalEvent<LatheComponent, GetMaterialWhitelistEvent>(OnGetWhitelist);
  64. SubscribeLocalEvent<LatheComponent, MapInitEvent>(OnMapInit);
  65. SubscribeLocalEvent<LatheComponent, PowerChangedEvent>(OnPowerChanged);
  66. SubscribeLocalEvent<LatheComponent, TechnologyDatabaseModifiedEvent>(OnDatabaseModified);
  67. SubscribeLocalEvent<LatheComponent, ResearchRegistrationChangedEvent>(OnResearchRegistrationChanged);
  68. SubscribeLocalEvent<LatheComponent, LatheQueueRecipeMessage>(OnLatheQueueRecipeMessage);
  69. SubscribeLocalEvent<LatheComponent, LatheSyncRequestMessage>(OnLatheSyncRequestMessage);
  70. SubscribeLocalEvent<LatheComponent, BeforeActivatableUIOpenEvent>((u, c, _) => UpdateUserInterfaceState(u, c));
  71. SubscribeLocalEvent<LatheComponent, MaterialAmountChangedEvent>(OnMaterialAmountChanged);
  72. SubscribeLocalEvent<TechnologyDatabaseComponent, LatheGetRecipesEvent>(OnGetRecipes);
  73. SubscribeLocalEvent<EmagLatheRecipesComponent, LatheGetRecipesEvent>(GetEmagLatheRecipes);
  74. SubscribeLocalEvent<LatheHeatProducingComponent, LatheStartPrintingEvent>(OnHeatStartPrinting);
  75. }
  76. public override void Update(float frameTime)
  77. {
  78. var query = EntityQueryEnumerator<LatheProducingComponent, LatheComponent>();
  79. while (query.MoveNext(out var uid, out var comp, out var lathe))
  80. {
  81. if (lathe.CurrentRecipe == null)
  82. continue;
  83. if (_timing.CurTime - comp.StartTime >= comp.ProductionLength)
  84. FinishProducing(uid, lathe);
  85. }
  86. var heatQuery = EntityQueryEnumerator<LatheHeatProducingComponent, LatheProducingComponent, TransformComponent>();
  87. while (heatQuery.MoveNext(out var uid, out var heatComp, out _, out var xform))
  88. {
  89. if (_timing.CurTime < heatComp.NextSecond)
  90. continue;
  91. heatComp.NextSecond += TimeSpan.FromSeconds(1);
  92. var position = _transform.GetGridTilePositionOrDefault((uid, xform));
  93. _environments.Clear();
  94. if (_atmosphere.GetTileMixture(xform.GridUid, xform.MapUid, position, true) is { } tileMix)
  95. _environments.Add(tileMix);
  96. if (xform.GridUid != null)
  97. {
  98. var enumerator = _atmosphere.GetAdjacentTileMixtures(xform.GridUid.Value, position, false, true);
  99. while (enumerator.MoveNext(out var mix))
  100. {
  101. _environments.Add(mix);
  102. }
  103. }
  104. if (_environments.Count > 0)
  105. {
  106. var heatPerTile = heatComp.EnergyPerSecond / _environments.Count;
  107. foreach (var env in _environments)
  108. {
  109. _atmosphere.AddHeat(env, heatPerTile);
  110. }
  111. }
  112. }
  113. }
  114. private void OnGetWhitelist(EntityUid uid, LatheComponent component, ref GetMaterialWhitelistEvent args)
  115. {
  116. if (args.Storage != uid)
  117. return;
  118. var materialWhitelist = new List<ProtoId<MaterialPrototype>>();
  119. var recipes = GetAvailableRecipes(uid, component, true);
  120. foreach (var id in recipes)
  121. {
  122. if (!_proto.TryIndex(id, out var proto))
  123. continue;
  124. foreach (var (mat, _) in proto.Materials)
  125. {
  126. if (!materialWhitelist.Contains(mat))
  127. {
  128. materialWhitelist.Add(mat);
  129. }
  130. }
  131. }
  132. var combined = args.Whitelist.Union(materialWhitelist).ToList();
  133. args.Whitelist = combined;
  134. }
  135. [PublicAPI]
  136. public bool TryGetAvailableRecipes(EntityUid uid, [NotNullWhen(true)] out List<ProtoId<LatheRecipePrototype>>? recipes, [NotNullWhen(true)] LatheComponent? component = null, bool getUnavailable = false)
  137. {
  138. recipes = null;
  139. if (!Resolve(uid, ref component))
  140. return false;
  141. recipes = GetAvailableRecipes(uid, component, getUnavailable);
  142. return true;
  143. }
  144. public List<ProtoId<LatheRecipePrototype>> GetAvailableRecipes(EntityUid uid, LatheComponent component, bool getUnavailable = false)
  145. {
  146. _availableRecipes.Clear();
  147. AddRecipesFromPacks(_availableRecipes, component.StaticPacks);
  148. var ev = new LatheGetRecipesEvent(uid, getUnavailable)
  149. {
  150. Recipes = _availableRecipes
  151. };
  152. RaiseLocalEvent(uid, ev);
  153. return ev.Recipes.ToList();
  154. }
  155. public bool TryAddToQueue(EntityUid uid, LatheRecipePrototype recipe, LatheComponent? component = null)
  156. {
  157. if (!Resolve(uid, ref component))
  158. return false;
  159. if (!CanProduce(uid, recipe, 1, component))
  160. return false;
  161. foreach (var (mat, amount) in recipe.Materials)
  162. {
  163. var adjustedAmount = recipe.ApplyMaterialDiscount
  164. ? (int) (-amount * component.MaterialUseMultiplier)
  165. : -amount;
  166. _materialStorage.TryChangeMaterialAmount(uid, mat, adjustedAmount);
  167. }
  168. component.Queue.Add(recipe);
  169. return true;
  170. }
  171. public bool TryStartProducing(EntityUid uid, LatheComponent? component = null)
  172. {
  173. if (!Resolve(uid, ref component))
  174. return false;
  175. if (component.CurrentRecipe != null || component.Queue.Count <= 0 || !this.IsPowered(uid, EntityManager))
  176. return false;
  177. var recipe = component.Queue.First();
  178. component.Queue.RemoveAt(0);
  179. var time = _reagentSpeed.ApplySpeed(uid, recipe.CompleteTime) * component.TimeMultiplier;
  180. var lathe = EnsureComp<LatheProducingComponent>(uid);
  181. lathe.StartTime = _timing.CurTime;
  182. lathe.ProductionLength = time;
  183. component.CurrentRecipe = recipe;
  184. var ev = new LatheStartPrintingEvent(recipe);
  185. RaiseLocalEvent(uid, ref ev);
  186. _audio.PlayPvs(component.ProducingSound, uid);
  187. UpdateRunningAppearance(uid, true);
  188. UpdateUserInterfaceState(uid, component);
  189. if (time == TimeSpan.Zero)
  190. {
  191. FinishProducing(uid, component, lathe);
  192. }
  193. return true;
  194. }
  195. public void FinishProducing(EntityUid uid, LatheComponent? comp = null, LatheProducingComponent? prodComp = null)
  196. {
  197. if (!Resolve(uid, ref comp, ref prodComp, false))
  198. return;
  199. if (comp.CurrentRecipe != null)
  200. {
  201. if (comp.CurrentRecipe.Result is { } resultProto)
  202. {
  203. var result = Spawn(resultProto, Transform(uid).Coordinates);
  204. _stack.TryMergeToContacts(result);
  205. }
  206. if (comp.CurrentRecipe.ResultReagents is { } resultReagents &&
  207. comp.ReagentOutputSlotId is { } slotId)
  208. {
  209. var toAdd = new Solution(
  210. resultReagents.Select(p => new ReagentQuantity(p.Key.Id, p.Value, null)));
  211. // dispense it in the container if we have it and dump it if we don't
  212. if (_container.TryGetContainer(uid, slotId, out var container) &&
  213. container.ContainedEntities.Count == 1 &&
  214. _solution.TryGetFitsInDispenser(container.ContainedEntities.First(), out var solution, out _))
  215. {
  216. _solution.AddSolution(solution.Value, toAdd);
  217. }
  218. else
  219. {
  220. _popup.PopupEntity(Loc.GetString("lathe-reagent-dispense-no-container", ("name", uid)), uid);
  221. _puddle.TrySpillAt(uid, toAdd, out _);
  222. }
  223. }
  224. }
  225. comp.CurrentRecipe = null;
  226. prodComp.StartTime = _timing.CurTime;
  227. if (!TryStartProducing(uid, comp))
  228. {
  229. RemCompDeferred(uid, prodComp);
  230. UpdateUserInterfaceState(uid, comp);
  231. UpdateRunningAppearance(uid, false);
  232. }
  233. }
  234. public void UpdateUserInterfaceState(EntityUid uid, LatheComponent? component = null)
  235. {
  236. if (!Resolve(uid, ref component))
  237. return;
  238. var producing = component.CurrentRecipe ?? component.Queue.FirstOrDefault();
  239. var state = new LatheUpdateState(GetAvailableRecipes(uid, component), component.Queue, producing);
  240. _uiSys.SetUiState(uid, LatheUiKey.Key, state);
  241. }
  242. /// <summary>
  243. /// Adds every unlocked recipe from each pack to the recipes list.
  244. /// </summary>
  245. public void AddRecipesFromDynamicPacks(ref LatheGetRecipesEvent args, TechnologyDatabaseComponent database, IEnumerable<ProtoId<LatheRecipePackPrototype>> packs)
  246. {
  247. foreach (var id in packs)
  248. {
  249. var pack = _proto.Index(id);
  250. foreach (var recipe in pack.Recipes)
  251. {
  252. if (args.getUnavailable || database.UnlockedRecipes.Contains(recipe))
  253. args.Recipes.Add(recipe);
  254. }
  255. }
  256. }
  257. private void OnGetRecipes(EntityUid uid, TechnologyDatabaseComponent component, LatheGetRecipesEvent args)
  258. {
  259. if (uid != args.Lathe || !TryComp<LatheComponent>(uid, out var latheComponent))
  260. return;
  261. AddRecipesFromDynamicPacks(ref args, component, latheComponent.DynamicPacks);
  262. }
  263. private void GetEmagLatheRecipes(EntityUid uid, EmagLatheRecipesComponent component, LatheGetRecipesEvent args)
  264. {
  265. if (uid != args.Lathe)
  266. return;
  267. if (!args.getUnavailable && !_emag.CheckFlag(uid, EmagType.Interaction))
  268. return;
  269. AddRecipesFromPacks(args.Recipes, component.EmagStaticPacks);
  270. if (TryComp<TechnologyDatabaseComponent>(uid, out var database))
  271. AddRecipesFromDynamicPacks(ref args, database, component.EmagDynamicPacks);
  272. }
  273. private void OnHeatStartPrinting(EntityUid uid, LatheHeatProducingComponent component, LatheStartPrintingEvent args)
  274. {
  275. component.NextSecond = _timing.CurTime;
  276. }
  277. private void OnMaterialAmountChanged(EntityUid uid, LatheComponent component, ref MaterialAmountChangedEvent args)
  278. {
  279. UpdateUserInterfaceState(uid, component);
  280. }
  281. /// <summary>
  282. /// Initialize the UI and appearance.
  283. /// Appearance requires initialization or the layers break
  284. /// </summary>
  285. private void OnMapInit(EntityUid uid, LatheComponent component, MapInitEvent args)
  286. {
  287. _appearance.SetData(uid, LatheVisuals.IsInserting, false);
  288. _appearance.SetData(uid, LatheVisuals.IsRunning, false);
  289. _materialStorage.UpdateMaterialWhitelist(uid);
  290. }
  291. /// <summary>
  292. /// Sets the machine sprite to either play the running animation
  293. /// or stop.
  294. /// </summary>
  295. private void UpdateRunningAppearance(EntityUid uid, bool isRunning)
  296. {
  297. _appearance.SetData(uid, LatheVisuals.IsRunning, isRunning);
  298. }
  299. private void OnPowerChanged(EntityUid uid, LatheComponent component, ref PowerChangedEvent args)
  300. {
  301. if (!args.Powered)
  302. {
  303. RemComp<LatheProducingComponent>(uid);
  304. UpdateRunningAppearance(uid, false);
  305. }
  306. else if (component.CurrentRecipe != null)
  307. {
  308. EnsureComp<LatheProducingComponent>(uid);
  309. TryStartProducing(uid, component);
  310. }
  311. }
  312. private void OnDatabaseModified(EntityUid uid, LatheComponent component, ref TechnologyDatabaseModifiedEvent args)
  313. {
  314. UpdateUserInterfaceState(uid, component);
  315. }
  316. private void OnResearchRegistrationChanged(EntityUid uid, LatheComponent component, ref ResearchRegistrationChangedEvent args)
  317. {
  318. UpdateUserInterfaceState(uid, component);
  319. }
  320. protected override bool HasRecipe(EntityUid uid, LatheRecipePrototype recipe, LatheComponent component)
  321. {
  322. return GetAvailableRecipes(uid, component).Contains(recipe.ID);
  323. }
  324. #region UI Messages
  325. private void OnLatheQueueRecipeMessage(EntityUid uid, LatheComponent component, LatheQueueRecipeMessage args)
  326. {
  327. if (_proto.TryIndex(args.ID, out LatheRecipePrototype? recipe))
  328. {
  329. var count = 0;
  330. for (var i = 0; i < args.Quantity; i++)
  331. {
  332. if (TryAddToQueue(uid, recipe, component))
  333. count++;
  334. else
  335. break;
  336. }
  337. if (count > 0)
  338. {
  339. _adminLogger.Add(LogType.Action,
  340. LogImpact.Low,
  341. $"{ToPrettyString(args.Actor):player} queued {count} {GetRecipeName(recipe)} at {ToPrettyString(uid):lathe}");
  342. }
  343. }
  344. TryStartProducing(uid, component);
  345. UpdateUserInterfaceState(uid, component);
  346. }
  347. private void OnLatheSyncRequestMessage(EntityUid uid, LatheComponent component, LatheSyncRequestMessage args)
  348. {
  349. UpdateUserInterfaceState(uid, component);
  350. }
  351. #endregion
  352. }
  353. }