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:
Leon Friedrich
2023-01-16 21:42:22 +13:00
committed by GitHub
parent abcdd04f3c
commit 22d72f56b5
65 changed files with 1626 additions and 16 deletions

View 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>

View 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;
}
}

View 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>

View 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;
}
}