using System.Diagnostics; using System.Linq; using Content.Client.Guidebook.RichText; using Content.Client.UserInterface.ControlExtensions; using Content.Client.UserInterface.Controls; using Content.Client.UserInterface.Controls.FancyTree; using Content.Client.UserInterface.Systems.Info; using Content.Shared.Guidebook; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared.ContentPack; using Robust.Shared.Prototypes; namespace Content.Client.Guidebook.Controls; [GenerateTypedNameReferences] public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler, IAnchorClickHandler { [Dependency] private readonly DocumentParsingManager _parsingMan = default!; [Dependency] private readonly IResourceManager _resourceManager = default!; private Dictionary, GuideEntry> _entries = new(); private readonly ISawmill _sawmill; public ProtoId LastEntry; public GuidebookWindow() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); _sawmill = Logger.GetSawmill("guidebook"); Tree.OnSelectedItemChanged += OnSelectionChanged; SearchBar.OnTextChanged += _ => { HandleFilter(); }; } public void HandleClick(string link) { if (!_entries.TryGetValue(link, out var entry)) return; if (Tree.TryGetIndexFromMetadata(entry, out var index)) { Tree.ExpandParentEntries(index.Value); Tree.SetSelectedIndex(index); } else ShowGuide(entry); } public void HandleAnchor(IPrototypeLinkControl prototypeLinkControl) { var prototype = prototypeLinkControl.LinkedPrototype; if (prototype == null) return; var (linkableControls, _) = GetLinkableControlsAndLinks(EntryContainer); foreach (var linkableControl in linkableControls) { if (linkableControl.RepresentedPrototype != prototype) continue; if (linkableControl is not Control control) return; // Check if the target item is currently filtered out if (!control.Visible) control.Visible = true; UserInterfaceManager.DeferAction(() => { if (control.GetControlScrollPosition() is not {} position) return; Scroll.HScrollTarget = position.X; Scroll.VScrollTarget = position.Y; }); break; } } private void OnSelectionChanged(TreeItem? item) { if (item != null && item.Metadata is GuideEntry entry) { ShowGuide(entry); var isRulesEntry = entry.RuleEntry; ReturnContainer.Visible = isRulesEntry; HomeButton.OnPressed += _ => ShowGuide(entry); } else ClearSelectedGuide(); } public void ClearSelectedGuide() { Placeholder.Visible = true; EntryContainer.Visible = false; SearchContainer.Visible = false; EntryContainer.RemoveAllChildren(); } private void ShowGuide(GuideEntry entry) { Scroll.SetScrollValue(default); Placeholder.Visible = false; EntryContainer.Visible = true; SearchBar.Text = ""; EntryContainer.RemoveAllChildren(); using var file = _resourceManager.ContentFileReadText(entry.Text); SearchContainer.Visible = entry.FilterEnabled; if (!_parsingMan.TryAddMarkup(EntryContainer, file.ReadToEnd())) { // The guidebook will automatically display the in-guidebook error if it fails _sawmill.Error($"Failed to parse contents of guide document {entry.Id}."); } LastEntry = entry.Id; var (linkableControls, linkControls) = GetLinkableControlsAndLinks(EntryContainer); HashSet availablePrototypeLinks = new(); foreach (var linkableControl in linkableControls) { var prototype = linkableControl.RepresentedPrototype; if (prototype != null) availablePrototypeLinks.Add(prototype); } foreach (var linkControl in linkControls) { var prototype = linkControl.LinkedPrototype; if (prototype != null && availablePrototypeLinks.Contains(prototype)) linkControl.EnablePrototypeLink(); } } public void UpdateGuides( Dictionary, GuideEntry> entries, List>? rootEntries = null, ProtoId? forceRoot = null, ProtoId? selected = null) { _entries = entries; RepopulateTree(rootEntries, forceRoot); ClearSelectedGuide(); Split.State = SplitContainer.SplitState.Auto; if (entries.Count == 1) { TreeBox.Visible = false; Split.ResizeMode = SplitContainer.SplitResizeMode.NotResizable; selected = entries.Keys.First(); } else { TreeBox.Visible = true; Split.ResizeMode = SplitContainer.SplitResizeMode.RespectChildrenMinSize; } if (selected != null) { var item = Tree.Items.FirstOrDefault(x => x.Metadata is GuideEntry entry && entry.Id == selected); Tree.SetSelectedIndex(item?.Index); } } private IEnumerable GetSortedEntries(List>? rootEntries) { if (rootEntries == null) { HashSet> entries = new(_entries.Keys); foreach (var entry in _entries.Values) { entries.ExceptWith(entry.Children); } rootEntries = entries.ToList(); } // Only roots need to be sorted. // As defined in the SS14 Dev Wiki, children are already sorted based on their child field order within their parent's prototype definition. // Roots are sorted by priority. If there is no defined priority for a root then it is by definition sorted undefined. return rootEntries .Select(rootEntryId => _entries[rootEntryId]) .OrderBy(rootEntry => rootEntry.Priority) .ThenBy(rootEntry => Loc.GetString(rootEntry.Name)); } private void RepopulateTree(List>? roots = null, ProtoId? forcedRoot = null) { Tree.Clear(); HashSet> addedEntries = new(); var parent = forcedRoot == null ? null : AddEntry(forcedRoot.Value, null, addedEntries); foreach (var entry in GetSortedEntries(roots)) { AddEntry(entry.Id, parent, addedEntries); } Tree.SetAllExpanded(true); } private TreeItem? AddEntry(ProtoId id, TreeItem? parent, HashSet> addedEntries) { if (!_entries.TryGetValue(id, out var entry)) return null; if (!addedEntries.Add(id)) { // TODO GUIDEBOOK Maybe allow duplicate entries? // E.g., for adding medicine under both chemicals & the chemist job _sawmill.Error($"Adding duplicate guide entry: {id}"); return null; } var rulesProto = UserInterfaceManager.GetUIController().GetCoreRuleEntry(); if (entry.RuleEntry && entry.Id != rulesProto.Id) return null; var item = Tree.AddItem(parent); item.Metadata = entry; var name = Loc.GetString(entry.Name); item.Label.Text = name; foreach (var child in entry.Children) { AddEntry(child, item, addedEntries); } return item; } private void HandleFilter() { var emptySearch = SearchBar.Text.Trim().Length == 0; if (Tree.SelectedItem != null && Tree.SelectedItem.Metadata is GuideEntry entry && entry.FilterEnabled) { var foundElements = EntryContainer.GetSearchableControls(); foreach (var element in foundElements) { element.SetHiddenState(true, SearchBar.Text.Trim()); } } } private static (List, List) GetLinkableControlsAndLinks(Control parent) { List linkableList = new(); List linkList = new(); foreach (var child in parent.Children) { var hasChildren = child.ChildCount > 0; if (child is IPrototypeLinkControl linkChild) linkList.Add(linkChild); else if (child is IPrototypeRepresentationControl linkableChild) linkableList.Add(linkableChild); if (!hasChildren) continue; var (childLinkableList, childLinkList) = GetLinkableControlsAndLinks(child); linkableList.AddRange(childLinkableList); linkList.AddRange(childLinkList); } return (linkableList, linkList); } }