ChemMasterWindow.xaml.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. using Content.Client.Stylesheets;
  2. using Content.Client.UserInterface.Controls;
  3. using Content.Shared.Chemistry;
  4. using Content.Shared.Chemistry.Reagent;
  5. using Robust.Client.AutoGenerated;
  6. using Robust.Client.UserInterface;
  7. using Robust.Client.UserInterface.Controls;
  8. using Robust.Client.UserInterface.XAML;
  9. using Robust.Client.Utility;
  10. using Robust.Shared.Prototypes;
  11. using Robust.Shared.Utility;
  12. using System.Linq;
  13. using System.Numerics;
  14. using Content.Shared.FixedPoint;
  15. using Robust.Client.Graphics;
  16. using static Robust.Client.UserInterface.Controls.BoxContainer;
  17. namespace Content.Client.Chemistry.UI
  18. {
  19. /// <summary>
  20. /// Client-side UI used to control a <see cref="SharedChemMasterComponent"/>
  21. /// </summary>
  22. [GenerateTypedNameReferences]
  23. public sealed partial class ChemMasterWindow : FancyWindow
  24. {
  25. [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
  26. public event Action<BaseButton.ButtonEventArgs, ReagentButton>? OnReagentButtonPressed;
  27. public readonly Button[] PillTypeButtons;
  28. private const string PillsRsiPath = "/Textures/Objects/Specific/Chemistry/pills.rsi";
  29. /// <summary>
  30. /// Create and initialize the chem master UI client-side. Creates the basic layout,
  31. /// actual data isn't filled in until the server sends data about the chem master.
  32. /// </summary>
  33. public ChemMasterWindow()
  34. {
  35. RobustXamlLoader.Load(this);
  36. IoCManager.InjectDependencies(this);
  37. // Pill type selection buttons, in total there are 20 pills.
  38. // Pill rsi file should have states named as pill1, pill2, and so on.
  39. var resourcePath = new ResPath(PillsRsiPath);
  40. var pillTypeGroup = new ButtonGroup();
  41. PillTypeButtons = new Button[20];
  42. for (uint i = 0; i < PillTypeButtons.Length; i++)
  43. {
  44. // For every button decide which stylebase to have
  45. // Every row has 10 buttons
  46. String styleBase = StyleBase.ButtonOpenBoth;
  47. uint modulo = i % 10;
  48. if (i > 0 && modulo == 0)
  49. styleBase = StyleBase.ButtonOpenRight;
  50. else if (i > 0 && modulo == 9)
  51. styleBase = StyleBase.ButtonOpenLeft;
  52. else if (i == 0)
  53. styleBase = StyleBase.ButtonOpenRight;
  54. // Generate buttons
  55. PillTypeButtons[i] = new Button
  56. {
  57. Access = AccessLevel.Public,
  58. StyleClasses = { styleBase },
  59. MaxSize = new Vector2(42, 28),
  60. Group = pillTypeGroup
  61. };
  62. // Generate buttons textures
  63. var specifier = new SpriteSpecifier.Rsi(resourcePath, "pill" + (i + 1));
  64. TextureRect pillTypeTexture = new TextureRect
  65. {
  66. Texture = specifier.Frame0(),
  67. TextureScale = new Vector2(1.75f, 1.75f),
  68. Stretch = TextureRect.StretchMode.KeepCentered,
  69. };
  70. PillTypeButtons[i].AddChild(pillTypeTexture);
  71. Grid.AddChild(PillTypeButtons[i]);
  72. }
  73. PillDosage.InitDefaultButtons();
  74. PillNumber.InitDefaultButtons();
  75. BottleDosage.InitDefaultButtons();
  76. // Ensure label length is within the character limit.
  77. LabelLineEdit.IsValid = s => s.Length <= SharedChemMaster.LabelMaxLength;
  78. Tabs.SetTabTitle(0, Loc.GetString("chem-master-window-input-tab"));
  79. Tabs.SetTabTitle(1, Loc.GetString("chem-master-window-output-tab"));
  80. }
  81. private ReagentButton MakeReagentButton(string text, ChemMasterReagentAmount amount, ReagentId id, bool isBuffer, string styleClass)
  82. {
  83. var reagentTransferButton = new ReagentButton(text, amount, id, isBuffer, styleClass);
  84. reagentTransferButton.OnPressed += args
  85. => OnReagentButtonPressed?.Invoke(args, reagentTransferButton);
  86. return reagentTransferButton;
  87. }
  88. /// <summary>
  89. /// Conditionally generates a set of reagent buttons based on the supplied boolean argument.
  90. /// This was moved outside of BuildReagentRow to facilitate conditional logic, stops indentation depth getting out of hand as well.
  91. /// </summary>
  92. private List<ReagentButton> CreateReagentTransferButtons(ReagentId reagent, bool isBuffer, bool addReagentButtons)
  93. {
  94. if (!addReagentButtons)
  95. return new List<ReagentButton>(); // Return an empty list if reagentTransferButton creation is disabled.
  96. var buttonConfigs = new (string text, ChemMasterReagentAmount amount, string styleClass)[]
  97. {
  98. ("1", ChemMasterReagentAmount.U1, StyleBase.ButtonOpenBoth),
  99. ("5", ChemMasterReagentAmount.U5, StyleBase.ButtonOpenBoth),
  100. ("10", ChemMasterReagentAmount.U10, StyleBase.ButtonOpenBoth),
  101. ("25", ChemMasterReagentAmount.U25, StyleBase.ButtonOpenBoth),
  102. ("50", ChemMasterReagentAmount.U50, StyleBase.ButtonOpenBoth),
  103. ("100", ChemMasterReagentAmount.U100, StyleBase.ButtonOpenBoth),
  104. (Loc.GetString("chem-master-window-buffer-all-amount"), ChemMasterReagentAmount.All, StyleBase.ButtonOpenLeft),
  105. };
  106. var buttons = new List<ReagentButton>();
  107. foreach (var (text, amount, styleClass) in buttonConfigs)
  108. {
  109. var reagentTransferButton = MakeReagentButton(text, amount, reagent, isBuffer, styleClass);
  110. buttons.Add(reagentTransferButton);
  111. }
  112. return buttons;
  113. }
  114. /// <summary>
  115. /// Update the UI state when new state data is received from the server.
  116. /// </summary>
  117. /// <param name="state">State data sent by the server.</param>
  118. public void UpdateState(BoundUserInterfaceState state)
  119. {
  120. var castState = (ChemMasterBoundUserInterfaceState)state;
  121. if (castState.UpdateLabel)
  122. LabelLine = GenerateLabel(castState);
  123. // Ensure the Panel Info is updated, including UI elements for Buffer Volume, Output Container and so on
  124. UpdatePanelInfo(castState);
  125. BufferCurrentVolume.Text = $" {castState.BufferCurrentVolume?.Int() ?? 0}u";
  126. InputEjectButton.Disabled = castState.InputContainerInfo is null;
  127. OutputEjectButton.Disabled = castState.OutputContainerInfo is null;
  128. CreateBottleButton.Disabled = castState.OutputContainerInfo?.Reagents == null;
  129. CreatePillButton.Disabled = castState.OutputContainerInfo?.Entities == null;
  130. UpdateDosageFields(castState);
  131. }
  132. //assign default values for pill and bottle fields.
  133. private void UpdateDosageFields(ChemMasterBoundUserInterfaceState castState)
  134. {
  135. var output = castState.OutputContainerInfo;
  136. var remainingCapacity = output is null ? 0 : (output.MaxVolume - output.CurrentVolume).Int();
  137. var holdsReagents = output?.Reagents != null;
  138. var pillNumberMax = holdsReagents ? 0 : remainingCapacity;
  139. var bottleAmountMax = holdsReagents ? remainingCapacity : 0;
  140. var bufferVolume = castState.BufferCurrentVolume?.Int() ?? 0;
  141. PillDosage.Value = (int)Math.Min(bufferVolume, castState.PillDosageLimit);
  142. PillTypeButtons[castState.SelectedPillType].Pressed = true;
  143. PillNumber.IsValid = x => x >= 0 && x <= pillNumberMax;
  144. PillDosage.IsValid = x => x > 0 && x <= castState.PillDosageLimit;
  145. BottleDosage.IsValid = x => x >= 0 && x <= bottleAmountMax;
  146. if (PillNumber.Value > pillNumberMax)
  147. PillNumber.Value = pillNumberMax;
  148. if (BottleDosage.Value > bottleAmountMax)
  149. BottleDosage.Value = bottleAmountMax;
  150. // Avoid division by zero
  151. if (PillDosage.Value > 0)
  152. {
  153. PillNumber.Value = Math.Min(bufferVolume / PillDosage.Value, pillNumberMax);
  154. }
  155. else
  156. {
  157. PillNumber.Value = 0;
  158. }
  159. BottleDosage.Value = Math.Min(bottleAmountMax, bufferVolume);
  160. }
  161. /// <summary>
  162. /// Generate a product label based on reagents in the buffer.
  163. /// </summary>
  164. /// <param name="state">State data sent by the server.</param>
  165. private string GenerateLabel(ChemMasterBoundUserInterfaceState state)
  166. {
  167. if (state.BufferCurrentVolume == 0)
  168. return "";
  169. var reagent = state.BufferReagents.OrderBy(r => r.Quantity).First().Reagent;
  170. _prototypeManager.TryIndex(reagent.Prototype, out ReagentPrototype? proto);
  171. return proto?.LocalizedName ?? "";
  172. }
  173. /// <summary>
  174. /// Update the container, buffer, and packaging panels.
  175. /// </summary>
  176. /// <param name="state">State data for the dispenser.</param>
  177. private void UpdatePanelInfo(ChemMasterBoundUserInterfaceState state)
  178. {
  179. BufferTransferButton.Pressed = state.Mode == ChemMasterMode.Transfer;
  180. BufferDiscardButton.Pressed = state.Mode == ChemMasterMode.Discard;
  181. BuildContainerUI(InputContainerInfo, state.InputContainerInfo, true);
  182. BuildContainerUI(OutputContainerInfo, state.OutputContainerInfo, false);
  183. BufferInfo.Children.Clear();
  184. // This has to happen here due to people possibly
  185. // setting sorting before putting any chemicals
  186. BufferSortButton.Text = state.SortingType switch
  187. {
  188. ChemMasterSortingType.Alphabetical => Loc.GetString("chem-master-window-sort-type-alphabetical"),
  189. ChemMasterSortingType.Quantity => Loc.GetString("chem-master-window-sort-type-quantity"),
  190. ChemMasterSortingType.Latest => Loc.GetString("chem-master-window-sort-type-latest"),
  191. _ => Loc.GetString("chem-master-window-sort-type-none")
  192. };
  193. if (!state.BufferReagents.Any())
  194. {
  195. BufferInfo.Children.Add(new Label { Text = Loc.GetString("chem-master-window-buffer-empty-text") });
  196. return;
  197. }
  198. var bufferHBox = new BoxContainer
  199. {
  200. Orientation = LayoutOrientation.Horizontal
  201. };
  202. BufferInfo.AddChild(bufferHBox);
  203. var bufferLabel = new Label { Text = $"{Loc.GetString("chem-master-window-buffer-label")} " };
  204. bufferHBox.AddChild(bufferLabel);
  205. var bufferVol = new Label
  206. {
  207. Text = $"{state.BufferCurrentVolume}u",
  208. StyleClasses = { StyleNano.StyleClassLabelSecondaryColor }
  209. };
  210. bufferHBox.AddChild(bufferVol);
  211. // This sets up the needed data for sorting later in a list
  212. // Its done this way to not repeat having to use same code twice (once for sorting
  213. // and once for displaying)
  214. var reagentList = new List<(ReagentId reagentId, string name, Color color, FixedPoint2 quantity)>();
  215. foreach (var (reagent, quantity) in state.BufferReagents)
  216. {
  217. var reagentId = reagent;
  218. _prototypeManager.TryIndex(reagentId.Prototype, out ReagentPrototype? proto);
  219. var name = proto?.LocalizedName ?? Loc.GetString("chem-master-window-unknown-reagent-text");
  220. var reagentColor = proto?.SubstanceColor ?? default(Color);
  221. reagentList.Add(new (reagentId, name, reagentColor, quantity));
  222. }
  223. // We sort here since we need sorted list to be filled first.
  224. // You can easily add any new params you need to it.
  225. switch (state.SortingType)
  226. {
  227. case ChemMasterSortingType.Alphabetical:
  228. reagentList = reagentList.OrderBy(x => x.name).ToList();
  229. break;
  230. case ChemMasterSortingType.Quantity:
  231. reagentList = reagentList.OrderByDescending(x => x.quantity).ToList();
  232. break;
  233. case ChemMasterSortingType.Latest:
  234. reagentList = Enumerable.Reverse(reagentList).ToList();
  235. break;
  236. case ChemMasterSortingType.None:
  237. default:
  238. // This case is pointless but it is there for readability
  239. break;
  240. }
  241. // initialises rowCount to allow for striped rows
  242. var rowCount = 0;
  243. foreach (var reagent in reagentList)
  244. {
  245. BufferInfo.Children.Add(BuildReagentRow(reagent.color, rowCount++, reagent.name, reagent.reagentId, reagent.quantity, true, true));
  246. }
  247. }
  248. private void BuildContainerUI(Control control, ContainerInfo? info, bool addReagentButtons)
  249. {
  250. control.Children.Clear();
  251. if (info is null)
  252. {
  253. control.Children.Add(new Label
  254. {
  255. Text = Loc.GetString("chem-master-window-no-container-loaded-text")
  256. });
  257. return;
  258. }
  259. // Name of the container and its fill status (Ex: 44/100u)
  260. control.Children.Add(new BoxContainer
  261. {
  262. Orientation = LayoutOrientation.Horizontal,
  263. Children =
  264. {
  265. new Label { Text = $"{info.DisplayName}: " },
  266. new Label
  267. {
  268. Text = $"{info.CurrentVolume}/{info.MaxVolume}",
  269. StyleClasses = { StyleNano.StyleClassLabelSecondaryColor }
  270. }
  271. }
  272. });
  273. // Initialises rowCount to allow for striped rows
  274. var rowCount = 0;
  275. // Handle entities if they are not null
  276. if (info.Entities != null)
  277. {
  278. foreach (var (id, quantity) in info.Entities.Select(x => (x.Id, x.Quantity)))
  279. {
  280. control.Children.Add(BuildReagentRow(default(Color), rowCount++, id, default(ReagentId), quantity, false, addReagentButtons));
  281. }
  282. }
  283. // Handle reagents if they are not null
  284. if (info.Reagents != null)
  285. {
  286. foreach (var reagent in info.Reagents)
  287. {
  288. _prototypeManager.TryIndex(reagent.Reagent.Prototype, out ReagentPrototype? proto);
  289. var name = proto?.LocalizedName ?? Loc.GetString("chem-master-window-unknown-reagent-text");
  290. var reagentColor = proto?.SubstanceColor ?? default(Color);
  291. control.Children.Add(BuildReagentRow(reagentColor, rowCount++, name, reagent.Reagent, reagent.Quantity, false, addReagentButtons));
  292. }
  293. }
  294. }
  295. /// <summary>
  296. /// Take reagent/entity data and present rows, labels, and buttons appropriately. todo sprites?
  297. /// </summary>
  298. private Control BuildReagentRow(Color reagentColor, int rowCount, string name, ReagentId reagent, FixedPoint2 quantity, bool isBuffer, bool addReagentButtons)
  299. {
  300. //Colors rows and sets fallback for reagentcolor to the same as background, this will hide colorPanel for entities hopefully
  301. var rowColor1 = Color.FromHex("#1B1B1E");
  302. var rowColor2 = Color.FromHex("#202025");
  303. var currentRowColor = (rowCount % 2 == 1) ? rowColor1 : rowColor2;
  304. if ((reagentColor == default(Color))|(!addReagentButtons))
  305. {
  306. reagentColor = currentRowColor;
  307. }
  308. //this calls the separated button builder, and stores the return to render after labels
  309. var reagentButtonConstructors = CreateReagentTransferButtons(reagent, isBuffer, addReagentButtons);
  310. // Create the row layout with the color panel
  311. var rowContainer = new BoxContainer
  312. {
  313. Orientation = LayoutOrientation.Horizontal,
  314. Children =
  315. {
  316. new Label { Text = $"{name}: " },
  317. new Label
  318. {
  319. Text = $"{quantity}u",
  320. StyleClasses = { StyleNano.StyleClassLabelSecondaryColor }
  321. },
  322. // Padding
  323. new Control { HorizontalExpand = true },
  324. // Colored panels for reagents
  325. new PanelContainer
  326. {
  327. Name = "colorPanel",
  328. VerticalExpand = true,
  329. MinWidth = 4,
  330. PanelOverride = new StyleBoxFlat
  331. {
  332. BackgroundColor = reagentColor
  333. },
  334. Margin = new Thickness(0, 1)
  335. }
  336. }
  337. };
  338. // Add the reagent buttons after the color panel
  339. foreach (var reagentTransferButton in reagentButtonConstructors)
  340. {
  341. rowContainer.AddChild(reagentTransferButton);
  342. }
  343. //Apply panencontainer to allow for striped rows
  344. return new PanelContainer
  345. {
  346. PanelOverride = new StyleBoxFlat(currentRowColor),
  347. Children = { rowContainer }
  348. };
  349. }
  350. public string LabelLine
  351. {
  352. get => LabelLineEdit.Text;
  353. set => LabelLineEdit.Text = value;
  354. }
  355. }
  356. public sealed class ReagentButton : Button
  357. {
  358. public ChemMasterReagentAmount Amount { get; set; }
  359. public bool IsBuffer = true;
  360. public ReagentId Id { get; set; }
  361. public ReagentButton(string text, ChemMasterReagentAmount amount, ReagentId id, bool isBuffer, string styleClass)
  362. {
  363. AddStyleClass(styleClass);
  364. Text = text;
  365. Amount = amount;
  366. Id = id;
  367. IsBuffer = isBuffer;
  368. }
  369. }
  370. }