From df5e6a2b5c2fcc71ada200f510e07ad07dc6510e Mon Sep 17 00:00:00 2001 From: Thomas <87614336+Aeshus@users.noreply.github.com> Date: Mon, 21 Apr 2025 06:24:44 -0400 Subject: [PATCH] Add RGA/RSI to Credits (#36704) * Add RGA and RSI to Credits * Move to thread + add directory field --- Content.Client/Credits/CreditsWindow.xaml | 12 +- Content.Client/Credits/CreditsWindow.xaml.cs | 423 +++++++++++++----- .../Locale/en-US/credits/credits-window.ftl | 10 +- 3 files changed, 328 insertions(+), 117 deletions(-) diff --git a/Content.Client/Credits/CreditsWindow.xaml b/Content.Client/Credits/CreditsWindow.xaml index 2cb6f7ab92..b7bc2329c1 100644 --- a/Content.Client/Credits/CreditsWindow.xaml +++ b/Content.Client/Credits/CreditsWindow.xaml @@ -1,7 +1,7 @@  - + Title="{Loc 'credits-window-title'}" + SetSize="650 650"> + + + + diff --git a/Content.Client/Credits/CreditsWindow.xaml.cs b/Content.Client/Credits/CreditsWindow.xaml.cs index ba24020953..64f5479421 100644 --- a/Content.Client/Credits/CreditsWindow.xaml.cs +++ b/Content.Client/Credits/CreditsWindow.xaml.cs @@ -1,184 +1,381 @@ -using System.Collections.Generic; using System.Linq; using System.Numerics; +using System.Threading.Tasks; using Content.Client.Stylesheets; using Content.Shared.CCVar; using Robust.Client.AutoGenerated; using Robust.Client.Credits; -using Robust.Client.ResourceManagement; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Configuration; using Robust.Shared.ContentPack; -using Robust.Shared.IoC; -using Robust.Shared.Localization; -using Robust.Shared.Maths; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Mapping; +using Robust.Shared.Serialization.Markdown.Sequence; using Robust.Shared.Utility; using YamlDotNet.RepresentationModel; using static Robust.Client.UserInterface.Controls.BoxContainer; -namespace Content.Client.Credits +namespace Content.Client.Credits; + +[GenerateTypedNameReferences] +public sealed partial class CreditsWindow : DefaultWindow { - [GenerateTypedNameReferences] - public sealed partial class CreditsWindow : DefaultWindow + [Dependency] private readonly IResourceManager _resourceManager = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly ISerializationManager _serialization = default!; + [Dependency] private readonly IPrototypeManager _protoManager = default!; + [Dependency] private readonly ILocalizationManager _loc = default!; + + private static readonly Dictionary PatronTierPriority = new() { - [Dependency] private readonly IResourceManager _resourceManager = default!; - [Dependency] private readonly IConfigurationManager _cfg = default!; + ["Nuclear Operative"] = 1, + ["Syndicate Agent"] = 2, + ["Revolutionary"] = 3, + }; + + private readonly List _attributions = []; + private readonly ISawmill _sawmill = Logger.GetSawmill("Credits"); + + private const int AttributionsSourcesPerPage = 50; + + public CreditsWindow() + { + IoCManager.InjectDependencies(this); + RobustXamlLoader.Load(this); + + TabContainer.SetTabTitle(Ss14ContributorsTab, Loc.GetString("credits-window-ss14contributorslist-tab")); + TabContainer.SetTabTitle(PatronsTab, Loc.GetString("credits-window-patrons-tab")); + TabContainer.SetTabTitle(LicensesTab, Loc.GetString("credits-window-licenses-tab")); + TabContainer.SetTabTitle(AttributionsTab, Loc.GetString("credits-window-attributions-tab")); - private static readonly Dictionary PatronTierPriority = new() + _protoManager.PrototypesReloaded += _ => { - ["Nuclear Operative"] = 1, - ["Syndicate Agent"] = 2, - ["Revolutionary"] = 3 + _attributions.Clear(); }; - public CreditsWindow() - { - IoCManager.InjectDependencies(this); - RobustXamlLoader.Load(this); + MasterTabContainer.OnTabChanged += OnTabChanged; - TabContainer.SetTabTitle(Ss14ContributorsTab, Loc.GetString("credits-window-ss14contributorslist-tab")); - TabContainer.SetTabTitle(PatronsTab, Loc.GetString("credits-window-patrons-tab")); - TabContainer.SetTabTitle(LicensesTab, Loc.GetString("credits-window-licenses-tab")); + PopulateContributors(Ss14ContributorsContainer); + } + /// + /// Only populates the tab when they are selected, which reduces lagspike when not looking at attributions. + /// + private void OnTabChanged(int tab) + { + if (tab == Ss14ContributorsTab.GetPositionInParent()) PopulateContributors(Ss14ContributorsContainer); + else if (tab == PatronsTab.GetPositionInParent()) PopulatePatrons(PatronsContainer); + else if (tab == LicensesTab.GetPositionInParent()) PopulateLicenses(LicensesContainer); + else if (tab == AttributionsTab.GetPositionInParent()) + PopulateAttributions(AttributionsContainer, 0); + } + + private async void PopulateAttributions(BoxContainer attributionsContainer, int count) + { + attributionsContainer.DisposeAllChildren(); + + if (_attributions.Count == 0) + { + var rsi = await CollectRSiAttributions(); + var rga = await CollectRgaAttributions(); + + _attributions.AddRange(rsi); + _attributions.AddRange(rga); + } + + foreach (var message in _attributions.Skip(count).Take(AttributionsSourcesPerPage)) + { + var rich = new RichTextLabel(); + rich.SetMessage(message); + attributionsContainer.AddChild(rich); } - private void PopulateLicenses(BoxContainer licensesContainer) + var container = new BoxContainer { Orientation = LayoutOrientation.Horizontal }; + + var previousPageButton = new Button { Text = "Previous Page" }; + previousPageButton.OnPressed += + _ => PopulateAttributions(attributionsContainer, count - AttributionsSourcesPerPage); + + var nextPageButton = new Button { Text = "Next Page" }; + nextPageButton.OnPressed += + _ => PopulateAttributions(attributionsContainer, count + AttributionsSourcesPerPage); + + if (count - AttributionsSourcesPerPage >= 0) + container.AddChild(previousPageButton); + if (count + AttributionsSourcesPerPage < _attributions.Count) + container.AddChild(nextPageButton); + + attributionsContainer.AddChild(container); + } + + private Task> CollectRSiAttributions() + { + return Task.Run(() => { - foreach (var entry in CreditsManager.GetLicenses(_resourceManager).OrderBy(p => p.Name)) + var rsiStreams = _resourceManager.ContentFindFiles("/Textures/") + .Where(p => p.ToString().EndsWith(".rsi/meta.json")); + + var attrs = new List(); + + foreach (var stream in rsiStreams) { - licensesContainer.AddChild(new Label {StyleClasses = {StyleBase.StyleClassLabelHeading}, Text = entry.Name}); + try + { + var m = new FormattedMessage(); + + var yamlStream = _resourceManager.ContentFileReadYaml(stream); + + if (yamlStream.Documents[0].RootNode.ToDataNode() is not MappingDataNode map) + throw new Exception("meta.json is not a mapping."); + + if (!map.TryGet("copyright", out var copyrightNode)) + throw new Exception("Missing the copyright field."); - // We split these line by line because otherwise - // the LGPL causes Clyde to go out of bounds in the rendering code. - foreach (var line in entry.License.Split("\n")) + if (!map.TryGet("states", out var statesNode)) + throw new Exception("Missing the states field."); + + if (statesNode is not SequenceDataNode states) + throw new Exception("Missing a list of states."); + + var copyright = copyrightNode.ToString(); + var files = states.Select(n => (MappingDataNode)n) + .Select(n => n.Get("name") + ".png"); + + m.AddMarkupPermissive(_loc.GetString("credits-window-attributions-directory", + ("directory", stream.Directory.ToString()))); + m.AddText("\n"); + m.AddMarkupPermissive(_loc.GetString("credits-window-attributions-files", + ("files", string.Join(", ", files)))); + m.AddText("\n"); + m.AddMarkupPermissive(_loc.GetString("credits-window-attributions-copyright", + ("copyright", copyright))); + m.AddText("\n"); + + attrs.Add(m); + } + catch (Exception e) { - licensesContainer.AddChild(new Label {Text = line, FontColorOverride = new Color(200, 200, 200)}); + var m = new FormattedMessage(); + m.AddMarkupPermissive(_loc.GetString("credits-window-attributions-failed", + ("file", stream.ToString()))); + m.AddText("\n"); + _sawmill.Error($"{stream.ToString()}\n{e}"); + attrs.Add(m); } } - } - private void PopulatePatrons(BoxContainer patronsContainer) + return attrs; + }); + } + + private Task> CollectRgaAttributions() + { + return Task.Run(() => { - var patrons = LoadPatrons(); + var rgaStreams = _resourceManager.ContentFindFiles("/") + .Where(p => p.Filename == "attributions.yml"); - // Do not show "become a patron" button on Steam builds - // since Patreon violates Valve's rules about alternative storefronts. - var linkPatreon = _cfg.GetCVar(CCVars.InfoLinksPatreon); - if (!_cfg.GetCVar(CCVars.BrandingSteam) && linkPatreon != "") + var attrs = new List(); + + foreach (var stream in rgaStreams) { - Button patronButton; - patronsContainer.AddChild(patronButton = new Button + try { - Text = Loc.GetString("credits-window-become-patron-button"), - HorizontalAlignment = HAlignment.Center - }); + var yamlStream = _resourceManager.ContentFileReadYaml(stream); - patronButton.OnPressed += - _ => IoCManager.Resolve().OpenUri(linkPatreon); - } + if (yamlStream.Documents[0].RootNode.ToDataNode() is not SequenceDataNode sequence) + throw new Exception("Attributions file is not a list of attributions."); - var first = true; - foreach (var tier in patrons.GroupBy(p => p.Tier).OrderBy(p => PatronTierPriority[p.Key])) - { - if (!first) - { - patronsContainer.AddChild(new Control {MinSize = new Vector2(0, 10)}); - } + foreach (var attribution in sequence.Sequence) + { + var m = new FormattedMessage(); + + if (attribution is not MappingDataNode map) + throw new Exception("Attribution is not a mapping."); + + if (!map.TryGet("files", out var filesNode)) + throw new Exception("Attribution does not list files."); + + if (!map.TryGet("copyright", out var copyrightNode)) + throw new Exception("Attribution does not copyright."); + + if (!map.TryGet("license", out var licenseNode)) + throw new Exception("Attribution does not identify a license."); - first = false; - patronsContainer.AddChild(new Label {StyleClasses = {StyleBase.StyleClassLabelHeading}, Text = $"{tier.Key}"}); + if (!map.TryGet("source", out var sourceNode)) + throw new Exception("Attribution does not identify a source."); - var msg = string.Join(", ", tier.OrderBy(p => p.Name).Select(p => p.Name)); + var files = _serialization.Read(filesNode, notNullableOverride: true); + var copyright = copyrightNode.ToString(); + var license = licenseNode.ToString(); + var source = sourceNode.ToString(); - var label = new RichTextLabel(); - label.SetMessage(msg); + m.AddMarkupPermissive(_loc.GetString("credits-window-attributions-directory", + ("directory", stream.Directory.ToString()))); + m.AddText("\n"); + m.AddMarkupPermissive(_loc.GetString("credits-window-attributions-files", + ("files", string.Join(", ", files)))); + m.AddText("\n"); + m.AddMarkupPermissive(_loc.GetString("credits-window-attributions-copyright", + ("copyright", copyright))); + m.AddText("\n"); + m.AddMarkupPermissive( + _loc.GetString("credits-window-attributions-license", ("license", license))); + m.AddText("\n"); + m.AddMarkupPermissive(_loc.GetString("credits-window-attributions-source", ("source", source))); + m.AddText("\n"); - patronsContainer.AddChild(label); + attrs.Add(m); + } + } + catch (Exception e) + { + var m = new FormattedMessage(); + m.AddMarkupPermissive(_loc.GetString("credits-window-attributions-failed", + ("file", stream.ToString()))); + m.AddText("\n"); + _sawmill.Error($"{stream.ToString()}\n{e}"); + attrs.Add(m); + } } - } - private IEnumerable LoadPatrons() + return attrs; + }); + } + + private void PopulateLicenses(BoxContainer licensesContainer) + { + foreach (var entry in CreditsManager.GetLicenses(_resourceManager).OrderBy(p => p.Name)) { - var yamlStream = _resourceManager.ContentFileReadYaml(new ("/Credits/Patrons.yml")); - var sequence = (YamlSequenceNode) yamlStream.Documents[0].RootNode; + licensesContainer.AddChild(new Label + { StyleClasses = { StyleBase.StyleClassLabelHeading }, Text = entry.Name }); - return sequence - .Cast() - .Select(m => new PatronEntry(m["Name"].AsString(), m["Tier"].AsString())); + // We split these line by line because otherwise + // the LGPL causes Clyde to go out of bounds in the rendering code. + foreach (var line in entry.License.Split("\n")) + { + licensesContainer.AddChild(new Label { Text = line, FontColorOverride = new Color(200, 200, 200) }); + } } + } - private void PopulateContributors(BoxContainer ss14ContributorsContainer) - { - Button contributeButton; + private void PopulatePatrons(BoxContainer patronsContainer) + { + var patrons = LoadPatrons(); - ss14ContributorsContainer.AddChild(new BoxContainer + // Do not show "become a patron" button on Steam builds + // since Patreon violates Valve's rules about alternative storefronts. + var linkPatreon = _cfg.GetCVar(CCVars.InfoLinksPatreon); + if (!_cfg.GetCVar(CCVars.BrandingSteam) && linkPatreon != "") + { + Button patronButton; + patronsContainer.AddChild(patronButton = new Button { - Orientation = LayoutOrientation.Horizontal, + Text = Loc.GetString("credits-window-become-patron-button"), HorizontalAlignment = HAlignment.Center, - SeparationOverride = 20, - Children = - { - new Label {Text = Loc.GetString("credits-window-contributor-encouragement-label") }, - (contributeButton = new Button {Text = Loc.GetString("credits-window-contribute-button")}) - } }); - var first = true; + patronButton.OnPressed += + _ => IoCManager.Resolve().OpenUri(linkPatreon); + } - void AddSection(string title, string path, bool markup = false) - { - if (!first) - { - ss14ContributorsContainer.AddChild(new Control {MinSize = new Vector2(0, 10)}); - } + var first = true; + foreach (var tier in patrons.GroupBy(p => p.Tier).OrderBy(p => PatronTierPriority[p.Key])) + { + if (!first) + patronsContainer.AddChild(new Control { MinSize = new Vector2(0, 10) }); - first = false; - ss14ContributorsContainer.AddChild(new Label {StyleClasses = {StyleBase.StyleClassLabelHeading}, Text = title}); + first = false; + patronsContainer.AddChild(new Label + { StyleClasses = { StyleBase.StyleClassLabelHeading }, Text = $"{tier.Key}" }); - var label = new RichTextLabel(); - var text = _resourceManager.ContentFileReadAllText($"/Credits/{path}"); - if (markup) - { - label.SetMessage(FormattedMessage.FromMarkupOrThrow(text.Trim())); - } - else - { - label.SetMessage(text); - } + var msg = string.Join(", ", tier.OrderBy(p => p.Name).Select(p => p.Name)); - ss14ContributorsContainer.AddChild(label); - } + var label = new RichTextLabel(); + label.SetMessage(msg); - AddSection(Loc.GetString("credits-window-contributors-section-title"), "GitHub.txt"); - AddSection(Loc.GetString("credits-window-codebases-section-title"), "SpaceStation13.txt"); - AddSection(Loc.GetString("credits-window-original-remake-team-section-title"), "OriginalRemake.txt"); - AddSection(Loc.GetString("credits-window-special-thanks-section-title"), "SpecialThanks.txt", true); + patronsContainer.AddChild(label); + } + } - var linkGithub = _cfg.GetCVar(CCVars.InfoLinksGithub); + private IEnumerable LoadPatrons() + { + var yamlStream = _resourceManager.ContentFileReadYaml(new ResPath("/Credits/Patrons.yml")); + var sequence = (YamlSequenceNode)yamlStream.Documents[0].RootNode; - contributeButton.OnPressed += _ => - IoCManager.Resolve().OpenUri(linkGithub); + return sequence + .Cast() + .Select(m => new PatronEntry(m["Name"].AsString(), m["Tier"].AsString())); + } - if (linkGithub == "") - contributeButton.Visible = false; - } + private void PopulateContributors(BoxContainer ss14ContributorsContainer) + { + Button contributeButton; - private sealed class PatronEntry + ss14ContributorsContainer.AddChild(new BoxContainer { - public string Name { get; } - public string Tier { get; } - - public PatronEntry(string name, string tier) + Orientation = LayoutOrientation.Horizontal, + HorizontalAlignment = HAlignment.Center, + SeparationOverride = 20, + Children = { - Name = name; - Tier = tier; - } + new Label { Text = Loc.GetString("credits-window-contributor-encouragement-label") }, + (contributeButton = new Button { Text = Loc.GetString("credits-window-contribute-button") }), + }, + }); + + var first = true; + + void AddSection(string title, string path, bool markup = false) + { + if (!first) + ss14ContributorsContainer.AddChild(new Control { MinSize = new Vector2(0, 10) }); + + first = false; + ss14ContributorsContainer.AddChild(new Label + { StyleClasses = { StyleBase.StyleClassLabelHeading }, Text = title }); + + var label = new RichTextLabel(); + var text = _resourceManager.ContentFileReadAllText($"/Credits/{path}"); + if (markup) + label.SetMessage(FormattedMessage.FromMarkupOrThrow(text.Trim())); + else + label.SetMessage(text); + + ss14ContributorsContainer.AddChild(label); + } + + AddSection(Loc.GetString("credits-window-contributors-section-title"), "GitHub.txt"); + AddSection(Loc.GetString("credits-window-codebases-section-title"), "SpaceStation13.txt"); + AddSection(Loc.GetString("credits-window-original-remake-team-section-title"), "OriginalRemake.txt"); + AddSection(Loc.GetString("credits-window-special-thanks-section-title"), "SpecialThanks.txt", true); + + var linkGithub = _cfg.GetCVar(CCVars.InfoLinksGithub); + + contributeButton.OnPressed += _ => + IoCManager.Resolve().OpenUri(linkGithub); + + if (linkGithub == "") + contributeButton.Visible = false; + } + + private sealed class PatronEntry + { + public string Name { get; } + public string Tier { get; } + + public PatronEntry(string name, string tier) + { + Name = name; + Tier = tier; } } } diff --git a/Resources/Locale/en-US/credits/credits-window.ftl b/Resources/Locale/en-US/credits/credits-window.ftl index 1b9636367b..4bd7f7a426 100644 --- a/Resources/Locale/en-US/credits/credits-window.ftl +++ b/Resources/Locale/en-US/credits/credits-window.ftl @@ -2,10 +2,18 @@ credits-window-title = Credits credits-window-patrons-tab = Patrons credits-window-ss14contributorslist-tab = Credits credits-window-licenses-tab = Open Source Licenses +credits-window-attributions-tab = Attributions credits-window-become-patron-button = Become a Patron credits-window-contributor-encouragement-label = Want to get on this list? credits-window-contribute-button = Contribute! credits-window-contributors-section-title = Space Station 14 Contributors credits-window-codebases-section-title = Space Station 13 Codebases credits-window-original-remake-team-section-title = Original Space Station 13 Remake Team -credits-window-special-thanks-section-title = Special Thanks \ No newline at end of file +credits-window-special-thanks-section-title = Special Thanks + +credits-window-attributions-directory = [color=white]Directory:[/color] {$directory} +credits-window-attributions-files = [color=white]Files:[/color] {$files} +credits-window-attributions-copyright = [color=white]Copyright:[/color] {$copyright} +credits-window-attributions-license = [color=white]License:[/color] {$license} +credits-window-attributions-source = [color=white]Source:[/color] {$source} +credits-window-attributions-failed = [color=red]Failed to read file:[/color] {$file} -- 2.51.2