Guidebook Revival (#13320)
* Fix some bugs in stations and do a little cleanup. * Begin backporting the guidebook. * wow that's a lot of work. * More work, gives the monkey some more interactions. * disco monkye. * monky * jobs entry. * more writing. * disco * im being harassed * fix spacing. * i hate writing. * Update Resources/Prototypes/Entities/Mobs/NPCs/animals.yml Co-authored-by: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> * builds again * a * pilfer changes from AL * fix and remove unused code * pilfer actual guide changes from AL * localization * more error logs & safety checks * replace controls button with command * add test * todos * pidgin parsing * remove old parser * Move files and change tree sorting * add localization and public methods. * Add help component/verb * rename ITag to IDocumentTag * Fix yml and tweak tooltips * autoclose tooltip * Split container * Fancier-tree * Hover color * txt to xml * oops * Curse you hidden merge conflicts * Rename parsing manager * Stricter arg parsing tag args must now be of the form key="value" * Change default args * Moar tests * nullable enable * Even fancier tree * extremely fancy trees * better indent icons * stricter xml and subheadings * tweak embed margin * Fix parsing bugs * quick fixes. * spain. * ogh * hn bmvdsyc Co-authored-by: moonheart08 <moonheart08@users.noreply.github.com>
This commit is contained in:
6
Content.Client/Guidebook/Controls/GuideEntityEmbed.xaml
Normal file
6
Content.Client/Guidebook/Controls/GuideEntityEmbed.xaml
Normal file
@@ -0,0 +1,6 @@
|
||||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
Orientation="Vertical"
|
||||
Margin="5 5 5 5">
|
||||
<SpriteView Name="View"/>
|
||||
<Label Name="Caption" HorizontalAlignment="Center"/>
|
||||
</BoxContainer>
|
||||
172
Content.Client/Guidebook/Controls/GuideEntityEmbed.xaml.cs
Normal file
172
Content.Client/Guidebook/Controls/GuideEntityEmbed.xaml.cs
Normal file
@@ -0,0 +1,172 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Client.ContextMenu.UI;
|
||||
using Content.Client.Examine;
|
||||
using Content.Client.Guidebook.Richtext;
|
||||
using Content.Client.Verbs;
|
||||
using Content.Client.Verbs.UI;
|
||||
using Content.Shared.Input;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Client.Guidebook.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Control for embedding an entity into a guidebook/document. This is effectively a sprite-view that supports
|
||||
/// examination, interactions, and captions.
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class GuideEntityEmbed : BoxContainer, IDocumentTag
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _systemManager = default!;
|
||||
[Dependency] private readonly IUserInterfaceManager _ui = default!;
|
||||
|
||||
private readonly TagSystem _tagSystem;
|
||||
private readonly ExamineSystem _examineSystem;
|
||||
private readonly GuidebookSystem _guidebookSystem;
|
||||
|
||||
public bool Interactive;
|
||||
|
||||
public SpriteComponent? Sprite
|
||||
{
|
||||
get => View.Sprite;
|
||||
set => View.Sprite = value;
|
||||
}
|
||||
|
||||
public Vector2 Scale
|
||||
{
|
||||
get => View.Scale;
|
||||
set => View.Scale = value;
|
||||
}
|
||||
|
||||
public GuideEntityEmbed()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
_tagSystem = _systemManager.GetEntitySystem<TagSystem>();
|
||||
_examineSystem = _systemManager.GetEntitySystem<ExamineSystem>();
|
||||
_guidebookSystem = _systemManager.GetEntitySystem<GuidebookSystem>();
|
||||
MouseFilter = MouseFilterMode.Stop;
|
||||
}
|
||||
|
||||
public GuideEntityEmbed(string proto, bool caption, bool interactive) : this()
|
||||
{
|
||||
Interactive = interactive;
|
||||
|
||||
var ent = _entityManager.SpawnEntity(proto, MapCoordinates.Nullspace);
|
||||
Sprite = _entityManager.GetComponent<SpriteComponent>(ent);
|
||||
|
||||
if (caption)
|
||||
Caption.Text = _entityManager.GetComponent<MetaDataComponent>(ent).EntityName;
|
||||
}
|
||||
|
||||
protected override void KeyBindDown(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
base.KeyBindDown(args);
|
||||
// get an entity associated with this element
|
||||
var entity = Sprite?.Owner;
|
||||
|
||||
// Deleted() automatically checks for null & existence.
|
||||
if (_entityManager.Deleted(entity))
|
||||
return;
|
||||
|
||||
// do examination?
|
||||
if (args.Function == ContentKeyFunctions.ExamineEntity)
|
||||
{
|
||||
_examineSystem.DoExamine(entity.Value);
|
||||
args.Handle();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Interactive)
|
||||
return;
|
||||
|
||||
// open verb menu?
|
||||
if (args.Function == EngineKeyFunctions.UseSecondary)
|
||||
{
|
||||
_ui.GetUIController<VerbMenuUIController>().OpenVerbMenu(entity.Value);
|
||||
args.Handle();
|
||||
return;
|
||||
}
|
||||
|
||||
// from here out we're faking interactions! sue me. --moony
|
||||
|
||||
if (args.Function == ContentKeyFunctions.ActivateItemInWorld)
|
||||
{
|
||||
_guidebookSystem.FakeClientActivateInWorld(entity.Value);
|
||||
_ui.GetUIController<ContextMenuUIController>().Close();
|
||||
args.Handle();
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Function == ContentKeyFunctions.AltActivateItemInWorld)
|
||||
{
|
||||
_guidebookSystem.FakeClientAltActivateInWorld(entity.Value);
|
||||
_ui.GetUIController<ContextMenuUIController>().Close();
|
||||
args.Handle();
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Function == ContentKeyFunctions.AltActivateItemInWorld)
|
||||
{
|
||||
_guidebookSystem.FakeClientUse(entity.Value);
|
||||
_ui.GetUIController<ContextMenuUIController>().Close();
|
||||
args.Handle();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (Sprite is not null)
|
||||
_entityManager.DeleteEntity(Sprite.Owner);
|
||||
}
|
||||
|
||||
public bool TryParseTag(Dictionary<string, string> args, [NotNullWhen(true)] out Control? control)
|
||||
{
|
||||
if (!args.TryGetValue("Entity", out var proto))
|
||||
{
|
||||
Logger.Error("Entity embed tag is missing entity prototype argument");
|
||||
control = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var ent = _entityManager.SpawnEntity(proto, MapCoordinates.Nullspace);
|
||||
|
||||
_tagSystem.AddTag(ent, GuidebookSystem.GuideEmbedTag);
|
||||
Sprite = _entityManager.GetComponent<SpriteComponent>(ent);
|
||||
|
||||
if (!args.TryGetValue("Caption", out var caption))
|
||||
caption = _entityManager.GetComponent<MetaDataComponent>(ent).EntityName;
|
||||
|
||||
if (!string.IsNullOrEmpty(caption))
|
||||
Caption.Text = caption;
|
||||
// else:
|
||||
// caption text already defaults to null
|
||||
|
||||
if (args.TryGetValue("Scale", out var scaleStr))
|
||||
{
|
||||
var scale = float.Parse(scaleStr);
|
||||
Scale = new Vector2(scale, scale);
|
||||
}
|
||||
else
|
||||
{
|
||||
Scale = (2, 2);
|
||||
}
|
||||
|
||||
if (args.TryGetValue("Interactive", out var interactive))
|
||||
Interactive = bool.Parse(interactive);
|
||||
|
||||
Margin = new Thickness(4, 8);
|
||||
|
||||
control = this;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
26
Content.Client/Guidebook/Controls/GuidebookWindow.xaml
Normal file
26
Content.Client/Guidebook/Controls/GuidebookWindow.xaml
Normal file
@@ -0,0 +1,26 @@
|
||||
<controls:FancyWindow xmlns:ui="clr-namespace:Content.Client.UserInterface"
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
xmlns:fancyTree="clr-namespace:Content.Client.UserInterface.Controls.FancyTree"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
SetSize="750 700"
|
||||
MinSize="100 200"
|
||||
Resizable="True"
|
||||
Title="{Loc 'guidebook-window-title'}">
|
||||
<SplitContainer Orientation="Horizontal" HorizontalExpand="True" Name="Split">
|
||||
<!-- Guide select -->
|
||||
<BoxContainer Orientation="Horizontal" Name="TreeBox">
|
||||
<fancyTree:FancyTree Name="Tree" VerticalExpand="True" HorizontalExpand="True"/>
|
||||
<cc:VSeparator StyleClasses="LowDivider" Margin="0 -2"/>
|
||||
</BoxContainer>
|
||||
<ScrollContainer Name="Scroll" HScrollEnabled="False" HorizontalExpand="True" VerticalExpand="True">
|
||||
<Control>
|
||||
<BoxContainer Orientation="Vertical" Name="EntryContainer" Margin="5 5 5 5" Visible="False"/>
|
||||
<BoxContainer Orientation="Vertical" Name="Placeholder" Margin="5 5 5 5">
|
||||
<Label HorizontalAlignment="Center" VerticalAlignment="Center" Text="{Loc 'guidebook-placeholder-text'}"/>
|
||||
<Label HorizontalAlignment="Center" VerticalAlignment="Center" Text="{Loc 'guidebook-placeholder-text-2'}"/>
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
</ScrollContainer>
|
||||
</SplitContainer>
|
||||
</controls:FancyWindow>
|
||||
142
Content.Client/Guidebook/Controls/GuidebookWindow.xaml.cs
Normal file
142
Content.Client/Guidebook/Controls/GuidebookWindow.xaml.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using System.Linq;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.UserInterface.Controls.FancyTree;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.ContentPack;
|
||||
|
||||
namespace Content.Client.Guidebook.Controls;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class GuidebookWindow : FancyWindow
|
||||
{
|
||||
[Dependency] private readonly IResourceManager _resourceManager = default!;
|
||||
[Dependency] private readonly DocumentParsingManager _parsingMan = default!;
|
||||
|
||||
private Dictionary<string, GuideEntry> _entries = new();
|
||||
|
||||
public GuidebookWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
Tree.OnSelectedItemChanged += OnSelectionChanged;
|
||||
}
|
||||
|
||||
private void OnSelectionChanged(TreeItem? item)
|
||||
{
|
||||
if (item != null && item.Metadata is GuideEntry entry)
|
||||
ShowGuide(entry);
|
||||
else
|
||||
ClearSelectedGuide();
|
||||
}
|
||||
|
||||
public void ClearSelectedGuide()
|
||||
{
|
||||
Placeholder.Visible = true;
|
||||
EntryContainer.Visible = false;
|
||||
EntryContainer.RemoveAllChildren();
|
||||
}
|
||||
|
||||
private void ShowGuide(GuideEntry entry)
|
||||
{
|
||||
Scroll.SetScrollValue(default);
|
||||
Placeholder.Visible = false;
|
||||
EntryContainer.Visible = true;
|
||||
EntryContainer.RemoveAllChildren();
|
||||
using var file = _resourceManager.ContentFileReadText(entry.Text);
|
||||
|
||||
if (!_parsingMan.TryAddMarkup(EntryContainer, file.ReadToEnd()))
|
||||
{
|
||||
EntryContainer.AddChild(new Label() { Text = "ERROR: Failed to parse document." });
|
||||
Logger.Error($"Failed to parse contents of guide document {entry.Id}.");
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateGuides(
|
||||
Dictionary<string, GuideEntry> entries,
|
||||
List<string>? rootEntries = null,
|
||||
string? forceRoot = null,
|
||||
string? 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<GuideEntry> GetSortedRootEntries(List<string>? rootEntries)
|
||||
{
|
||||
if (rootEntries == null)
|
||||
{
|
||||
HashSet<string> entries = new(_entries.Keys);
|
||||
foreach (var entry in _entries.Values)
|
||||
{
|
||||
entries.ExceptWith(entry.Children);
|
||||
}
|
||||
rootEntries = entries.ToList();
|
||||
}
|
||||
|
||||
return rootEntries
|
||||
.Select(x => _entries[x])
|
||||
.OrderBy(x => x.Priority)
|
||||
.ThenBy(x => Loc.GetString(x.Name));
|
||||
}
|
||||
|
||||
private void RepopulateTree(List<string>? roots = null, string? forcedRoot = null)
|
||||
{
|
||||
Tree.Clear();
|
||||
|
||||
HashSet<string> addedEntries = new();
|
||||
|
||||
TreeItem? parent = forcedRoot == null ? null : AddEntry(forcedRoot, null, addedEntries);
|
||||
foreach (var entry in GetSortedRootEntries(roots))
|
||||
{
|
||||
AddEntry(entry.Id, parent, addedEntries);
|
||||
}
|
||||
Tree.SetAllExpanded(true);
|
||||
}
|
||||
|
||||
private TreeItem? AddEntry(string id, TreeItem? parent, HashSet<string> addedEntries)
|
||||
{
|
||||
if (!_entries.TryGetValue(id, out var entry))
|
||||
return null;
|
||||
|
||||
if (!addedEntries.Add(id))
|
||||
{
|
||||
Logger.Error($"Adding duplicate guide entry: {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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user