diff --git a/Content.Client/Administration/Managers/ClientAdminManager.cs b/Content.Client/Administration/Managers/ClientAdminManager.cs
index 66c8b8a0630..8978e2fd6dd 100644
--- a/Content.Client/Administration/Managers/ClientAdminManager.cs
+++ b/Content.Client/Administration/Managers/ClientAdminManager.cs
@@ -130,5 +130,13 @@ void IPostInjectInit.PostInject()
return null;
}
+
+ public AdminData? GetAdminData(bool includeDeAdmin = false)
+ {
+ if (_player.LocalPlayer is { Session: { } session })
+ return GetAdminData(session, includeDeAdmin);
+
+ return null;
+ }
}
}
diff --git a/Content.Client/Administration/Managers/IClientAdminManager.cs b/Content.Client/Administration/Managers/IClientAdminManager.cs
index 46e3a01537b..b4b5b48b814 100644
--- a/Content.Client/Administration/Managers/IClientAdminManager.cs
+++ b/Content.Client/Administration/Managers/IClientAdminManager.cs
@@ -1,5 +1,4 @@
-using System;
-using Content.Shared.Administration;
+using Content.Shared.Administration;
namespace Content.Client.Administration.Managers
{
@@ -13,6 +12,15 @@ public interface IClientAdminManager
///
event Action AdminStatusUpdated;
+ ///
+ /// Gets the admin data for the client, if they are an admin.
+ ///
+ ///
+ /// Whether to return admin data for admins that are current de-adminned.
+ ///
+ /// if the player is not an admin.
+ AdminData? GetAdminData(bool includeDeAdmin = false);
+
///
/// Checks whether the local player is an admin.
///
@@ -52,5 +60,17 @@ public interface IClientAdminManager
bool CanAdminMenu();
void Initialize();
+
+ ///
+ /// Checks if the client is an admin.
+ ///
+ ///
+ /// Whether to return admin data for admins that are current de-adminned.
+ ///
+ /// true if the player is an admin, false otherwise.
+ bool IsAdmin(bool includeDeAdmin = false)
+ {
+ return GetAdminData(includeDeAdmin) != null;
+ }
}
}
diff --git a/Content.Client/Administration/UI/AdminMenuWindow.xaml b/Content.Client/Administration/UI/AdminMenuWindow.xaml
index 49eb9c0de60..311d67b826c 100644
--- a/Content.Client/Administration/UI/AdminMenuWindow.xaml
+++ b/Content.Client/Administration/UI/AdminMenuWindow.xaml
@@ -5,13 +5,15 @@
xmlns:atmosTab="clr-namespace:Content.Client.Administration.UI.Tabs.AtmosTab"
xmlns:tabs="clr-namespace:Content.Client.Administration.UI.Tabs"
xmlns:playerTab="clr-namespace:Content.Client.Administration.UI.Tabs.PlayerTab"
- xmlns:objectsTab="clr-namespace:Content.Client.Administration.UI.Tabs.ObjectsTab">
+ xmlns:objectsTab="clr-namespace:Content.Client.Administration.UI.Tabs.ObjectsTab"
+ xmlns:panic="clr-namespace:Content.Client.Administration.UI.Tabs.PanicBunkerTab">
+
diff --git a/Content.Client/Administration/UI/AdminMenuWindow.xaml.cs b/Content.Client/Administration/UI/AdminMenuWindow.xaml.cs
index c15e56147dd..c3ea67a3edb 100644
--- a/Content.Client/Administration/UI/AdminMenuWindow.xaml.cs
+++ b/Content.Client/Administration/UI/AdminMenuWindow.xaml.cs
@@ -12,7 +12,7 @@ public sealed partial class AdminMenuWindow : DefaultWindow
public AdminMenuWindow()
{
- MinSize = new Vector2(600, 250); // Corvax-Resize
+ MinSize = new Vector2(650, 250);
Title = Loc.GetString("admin-menu-title");
RobustXamlLoader.Load(this);
MasterTabContainer.SetTabTitle(0, Loc.GetString("admin-menu-admin-tab"));
@@ -20,8 +20,9 @@ public AdminMenuWindow()
MasterTabContainer.SetTabTitle(2, Loc.GetString("admin-menu-atmos-tab"));
MasterTabContainer.SetTabTitle(3, Loc.GetString("admin-menu-round-tab"));
MasterTabContainer.SetTabTitle(4, Loc.GetString("admin-menu-server-tab"));
- MasterTabContainer.SetTabTitle(5, Loc.GetString("admin-menu-players-tab"));
- MasterTabContainer.SetTabTitle(6, Loc.GetString("admin-menu-objects-tab"));
+ MasterTabContainer.SetTabTitle(5, Loc.GetString("admin-menu-panic-bunker-tab"));
+ MasterTabContainer.SetTabTitle(6, Loc.GetString("admin-menu-players-tab"));
+ MasterTabContainer.SetTabTitle(7, Loc.GetString("admin-menu-objects-tab"));
}
protected override void Dispose(bool disposing)
diff --git a/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerStatusWindow.xaml b/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerStatusWindow.xaml
new file mode 100644
index 00000000000..633bef05148
--- /dev/null
+++ b/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerStatusWindow.xaml
@@ -0,0 +1,6 @@
+
+
+
diff --git a/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerStatusWindow.xaml.cs b/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerStatusWindow.xaml.cs
new file mode 100644
index 00000000000..ec16bf6aea7
--- /dev/null
+++ b/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerStatusWindow.xaml.cs
@@ -0,0 +1,14 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Administration.UI.Tabs.PanicBunkerTab;
+
+[GenerateTypedNameReferences]
+public sealed partial class PanicBunkerStatusWindow : DefaultWindow
+{
+ public PanicBunkerStatusWindow()
+ {
+ RobustXamlLoader.Load(this);
+ }
+}
diff --git a/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerTab.xaml b/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerTab.xaml
new file mode 100644
index 00000000000..89827d06424
--- /dev/null
+++ b/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerTab.xaml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerTab.xaml.cs b/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerTab.xaml.cs
new file mode 100644
index 00000000000..e9d3b95c5d8
--- /dev/null
+++ b/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerTab.xaml.cs
@@ -0,0 +1,54 @@
+using Content.Shared.Administration.Events;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Console;
+
+namespace Content.Client.Administration.UI.Tabs.PanicBunkerTab;
+
+[GenerateTypedNameReferences]
+public sealed partial class PanicBunkerTab : Control
+{
+ [Dependency] private readonly IConsoleHost _console = default!;
+
+ public PanicBunkerTab()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ DisableAutomaticallyButton.ToolTip = Loc.GetString("admin-ui-panic-bunker-disable-automatically-tooltip");
+
+ MinAccountAge.OnTextEntered += args =>
+ {
+ if (string.IsNullOrWhiteSpace(args.Text) || !int.TryParse(args.Text, out var minutes))
+ return;
+
+ _console.ExecuteCommand($"panicbunker_min_account_age {minutes}");
+ };
+
+ MinOverallHours.OnTextEntered += args =>
+ {
+ if (string.IsNullOrWhiteSpace(args.Text) || !int.TryParse(args.Text, out var hours))
+ return;
+
+ _console.ExecuteCommand($"panicbunker_min_overall_hours {hours}");
+ };
+ }
+
+ public void UpdateStatus(PanicBunkerStatus status)
+ {
+ EnabledButton.Pressed = status.Enabled;
+ EnabledButton.Text = Loc.GetString(status.Enabled
+ ? "admin-ui-panic-bunker-enabled"
+ : "admin-ui-panic-bunker-disabled"
+ );
+ EnabledButton.ModulateSelfOverride = status.Enabled ? Color.Red : null;
+
+ DisableAutomaticallyButton.Pressed = status.DisableWithAdmins;
+ EnableAutomaticallyButton.Pressed = status.EnableWithoutAdmins;
+ CountDeadminnedButton.Pressed = status.CountDeadminnedAdmins;
+ ShowReasonButton.Pressed = status.ShowReason;
+ MinAccountAge.Text = status.MinAccountAgeHours.ToString();
+ MinOverallHours.Text = status.MinOverallHours.ToString();
+ }
+}
diff --git a/Content.Client/Administration/UI/Tabs/ServerTab.xaml b/Content.Client/Administration/UI/Tabs/ServerTab.xaml
index 7e15bc27539..b9984058358 100644
--- a/Content.Client/Administration/UI/Tabs/ServerTab.xaml
+++ b/Content.Client/Administration/UI/Tabs/ServerTab.xaml
@@ -8,6 +8,5 @@
-
diff --git a/Content.Client/Administration/UI/Tabs/ServerTab.xaml.cs b/Content.Client/Administration/UI/Tabs/ServerTab.xaml.cs
index b83a3d1ec03..24b92e42ce7 100644
--- a/Content.Client/Administration/UI/Tabs/ServerTab.xaml.cs
+++ b/Content.Client/Administration/UI/Tabs/ServerTab.xaml.cs
@@ -18,7 +18,6 @@ public ServerTab()
_config.OnValueChanged(CCVars.OocEnabled, OocEnabledChanged, true);
_config.OnValueChanged(CCVars.LoocEnabled, LoocEnabledChanged, true);
- _config.OnValueChanged(CCVars.PanicBunkerEnabled, BunkerEnabledChanged, true);
}
private void OocEnabledChanged(bool value)
@@ -31,11 +30,6 @@ private void LoocEnabledChanged(bool value)
SetLoocButton.Pressed = value;
}
- private void BunkerEnabledChanged(bool value)
- {
- SetPanicbunkerButton.Pressed = value;
- }
-
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
@@ -44,7 +38,6 @@ protected override void Dispose(bool disposing)
{
_config.UnsubValueChanged(CCVars.OocEnabled, OocEnabledChanged);
_config.UnsubValueChanged(CCVars.LoocEnabled, LoocEnabledChanged);
- _config.UnsubValueChanged(CCVars.PanicBunkerEnabled, BunkerEnabledChanged);
}
}
}
diff --git a/Content.Client/Changelog/ChangelogManager.cs b/Content.Client/Changelog/ChangelogManager.cs
index 40a8435e6ea..396af99d2cf 100644
--- a/Content.Client/Changelog/ChangelogManager.cs
+++ b/Content.Client/Changelog/ChangelogManager.cs
@@ -1,29 +1,29 @@
-using System;
-using System.Collections.Generic;
using System.Globalization;
-using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Content.Shared.CCVar;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
-using Robust.Shared.IoC;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
-using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Utility;
-
namespace Content.Client.Changelog
{
- public sealed partial class ChangelogManager
+ 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; }
@@ -51,17 +51,39 @@ public void SaveNewReadId()
public async void Initialize()
{
// Open changelog purely to compare to the last viewed date.
- var changelog = await LoadChangelog();
+ 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.Count == 0)
+ if (changelog.Entries.Count == 0)
{
return;
}
- MaxId = changelog.Max(c => c.Id);
+ 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))
+ if (_resource.UserData.TryReadAllText(path, out var lastReadIdText))
{
LastReadId = int.Parse(lastReadIdText);
}
@@ -71,37 +93,74 @@ public async void Initialize()
NewChangelogEntriesChanged?.Invoke();
}
- // Corvax-MultiChangelog-Start
- public async Task> LoadChangelog()
- {
- var paths = _resource.ContentFindFiles("/Changelog/")
- .Where(filePath => filePath.Extension == "yml")
- .ToArray();
-
- var result = new List();
- foreach (var path in paths)
- {
- var changelog = await LoadChangelogFile(path);
- result = result.Union(changelog).ToList();
- }
- return result.OrderBy(x => x.Time).ToList();
- }
- // Corvax-MultiChangelog-End
-
- private Task> LoadChangelogFile(ResPath path)
+ public Task> LoadChangelog()
{
return Task.Run(() =>
{
- var yamlData = _resource.ContentFileReadYaml(path);
+ 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;
- if (yamlData.Documents.Count == 0)
- return new List();
+ var node = yamlData.Documents[0].RootNode.ToDataNodeCast();
+ var changelog = _serialization.Read(node, notNullableOverride: true);
+ if (string.IsNullOrWhiteSpace(changelog.Name))
+ changelog.Name = file.FilenameWithoutExtension;
- var node = (MappingDataNode)yamlData.Documents[0].RootNode.ToDataNode();
- return _serialization.Read>(node["Entries"], notNullableOverride: true);
+ 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
{
@@ -125,7 +184,7 @@ void ISerializationHooks.AfterDeserialization()
}
[DataDefinition]
- public sealed partial class ChangelogChange : ISerializationHooks
+ public sealed partial class ChangelogChange
{
[DataField("type")]
public ChangelogLineType Type { get; private set; }
diff --git a/Content.Client/Changelog/ChangelogTab.xaml b/Content.Client/Changelog/ChangelogTab.xaml
new file mode 100644
index 00000000000..7c049efacc7
--- /dev/null
+++ b/Content.Client/Changelog/ChangelogTab.xaml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
diff --git a/Content.Client/Changelog/ChangelogTab.xaml.cs b/Content.Client/Changelog/ChangelogTab.xaml.cs
new file mode 100644
index 00000000000..d1e2bc7533e
--- /dev/null
+++ b/Content.Client/Changelog/ChangelogTab.xaml.cs
@@ -0,0 +1,175 @@
+using System.Linq;
+using System.Numerics;
+using Content.Client.Resources;
+using Content.Client.Stylesheets;
+using Robust.Client.AutoGenerated;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Utility;
+using static Content.Client.Changelog.ChangelogManager;
+using static Robust.Client.UserInterface.Controls.BoxContainer;
+
+namespace Content.Client.Changelog;
+
+[GenerateTypedNameReferences]
+public sealed partial class ChangelogTab : Control
+{
+ [Dependency] private readonly ChangelogManager _changelog = default!;
+ [Dependency] private readonly IResourceCache _resourceCache = default!;
+
+ public bool AdminOnly;
+
+ public ChangelogTab()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+ }
+
+ public void PopulateChangelog(ChangelogManager.Changelog changelog)
+ {
+ var byDay = changelog.Entries
+ .GroupBy(e => e.Time.ToLocalTime().Date)
+ .OrderByDescending(c => c.Key);
+
+ var hasRead = changelog.Name != MainChangelogName ||
+ _changelog.MaxId <= _changelog.LastReadId;
+
+ foreach (var dayEntries in byDay)
+ {
+ var day = dayEntries.Key;
+
+ var groupedEntries = dayEntries
+ .GroupBy(c => (c.Author, Read: c.Id <= _changelog.LastReadId))
+ .OrderBy(c => c.Key.Read)
+ .ThenBy(c => c.Key.Author);
+
+ string dayNice;
+ var today = DateTime.Today;
+ if (day == today)
+ dayNice = Loc.GetString("changelog-today");
+ else if (day == today.AddDays(-1))
+ dayNice = Loc.GetString("changelog-yesterday");
+ else
+ dayNice = day.ToShortDateString();
+
+ ChangelogBody.AddChild(new Label
+ {
+ Text = dayNice,
+ StyleClasses = { StyleBase.StyleClassLabelHeading },
+ Margin = new Thickness(4, 6, 0, 0)
+ });
+
+ var first = true;
+
+ foreach (var groupedEntry in groupedEntries)
+ {
+ var (author, read) = groupedEntry.Key;
+
+ if (!first)
+ {
+ ChangelogBody.AddChild(new Control { Margin = new Thickness(4) });
+ }
+
+ if (read && !hasRead)
+ {
+ hasRead = true;
+
+ var upArrow =
+ _resourceCache.GetTexture("/Textures/Interface/Changelog/up_arrow.svg.192dpi.png");
+
+ var readDivider = new BoxContainer
+ {
+ Orientation = LayoutOrientation.Vertical
+ };
+
+ var hBox = new BoxContainer
+ {
+ Orientation = LayoutOrientation.Horizontal,
+ HorizontalAlignment = HAlignment.Center,
+ Children =
+ {
+ new TextureRect
+ {
+ Texture = upArrow,
+ ModulateSelfOverride = Color.FromHex("#888"),
+ TextureScale = new Vector2(0.5f, 0.5f),
+ Margin = new Thickness(4, 3),
+ VerticalAlignment = VAlignment.Bottom
+ },
+ new Label
+ {
+ Align = Label.AlignMode.Center,
+ Text = Loc.GetString("changelog-new-changes"),
+ FontColorOverride = Color.FromHex("#888"),
+ },
+ new TextureRect
+ {
+ Texture = upArrow,
+ ModulateSelfOverride = Color.FromHex("#888"),
+ TextureScale = new Vector2(0.5f, 0.5f),
+ Margin = new Thickness(4, 3),
+ VerticalAlignment = VAlignment.Bottom
+ }
+ }
+ };
+
+ readDivider.AddChild(hBox);
+ readDivider.AddChild(new PanelContainer { StyleClasses = { StyleBase.ClassLowDivider } });
+ ChangelogBody.AddChild(readDivider);
+
+ if (first)
+ readDivider.SetPositionInParent(ChangelogBody.ChildCount - 2);
+ }
+
+ first = false;
+
+ var authorLabel = new RichTextLabel
+ {
+ Margin = new Thickness(6, 0, 0, 0),
+ };
+ authorLabel.SetMessage(
+ FormattedMessage.FromMarkup(Loc.GetString("changelog-author-changed", ("author", author))));
+ ChangelogBody.AddChild(authorLabel);
+
+ foreach (var change in groupedEntry.SelectMany(c => c.Changes))
+ {
+ var text = new RichTextLabel();
+ text.SetMessage(FormattedMessage.FromMarkup(change.Message));
+ ChangelogBody.AddChild(new BoxContainer
+ {
+ Orientation = LayoutOrientation.Horizontal,
+ Margin = new Thickness(14, 1, 10, 2),
+ Children =
+ {
+ GetIcon(change.Type),
+ text
+ }
+ });
+ }
+ }
+ }
+ }
+
+ private TextureRect GetIcon(ChangelogLineType type)
+ {
+ var (file, color) = type switch
+ {
+ ChangelogLineType.Add => ("plus.svg.192dpi.png", "#6ED18D"),
+ ChangelogLineType.Remove => ("minus.svg.192dpi.png", "#D16E6E"),
+ ChangelogLineType.Fix => ("bug.svg.192dpi.png", "#D1BA6E"),
+ ChangelogLineType.Tweak => ("wrench.svg.192dpi.png", "#6E96D1"),
+ _ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
+ };
+
+ return new TextureRect
+ {
+ Texture = _resourceCache.GetTexture(new ResPath($"/Textures/Interface/Changelog/{file}")),
+ VerticalAlignment = VAlignment.Top,
+ TextureScale = new Vector2(0.5f, 0.5f),
+ Margin = new Thickness(2, 4, 6, 2),
+ ModulateSelfOverride = Color.FromHex(color)
+ };
+ }
+}
diff --git a/Content.Client/Changelog/ChangelogWindow.xaml b/Content.Client/Changelog/ChangelogWindow.xaml
index 888a8528d91..355452dbfad 100644
--- a/Content.Client/Changelog/ChangelogWindow.xaml
+++ b/Content.Client/Changelog/ChangelogWindow.xaml
@@ -3,15 +3,10 @@
Title="{Loc 'changelog-window-title'}"
MinSize="500 400"
SetSize="500 400">
-
-
-
-
-
-
+
-
+
diff --git a/Content.Client/Changelog/ChangelogWindow.xaml.cs b/Content.Client/Changelog/ChangelogWindow.xaml.cs
index cea5bd9e7c2..e5f492900c2 100644
--- a/Content.Client/Changelog/ChangelogWindow.xaml.cs
+++ b/Content.Client/Changelog/ChangelogWindow.xaml.cs
@@ -1,28 +1,22 @@
using System.Linq;
-using System.Numerics;
-using Content.Client.Resources;
+using Content.Client.Administration.Managers;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls;
using Content.Client.UserInterface.Systems.EscapeMenu;
using Content.Shared.Administration;
using JetBrains.Annotations;
using Robust.Client.AutoGenerated;
-using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Console;
-using Robust.Shared.Utility;
-using static Content.Client.Changelog.ChangelogManager;
-using static Robust.Client.UserInterface.Controls.BoxContainer;
namespace Content.Client.Changelog
{
[GenerateTypedNameReferences]
public sealed partial class ChangelogWindow : FancyWindow
{
+ [Dependency] private readonly IClientAdminManager _adminManager = default!;
[Dependency] private readonly ChangelogManager _changelog = default!;
- [Dependency] private readonly IResourceCache _resourceCache = default!;
public ChangelogWindow()
{
@@ -39,154 +33,84 @@ protected override void Opened()
PopulateChangelog();
}
+ protected override void EnteredTree()
+ {
+ base.EnteredTree();
+ _adminManager.AdminStatusUpdated += OnAdminStatusUpdated;
+ }
+
+ protected override void ExitedTree()
+ {
+ base.ExitedTree();
+ _adminManager.AdminStatusUpdated -= OnAdminStatusUpdated;
+ }
+
+ private void OnAdminStatusUpdated()
+ {
+ TabsUpdated();
+ }
+
private async void PopulateChangelog()
{
// Changelog is not kept in memory so load it again.
- var changelog = await _changelog.LoadChangelog();
+ var changelogs = await _changelog.LoadChangelog();
- var byDay = changelog
- .GroupBy(e => e.Time.ToLocalTime().Date)
- .OrderByDescending(c => c.Key);
+ Tabs.DisposeAllChildren();
- var hasRead = _changelog.MaxId <= _changelog.LastReadId;
- foreach (var dayEntries in byDay)
+ var i = 0;
+ foreach (var changelog in changelogs)
{
- var day = dayEntries.Key;
-
- var groupedEntries = dayEntries
- .GroupBy(c => (c.Author, Read: c.Id <= _changelog.LastReadId))
- .OrderBy(c => c.Key.Read)
- .ThenBy(c => c.Key.Author);
-
- string dayNice;
- var today = DateTime.Today;
- if (day == today)
- dayNice = Loc.GetString("changelog-today");
- else if (day == today.AddDays(-1))
- dayNice = Loc.GetString("changelog-yesterday");
- else
- dayNice = day.ToShortDateString();
-
- ChangelogBody.AddChild(new Label
- {
- Text = dayNice,
- StyleClasses = { StyleBase.StyleClassLabelHeading },
- Margin = new Thickness(4, 6, 0, 0)
- });
+ var tab = new ChangelogTab { AdminOnly = changelog.AdminOnly };
+ tab.PopulateChangelog(changelog);
- var first = true;
-
- foreach (var groupedEntry in groupedEntries)
- {
- var (author, read) = groupedEntry.Key;
-
- if (!first)
- {
- ChangelogBody.AddChild(new Control { Margin = new Thickness(4) });
- }
-
- if (read && !hasRead)
- {
- hasRead = true;
-
- var upArrow =
- _resourceCache.GetTexture("/Textures/Interface/Changelog/up_arrow.svg.192dpi.png");
-
- var readDivider = new BoxContainer
- {
- Orientation = LayoutOrientation.Vertical
- };
-
- var hBox = new BoxContainer
- {
- Orientation = LayoutOrientation.Horizontal,
- HorizontalAlignment = HAlignment.Center,
- Children =
- {
- new TextureRect
- {
- Texture = upArrow,
- ModulateSelfOverride = Color.FromHex("#888"),
- TextureScale = new Vector2(0.5f, 0.5f),
- Margin = new Thickness(4, 3),
- VerticalAlignment = VAlignment.Bottom
- },
- new Label
- {
- Align = Label.AlignMode.Center,
- Text = Loc.GetString("changelog-new-changes"),
- FontColorOverride = Color.FromHex("#888"),
- },
- new TextureRect
- {
- Texture = upArrow,
- ModulateSelfOverride = Color.FromHex("#888"),
- TextureScale = new Vector2(0.5f, 0.5f),
- Margin = new Thickness(4, 3),
- VerticalAlignment = VAlignment.Bottom
- }
- }
- };
-
- readDivider.AddChild(hBox);
- readDivider.AddChild(new PanelContainer { StyleClasses = { StyleBase.ClassLowDivider } });
- ChangelogBody.AddChild(readDivider);
-
- if (first)
- readDivider.SetPositionInParent(ChangelogBody.ChildCount - 2);
- }
-
- first = false;
-
- var authorLabel = new RichTextLabel
- {
- Margin = new Thickness(6, 0, 0, 0),
- };
- authorLabel.SetMessage(
- FormattedMessage.FromMarkup(Loc.GetString("changelog-author-changed", ("author", author))));
- ChangelogBody.AddChild(authorLabel);
-
- foreach (var change in groupedEntry.SelectMany(c => c.Changes))
- {
- var text = new RichTextLabel();
- text.SetMessage(FormattedMessage.FromMarkup(change.Message));
- ChangelogBody.AddChild(new BoxContainer
- {
- Orientation = LayoutOrientation.Horizontal,
- Margin = new Thickness(14, 1, 10, 2),
- Children =
- {
- GetIcon(change.Type),
- text
- }
- });
- }
- }
+ Tabs.AddChild(tab);
+ Tabs.SetTabTitle(i++, Loc.GetString($"changelog-tab-title-{changelog.Name}"));
}
var version = typeof(ChangelogWindow).Assembly.GetName().Version ?? new Version(1, 0);
VersionLabel.Text = Loc.GetString("changelog-version-tag", ("version", version.ToString()));
+
+ TabsUpdated();
}
- private TextureRect GetIcon(ChangelogLineType type)
+ private void TabsUpdated()
{
- var (file, color) = type switch
+ var tabs = Tabs.Children.OfType().ToArray();
+ var isAdmin = _adminManager.IsAdmin(true);
+
+ var visibleTabs = 0;
+ int? firstVisible = null;
+ for (var i = 0; i < tabs.Length; i++)
{
- ChangelogLineType.Add => ("plus.svg.192dpi.png", "#6ED18D"),
- ChangelogLineType.Remove => ("minus.svg.192dpi.png", "#D16E6E"),
- ChangelogLineType.Fix => ("bug.svg.192dpi.png", "#D1BA6E"),
- ChangelogLineType.Tweak => ("wrench.svg.192dpi.png", "#6E96D1"),
- _ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
- };
-
- return new TextureRect
+ var tab = tabs[i];
+
+ if (!tab.AdminOnly || isAdmin)
+ {
+ Tabs.SetTabVisible(i, true);
+ tab.Visible = true;
+ visibleTabs++;
+ firstVisible ??= i;
+ }
+ else
+ {
+ Tabs.SetTabVisible(i, false);
+ tab.Visible = false;
+ }
+ }
+
+ Tabs.TabsVisible = visibleTabs > 1;
+
+ // Current tab became invisible, select the first one that is visible
+ if (!Tabs.GetTabVisible(Tabs.CurrentTab) && firstVisible != null)
{
- Texture = _resourceCache.GetTexture(new ResPath($"/Textures/Interface/Changelog/{file}")),
- VerticalAlignment = VAlignment.Top,
- TextureScale = new Vector2(0.5f, 0.5f),
- Margin = new Thickness(2, 4, 6, 2),
- ModulateSelfOverride = Color.FromHex(color)
- };
+ Tabs.CurrentTab = firstVisible.Value;
+ }
+
+ // We are only displaying one tab, hide its header
+ if (!Tabs.TabsVisible && firstVisible != null)
+ {
+ Tabs.SetTabVisible(firstVisible.Value, false);
+ }
}
}
diff --git a/Content.Client/Jittering/JitteringSystem.cs b/Content.Client/Jittering/JitteringSystem.cs
index 032eb3e18f2..41f20634ab5 100644
--- a/Content.Client/Jittering/JitteringSystem.cs
+++ b/Content.Client/Jittering/JitteringSystem.cs
@@ -1,13 +1,7 @@
-using System;
-using System.Collections.Immutable;
using System.Numerics;
using Content.Shared.Jittering;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
-using Robust.Shared.Animations;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Maths;
using Robust.Shared.Random;
namespace Content.Client.Jittering
@@ -15,6 +9,7 @@ namespace Content.Client.Jittering
public sealed class JitteringSystem : SharedJitteringSystem
{
[Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly AnimationPlayerSystem _animationPlayer = default!;
private readonly float[] _sign = { -1, 1 };
private readonly string _jitterAnimationKey = "jittering";
@@ -35,13 +30,13 @@ private void OnStartup(EntityUid uid, JitteringComponent jittering, ComponentSta
var animationPlayer = EntityManager.EnsureComponent(uid);
- animationPlayer.Play(GetAnimation(jittering, sprite), _jitterAnimationKey);
+ _animationPlayer.Play(animationPlayer, GetAnimation(jittering, sprite), _jitterAnimationKey);
}
private void OnShutdown(EntityUid uid, JitteringComponent jittering, ComponentShutdown args)
{
if (EntityManager.TryGetComponent(uid, out AnimationPlayerComponent? animationPlayer))
- animationPlayer.Stop(_jitterAnimationKey);
+ _animationPlayer.Stop(animationPlayer, _jitterAnimationKey);
if (EntityManager.TryGetComponent(uid, out SpriteComponent? sprite))
sprite.Offset = Vector2.Zero;
@@ -52,9 +47,9 @@ private void OnAnimationCompleted(EntityUid uid, JitteringComponent jittering, A
if(args.Key != _jitterAnimationKey)
return;
- if(EntityManager.TryGetComponent(uid, out AnimationPlayerComponent? animationPlayer)
+ if (EntityManager.TryGetComponent(uid, out AnimationPlayerComponent? animationPlayer)
&& EntityManager.TryGetComponent(uid, out SpriteComponent? sprite))
- animationPlayer.Play(GetAnimation(jittering, sprite), _jitterAnimationKey);
+ _animationPlayer.Play(animationPlayer, GetAnimation(jittering, sprite), _jitterAnimationKey);
}
private Animation GetAnimation(JitteringComponent jittering, SpriteComponent sprite)
@@ -77,8 +72,10 @@ private Animation GetAnimation(JitteringComponent jittering, SpriteComponent spr
offset.Y *= -1;
}
- // Animation length shouldn't be too high so we will cap it at 2 seconds...
- var length = Math.Min((1f/jittering.Frequency), 2f);
+ var length = 0f;
+ // avoid dividing by 0 so animations don't try to be infinitely long
+ if (jittering.Frequency > 0)
+ length = 1f / jittering.Frequency;
jittering.LastJitter = offset;
diff --git a/Content.Client/Stylesheets/StyleSpace.cs b/Content.Client/Stylesheets/StyleSpace.cs
index a82dba65bcc..3bb4e986af5 100644
--- a/Content.Client/Stylesheets/StyleSpace.cs
+++ b/Content.Client/Stylesheets/StyleSpace.cs
@@ -4,7 +4,6 @@
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
-using Robust.Shared.Maths;
using static Robust.Client.UserInterface.StylesheetHelpers;
namespace Content.Client.Stylesheets
@@ -62,6 +61,14 @@ public StyleSpace(IResourceCache resCache) : base(resCache)
var textureInvertedTriangle = resCache.GetTexture("/Textures/Interface/Nano/inverted_triangle.svg.png");
+ var tabContainerPanel = new StyleBoxTexture();
+ tabContainerPanel.SetPatchMargin(StyleBox.Margin.All, 2);
+
+ var tabContainerBoxActive = new StyleBoxFlat {BackgroundColor = new Color(64, 64, 64)};
+ tabContainerBoxActive.SetContentMarginOverride(StyleBox.Margin.Horizontal, 5);
+ var tabContainerBoxInactive = new StyleBoxFlat {BackgroundColor = new Color(32, 32, 32)};
+ tabContainerBoxInactive.SetContentMarginOverride(StyleBox.Margin.Horizontal, 5);
+
Stylesheet = new Stylesheet(BaseRules.Concat(new StyleRule[]
{
Element