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 | 469 +++++++++++++----- .../Locale/en-US/credits/credits-window.ftl | 10 +- 3 files changed, 351 insertions(+), 140 deletions(-) diff --git a/Content.Client/Credits/CreditsWindow.xaml b/Content.Client/Credits/CreditsWindow.xaml index 2cb6f7ab929..b7bc2329c12 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 ba240209533..64f5479421c 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 -{ - [GenerateTypedNameReferences] - public sealed partial class CreditsWindow : DefaultWindow - { - [Dependency] private readonly IResourceManager _resourceManager = default!; - [Dependency] private readonly IConfigurationManager _cfg = default!; +namespace Content.Client.Credits; - private static readonly Dictionary PatronTierPriority = new() +[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() + { + ["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")); + + _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); } - private void PopulateLicenses(BoxContainer licensesContainer) + foreach (var message in _attributions.Skip(count).Take(AttributionsSourcesPerPage)) { - foreach (var entry in CreditsManager.GetLicenses(_resourceManager).OrderBy(p => p.Name)) - { - licensesContainer.AddChild(new Label {StyleClasses = {StyleBase.StyleClassLabelHeading}, Text = entry.Name}); + var rich = new RichTextLabel(); + rich.SetMessage(message); + attributionsContainer.AddChild(rich); + } - // 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")) + 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(() => + { + var rsiStreams = _resourceManager.ContentFindFiles("/Textures/") + .Where(p => p.ToString().EndsWith(".rsi/meta.json")); + + var attrs = new List(); + + foreach (var stream in rsiStreams) + { + try { - licensesContainer.AddChild(new Label {Text = line, FontColorOverride = new Color(200, 200, 200)}); + 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."); + + 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) + { + 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."); + + 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."); + + if (!map.TryGet("source", out var sourceNode)) + throw new Exception("Attribution does not identify a source."); + + var files = _serialization.Read(filesNode, notNullableOverride: true); + var copyright = copyrightNode.ToString(); + var license = licenseNode.ToString(); + var source = sourceNode.ToString(); + + 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"); + + 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); + } } - 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)}); - } + return attrs; + }); + } - first = false; - patronsContainer.AddChild(new Label {StyleClasses = {StyleBase.StyleClassLabelHeading}, Text = $"{tier.Key}"}); - - var msg = string.Join(", ", tier.OrderBy(p => p.Name).Select(p => p.Name)); - - var label = new RichTextLabel(); - label.SetMessage(msg); - - patronsContainer.AddChild(label); - } - } - - private IEnumerable LoadPatrons() + 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())); - } - - private void PopulateContributors(BoxContainer ss14ContributorsContainer) - { - Button contributeButton; - - ss14ContributorsContainer.AddChild(new BoxContainer + // 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")) { - Orientation = LayoutOrientation.Horizontal, - 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; - - 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; + licensesContainer.AddChild(new Label { Text = line, FontColorOverride = new Color(200, 200, 200) }); } } } + + private void PopulatePatrons(BoxContainer patronsContainer) + { + var patrons = LoadPatrons(); + + // 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 + { + Text = Loc.GetString("credits-window-become-patron-button"), + HorizontalAlignment = HAlignment.Center, + }); + + patronButton.OnPressed += + _ => IoCManager.Resolve().OpenUri(linkPatreon); + } + + 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; + patronsContainer.AddChild(new Label + { StyleClasses = { StyleBase.StyleClassLabelHeading }, Text = $"{tier.Key}" }); + + var msg = string.Join(", ", tier.OrderBy(p => p.Name).Select(p => p.Name)); + + var label = new RichTextLabel(); + label.SetMessage(msg); + + patronsContainer.AddChild(label); + } + } + + private IEnumerable LoadPatrons() + { + var yamlStream = _resourceManager.ContentFileReadYaml(new ResPath("/Credits/Patrons.yml")); + var sequence = (YamlSequenceNode)yamlStream.Documents[0].RootNode; + + return sequence + .Cast() + .Select(m => new PatronEntry(m["Name"].AsString(), m["Tier"].AsString())); + } + + private void PopulateContributors(BoxContainer ss14ContributorsContainer) + { + Button contributeButton; + + ss14ContributorsContainer.AddChild(new BoxContainer + { + Orientation = LayoutOrientation.Horizontal, + 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; + + 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 1b9636367b6..4bd7f7a4265 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}