1
0

ChangelogManager.cs 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. using System.Linq;
  2. using System.Threading.Tasks;
  3. using Content.Shared.CCVar;
  4. using Robust.Shared;
  5. using Robust.Shared.Configuration;
  6. using Robust.Shared.ContentPack;
  7. using Robust.Shared.Serialization.Manager;
  8. using Robust.Shared.Serialization.Markdown;
  9. using Robust.Shared.Serialization.Markdown.Mapping;
  10. using Robust.Shared.Utility;
  11. namespace Content.Client.Changelog
  12. {
  13. public sealed partial class ChangelogManager : IPostInjectInit
  14. {
  15. [Dependency] private readonly ILogManager _logManager = default!;
  16. [Dependency] private readonly IResourceManager _resource = default!;
  17. [Dependency] private readonly ISerializationManager _serialization = default!;
  18. [Dependency] private readonly IConfigurationManager _configManager = default!;
  19. private const string SawmillName = "changelog";
  20. public const string MainChangelogName = "Changelog";
  21. private ISawmill _sawmill = default!;
  22. public bool NewChangelogEntries { get; private set; }
  23. public int LastReadId { get; private set; }
  24. public int MaxId { get; private set; }
  25. public event Action? NewChangelogEntriesChanged;
  26. /// <summary>
  27. /// Ran when the user opens ("read") the changelog,
  28. /// stores the new ID to disk and clears <see cref="NewChangelogEntries"/>.
  29. /// </summary>
  30. /// <remarks>
  31. /// <see cref="LastReadId"/> is NOT cleared
  32. /// since that's used in the changelog menu to show the "since you last read" bar.
  33. /// </remarks>
  34. public void SaveNewReadId()
  35. {
  36. NewChangelogEntries = false;
  37. NewChangelogEntriesChanged?.Invoke();
  38. using var sw = _resource.UserData.OpenWriteText(new ($"/changelog_last_seen_{_configManager.GetCVar(CCVars.ServerId)}"));
  39. sw.Write(MaxId.ToString());
  40. }
  41. public async void Initialize()
  42. {
  43. // Open changelog purely to compare to the last viewed date.
  44. var changelogs = await LoadChangelog();
  45. UpdateChangelogs(changelogs);
  46. }
  47. private void UpdateChangelogs(List<Changelog> changelogs)
  48. {
  49. if (changelogs.Count == 0)
  50. {
  51. return;
  52. }
  53. var mainChangelogs = changelogs.Where(c => c.Name == MainChangelogName).ToArray();
  54. if (mainChangelogs.Length == 0)
  55. {
  56. _sawmill.Error($"No changelog file found in Resources/Changelog with name {MainChangelogName}");
  57. return;
  58. }
  59. var changelog = changelogs[0];
  60. if (mainChangelogs.Length > 1)
  61. {
  62. _sawmill.Error($"More than one file found in Resource/Changelog with name {MainChangelogName}");
  63. }
  64. if (changelog.Entries.Count == 0)
  65. {
  66. return;
  67. }
  68. MaxId = changelog.Entries.Max(c => c.Id);
  69. var path = new ResPath($"/changelog_last_seen_{_configManager.GetCVar(CCVars.ServerId)}");
  70. if (_resource.UserData.TryReadAllText(path, out var lastReadIdText))
  71. {
  72. LastReadId = int.Parse(lastReadIdText);
  73. }
  74. NewChangelogEntries = LastReadId < MaxId;
  75. NewChangelogEntriesChanged?.Invoke();
  76. }
  77. public Task<List<Changelog>> LoadChangelog()
  78. {
  79. return Task.Run(() =>
  80. {
  81. var changelogs = new List<Changelog>();
  82. var directory = new ResPath("/Changelog");
  83. foreach (var file in _resource.ContentFindFiles(new ResPath("/Changelog/")))
  84. {
  85. if (file.Directory != directory || file.Extension != "yml")
  86. continue;
  87. var yamlData = _resource.ContentFileReadYaml(file);
  88. if (yamlData.Documents.Count == 0)
  89. continue;
  90. var node = yamlData.Documents[0].RootNode.ToDataNodeCast<MappingDataNode>();
  91. var changelog = _serialization.Read<Changelog>(node, notNullableOverride: true);
  92. if (string.IsNullOrWhiteSpace(changelog.Name))
  93. changelog.Name = file.FilenameWithoutExtension;
  94. changelogs.Add(changelog);
  95. }
  96. changelogs.Sort((a, b) => a.Order.CompareTo(b.Order));
  97. return changelogs;
  98. });
  99. }
  100. public void PostInject()
  101. {
  102. _sawmill = _logManager.GetSawmill(SawmillName);
  103. }
  104. /// <summary>
  105. /// Tries to return a human-readable version number from the build.json file
  106. /// </summary>
  107. public string GetClientVersion()
  108. {
  109. var fork = _configManager.GetCVar(CVars.BuildForkId);
  110. var version = _configManager.GetCVar(CVars.BuildVersion);
  111. // This trimming might become annoying if down the line some codebases want to switch to a real
  112. // version format like "104.11.3" while others are still using the git hashes
  113. if (version.Length > 7)
  114. version = version[..7];
  115. if (string.IsNullOrEmpty(version) || string.IsNullOrEmpty(fork))
  116. return Loc.GetString("changelog-version-unknown");
  117. return Loc.GetString("changelog-version-tag",
  118. ("fork", fork),
  119. ("version", version));
  120. }
  121. [DataDefinition]
  122. public sealed partial class Changelog
  123. {
  124. /// <summary>
  125. /// The name to use for this changelog.
  126. /// If left unspecified, the name of the file is used instead.
  127. /// Used during localization to find the user-displayed name of this changelog.
  128. /// </summary>
  129. [DataField("Name")]
  130. public string Name = string.Empty;
  131. /// <summary>
  132. /// The individual entries in this changelog.
  133. /// These are not kept around in memory in the changelog manager.
  134. /// </summary>
  135. [DataField("Entries")]
  136. public List<ChangelogEntry> Entries = new();
  137. /// <summary>
  138. /// Whether or not this changelog will be displayed as a tab to non-admins.
  139. /// These are still loaded by all clients, but not shown if they aren't an admin,
  140. /// as they do not contain sensitive data and are publicly visible on GitHub.
  141. /// </summary>
  142. [DataField("AdminOnly")]
  143. public bool AdminOnly;
  144. /// <summary>
  145. /// Used when ordering the changelog tabs for the user to see.
  146. /// Larger numbers are displayed later, smaller numbers are displayed earlier.
  147. /// </summary>
  148. [DataField("Order")]
  149. public int Order;
  150. }
  151. [DataDefinition]
  152. public sealed partial class ChangelogEntry
  153. {
  154. [DataField("id")]
  155. public int Id { get; private set; }
  156. [DataField("author")]
  157. public string Author { get; private set; } = "";
  158. [DataField]
  159. public DateTime Time { get; private set; }
  160. [DataField("changes")]
  161. public List<ChangelogChange> Changes { get; private set; } = default!;
  162. }
  163. [DataDefinition]
  164. public sealed partial class ChangelogChange
  165. {
  166. [DataField("type")]
  167. public ChangelogLineType Type { get; private set; }
  168. [DataField("message")]
  169. public string Message { get; private set; } = "";
  170. }
  171. public enum ChangelogLineType
  172. {
  173. Add,
  174. Remove,
  175. Fix,
  176. Tweak,
  177. }
  178. }
  179. }