using System; using System.Collections.Generic; using System.Linq; using Content.Client.HUD; using Content.Client.Resources; using Content.Shared.Construction.Prototypes; using Content.Shared.Construction.Steps; using Content.Shared.Tools; using Content.Shared.Tools.Components; 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.UI { /// /// 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 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(); if(_selected != null) PopulateInfo(_selected); } 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 += OnHudCraftingButtonToggled; } private void OnHudCraftingButtonToggled(bool b) { WindowOpen = b; } /// public void Dispose() { _constructionView.Dispose(); SystemBindingChanged(null); _systemManager.SystemLoaded -= OnSystemLoaded; _systemManager.SystemUnloaded -= OnSystemUnloaded; _placementManager.PlacementChanged -= OnPlacementChanged; _gameHud.CraftingButtonToggled -= OnHudCraftingButtonToggled; } 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(); var recipes = new List(); 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("construction-presenter-category-all")) { if (recipe.Category != category) continue; } recipes.Add(recipe); } recipes.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.InvariantCulture)); foreach (var recipe in recipes) { 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("construction-presenter-category-all")); foreach (var prototype in _prototypeManager.EnumeratePrototypes()) { var category = Loc.GetString(prototype.Category); if (!string.IsNullOrEmpty(category)) uniqueCategories.Add(category); } _constructionView.Category.Clear(); var array = uniqueCategories.ToArray(); Array.Sort(array); for (var i = 0; i < array.Length; i++) { var category = array[i]; _constructionView.Category.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 (_constructionSystem?.GetGuide(prototype) is not { } guide) return; foreach (var entry in guide.Entries) { var text = entry.Arguments != null ? Loc.GetString(entry.Localization, entry.Arguments) : Loc.GetString(entry.Localization); if (entry.EntryNumber is {} number) text = Loc.GetString("construction-presenter-step-wrapper", ("step-number", number), ("text", text)); // The padding needs to be applied regardless of text length... (See PadLeft documentation) text = text.PadLeft(text.Length + entry.Padding); stepList.AddItem(text, entry.Icon?.Frame0(), false); } } 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; system.ConstructionGuideAvailable += SystemGuideAvailable; CraftingAvailable = system.CraftingEnabled; } private void UnbindFromSystem() { var system = _constructionSystem; if (system is null) throw new InvalidOperationException(); system.ToggleCraftingWindow -= SystemOnToggleMenu; system.CraftingAvailabilityChanged -= SystemCraftingAvailabilityChanged; system.ConstructionGuideAvailable -= SystemGuideAvailable; _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 } } private void SystemGuideAvailable(object? sender, string e) { if (!CraftingAvailable) return; if (!WindowOpen) return; if (_selected == null) return; PopulateInfo(_selected); } } }