ConstructionSystem.Guided.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. using Content.Server.Construction.Components;
  2. using Content.Shared.Construction;
  3. using Content.Shared.Construction.Prototypes;
  4. using Content.Shared.Construction.Steps;
  5. using Content.Shared.Examine;
  6. using Content.Shared.Popups;
  7. using Content.Shared.Verbs;
  8. using Robust.Shared.Player;
  9. using Robust.Shared.Utility;
  10. namespace Content.Server.Construction
  11. {
  12. public sealed partial class ConstructionSystem
  13. {
  14. [Dependency] private readonly SharedPopupSystem _popup = default!;
  15. private readonly Dictionary<ConstructionPrototype, ConstructionGuide> _guideCache = new();
  16. private void InitializeGuided()
  17. {
  18. SubscribeNetworkEvent<RequestConstructionGuide>(OnGuideRequested);
  19. SubscribeLocalEvent<ConstructionComponent, GetVerbsEvent<Verb>>(AddDeconstructVerb);
  20. SubscribeLocalEvent<ConstructionComponent, ExaminedEvent>(HandleConstructionExamined);
  21. }
  22. private void OnGuideRequested(RequestConstructionGuide msg, EntitySessionEventArgs args)
  23. {
  24. if (!PrototypeManager.TryIndex(msg.ConstructionId, out ConstructionPrototype? prototype))
  25. return;
  26. if(GetGuide(prototype) is {} guide)
  27. RaiseNetworkEvent(new ResponseConstructionGuide(msg.ConstructionId, guide), args.SenderSession.Channel);
  28. }
  29. private void AddDeconstructVerb(EntityUid uid, ConstructionComponent component, GetVerbsEvent<Verb> args)
  30. {
  31. if (!args.CanAccess || !args.CanInteract || args.Hands == null)
  32. return;
  33. if (component.TargetNode == component.DeconstructionNode ||
  34. component.Node == component.DeconstructionNode)
  35. return;
  36. if (!PrototypeManager.TryIndex(component.Graph, out ConstructionGraphPrototype? graph))
  37. return;
  38. if (component.DeconstructionNode == null)
  39. return;
  40. if (GetCurrentNode(uid, component) is not {} currentNode)
  41. return;
  42. if (graph.Path(currentNode.Name, component.DeconstructionNode) is not {} path || path.Length == 0)
  43. return;
  44. Verb verb = new();
  45. //verb.Category = VerbCategories.Construction;
  46. //TODO VERBS add more construction verbs? Until then, removing construction category
  47. verb.Text = Loc.GetString("deconstructible-verb-begin-deconstruct");
  48. verb.Icon = new SpriteSpecifier.Texture(
  49. new ("/Textures/Interface/hammer_scaled.svg.192dpi.png"));
  50. verb.Act = () =>
  51. {
  52. SetPathfindingTarget(uid, component.DeconstructionNode, component);
  53. if (component.TargetNode == null)
  54. {
  55. // Maybe check, but on the flip-side a better solution might be to not make it undeconstructible in the first place, no?
  56. _popup.PopupEntity(Loc.GetString("deconstructible-verb-activate-no-target-text"), uid, uid);
  57. }
  58. else
  59. {
  60. _popup.PopupEntity(Loc.GetString("deconstructible-verb-activate-text"), args.User, args.User);
  61. }
  62. };
  63. args.Verbs.Add(verb);
  64. }
  65. private void HandleConstructionExamined(EntityUid uid, ConstructionComponent component, ExaminedEvent args)
  66. {
  67. using (args.PushGroup(nameof(ConstructionComponent)))
  68. {
  69. if (GetTargetNode(uid, component) is {} target)
  70. {
  71. if (target.Name == component.DeconstructionNode)
  72. {
  73. args.PushMarkup(Loc.GetString("deconstruction-header-text") + "\n");
  74. }
  75. else
  76. {
  77. args.PushMarkup(Loc.GetString(
  78. "construction-component-to-create-header",
  79. ("targetName", target.Name)) + "\n");
  80. }
  81. }
  82. if (component.EdgeIndex == null && GetTargetEdge(uid, component) is {} targetEdge)
  83. {
  84. var preventStepExamine = false;
  85. foreach (var condition in targetEdge.Conditions)
  86. {
  87. preventStepExamine |= condition.DoExamine(args);
  88. }
  89. if (!preventStepExamine)
  90. targetEdge.Steps[0].DoExamine(args);
  91. return;
  92. }
  93. if (GetCurrentEdge(uid, component) is {} edge)
  94. {
  95. var preventStepExamine = false;
  96. foreach (var condition in edge.Conditions)
  97. {
  98. preventStepExamine |= condition.DoExamine(args);
  99. }
  100. if (!preventStepExamine && component.StepIndex < edge.Steps.Count)
  101. edge.Steps[component.StepIndex].DoExamine(args);
  102. }
  103. }
  104. }
  105. /// <summary>
  106. /// Returns a <see cref="ConstructionGuide"/> for a given <see cref="ConstructionPrototype"/>,
  107. /// generating and caching it as needed.
  108. /// </summary>
  109. /// <param name="construction">The construction prototype to generate the guide for. We must be able to pathfind
  110. /// from its starting node to its ending node to be able to generate a guide for it.</param>
  111. /// <returns>The guide for the given construction, or null if we can't pathfind from the start node to the
  112. /// end node on that construction.</returns>
  113. private ConstructionGuide? GetGuide(ConstructionPrototype construction)
  114. {
  115. // NOTE: This method might be allocate a fair bit, but do not worry!
  116. // This method is specifically designed to generate guides once and cache the results,
  117. // therefore we don't need to worry *too much* about the performance of this.
  118. // If we've generated and cached this guide before, return it.
  119. if (_guideCache.TryGetValue(construction, out var guide))
  120. return guide;
  121. // If the graph doesn't actually exist, do nothing.
  122. if (!PrototypeManager.TryIndex(construction.Graph, out ConstructionGraphPrototype? graph))
  123. return null;
  124. // If either the start node or the target node are missing, do nothing.
  125. if (GetNodeFromGraph(graph, construction.StartNode) is not {} startNode
  126. || GetNodeFromGraph(graph, construction.TargetNode) is not {} targetNode)
  127. return null;
  128. // If there's no path from start to target, do nothing.
  129. if (graph.Path(construction.StartNode, construction.TargetNode) is not {} path
  130. || path.Length == 0)
  131. return null;
  132. var step = 1;
  133. var entries = new List<ConstructionGuideEntry>()
  134. {
  135. // Initial construction header.
  136. new()
  137. {
  138. Localization = construction.Type == ConstructionType.Structure
  139. ? "construction-presenter-to-build" : "construction-presenter-to-craft",
  140. EntryNumber = step,
  141. }
  142. };
  143. var conditions = new HashSet<string>();
  144. // Iterate until the penultimate node.
  145. var node = startNode;
  146. var index = 0;
  147. while(node != targetNode)
  148. {
  149. // Can't find path, therefore can't generate guide...
  150. if (!node.TryGetEdge(path[index].Name, out var edge))
  151. return null;
  152. // First steps are handled specially.
  153. if (step == 1)
  154. {
  155. foreach (var graphStep in edge.Steps)
  156. {
  157. // This graph is invalid, we only allow insert steps as the initial construction steps.
  158. if (graphStep is not EntityInsertConstructionGraphStep insertStep)
  159. return null;
  160. entries.Add(insertStep.GenerateGuideEntry());
  161. }
  162. // Now actually list the construction conditions.
  163. foreach (var condition in construction.Conditions)
  164. {
  165. if (condition.GenerateGuideEntry() is not {} conditionEntry)
  166. continue;
  167. conditionEntry.Padding += 4;
  168. entries.Add(conditionEntry);
  169. }
  170. step++;
  171. node = path[index++];
  172. // Add a bit of padding if there will be more steps after this.
  173. if(node != targetNode)
  174. entries.Add(new ConstructionGuideEntry());
  175. continue;
  176. }
  177. var old = conditions;
  178. conditions = new HashSet<string>();
  179. foreach (var condition in edge.Conditions)
  180. {
  181. foreach (var conditionEntry in condition.GenerateGuideEntry())
  182. {
  183. conditions.Add(conditionEntry.Localization);
  184. // Okay so if the condition entry had a non-null value here, we take it as a numbered step.
  185. // This is for cases where there is a lot of snowflake behavior, such as machine frames...
  186. // So that the step of inserting a machine board and inserting all of its parts is numbered.
  187. if (conditionEntry.EntryNumber != null)
  188. conditionEntry.EntryNumber = step++;
  189. // To prevent spamming the same stuff over and over again. This is a bit naive, but..ye.
  190. // Also we will only hide this condition *if* it isn't numbered.
  191. else
  192. {
  193. if (old.Contains(conditionEntry.Localization))
  194. continue;
  195. // We only add padding for non-numbered entries.
  196. conditionEntry.Padding += 4;
  197. }
  198. entries.Add(conditionEntry);
  199. }
  200. }
  201. foreach (var graphStep in edge.Steps)
  202. {
  203. var entry = graphStep.GenerateGuideEntry();
  204. entry.EntryNumber = step++;
  205. entries.Add(entry);
  206. }
  207. node = path[index++];
  208. }
  209. guide = new ConstructionGuide(entries.ToArray());
  210. _guideCache[construction] = guide;
  211. return guide;
  212. }
  213. }
  214. }