diff --git a/Content.Client/Construction/ConstructionMenu.xaml.cs b/Content.Client/Construction/ConstructionMenu.xaml.cs
index c899923859..f5831b0fce 100644
--- a/Content.Client/Construction/ConstructionMenu.xaml.cs
+++ b/Content.Client/Construction/ConstructionMenu.xaml.cs
@@ -1,314 +1,134 @@
-#nullable enable
using System;
-using System.Collections.Generic;
-using System.Linq;
-using Content.Client.GameObjects.EntitySystems;
-using Content.Client.Utility;
-using Content.Shared.Construction;
-using Content.Shared.GameObjects.Components;
-using Content.Shared.GameObjects.Components.Interactable;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
-using Robust.Client.Placement;
-using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
-using Robust.Client.Utility;
-using Robust.Shared.Enums;
-using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
-using Robust.Shared.Prototypes;
+
+#nullable enable
namespace Content.Client.Construction
{
- [GenerateTypedNameReferences]
- public partial class ConstructionMenu : SS14Window
+ ///
+ /// This is the interface for a UI View of the construction window. The point of it is to abstract away the actual
+ /// UI controls and just provide higher level operations on the entire window. This View is completely passive and
+ /// just raises events to the outside world. This class is controlled by the .
+ ///
+ public interface IConstructionMenuView : IDisposable
{
- [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
- [Dependency] private readonly IResourceCache _resourceCache = default!;
- [Dependency] private readonly IEntitySystemManager _systemManager = default!;
- [Dependency] private readonly IPlacementManager _placementManager = default!;
+ // It isn't optimal to expose UI controls like this, but the UI control design is
+ // questionable so it can't be helped.
+ string[] Categories { get; set; }
+ OptionButton CategoryButton { get; }
+ bool EraseButtonPressed { get; set; }
+ bool BuildButtonPressed { get; set; }
+
+ ItemList Recipes { get; }
+ ItemList RecipeStepList { get; }
+
+ event EventHandler<(string search, string catagory)> PopulateRecipes;
+ event EventHandler RecipeSelected;
+ event EventHandler BuildButtonToggled;
+ event EventHandler EraseButtonToggled;
+ event EventHandler ClearAllGhosts;
+
+ void ClearRecipeInfo();
+ void SetRecipeInfo(string name, string description, Texture iconTexture, bool isItem);
+ void ResetPlacement();
+
+ #region Window Control
+
+ event Action? OnClose;
+
+ bool IsOpen { get; }
+
+ void OpenCentered();
+ void MoveToFront();
+ bool IsAtFront();
+ void Close();
+
+ #endregion
+ }
+
+ [GenerateTypedNameReferences]
+ public partial class ConstructionMenu : SS14Window, IConstructionMenuView
+ {
protected override Vector2? CustomSize => (720, 320);
- private ConstructionPrototype? _selected;
- private string[] _categories = Array.Empty();
+ public bool BuildButtonPressed
+ {
+ get => BuildButton.Pressed;
+ set => BuildButton.Pressed = value;
+ }
+
+ public string[] Categories { get; set; } = Array.Empty();
+
+ public OptionButton CategoryButton => Category;
+
+ public bool EraseButtonPressed
+ {
+ get => EraseButton.Pressed;
+ set => EraseButton.Pressed = value;
+ }
+
+ ///
+ public ItemList Recipes => RecipesList;
+
+ public ItemList RecipeStepList => StepList;
public ConstructionMenu()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
- _placementManager.PlacementChanged += PlacementChanged;
-
Title = Loc.GetString("Construction");
+
+ BuildButton.Text = Loc.GetString("Place construction ghost");
+ RecipesList.OnItemSelected += obj => RecipeSelected?.Invoke(this, obj.ItemList[obj.ItemIndex]);
+ RecipesList.OnItemDeselected += _ => RecipeSelected?.Invoke(this, null);
+
+ SearchBar.OnTextChanged += _ => PopulateRecipes?.Invoke(this, (SearchBar.Text, Categories[Category.SelectedId]));
+ Category.OnItemSelected += obj =>
+ {
+ Category.SelectId(obj.Id);
+ PopulateRecipes?.Invoke(this, (SearchBar.Text, Categories[obj.Id]));
+ };
BuildButton.Text = Loc.GetString("Place construction ghost");
- RecipesList.OnItemSelected += RecipeSelected;
- RecipesList.OnItemDeselected += RecipeDeselected;
-
- SearchBar.OnTextChanged += SearchTextChanged;
- Category.OnItemSelected += CategorySelected;
-
- BuildButton.Text = Loc.GetString("Place construction ghost");
- BuildButton.OnToggled += BuildButtonToggled;
+ BuildButton.OnToggled += args => BuildButtonToggled?.Invoke(this, args.Pressed);
ClearButton.Text = Loc.GetString("Clear All");
- ClearButton.OnPressed += ClearAllButtonPressed;
+ ClearButton.OnPressed += _ => ClearAllGhosts?.Invoke(this, EventArgs.Empty);
EraseButton.Text = Loc.GetString("Eraser Mode");
- EraseButton.OnToggled += EraseButtonToggled;
-
- PopulateCategories();
- PopulateAll();
+ EraseButton.OnToggled += args => EraseButtonToggled?.Invoke(this, args.Pressed);
}
- private void PlacementChanged(object? sender, EventArgs e)
+ public event EventHandler? ClearAllGhosts;
+
+ public event EventHandler<(string search, string catagory)>? PopulateRecipes;
+ public event EventHandler? RecipeSelected;
+ public event EventHandler? BuildButtonToggled;
+ public event EventHandler? EraseButtonToggled;
+
+ public void ResetPlacement()
{
BuildButton.Pressed = false;
EraseButton.Pressed = false;
}
- private void PopulateAll()
+ public void SetRecipeInfo(string name, string description, Texture iconTexture, bool isItem)
{
- foreach (var recipe in _prototypeManager.EnumeratePrototypes())
- {
- RecipesList.Add(GetItem(recipe, RecipesList));
- }
- }
-
- private static ItemList.Item GetItem(ConstructionPrototype recipe, ItemList itemList)
- {
- return new(itemList)
- {
- Metadata = recipe,
- Text = recipe.Name,
- Icon = recipe.Icon.Frame0(),
- TooltipEnabled = true,
- TooltipText = recipe.Description,
- };
- }
-
- private void PopulateBy(string search, string category)
- {
- RecipesList.Clear();
-
- foreach (var recipe in _prototypeManager.EnumeratePrototypes())
- {
- if (!string.IsNullOrEmpty(search))
- {
- if (!recipe.Name.ToLowerInvariant().Contains(search.Trim().ToLowerInvariant()))
- continue;
- }
-
- if (!string.IsNullOrEmpty(category) && category != Loc.GetString("All"))
- {
- if (recipe.Category != category)
- continue;
- }
-
- RecipesList.Add(GetItem(recipe, RecipesList));
- }
- }
-
- private void PopulateCategories()
- {
- var uniqueCategories = new HashSet();
-
- // hard-coded to show all recipes
- uniqueCategories.Add(Loc.GetString("All"));
-
- foreach (var prototype in _prototypeManager.EnumeratePrototypes())
- {
- var category = Loc.GetString(prototype.Category);
-
- if (!string.IsNullOrEmpty(category))
- uniqueCategories.Add(category);
- }
-
- Category.Clear();
-
- var array = uniqueCategories.ToArray();
- Array.Sort(array);
-
- for (var i = 0; i < array.Length; i++)
- {
- var category = array[i];
- Category.AddItem(category, i);
- }
-
- _categories = array;
- }
-
- private void PopulateInfo(ConstructionPrototype prototype)
- {
- ClearInfo();
-
- var isItem = prototype.Type == ConstructionType.Item;
-
BuildButton.Disabled = false;
- BuildButton.Text = Loc.GetString(!isItem ? "Place construction ghost" : "Craft");
- TargetName.SetMessage(prototype.Name);
- TargetDesc.SetMessage(prototype.Description);
- TargetTexture.Texture = prototype.Icon.Frame0();
-
- if (!_prototypeManager.TryIndex(prototype.Graph, out ConstructionGraphPrototype graph))
- return;
-
- var startNode = graph.Nodes[prototype.StartNode];
- var targetNode = graph.Nodes[prototype.TargetNode];
-
- var path = graph.Path(startNode.Name, targetNode.Name);
-
- var current = startNode;
-
- var stepNumber = 1;
-
- Texture? GetTextureForStep(ConstructionGraphStep step)
- {
- switch (step)
- {
- case MaterialConstructionGraphStep materialStep:
- switch (materialStep.Material)
- {
- case StackType.Metal:
- return _resourceCache.GetTexture("/Textures/Objects/Materials/sheets.rsi/metal.png");
-
- case StackType.Glass:
- return _resourceCache.GetTexture("/Textures/Objects/Materials/sheets.rsi/glass.png");
-
- case StackType.Plasteel:
- return _resourceCache.GetTexture("/Textures/Objects/Materials/sheets.rsi/plasteel.png");
-
- case StackType.Plasma:
- return _resourceCache.GetTexture("/Textures/Objects/Materials/sheets.rsi/plasma.png");
-
- case StackType.Cable:
- return _resourceCache.GetTexture("/Textures/Objects/Tools/cables.rsi/coil-30.png");
-
- case StackType.MetalRod:
- return _resourceCache.GetTexture("/Textures/Objects/Materials/materials.rsi/rods.png");
-
- }
- break;
-
- case ToolConstructionGraphStep toolStep:
- switch (toolStep.Tool)
- {
- case ToolQuality.Anchoring:
- return _resourceCache.GetTexture("/Textures/Objects/Tools/wrench.rsi/icon.png");
- case ToolQuality.Prying:
- return _resourceCache.GetTexture("/Textures/Objects/Tools/crowbar.rsi/icon.png");
- case ToolQuality.Screwing:
- return _resourceCache.GetTexture("/Textures/Objects/Tools/screwdriver.rsi/screwdriver-map.png");
- case ToolQuality.Cutting:
- return _resourceCache.GetTexture("/Textures/Objects/Tools/wirecutters.rsi/cutters-map.png");
- case ToolQuality.Welding:
- return _resourceCache.GetTexture("/Textures/Objects/Tools/welder.rsi/welder.png");
- case ToolQuality.Multitool:
- return _resourceCache.GetTexture("/Textures/Objects/Tools/multitool.rsi/multitool.png");
- }
-
- break;
-
- case ComponentConstructionGraphStep componentStep:
- return componentStep.Icon?.Frame0();
-
- case PrototypeConstructionGraphStep prototypeStep:
- return prototypeStep.Icon?.Frame0();
-
- case NestedConstructionGraphStep _:
- return null;
- }
-
- return null;
- }
-
- foreach (var node in path)
- {
- var edge = current.GetEdge(node.Name);
- var firstNode = current == startNode;
-
- if (firstNode)
- {
- StepList.AddItem(isItem
- ? Loc.GetString($"{stepNumber++}. To craft this item, you need:")
- : Loc.GetString($"{stepNumber++}. To build this, first you need:"));
- }
-
- foreach (var step in edge.Steps)
- {
- var icon = GetTextureForStep(step);
-
- switch (step)
- {
- case MaterialConstructionGraphStep materialStep:
- StepList.AddItem(
- !firstNode
- ? Loc.GetString(
- "{0}. Add {1}x {2}.", stepNumber++, materialStep.Amount, materialStep.Material)
- : Loc.GetString(" {0}x {1}", materialStep.Amount, materialStep.Material), icon);
-
- break;
-
- case ToolConstructionGraphStep toolStep:
- StepList.AddItem(Loc.GetString("{0}. Use a {1}.", stepNumber++, toolStep.Tool.GetToolName()), icon);
- break;
-
- case PrototypeConstructionGraphStep prototypeStep:
- StepList.AddItem(Loc.GetString("{0}. Add {1}.", stepNumber++, prototypeStep.Name), icon);
- break;
-
- case ComponentConstructionGraphStep componentStep:
- StepList.AddItem(Loc.GetString("{0}. Add {1}.", stepNumber++, componentStep.Name), icon);
- break;
-
- case NestedConstructionGraphStep nestedStep:
- var parallelNumber = 1;
- StepList.AddItem(Loc.GetString("{0}. In parallel...", stepNumber++));
-
- foreach (var steps in nestedStep.Steps)
- {
- var subStepNumber = 1;
-
- foreach (var subStep in steps)
- {
- icon = GetTextureForStep(subStep);
-
- switch (subStep)
- {
- case MaterialConstructionGraphStep materialStep:
- if (!isItem)
- StepList.AddItem(Loc.GetString(" {0}.{1}.{2}. Add {3}x {4}.", stepNumber, parallelNumber, subStepNumber++, materialStep.Amount, materialStep.Material), icon);
- break;
-
- case ToolConstructionGraphStep toolStep:
- StepList.AddItem(Loc.GetString(" {0}.{1}.{2}. Use a {3}.", stepNumber, parallelNumber, subStepNumber++, toolStep.Tool.GetToolName()), icon);
- break;
-
- case PrototypeConstructionGraphStep prototypeStep:
- StepList.AddItem(Loc.GetString(" {0}.{1}.{2}. Add {3}.", stepNumber, parallelNumber, subStepNumber++, prototypeStep.Name), icon);
- break;
-
- case ComponentConstructionGraphStep componentStep:
- StepList.AddItem(Loc.GetString(" {0}.{1}.{2}. Add {3}.", stepNumber, parallelNumber, subStepNumber++, componentStep.Name), icon);
- break;
- }
- }
-
- parallelNumber++;
- }
-
- break;
- }
- }
-
- current = node;
- }
+ BuildButton.Text = Loc.GetString(isItem ? "Place construction ghost" : "Craft");
+ TargetName.SetMessage(name);
+ TargetDesc.SetMessage(description);
+ TargetTexture.Texture = iconTexture;
}
- private void ClearInfo()
+ public void ClearRecipeInfo()
{
BuildButton.Disabled = true;
TargetName.SetMessage(string.Empty);
@@ -317,92 +137,12 @@ namespace Content.Client.Construction
StepList.Clear();
}
- private void RecipeSelected(ItemList.ItemListSelectedEventArgs obj)
- {
- _selected = (ConstructionPrototype) obj.ItemList[obj.ItemIndex].Metadata!;
- if(_placementManager.IsActive && !_placementManager.Eraser) UpdateGhostPlacement();
- PopulateInfo(_selected);
- }
-
- private void RecipeDeselected(ItemList.ItemListDeselectedEventArgs obj)
- {
- _selected = null;
- ClearInfo();
- }
-
- private void CategorySelected(OptionButton.ItemSelectedEventArgs obj)
- {
- Category.SelectId(obj.Id);
- PopulateBy(SearchBar.Text, _categories[obj.Id]);
- }
-
- private void SearchTextChanged(LineEdit.LineEditEventArgs obj)
- {
- PopulateBy(SearchBar.Text, _categories[Category.SelectedId]);
- }
-
- private void BuildButtonToggled(BaseButton.ButtonToggledEventArgs args)
- {
- if (args.Pressed)
- {
- if(_selected == null) return;
-
- var constructSystem = EntitySystem.Get();
-
- if (_selected.Type == ConstructionType.Item)
- {
- constructSystem.TryStartItemConstruction(_selected.ID);
- BuildButton.Pressed = false;
- return;
- }
-
- UpdateGhostPlacement();
- }
- else
- {
- _placementManager.Clear();
- }
-
- BuildButton.Pressed = args.Pressed;
- }
-
- private void UpdateGhostPlacement()
- {
- if (_selected == null || _selected.Type != ConstructionType.Structure) return;
-
- var constructSystem = EntitySystem.Get();
-
- _placementManager.BeginPlacing(new PlacementInformation()
- {
- IsTile = false,
- PlacementOption = _selected.PlacementMode,
- }, new ConstructionPlacementHijack(constructSystem, _selected));
-
- BuildButton.Pressed = true;
- }
-
- private void EraseButtonToggled(BaseButton.ButtonToggledEventArgs args)
- {
- if (args.Pressed) _placementManager.Clear();
- _placementManager.ToggleEraserHijacked(new ConstructionPlacementHijack(_systemManager.GetEntitySystem(), null));
- EraseButton.Pressed = args.Pressed;
- }
-
- private void ClearAllButtonPressed(BaseButton.ButtonEventArgs obj)
- {
- var constructionSystem = EntitySystem.Get();
-
- constructionSystem.ClearAllGhosts();
- }
-
+ ///
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
- if (disposing)
- {
- _placementManager.PlacementChanged -= PlacementChanged;
- }
+ if (disposing) { }
}
}
}
diff --git a/Content.Client/Construction/ConstructionMenuPresenter.cs b/Content.Client/Construction/ConstructionMenuPresenter.cs
new file mode 100644
index 0000000000..efff7f3256
--- /dev/null
+++ b/Content.Client/Construction/ConstructionMenuPresenter.cs
@@ -0,0 +1,505 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Content.Client.GameObjects.EntitySystems;
+using Content.Client.UserInterface;
+using Content.Client.Utility;
+using Content.Shared.Construction;
+using Content.Shared.GameObjects.Components;
+using Content.Shared.GameObjects.Components.Interactable;
+using Robust.Client.Graphics;
+using Robust.Client.Placement;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.Utility;
+using Robust.Shared.Enums;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Construction
+{
+ ///
+ /// This class presents the Construction/Crafting UI to the client, linking the with the
+ /// model. This is where the bulk of UI work is done, either calling functions in the model to change state, or collecting
+ /// data out of the model to *present* to the screen though the UI framework.
+ ///
+ internal class ConstructionMenuPresenter : IDisposable
+ {
+ [Dependency] private readonly IEntitySystemManager _systemManager = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IResourceCache _resourceCache = default!;
+ [Dependency] private readonly IPlacementManager _placementManager = default!;
+
+ private readonly IGameHud _gameHud;
+ private readonly IConstructionMenuView _constructionView;
+
+ private ConstructionSystem? _constructionSystem;
+ private ConstructionPrototype? _selected;
+
+ private bool CraftingAvailable
+ {
+ get => _gameHud.CraftingButtonVisible;
+ set
+ {
+ _gameHud.CraftingButtonVisible = value;
+ if (!value)
+ _constructionView.Close();
+ }
+ }
+
+ ///
+ /// Does the window have focus? If the window is closed, this will always return false.
+ ///
+ private bool IsAtFront => _constructionView.IsOpen && _constructionView.IsAtFront();
+
+ private bool WindowOpen
+ {
+ get => _constructionView.IsOpen;
+ set
+ {
+ if (value && CraftingAvailable)
+ {
+ if (_constructionView.IsOpen)
+ _constructionView.MoveToFront();
+ else
+ _constructionView.OpenCentered();
+ }
+ else
+ _constructionView.Close();
+ }
+ }
+
+ ///
+ /// Constructs a new instance of .
+ ///
+ /// GUI that is being presented to.
+ public ConstructionMenuPresenter(IGameHud gameHud)
+ {
+ // This is a lot easier than a factory
+ IoCManager.InjectDependencies(this);
+
+ _gameHud = gameHud;
+ _constructionView = new ConstructionMenu();
+
+ // This is required so that if we load after the system is initialized, we can bind to it immediately
+ if (_systemManager.TryGetEntitySystem(out var constructionSystem))
+ SystemBindingChanged(constructionSystem);
+
+ _systemManager.SystemLoaded += OnSystemLoaded;
+ _systemManager.SystemUnloaded += OnSystemUnloaded;
+
+ _placementManager.PlacementChanged += OnPlacementChanged;
+
+ _constructionView.OnClose += () => _gameHud.CraftingButtonDown = false;
+ _constructionView.ClearAllGhosts += (_, _) => _constructionSystem?.ClearAllGhosts();
+ _constructionView.PopulateRecipes += OnViewPopulateRecipes;
+ _constructionView.RecipeSelected += OnViewRecipeSelected;
+ _constructionView.BuildButtonToggled += (_, b) => BuildButtonToggled(b);
+ _constructionView.EraseButtonToggled += (_, b) =>
+ {
+ if (_constructionSystem is null) return;
+ if (b) _placementManager.Clear();
+ _placementManager.ToggleEraserHijacked(new ConstructionPlacementHijack(_constructionSystem, null));
+ _constructionView.EraseButtonPressed = b;
+ };
+
+ PopulateCategories();
+ OnViewPopulateRecipes(_constructionView, (string.Empty, string.Empty));
+
+ _gameHud.CraftingButtonToggled += b => WindowOpen = b;
+ }
+
+ ///
+ public void Dispose()
+ {
+ _constructionView.Dispose();
+
+ _systemManager.SystemLoaded -= OnSystemLoaded;
+ _systemManager.SystemUnloaded -= OnSystemUnloaded;
+
+ _placementManager.PlacementChanged -= OnPlacementChanged;
+ }
+
+ private void OnPlacementChanged(object? sender, EventArgs e)
+ {
+ _constructionView.ResetPlacement();
+ }
+
+ private void OnViewRecipeSelected(object? sender, ItemList.Item? item)
+ {
+ if (item is null)
+ {
+ _selected = null;
+ _constructionView.ClearRecipeInfo();
+ return;
+ }
+
+ _selected = (ConstructionPrototype) item.Metadata!;
+ if (_placementManager.IsActive && !_placementManager.Eraser) UpdateGhostPlacement();
+ PopulateInfo(_selected);
+ }
+
+ private void OnViewPopulateRecipes(object? sender, (string search, string catagory) args)
+ {
+ var (search, category) = args;
+ var recipesList = _constructionView.Recipes;
+
+ recipesList.Clear();
+
+ foreach (var recipe in _prototypeManager.EnumeratePrototypes())
+ {
+ if (!string.IsNullOrEmpty(search))
+ {
+ if (!recipe.Name.ToLowerInvariant().Contains(search.Trim().ToLowerInvariant()))
+ continue;
+ }
+
+ if (!string.IsNullOrEmpty(category) && category != Loc.GetString("All"))
+ {
+ if (recipe.Category != category)
+ continue;
+ }
+
+ recipesList.Add(GetItem(recipe, recipesList));
+ }
+
+ // There is apparently no way to set which
+ }
+
+ private void PopulateCategories()
+ {
+ var uniqueCategories = new HashSet();
+
+ // hard-coded to show all recipes
+ uniqueCategories.Add(Loc.GetString("All"));
+
+ foreach (var prototype in _prototypeManager.EnumeratePrototypes())
+ {
+ var category = Loc.GetString(prototype.Category);
+
+ if (!string.IsNullOrEmpty(category))
+ uniqueCategories.Add(category);
+ }
+
+ _constructionView.CategoryButton.Clear();
+
+ var array = uniqueCategories.ToArray();
+ Array.Sort(array);
+
+ for (var i = 0; i < array.Length; i++)
+ {
+ var category = array[i];
+ _constructionView.CategoryButton.AddItem(category, i);
+ }
+
+ _constructionView.Categories = array;
+ }
+
+ private void PopulateInfo(ConstructionPrototype prototype)
+ {
+ _constructionView.ClearRecipeInfo();
+ _constructionView.SetRecipeInfo(prototype.Name, prototype.Description, prototype.Icon.Frame0(), prototype.Type != ConstructionType.Item);
+
+ var stepList = _constructionView.RecipeStepList;
+ GenerateStepList(prototype, stepList);
+ }
+
+ private void GenerateStepList(ConstructionPrototype prototype, ItemList stepList)
+ {
+ if (!_prototypeManager.TryIndex(prototype.Graph, out ConstructionGraphPrototype graph))
+ return;
+
+ var startNode = graph.Nodes[prototype.StartNode];
+ var targetNode = graph.Nodes[prototype.TargetNode];
+
+ var path = graph.Path(startNode.Name, targetNode.Name);
+
+ var current = startNode;
+
+ var stepNumber = 1;
+
+ foreach (var node in path)
+ {
+ var edge = current.GetEdge(node.Name);
+ var firstNode = current == startNode;
+
+ if (firstNode)
+ {
+ stepList.AddItem(prototype.Type == ConstructionType.Item
+ ? Loc.GetString($"{stepNumber++}. To craft this item, you need:")
+ : Loc.GetString($"{stepNumber++}. To build this, first you need:"));
+ }
+
+ foreach (var step in edge.Steps)
+ {
+ var icon = GetTextureForStep(_resourceCache, step);
+
+ switch (step)
+ {
+ case MaterialConstructionGraphStep materialStep:
+ stepList.AddItem(
+ !firstNode
+ ? Loc.GetString(
+ "{0}. Add {1}x {2}.", stepNumber++, materialStep.Amount, materialStep.Material)
+ : Loc.GetString(" {0}x {1}", materialStep.Amount, materialStep.Material), icon);
+
+ break;
+
+ case ToolConstructionGraphStep toolStep:
+ stepList.AddItem(Loc.GetString("{0}. Use a {1}.", stepNumber++, toolStep.Tool.GetToolName()), icon);
+ break;
+
+ case PrototypeConstructionGraphStep prototypeStep:
+ stepList.AddItem(Loc.GetString("{0}. Add {1}.", stepNumber++, prototypeStep.Name), icon);
+ break;
+
+ case ComponentConstructionGraphStep componentStep:
+ stepList.AddItem(Loc.GetString("{0}. Add {1}.", stepNumber++, componentStep.Name), icon);
+ break;
+
+ case NestedConstructionGraphStep nestedStep:
+ var parallelNumber = 1;
+ stepList.AddItem(Loc.GetString("{0}. In parallel...", stepNumber++));
+
+ foreach (var steps in nestedStep.Steps)
+ {
+ var subStepNumber = 1;
+
+ foreach (var subStep in steps)
+ {
+ icon = GetTextureForStep(_resourceCache, subStep);
+
+ switch (subStep)
+ {
+ case MaterialConstructionGraphStep materialStep:
+ if (!(prototype.Type == ConstructionType.Item)) stepList.AddItem(Loc.GetString(" {0}.{1}.{2}. Add {3}x {4}.", stepNumber, parallelNumber, subStepNumber++, materialStep.Amount, materialStep.Material), icon);
+ break;
+
+ case ToolConstructionGraphStep toolStep:
+ stepList.AddItem(Loc.GetString(" {0}.{1}.{2}. Use a {3}.", stepNumber, parallelNumber, subStepNumber++, toolStep.Tool.GetToolName()), icon);
+ break;
+
+ case PrototypeConstructionGraphStep prototypeStep:
+ stepList.AddItem(Loc.GetString(" {0}.{1}.{2}. Add {3}.", stepNumber, parallelNumber, subStepNumber++, prototypeStep.Name), icon);
+ break;
+
+ case ComponentConstructionGraphStep componentStep:
+ stepList.AddItem(Loc.GetString(" {0}.{1}.{2}. Add {3}.", stepNumber, parallelNumber, subStepNumber++, componentStep.Name), icon);
+ break;
+ }
+ }
+
+ parallelNumber++;
+ }
+
+ break;
+ }
+ }
+
+ current = node;
+ }
+ }
+
+ private static Texture? GetTextureForStep(IResourceCache resourceCache, ConstructionGraphStep step)
+ {
+ switch (step)
+ {
+ case MaterialConstructionGraphStep materialStep:
+ switch (materialStep.Material)
+ {
+ case StackType.Metal:
+ return resourceCache.GetTexture("/Textures/Objects/Materials/sheets.rsi/metal.png");
+
+ case StackType.Glass:
+ return resourceCache.GetTexture("/Textures/Objects/Materials/sheets.rsi/glass.png");
+
+ case StackType.Plasteel:
+ return resourceCache.GetTexture("/Textures/Objects/Materials/sheets.rsi/plasteel.png");
+
+ case StackType.Plasma:
+ return resourceCache.GetTexture("/Textures/Objects/Materials/sheets.rsi/phoron.png");
+
+ case StackType.Cable:
+ return resourceCache.GetTexture("/Textures/Objects/Tools/cables.rsi/coil-30.png");
+
+ case StackType.MetalRod:
+ return resourceCache.GetTexture("/Textures/Objects/Materials/materials.rsi/rods.png");
+ }
+
+ break;
+
+ case ToolConstructionGraphStep toolStep:
+ switch (toolStep.Tool)
+ {
+ case ToolQuality.Anchoring:
+ return resourceCache.GetTexture("/Textures/Objects/Tools/wrench.rsi/icon.png");
+ case ToolQuality.Prying:
+ return resourceCache.GetTexture("/Textures/Objects/Tools/crowbar.rsi/icon.png");
+ case ToolQuality.Screwing:
+ return resourceCache.GetTexture("/Textures/Objects/Tools/screwdriver.rsi/screwdriver-map.png");
+ case ToolQuality.Cutting:
+ return resourceCache.GetTexture("/Textures/Objects/Tools/wirecutters.rsi/cutters-map.png");
+ case ToolQuality.Welding:
+ return resourceCache.GetTexture("/Textures/Objects/Tools/welder.rsi/welder.png");
+ case ToolQuality.Multitool:
+ return resourceCache.GetTexture("/Textures/Objects/Tools/multitool.rsi/multitool.png");
+ }
+
+ break;
+
+ case ComponentConstructionGraphStep componentStep:
+ return componentStep.Icon?.Frame0();
+
+ case PrototypeConstructionGraphStep prototypeStep:
+ return prototypeStep.Icon?.Frame0();
+
+ case NestedConstructionGraphStep:
+ return null;
+ }
+
+ return null;
+ }
+
+ private static ItemList.Item GetItem(ConstructionPrototype recipe, ItemList itemList)
+ {
+ return new(itemList)
+ {
+ Metadata = recipe,
+ Text = recipe.Name,
+ Icon = recipe.Icon.Frame0(),
+ TooltipEnabled = true,
+ TooltipText = recipe.Description
+ };
+ }
+
+ private void BuildButtonToggled(bool pressed)
+ {
+ if (pressed)
+ {
+ if (_selected == null) return;
+
+ // not bound to a construction system
+ if (_constructionSystem is null)
+ {
+ _constructionView.BuildButtonPressed = false;
+ return;
+ }
+
+ if (_selected.Type == ConstructionType.Item)
+ {
+ _constructionSystem.TryStartItemConstruction(_selected.ID);
+ _constructionView.BuildButtonPressed = false;
+ return;
+ }
+
+ _placementManager.BeginPlacing(new PlacementInformation
+ {
+ IsTile = false,
+ PlacementOption = _selected.PlacementMode
+ }, new ConstructionPlacementHijack(_constructionSystem, _selected));
+
+ UpdateGhostPlacement();
+ }
+ else
+ _placementManager.Clear();
+
+ _constructionView.BuildButtonPressed = pressed;
+ }
+
+ private void UpdateGhostPlacement()
+ {
+ if (_selected == null || _selected.Type != ConstructionType.Structure) return;
+
+ var constructSystem = EntitySystem.Get();
+
+ _placementManager.BeginPlacing(new PlacementInformation()
+ {
+ IsTile = false,
+ PlacementOption = _selected.PlacementMode,
+ }, new ConstructionPlacementHijack(constructSystem, _selected));
+
+ _constructionView.BuildButtonPressed = true;
+ }
+
+ private void OnSystemLoaded(object? sender, SystemChangedArgs args)
+ {
+ if (args.System is ConstructionSystem system) SystemBindingChanged(system);
+ }
+
+ private void OnSystemUnloaded(object? sender, SystemChangedArgs args)
+ {
+ if (args.System is ConstructionSystem) SystemBindingChanged(null);
+ }
+
+ private void SystemBindingChanged(ConstructionSystem? newSystem)
+ {
+ if (newSystem is null)
+ {
+ if (_constructionSystem is null)
+ return;
+
+ UnbindFromSystem();
+ }
+ else
+ {
+ if (_constructionSystem is null)
+ {
+ BindToSystem(newSystem);
+ return;
+ }
+
+ UnbindFromSystem();
+ BindToSystem(newSystem);
+ }
+ }
+
+ private void BindToSystem(ConstructionSystem system)
+ {
+ _constructionSystem = system;
+ system.ToggleCraftingWindow += SystemOnToggleMenu;
+ system.CraftingAvailabilityChanged += SystemCraftingAvailabilityChanged;
+ }
+
+ private void UnbindFromSystem()
+ {
+ var system = _constructionSystem;
+
+ if (system is null)
+ throw new InvalidOperationException();
+
+ system.ToggleCraftingWindow -= SystemOnToggleMenu;
+ system.CraftingAvailabilityChanged -= SystemCraftingAvailabilityChanged;
+ _constructionSystem = null;
+ }
+
+ private void SystemCraftingAvailabilityChanged(object? sender, CraftingAvailabilityChangedArgs e)
+ {
+ CraftingAvailable = e.Available;
+ }
+
+ private void SystemOnToggleMenu(object? sender, EventArgs eventArgs)
+ {
+ if (!CraftingAvailable)
+ return;
+
+ if (WindowOpen)
+ {
+ if (IsAtFront)
+ {
+ WindowOpen = false;
+ _gameHud.CraftingButtonDown = false; // This does not call CraftingButtonToggled
+ }
+ else
+ _constructionView.MoveToFront();
+ }
+ else
+ {
+ WindowOpen = true;
+ _gameHud.CraftingButtonDown = true; // This does not call CraftingButtonToggled
+ }
+ }
+ }
+}
diff --git a/Content.Client/GameObjects/EntitySystems/ConstructionSystem.cs b/Content.Client/GameObjects/EntitySystems/ConstructionSystem.cs
index b4b688a3f4..2e429d9927 100644
--- a/Content.Client/GameObjects/EntitySystems/ConstructionSystem.cs
+++ b/Content.Client/GameObjects/EntitySystems/ConstructionSystem.cs
@@ -1,7 +1,6 @@
-using System.Collections.Generic;
-using Content.Client.Construction;
+using System;
+using System.Collections.Generic;
using Content.Client.GameObjects.Components.Construction;
-using Content.Client.UserInterface;
using Content.Shared.Construction;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Input;
@@ -9,12 +8,15 @@ using Content.Shared.Utility;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.Player;
+using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
+#nullable enable
+
namespace Content.Client.GameObjects.EntitySystems
{
///
@@ -23,12 +25,12 @@ namespace Content.Client.GameObjects.EntitySystems
[UsedImplicitly]
public class ConstructionSystem : SharedConstructionSystem
{
- [Dependency] private readonly IGameHud _gameHud = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
+ private readonly Dictionary _ghosts = new();
private int _nextId;
- private readonly Dictionary _ghosts = new();
- private ConstructionMenu _constructionMenu;
+
+ private bool CraftingEnabled { get; set; }
///
public override void Initialize()
@@ -46,6 +48,16 @@ namespace Content.Client.GameObjects.EntitySystems
.Register();
}
+ ///
+ public override void Shutdown()
+ {
+ CommandBinds.Unregister();
+ base.Shutdown();
+ }
+
+ public event EventHandler? CraftingAvailabilityChanged;
+ public event EventHandler? ToggleCraftingWindow;
+
private void HandleAckStructure(AckStructureConstructionMessage msg)
{
ClearGhost(msg.GhostId);
@@ -53,66 +65,32 @@ namespace Content.Client.GameObjects.EntitySystems
private void HandlePlayerAttached(PlayerAttachSysMessage msg)
{
- if (msg.AttachedEntity == null)
- {
- _gameHud.CraftingButtonVisible = false;
- return;
- }
-
- if (_constructionMenu == null)
- {
- _constructionMenu = new ConstructionMenu();
- _constructionMenu.OnClose += () => _gameHud.CraftingButtonDown = false;
- }
-
- _gameHud.CraftingButtonVisible = true;
- _gameHud.CraftingButtonToggled = b =>
- {
- if (b)
- {
- _constructionMenu.Open();
- }
- else
- {
- _constructionMenu.Close();
- }
- };
- }
-
- ///
- public override void Shutdown()
- {
- _constructionMenu?.Dispose();
-
- CommandBinds.Unregister();
- base.Shutdown();
+ var available = IsCrafingAvailable(msg.AttachedEntity);
+ UpdateCraftingAvailability(available);
}
private bool HandleOpenCraftingMenu(in PointerInputCmdHandler.PointerInputCmdArgs args)
{
- if (_playerManager.LocalPlayer.ControlledEntity == null)
- {
+ if (args.State == BoundKeyState.Down)
+ ToggleCraftingWindow?.Invoke(this, EventArgs.Empty);
+ return true;
+ }
+
+ private void UpdateCraftingAvailability(bool available)
+ {
+ if (CraftingEnabled == available)
+ return;
+
+ CraftingAvailabilityChanged?.Invoke(this, new CraftingAvailabilityChangedArgs(available));
+ CraftingEnabled = available;
+ }
+
+ private static bool IsCrafingAvailable(IEntity? entity)
+ {
+ if (entity == null)
return false;
- }
-
- var menu = _constructionMenu;
-
- if (menu.IsOpen)
- {
- if (menu.IsAtFront())
- {
- SetOpenValue(menu, false);
- }
- else
- {
- menu.MoveToFront();
- }
- }
- else
- {
- SetOpenValue(menu, true);
- }
+ // TODO: Decide if entity can craft, using capabilities or something
return true;
}
@@ -123,26 +101,11 @@ namespace Content.Client.GameObjects.EntitySystems
var entity = EntityManager.GetEntity(args.EntityUid);
- if (!entity.TryGetComponent(out ConstructionGhostComponent ghostComp))
+ if (!entity.TryGetComponent(out var ghostComp))
return false;
TryStartConstruction(ghostComp.GhostID);
return true;
-
- }
-
- private void SetOpenValue(ConstructionMenu menu, bool value)
- {
- if (value)
- {
- _gameHud.CraftingButtonDown = true;
- menu.OpenCentered();
- }
- else
- {
- _gameHud.CraftingButtonDown = false;
- menu.Close();
- }
}
///
@@ -153,10 +116,7 @@ namespace Content.Client.GameObjects.EntitySystems
var user = _playerManager.LocalPlayer?.ControlledEntity;
// This InRangeUnobstructed should probably be replaced with "is there something blocking us in that tile?"
- if (user == null || GhostPresent(loc) || !user.InRangeUnobstructed(loc, 20f, ignoreInsideBlocker:prototype.CanBuildInImpassable))
- {
- return;
- }
+ if (user == null || GhostPresent(loc) || !user.InRangeUnobstructed(loc, 20f, ignoreInsideBlocker: prototype.CanBuildInImpassable)) return;
foreach (var condition in prototype.Conditions)
{
@@ -185,10 +145,7 @@ namespace Content.Client.GameObjects.EntitySystems
{
foreach (var ghost in _ghosts)
{
- if (ghost.Value.Owner.Transform.Coordinates.Equals(loc))
- {
- return true;
- }
+ if (ghost.Value.Owner.Transform.Coordinates.Equals(loc)) return true;
}
return false;
@@ -211,7 +168,7 @@ namespace Content.Client.GameObjects.EntitySystems
}
///
- /// Removes a construction ghost entity with the given ID.
+ /// Removes a construction ghost entity with the given ID.
///
public void ClearGhost(int ghostId)
{
@@ -223,7 +180,7 @@ namespace Content.Client.GameObjects.EntitySystems
}
///
- /// Removes all construction ghosts.
+ /// Removes all construction ghosts.
///
public void ClearAllGhosts()
{
@@ -235,4 +192,14 @@ namespace Content.Client.GameObjects.EntitySystems
_ghosts.Clear();
}
}
+
+ public class CraftingAvailabilityChangedArgs : EventArgs
+ {
+ public bool Available { get; }
+
+ public CraftingAvailabilityChangedArgs(bool available)
+ {
+ Available = available;
+ }
+ }
}
diff --git a/Content.Client/State/GameScreen.cs b/Content.Client/State/GameScreen.cs
index dbbc4a2ab3..fcccc3693b 100644
--- a/Content.Client/State/GameScreen.cs
+++ b/Content.Client/State/GameScreen.cs
@@ -1,5 +1,7 @@
+#nullable enable
using Content.Client.Administration;
using Content.Client.Chat;
+using Content.Client.Construction;
using Content.Client.Interfaces.Chat;
using Content.Client.UserInterface;
using Content.Client.Voting;
@@ -26,7 +28,8 @@ namespace Content.Client.State
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IClientAdminManager _adminManager = default!;
- [ViewVariables] private ChatBox _gameChat;
+ [ViewVariables] private ChatBox? _gameChat;
+ private ConstructionMenuPresenter? _constructionMenu;
private bool _oocEnabled;
private bool _adminOocEnabled;
@@ -61,16 +64,38 @@ namespace Content.Client.State
_configurationManager.OnValueChanged(CCVars.OocEnabled, OnOocEnabledChanged, true);
_configurationManager.OnValueChanged(CCVars.AdminOocEnabled, OnAdminOocEnabledChanged, true);
_adminManager.AdminStatusUpdated += OnAdminStatusUpdated;
+
+ SetupPresenters();
}
public override void Shutdown()
{
+ DisposePresenters();
+
base.Shutdown();
- _gameChat.Dispose();
+ _gameChat?.Dispose();
_gameHud.RootControl.Orphan();
+
}
+ ///
+ /// All UI Presenters should be constructed in here.
+ ///
+ private void SetupPresenters()
+ {
+ _constructionMenu = new ConstructionMenuPresenter(_gameHud);
+ }
+
+ ///
+ /// All UI Presenters should be disposed in here.
+ ///
+ private void DisposePresenters()
+ {
+ _constructionMenu?.Dispose();
+ }
+
+
private void OnOocEnabledChanged(bool val)
{
_oocEnabled = val;
@@ -80,6 +105,9 @@ namespace Content.Client.State
return;
}
+ if(_gameChat is null)
+ return;
+
_gameChat.Input.PlaceHolder = Loc.GetString(_oocEnabled ? "Say something! [ for OOC" : "Say something!");
}
@@ -92,11 +120,17 @@ namespace Content.Client.State
return;
}
+ if (_gameChat is null)
+ return;
+
_gameChat.Input.PlaceHolder = Loc.GetString(_adminOocEnabled ? "Say something! [ for OOC" : "Say something!");
}
private void OnAdminStatusUpdated()
{
+ if (_gameChat is null)
+ return;
+
_gameChat.Input.PlaceHolder = _adminManager.IsActive()
? Loc.GetString(_adminOocEnabled ? "Say something! [ for OOC" : "Say something!")
: Loc.GetString(_oocEnabled ? "Say something! [ for OOC" : "Say something!");