using System; using System.Collections.Generic; using System.Linq; using Content.Client.GameObjects.Components.Construction; using Content.Shared.Construction; using Robust.Client.Graphics; using Robust.Client.Interfaces.Placement; using Robust.Client.Interfaces.ResourceManagement; using Robust.Client.Placement; using Robust.Client.ResourceManagement; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; using Robust.Client.Utility; using Robust.Shared.Enums; using Robust.Shared.Interfaces.GameObjects.Components; using Robust.Shared.IoC; using Robust.Shared.Maths; using Robust.Shared.Prototypes; namespace Content.Client.Construction { public class ConstructionMenu : SS14Window { #pragma warning disable CS0649 [Dependency] readonly IPrototypeManager PrototypeManager; [Dependency] readonly IResourceCache ResourceCache; #pragma warning restore public ConstructorComponent Owner { get; set; } private readonly Button BuildButton; private readonly Button EraseButton; private readonly LineEdit SearchBar; private readonly Tree RecipeList; private readonly TextureRect InfoIcon; private readonly Label InfoLabel; private readonly ItemList StepList; private CategoryNode RootCategory; // This list is flattened in such a way that the top most deepest category is first. private List FlattenedCategories; private readonly PlacementManager Placement; protected override Vector2? CustomSize => (500, 350); public ConstructionMenu() { IoCManager.InjectDependencies(this); Placement = (PlacementManager) IoCManager.Resolve(); Placement.PlacementCanceled += OnPlacementCanceled; Title = "Construction"; var hSplitContainer = new HSplitContainer(); // Left side var recipes = new VBoxContainer {CustomMinimumSize = new Vector2(150.0f, 0.0f)}; SearchBar = new LineEdit {PlaceHolder = "Search"}; RecipeList = new Tree {SizeFlagsVertical = SizeFlags.FillExpand, HideRoot = true}; recipes.AddChild(SearchBar); recipes.AddChild(RecipeList); hSplitContainer.AddChild(recipes); // Right side var guide = new VBoxContainer(); var info = new HBoxContainer(); InfoIcon = new TextureRect(); InfoLabel = new Label { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsVertical = SizeFlags.ShrinkCenter }; info.AddChild(InfoIcon); info.AddChild(InfoLabel); guide.AddChild(info); var stepsLabel = new Label { SizeFlagsHorizontal = SizeFlags.ShrinkCenter, SizeFlagsVertical = SizeFlags.ShrinkCenter, Text = "Steps" }; guide.AddChild(stepsLabel); StepList = new ItemList { SizeFlagsVertical = SizeFlags.FillExpand, SelectMode = ItemList.ItemListSelectMode.None }; guide.AddChild(StepList); var buttonsContainer = new HBoxContainer(); BuildButton = new Button { SizeFlagsHorizontal = SizeFlags.FillExpand, TextAlign = Button.AlignMode.Center, Text = "Build!", Disabled = true, ToggleMode = false }; EraseButton = new Button { TextAlign = Button.AlignMode.Center, Text = "Clear Ghosts", ToggleMode = true }; buttonsContainer.AddChild(BuildButton); buttonsContainer.AddChild(EraseButton); guide.AddChild(buttonsContainer); hSplitContainer.AddChild(guide); Contents.AddChild(hSplitContainer); BuildButton.OnPressed += OnBuildPressed; EraseButton.OnToggled += OnEraseToggled; SearchBar.OnTextChanged += OnTextEntered; RecipeList.OnItemSelected += OnItemSelected; PopulatePrototypeList(); PopulateTree(); } protected override void Dispose(bool disposing) { base.Dispose(disposing); if (disposing) { Placement.PlacementCanceled -= OnPlacementCanceled; } } void OnItemSelected() { var prototype = (ConstructionPrototype) RecipeList.Selected.Metadata; if (prototype == null) { InfoLabel.Text = ""; InfoIcon.Texture = null; StepList.Clear(); BuildButton.Disabled = true; } else { BuildButton.Disabled = false; InfoLabel.Text = prototype.Description; InfoIcon.Texture = prototype.Icon.Frame0(); StepList.Clear(); foreach (var forward in prototype.Stages.Select(a => a.Forward)) { if (forward == null) { continue; } Texture icon; string text; switch (forward) { case ConstructionStepMaterial mat: switch (mat.Material) { case ConstructionStepMaterial.MaterialType.Metal: icon = ResourceCache.GetResource( "/Textures/Objects/Materials/sheet_metal.png"); text = $"Metal x{mat.Amount}"; break; case ConstructionStepMaterial.MaterialType.Glass: icon = ResourceCache.GetResource( "/Textures/Objects/Materials/sheet_glass.png"); text = $"Glass x{mat.Amount}"; break; case ConstructionStepMaterial.MaterialType.Cable: icon = ResourceCache.GetResource( "/Textures/Objects/Tools/cable_coil.png"); text = $"Cable Coil x{mat.Amount}"; break; default: throw new NotImplementedException(); } break; case ConstructionStepTool tool: switch (tool.Tool) { case ConstructionStepTool.ToolType.Wrench: icon = ResourceCache.GetResource("/Textures/Objects/Tools/wrench.png"); text = "Wrench"; break; case ConstructionStepTool.ToolType.Crowbar: icon = ResourceCache.GetResource("/Textures/Objects/Tools/crowbar.png"); text = "Crowbar"; break; case ConstructionStepTool.ToolType.Screwdriver: icon = ResourceCache.GetResource( "/Textures/Objects/Tools/screwdriver.png"); text = "Screwdriver"; break; case ConstructionStepTool.ToolType.Welder: icon = ResourceCache.GetResource("/Textures/Objects/tools.rsi") .RSI["welder"].Frame0; text = $"Welding tool ({tool.Amount} fuel)"; break; case ConstructionStepTool.ToolType.Wirecutters: icon = ResourceCache.GetResource( "/Textures/Objects/Tools/wirecutter.png"); text = "Wirecutters"; break; default: throw new NotImplementedException(); } break; default: throw new NotImplementedException(); } StepList.AddItem(text, icon, false); } } } void OnTextEntered(LineEdit.LineEditEventArgs args) { var str = args.Text; PopulateTree(string.IsNullOrWhiteSpace(str) ? null : str.ToLowerInvariant()); } void OnBuildPressed(BaseButton.ButtonEventArgs args) { var prototype = (ConstructionPrototype) RecipeList.Selected.Metadata; if (prototype == null) { return; } if (prototype.Type != ConstructionType.Structure) { // In-hand attackby doesn't exist so this is the best alternative. var loc = Owner.Owner.GetComponent().GridPosition; Owner.SpawnGhost(prototype, loc, Direction.North); return; } var hijack = new ConstructionPlacementHijack(prototype, Owner); var info = new PlacementInformation { IsTile = false, PlacementOption = prototype.PlacementMode, }; Placement.BeginHijackedPlacing(info, hijack); } private void OnEraseToggled(BaseButton.ButtonToggledEventArgs args) { var hijack = new ConstructionPlacementHijack(null, Owner); Placement.ToggleEraserHijacked(hijack); } void PopulatePrototypeList() { RootCategory = new CategoryNode("", null); int count = 1; foreach (var prototype in PrototypeManager.EnumeratePrototypes()) { var currentNode = RootCategory; foreach (var category in prototype.CategorySegments) { if (!currentNode.ChildCategories.TryGetValue(category, out var subNode)) { count++; subNode = new CategoryNode(category, currentNode); currentNode.ChildCategories.Add(category, subNode); } currentNode = subNode; } currentNode.Prototypes.Add(prototype); } // Do a pass to sort the prototype lists and flatten the hierarchy. void Recurse(CategoryNode node) { // I give up we're using recursion to flatten this. // There probably IS a way to do it. // I'm too stupid to think of what that way is. foreach (var child in node.ChildCategories.Values) { Recurse(child); } node.Prototypes.Sort(ComparePrototype); FlattenedCategories.Add(node); node.FlattenedIndex = FlattenedCategories.Count - 1; } FlattenedCategories = new List(count); Recurse(RootCategory); } void PopulateTree(string searchTerm = null) { RecipeList.Clear(); var categoryItems = new Tree.Item[FlattenedCategories.Count]; categoryItems[RootCategory.FlattenedIndex] = RecipeList.CreateItem(); // Yay more recursion. Tree.Item ItemForNode(CategoryNode node) { if (categoryItems[node.FlattenedIndex] != null) { return categoryItems[node.FlattenedIndex]; } var item = RecipeList.CreateItem(ItemForNode(node.Parent)); item.Text = node.Name; item.Selectable = false; categoryItems[node.FlattenedIndex] = item; return item; } foreach (var node in FlattenedCategories) { foreach (var prototype in node.Prototypes) { if (searchTerm != null) { var found = false; // TODO: don't run ToLowerInvariant() constantly. if (prototype.Name.ToLowerInvariant().IndexOf(searchTerm, StringComparison.Ordinal) != -1) { found = true; } else { foreach (var keyw in prototype.Keywords.Concat(prototype.CategorySegments)) { // TODO: don't run ToLowerInvariant() constantly. if (keyw.ToLowerInvariant().IndexOf(searchTerm, StringComparison.Ordinal) != -1) { found = true; break; } } } if (!found) { continue; } } var subItem = RecipeList.CreateItem(ItemForNode(node)); subItem.Text = prototype.Name; subItem.Metadata = prototype; } } } private void OnPlacementCanceled(object sender, EventArgs e) { EraseButton.Pressed = false; } private static int ComparePrototype(ConstructionPrototype x, ConstructionPrototype y) { return String.Compare(x.Name, y.Name, StringComparison.Ordinal); } class CategoryNode { public readonly string Name; public readonly CategoryNode Parent; public SortedDictionary ChildCategories = new SortedDictionary(); public List Prototypes = new List(); public int FlattenedIndex = -1; public CategoryNode(string name, CategoryNode parent) { Name = name; Parent = parent; } } } }