using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Content.Shared.CCVar;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Utility;
namespace Content.Client.Changelog
{
public sealed partial class ChangelogManager : IPostInjectInit
{
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IResourceManager _resource = default!;
[Dependency] private readonly ISerializationManager _serialization = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
private const string SawmillName = "changelog";
public const string MainChangelogName = "Changelog";
private ISawmill _sawmill = default!;
public bool NewChangelogEntries { get; private set; }
public int LastReadId { get; private set; }
public int MaxId { get; private set; }
public event Action? NewChangelogEntriesChanged;
///
/// Ran when the user opens ("read") the changelog,
/// stores the new ID to disk and clears .
///
///
/// is NOT cleared
/// since that's used in the changelog menu to show the "since you last read" bar.
///
public void SaveNewReadId()
{
NewChangelogEntries = false;
NewChangelogEntriesChanged?.Invoke();
using var sw = _resource.UserData.OpenWriteText(new ($"/changelog_last_seen_{_configManager.GetCVar(CCVars.ServerId)}"));
sw.Write(MaxId.ToString());
}
public async void Initialize()
{
// Open changelog purely to compare to the last viewed date.
var changelogs = await LoadChangelog();
UpdateChangelogs(changelogs);
}
private void UpdateChangelogs(List changelogs)
{
if (changelogs.Count == 0)
{
return;
}
var mainChangelogs = changelogs.Where(c => c.Name == MainChangelogName).ToArray();
if (mainChangelogs.Length == 0)
{
_sawmill.Error($"No changelog file found in Resources/Changelog with name {MainChangelogName}");
return;
}
var changelog = changelogs[0];
if (mainChangelogs.Length > 1)
{
_sawmill.Error($"More than one file found in Resource/Changelog with name {MainChangelogName}");
}
if (changelog.Entries.Count == 0)
{
return;
}
MaxId = changelog.Entries.Max(c => c.Id);
var path = new ResPath($"/changelog_last_seen_{_configManager.GetCVar(CCVars.ServerId)}");
if (_resource.UserData.TryReadAllText(path, out var lastReadIdText))
{
LastReadId = int.Parse(lastReadIdText);
}
NewChangelogEntries = LastReadId < MaxId;
NewChangelogEntriesChanged?.Invoke();
}
public Task> LoadChangelog()
{
return Task.Run(() =>
{
var changelogs = new List();
var directory = new ResPath("/Changelog");
foreach (var file in _resource.ContentFindFiles(new ResPath("/Changelog/")))
{
if (file.Directory != directory || file.Extension != "yml")
continue;
var yamlData = _resource.ContentFileReadYaml(file);
if (yamlData.Documents.Count == 0)
continue;
var node = yamlData.Documents[0].RootNode.ToDataNodeCast();
var changelog = _serialization.Read(node, notNullableOverride: true);
if (string.IsNullOrWhiteSpace(changelog.Name))
changelog.Name = file.FilenameWithoutExtension;
changelogs.Add(changelog);
}
changelogs.Sort((a, b) => a.Order.CompareTo(b.Order));
return changelogs;
});
}
public void PostInject()
{
_sawmill = _logManager.GetSawmill(SawmillName);
}
[DataDefinition]
public sealed partial class Changelog
{
///
/// The name to use for this changelog.
/// If left unspecified, the name of the file is used instead.
/// Used during localization to find the user-displayed name of this changelog.
///
[DataField("Name")]
public string Name = string.Empty;
///
/// The individual entries in this changelog.
/// These are not kept around in memory in the changelog manager.
///
[DataField("Entries")]
public List Entries = new();
///
/// Whether or not this changelog will be displayed as a tab to non-admins.
/// These are still loaded by all clients, but not shown if they aren't an admin,
/// as they do not contain sensitive data and are publicly visible on GitHub.
///
[DataField("AdminOnly")]
public bool AdminOnly;
///
/// Used when ordering the changelog tabs for the user to see.
/// Larger numbers are displayed later, smaller numbers are displayed earlier.
///
[DataField("Order")]
public int Order;
}
[DataDefinition]
public sealed partial class ChangelogEntry : ISerializationHooks
{
[DataField("id")]
public int Id { get; private set; }
[DataField("author")]
public string Author { get; private set; } = "";
[DataField("time")] private string _time = default!;
public DateTime Time { get; private set; }
[DataField("changes")]
public List Changes { get; private set; } = default!;
void ISerializationHooks.AfterDeserialization()
{
Time = DateTime.Parse(_time, null, DateTimeStyles.RoundtripKind);
}
}
[DataDefinition]
public sealed partial class ChangelogChange
{
[DataField("type")]
public ChangelogLineType Type { get; private set; }
[DataField("message")]
public string Message { get; private set; } = "";
}
public enum ChangelogLineType
{
Add,
Remove,
Fix,
Tweak,
}
}
}