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.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.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!; [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 += _ => { _attributions.Clear(); }; MasterTabContainer.OnTabChanged += OnTabChanged; 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.RemoveAllChildren(); 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); } var container = new BoxContainer { Orientation = LayoutOrientation.Horizontal }; var previousPageButton = new Button { Text = Loc.GetString("credits-window-previous-page-button") }; previousPageButton.OnPressed += _ => PopulateAttributions(attributionsContainer, count - AttributionsSourcesPerPage); var nextPageButton = new Button { Text = Loc.GetString("credits-window-next-page-button") }; 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 { 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); } } return attrs; }); } private Task> CollectRgaAttributions() { return Task.Run(() => { var rgaStreams = _resourceManager.ContentFindFiles("/") .Where(p => p.Filename == "attributions.yml"); var attrs = new List(); foreach (var stream in rgaStreams) { try { var yamlStream = _resourceManager.ContentFileReadYaml(stream); 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); } } return attrs; }); } private void PopulateLicenses(BoxContainer licensesContainer) { licensesContainer.RemoveAllChildren(); foreach (var entry in CreditsManager.GetLicenses(_resourceManager).OrderBy(p => p.Name)) { licensesContainer.AddChild(new Label { StyleClasses = { StyleBase.StyleClassLabelHeading }, Text = entry.Name }); // 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 PopulatePatrons(BoxContainer patronsContainer) { patronsContainer.RemoveAllChildren(); 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) { ss14ContributorsContainer.RemoveAllChildren(); 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-immortals-title"), "Immortals.txt", true); 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; } } }