diff --git a/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml b/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml
index 39ea50edbe..32a3e41ca6 100644
--- a/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml
+++ b/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml
@@ -16,6 +16,7 @@
+
diff --git a/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs b/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs
index 8477d26bf6..c24946a53f 100644
--- a/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs
+++ b/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs
@@ -167,6 +167,19 @@ namespace Content.Client.Administration.UI.Bwoink
_console.ExecuteCommand($"kick \"{_currentPlayer.Username}\"");
};
+ // Sunrise-start
+ Screenshot.OnPressed += _ =>
+ {
+ if (!AdminUIHelpers.TryConfirm(Screenshot, _confirmations))
+ {
+ return;
+ }
+
+ if (_currentPlayer is not null)
+ _console.ExecuteCommand($"screenshot \"{_currentPlayer.Username}\"");
+ };
+ // Sunrise-end
+
Follow.OnPressed += _ =>
{
if (_currentPlayer is not null)
@@ -233,6 +246,11 @@ namespace Content.Client.Administration.UI.Bwoink
Respawn.Visible = _adminManager.CanCommand("respawn");
Respawn.Disabled = !Respawn.Visible || disabled;
+ // Sunrise-start
+ Screenshot.Visible = _adminManager.CanCommand("kick");
+ Screenshot.Disabled = !Screenshot.Visible || disabled;
+ // Sunrise-end
+
Follow.Visible = _adminManager.CanCommand("follow");
Follow.Disabled = !Follow.Visible || disabled;
}
diff --git a/Content.Client/Administration/UI/Bwoink/BwoinkWindow.xaml b/Content.Client/Administration/UI/Bwoink/BwoinkWindow.xaml
index 4e0156d7a0..0e86d5282e 100644
--- a/Content.Client/Administration/UI/Bwoink/BwoinkWindow.xaml
+++ b/Content.Client/Administration/UI/Bwoink/BwoinkWindow.xaml
@@ -1,6 +1,6 @@
diff --git a/Content.Client/Administration/UI/Tabs/AdminTab/PlayerActionsWindow.xaml b/Content.Client/Administration/UI/Tabs/AdminTab/PlayerActionsWindow.xaml
index 3df57de57b..5873131ce4 100644
--- a/Content.Client/Administration/UI/Tabs/AdminTab/PlayerActionsWindow.xaml
+++ b/Content.Client/Administration/UI/Tabs/AdminTab/PlayerActionsWindow.xaml
@@ -13,6 +13,7 @@
+
diff --git a/Content.Client/Administration/UI/Tabs/AdminTab/PlayerActionsWindow.xaml.cs b/Content.Client/Administration/UI/Tabs/AdminTab/PlayerActionsWindow.xaml.cs
index 6f960aaae1..257d62cd13 100644
--- a/Content.Client/Administration/UI/Tabs/AdminTab/PlayerActionsWindow.xaml.cs
+++ b/Content.Client/Administration/UI/Tabs/AdminTab/PlayerActionsWindow.xaml.cs
@@ -23,6 +23,7 @@ namespace Content.Client.Administration.UI.Tabs.AdminTab
SubmitKickButton.OnPressed += SubmitKickButtonOnPressed;
SubmitAHelpButton.OnPressed += SubmitAhelpButtonOnPressed;
SubmitRespawnButton.OnPressed += SubmitRespawnButtonOnPressed;
+ ScreenshotButton.OnPressed += ScreenshotButtonOnPressed; // Sunrise-edit
PlayerList.OnSelectionChanged += OnListOnOnSelectionChanged;
}
@@ -36,8 +37,22 @@ namespace Content.Client.Administration.UI.Tabs.AdminTab
SubmitKickButton.Disabled = disableButtons;
SubmitAHelpButton.Disabled = disableButtons;
SubmitRespawnButton.Disabled = disableButtons;
+ ScreenshotButton.Disabled = disableButtons; // Sunrise-edit
}
+ // Sunrise-start
+ private void ScreenshotButtonOnPressed(BaseButton.ButtonEventArgs obj)
+ {
+ if (_selectedPlayer == null)
+ return;
+
+ if (!AdminUIHelpers.TryConfirm(ScreenshotButton, _confirmations))
+ return;
+
+ IoCManager.Resolve().ExecuteCommand($"screenshot \"{_selectedPlayer.Username}\"");
+ }
+ // Sunrise-end
+
private void SubmitKickButtonOnPressed(BaseButton.ButtonEventArgs obj)
{
if (_selectedPlayer == null)
diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs
index 11b4d55248..1610f7b1bb 100644
--- a/Content.Client/Entry/EntryPoint.cs
+++ b/Content.Client/Entry/EntryPoint.cs
@@ -1,3 +1,4 @@
+using Content.Client._Sunrise.Proton; // Sunrise-Proton
using Content.Client._Sunrise.ServersHub;
using Content.Client.Administration.Managers;
using Content.Client.Changelog;
@@ -73,6 +74,7 @@ namespace Content.Client.Entry
[Dependency] private readonly ContentReplayPlaybackManager _replayMan = default!;
[Dependency] private readonly DebugMonitorManager _debugMonitorManager = default!;
[Dependency] private readonly ServersHubManager _serversHubManager = default!; // Sunrise-Hub
+ [Dependency] private readonly ProtonManager _proton = default!; // Sunrise-Proton
public override void Init()
{
@@ -137,6 +139,7 @@ namespace Content.Client.Entry
_playbackMan.Initialize();
_serversHubManager.Initialize(); // Sunrise-Hub
+ _proton.Initialize(); // Sunrise-Proton
//AUTOSCALING default Setup!
_configManager.SetCVar("interface.resolutionAutoScaleUpperCutoffX", 1080);
diff --git a/Content.Client/IoC/ClientContentIoC.cs b/Content.Client/IoC/ClientContentIoC.cs
index 914efd2228..548632a912 100644
--- a/Content.Client/IoC/ClientContentIoC.cs
+++ b/Content.Client/IoC/ClientContentIoC.cs
@@ -1,4 +1,5 @@
-using Content.Client._Sunrise.ServersHub;
+using Content.Client._Sunrise.Proton; // Sunrise-Proton
+using Content.Client._Sunrise.ServersHub;
using Content.Client.Administration.Managers;
using Content.Client.Changelog;
using Content.Client.Chat.Managers;
@@ -56,6 +57,7 @@ namespace Content.Client.IoC
// Sunrise
collection.Register(); // Sunrise-Hub
+ collection.Register(); // Sunrise-Proton
}
}
}
diff --git a/Content.Client/_Sunrise/Proton/ClientProtonManager.cs b/Content.Client/_Sunrise/Proton/ClientProtonManager.cs
new file mode 100644
index 0000000000..0bf89a6047
--- /dev/null
+++ b/Content.Client/_Sunrise/Proton/ClientProtonManager.cs
@@ -0,0 +1,80 @@
+using Content.Shared._Sunrise.Proton;
+using Robust.Client.Graphics;
+using Robust.Shared.ContentPack;
+using Robust.Shared.Network;
+using Robust.Shared.Utility;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace Content.Client._Sunrise.Proton;
+
+public sealed class ProtonManager
+{
+ [Dependency] private readonly IClyde _clyde = default!;
+ [Dependency] private readonly IResourceManager _resourceManager = default!;
+ [Dependency] private readonly IClientNetManager _netManager = default!;
+ [Dependency] private readonly ILogManager _logManager = default!;
+
+ private ISawmill _sawmill = default!;
+
+ public static readonly ResPath ProtonPath = new ResPath("/Proton");
+
+ public void Initialize()
+ {
+ IoCManager.InjectDependencies(this);
+
+ _sawmill = _logManager.GetSawmill("proton");
+
+ _resourceManager.UserData.CreateDir(ProtonPath);
+
+ _sawmill.Debug($"Proton Manager successfully initialized");
+
+ // Registration of messages
+ _netManager.RegisterNetMessage(HandleRequestScreenshot);
+ _netManager.RegisterNetMessage();
+ _netManager.RegisterNetMessage();
+ _netManager.RegisterNetMessage(HandleResponseScreenshot);
+ }
+
+ private void HandleRequestScreenshot(ProtonRequestScreenshotClient request)
+ {
+ _clyde.Screenshot(ScreenshotType.Final, pixels => {HandleScreenshot(ref request, pixels);});
+
+ _sawmill.Debug($"Successfully handled screenshot request");
+ }
+
+ private void HandleScreenshot(ref ProtonRequestScreenshotClient request, Image screenshot)
+ {
+ var response = new ProtonResponseScreenshotClient()
+ {
+ Screenshot = screenshot,
+ };
+ _netManager.ClientSendMessage(response);
+
+ _sawmill.Debug($"Successfully sent screenshot request");
+ }
+
+ private void HandleResponseScreenshot(ProtonResponseScreenshotServer response)
+ {
+ if (response.Screenshot == null)
+ {
+ _sawmill.Debug($"Received response with null screenshot");
+ return;
+ }
+
+ var path = ProtonPath / $"test-{response.Screenshot.GetHashCode()}.png";
+
+ using var stream = _resourceManager.UserData.OpenWrite(path);
+ response.Screenshot.SaveAsPng(stream);
+
+ _sawmill.Debug($"Successfully received screenshot and saved it to {path}");
+ stream.Close();
+
+ using var imageStream = _resourceManager.UserData.OpenRead(path);
+ var view = new ProtonScreenshotView(Texture.LoadFromPNGStream(imageStream, "screenshot"));
+ view.OpenCentered();
+ imageStream.Close();
+
+ _sawmill.Debug($"Opened window (centered) successfully");
+ }
+}
diff --git a/Content.Client/_Sunrise/Proton/ProtonScreenshotView.xaml b/Content.Client/_Sunrise/Proton/ProtonScreenshotView.xaml
new file mode 100644
index 0000000000..6db765fe24
--- /dev/null
+++ b/Content.Client/_Sunrise/Proton/ProtonScreenshotView.xaml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/Content.Client/_Sunrise/Proton/ProtonScreenshotView.xaml.cs b/Content.Client/_Sunrise/Proton/ProtonScreenshotView.xaml.cs
new file mode 100644
index 0000000000..976e1e4a8f
--- /dev/null
+++ b/Content.Client/_Sunrise/Proton/ProtonScreenshotView.xaml.cs
@@ -0,0 +1,20 @@
+using System.Numerics;
+using Content.Client.UserInterface.Controls;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client._Sunrise.Proton;
+
+[GenerateTypedNameReferences]
+public sealed partial class ProtonScreenshotView : FancyWindow
+{
+ public ProtonScreenshotView(Texture image)
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ TextureRect.Texture = image;
+ TextureRect.TextureScale = new Vector2(0.7f, 0.7f);
+ }
+}
diff --git a/Content.Client/_Sunrise/Proton/ScreenshotCommand.cs b/Content.Client/_Sunrise/Proton/ScreenshotCommand.cs
new file mode 100644
index 0000000000..09dc897516
--- /dev/null
+++ b/Content.Client/_Sunrise/Proton/ScreenshotCommand.cs
@@ -0,0 +1,28 @@
+using Content.Shared._Sunrise.Proton;
+using Robust.Shared.Console;
+using Robust.Shared.Network;
+
+namespace Content.Client._Sunrise.Proton;
+
+public sealed class ScreenshotCommand : LocalizedCommands
+{
+ [Dependency] private readonly IClientNetManager _netManager = default!;
+
+ public override string Command => "screenshot";
+
+ public override void Execute(IConsoleShell shell, string argStr, string[] args)
+ {
+ if (args.Length < 1)
+ {
+ shell.WriteLine($"You need to provide a player to screenshot. Try running 'screenshot (playername)' as an example");
+ return;
+ }
+
+ var name = args[0];
+ var message = new ProtonRequestScreenshotServer()
+ {
+ Target = name,
+ };
+ _netManager.ClientSendMessage(message);
+ }
+}
diff --git a/Content.Server/Entry/EntryPoint.cs b/Content.Server/Entry/EntryPoint.cs
index 7934dc9c2a..151f54365a 100644
--- a/Content.Server/Entry/EntryPoint.cs
+++ b/Content.Server/Entry/EntryPoint.cs
@@ -1,4 +1,5 @@
using Content.Server._Sunrise.GuideGenerator;
+using Content.Server._Sunrise.Proton; // Sunrise-Proton
using Content.Server._Sunrise.ServersHub;
using Content.Server._Sunrise.TTS;
using Content.Server.Acz;
@@ -48,6 +49,7 @@ namespace Content.Server.Entry
private IVoteManager _voteManager = default!;
private ServerUpdateManager _updateManager = default!;
private ServersHubManager _serversHubManager = default!; // Sunrise-Edit
+ private ProtonManager _proton = default!; // Proton
private PlayTimeTrackingManager? _playTimeTracking;
private IEntitySystemManager? _sysMan;
private IServerDbManager? _dbManager;
@@ -95,6 +97,7 @@ namespace Content.Server.Entry
_voteManager = IoCManager.Resolve();
_updateManager = IoCManager.Resolve();
_serversHubManager = IoCManager.Resolve(); // Sunrise-Edit
+ _proton = IoCManager.Resolve(); // Proton
_playTimeTracking = IoCManager.Resolve();
_sysMan = IoCManager.Resolve();
_dbManager = IoCManager.Resolve();
@@ -114,6 +117,7 @@ namespace Content.Server.Entry
IoCManager.Resolve().Initialize();
IoCManager.Resolve().Initialize(); // Sunrise-Hub
+ IoCManager.Resolve().Initialize(); // Proton
_voteManager.Initialize();
_updateManager.Initialize();
diff --git a/Content.Server/IoC/ServerContentIoC.cs b/Content.Server/IoC/ServerContentIoC.cs
index 543760aa5d..ae98047aec 100644
--- a/Content.Server/IoC/ServerContentIoC.cs
+++ b/Content.Server/IoC/ServerContentIoC.cs
@@ -1,3 +1,4 @@
+using Content.Server._Sunrise.Proton; // Sunrise-Proton
using Content.Server._Sunrise.ServersHub;
using Content.Server._Sunrise.TTS;
using Content.Server.Administration;
@@ -74,6 +75,7 @@ namespace Content.Server.IoC
IoCManager.Register();
IoCManager.Register(); // Sunrise-Hub
+ IoCManager.Register(); // Sunrise-Proton
}
}
}
diff --git a/Content.Server/_Sunrise/Proton/ServerProtonManager.cs b/Content.Server/_Sunrise/Proton/ServerProtonManager.cs
new file mode 100644
index 0000000000..1bacd42bb1
--- /dev/null
+++ b/Content.Server/_Sunrise/Proton/ServerProtonManager.cs
@@ -0,0 +1,144 @@
+using Content.Server.Administration;
+using Content.Server.Administration.Managers;
+using Content.Shared._Sunrise.Proton;
+using Content.Shared.Administration;
+using Content.Shared.Administration.Managers;
+using Robust.Server.Console;
+using Robust.Server.Player;
+using Robust.Shared.Console;
+using Robust.Shared.Network;
+using Robust.Shared.Player;
+using Robust.Shared.Timing;
+using Timer = Robust.Shared.Timing.Timer;
+
+namespace Content.Server._Sunrise.Proton;
+
+public struct ProtonRequest
+{
+ public NetUserId Admin { get; set; }
+ public TimeSpan TimeStamp { get; set; }
+}
+
+public sealed class ProtonManager
+{
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly IServerNetManager _netManager = default!;
+ [Dependency] private readonly ILogManager _logManager = default!;
+ [Dependency] private readonly ISharedAdminManager _adminManager = default!;
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
+ [Dependency] private readonly IBanManager _banManager = default!;
+ [Dependency] private readonly IPlayerLocator _locator = default!;
+ [Dependency] private readonly IServerConsoleHost _console = default!;
+
+ private ISawmill _sawmill = default!;
+
+ ///
+ /// Стэк реквестов,
+ /// ключ - кому был дан запрос
+ /// значение - от кого был запрос
+ ///
+ private Dictionary _requests = new ();
+
+ public void Initialize()
+ {
+ IoCManager.InjectDependencies(this);
+ _sawmill = _logManager.GetSawmill("proton");
+ _sawmill.Debug($"Proton Manager successfully initialized");
+
+ // Registration of messages
+ _netManager.RegisterNetMessage();
+ _netManager.RegisterNetMessage(HandleScreenshotRequest);
+ _netManager.RegisterNetMessage(HandleScreenshotResponse);
+ _netManager.RegisterNetMessage();
+ }
+
+ private void HandleScreenshotRequest(ProtonRequestScreenshotServer request)
+ {
+ var admin = _playerManager.GetSessionById(request.MsgChannel.UserId);
+ if (!_adminManager.HasAdminFlag(admin, AdminFlags.Moderator))
+ {
+ _sawmill.Debug($"{request.MsgChannel.UserName} requested screenshot without proper admin flags");
+ return;
+ }
+
+ if (request.Target == null)
+ {
+ _sawmill.Debug($"requested screenshot for null player");
+ return;
+ }
+
+ var req = new ProtonRequestScreenshotClient();
+ if (!_playerManager.TryGetSessionByUsername(request.Target, out var targetSession))
+ {
+ _sawmill.Debug($"requested screenshot for unexisting player");
+ return;
+ }
+
+ if (_requests.ContainsKey(targetSession.UserId))
+ {
+ _sawmill.Error($"Multiple requests of one screenshot");
+ return;
+ }
+
+ _netManager.ServerSendMessage(req, targetSession.Channel);
+
+ _requests.Add(targetSession.UserId, new ProtonRequest() { Admin = admin.UserId, TimeStamp = _gameTiming.CurTime });
+
+ _sawmill.Debug($"{request.MsgChannel.UserName} successfully requested screenshot of {targetSession.Channel.UserName}");
+
+ Timer.Spawn(TimeSpan.FromSeconds(30), () => {CheckTimerOnFired(targetSession);});
+ }
+
+ private async void CheckTimerOnFired(ICommonSession session)
+ {
+ if (!_requests.ContainsKey(session.UserId))
+ return;
+
+ BanPlayer(session, "Это автоматический бан. Клиент не ответил на пакет в течении заданного времени");
+
+ _sawmill.Debug($"Successfully banned {session.UserId} for not answering a packet");
+ }
+
+ private void BanPlayer(ICommonSession session, string message)
+ {
+ _console.ExecuteCommand($"ban {session.UserId} \"{message}\" 0 high");
+ }
+
+ private void HandleScreenshotResponse(ProtonResponseScreenshotClient response)
+ {
+ var session = _playerManager.GetSessionById(response.MsgChannel.UserId);
+
+ if (!_requests.ContainsKey(response.MsgChannel.UserId))
+ {
+ _sawmill.Debug($"Received screenshot response without screenshot");
+ BanPlayer(session, "Это автоматический бан. Клиент отправил пустой пакет.");
+ return;
+ }
+
+ if (response.Screenshot == null)
+ {
+ _sawmill.Debug($"Received screenshot response with null screenshot");
+ BanPlayer(session, "Это автоматический бан. Клиент отправил пустой пакет.");
+ return;
+ }
+
+ if (response.Screenshot.Height > 5000 || response.Screenshot.Width > 5000)
+ {
+ _sawmill.Debug($"Received screenshot response with too big screenshot");
+ BanPlayer(session, "Это автоматический бан. Клиент отправил слишком большой пакет.");
+ return;
+ }
+
+ var entry = _requests[response.MsgChannel.UserId];
+
+ var resp = new ProtonResponseScreenshotServer()
+ {
+ Screenshot = response.Screenshot,
+ };
+
+ var admin = _playerManager.GetSessionById(entry.Admin);
+ _netManager.ServerSendMessage(resp, admin.Channel);
+
+ _requests.Remove(response.MsgChannel.UserId);
+ }
+}
diff --git a/Content.Shared/_Sunrise/Proton/ProtonRequests.cs b/Content.Shared/_Sunrise/Proton/ProtonRequests.cs
new file mode 100644
index 0000000000..3033398d9d
--- /dev/null
+++ b/Content.Shared/_Sunrise/Proton/ProtonRequests.cs
@@ -0,0 +1,42 @@
+using Lidgren.Network;
+using Robust.Shared.Network;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared._Sunrise.Proton;
+
+///
+/// Request client to send screenshot
+///
+public sealed class ProtonRequestScreenshotClient : NetMessage
+{
+ public override MsgGroups MsgGroup => MsgGroups.Core;
+
+ public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
+ {
+ }
+
+ public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
+ {
+ }
+}
+
+///
+/// Send request for a screenshot of screen of a specific player
+///
+public sealed class ProtonRequestScreenshotServer : NetMessage
+{
+ public override MsgGroups MsgGroup => MsgGroups.Command;
+
+ // Data sent
+ public string? Target;
+
+ public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
+ {
+ buffer.Write(Target);
+ }
+
+ public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
+ {
+ Target = buffer.ReadString();
+ }
+}
diff --git a/Content.Shared/_Sunrise/Proton/ProtonResponses.cs b/Content.Shared/_Sunrise/Proton/ProtonResponses.cs
new file mode 100644
index 0000000000..ec4d6fd802
--- /dev/null
+++ b/Content.Shared/_Sunrise/Proton/ProtonResponses.cs
@@ -0,0 +1,99 @@
+using System.IO;
+using Lidgren.Network;
+using Robust.Shared.Network;
+using Robust.Shared.Serialization;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace Content.Shared._Sunrise.Proton;
+
+// Maybe we could use one type of message to both get screenshot from client to server and send screenshot from server
+// to admin, who requested this screenshot.
+
+///
+/// Response, containing screenshot of client's window
+///
+public sealed class ProtonResponseScreenshotClient : NetMessage
+{
+ public override MsgGroups MsgGroup => MsgGroups.Command;
+
+ // Data sent
+ public Image? Screenshot;
+
+ public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
+ {
+ var imageDataLength = buffer.ReadInt32();
+
+ if (imageDataLength == 0)
+ {
+ Screenshot = null;
+ return;
+ }
+
+ var imageData = buffer.ReadBytes(imageDataLength);
+
+ using var memoryStream = new MemoryStream(imageData);
+ Screenshot = Image.Load(memoryStream);
+ }
+
+ public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
+ {
+ if (Screenshot == null)
+ {
+ buffer.Write(0);
+ return;
+ }
+
+ using var memoryStream = new MemoryStream();
+ Screenshot.SaveAsPng(memoryStream);
+ var imageData = memoryStream.ToArray();
+
+ buffer.Write(imageData.Length);
+
+ buffer.Write(imageData);
+ }
+}
+
+///
+/// Screenshot, sent from server to target admin
+///
+public sealed class ProtonResponseScreenshotServer : NetMessage
+{
+ public override MsgGroups MsgGroup => MsgGroups.Command;
+
+ // Data sent
+ public Image? Screenshot;
+
+ public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
+ {
+ var imageDataLength = buffer.ReadInt32();
+
+ if (imageDataLength == 0)
+ {
+ Screenshot = null;
+ return;
+ }
+
+ var imageData = buffer.ReadBytes(imageDataLength);
+
+ using var memoryStream = new MemoryStream(imageData);
+ Screenshot = Image.Load(memoryStream);
+ }
+
+ public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
+ {
+ if (Screenshot == null)
+ {
+ buffer.Write(0);
+ return;
+ }
+
+ using var memoryStream = new MemoryStream();
+ Screenshot.SaveAsPng(memoryStream);
+ var imageData = memoryStream.ToArray();
+
+ buffer.Write(imageData.Length);
+
+ buffer.Write(imageData);
+ }
+}
diff --git a/Resources/Locale/en-US/_strings/_sunrise/administration/ui/actions.ftl b/Resources/Locale/en-US/_strings/_sunrise/administration/ui/actions.ftl
new file mode 100644
index 0000000000..5c76a4a933
--- /dev/null
+++ b/Resources/Locale/en-US/_strings/_sunrise/administration/ui/actions.ftl
@@ -0,0 +1 @@
+admin-player-actions-screenshot = Screenshot
diff --git a/Resources/Locale/ru-RU/_strings/_sunrise/administration/ui/actions.ftl b/Resources/Locale/ru-RU/_strings/_sunrise/administration/ui/actions.ftl
new file mode 100644
index 0000000000..72024ced49
--- /dev/null
+++ b/Resources/Locale/ru-RU/_strings/_sunrise/administration/ui/actions.ftl
@@ -0,0 +1 @@
+admin-player-actions-screenshot = Просмотр экрана