]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add sprite exporting (#29874)
authormetalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Wed, 31 Jul 2024 15:14:19 +0000 (01:14 +1000)
committerGitHub <noreply@github.com>
Wed, 31 Jul 2024 15:14:19 +0000 (11:14 -0400)
* Redo of code

* Dump IDs on lobby exports

Content.Client/Lobby/LobbyUIController.cs
Content.Client/Lobby/UI/HumanoidProfileEditor.xaml
Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
Content.Client/Sprite/ContentSpriteSystem.cs [new file with mode: 0644]
Resources/Locale/en-US/administration/admin-verbs.ftl
Resources/Locale/en-US/preferences/ui/humanoid-profile-editor.ftl

index 824a842d560744cc3cebae71ee029c21e54c9a56..1cdaaccc4e8d1cc48486fdd106a7dbec4ed90687 100644 (file)
@@ -272,6 +272,7 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
             _logManager,
             _playerManager,
             _prototypeManager,
+            _resourceCache,
             _requirements,
             _markings);
 
index 03a205e94a80475d8344ffcd547ee1ca5bf10769..2f6c2d5aa2337033f89d0fa1e05db70ad0ede0d6 100644 (file)
@@ -38,6 +38,8 @@
                             <Button Name="ResetButton" Disabled="True" Text="{Loc 'humanoid-profile-editor-reset-button'}"/>
                             <Button Name="ImportButton" Text="{Loc 'humanoid-profile-editor-import-button'}"/>
                             <Button Name="ExportButton" Text="{Loc 'humanoid-profile-editor-export-button'}"/>
+                            <Button Name="ExportImageButton" Text="{Loc 'humanoid-profile-editor-export-image-button'}"/>
+                            <Button Name="OpenImagesButton" Text="{Loc 'humanoid-profile-editor-open-image-button'}"/>
                         </BoxContainer>
                     </ui:HighlightedContainer>
                 </BoxContainer>
index 87ef41c0b738beea6fa37d1f10405ffa69de3f8e..1509a2fed13ba00bb9837c8cae04b86c3da5af1d 100644 (file)
@@ -6,6 +6,7 @@ using Content.Client.Lobby.UI.Loadouts;
 using Content.Client.Lobby.UI.Roles;
 using Content.Client.Message;
 using Content.Client.Players.PlayTimeTracking;
+using Content.Client.Sprite;
 using Content.Client.Stylesheets;
 using Content.Client.UserInterface.Systems.Guidebook;
 using Content.Shared.CCVar;
@@ -27,6 +28,7 @@ using Robust.Client.UserInterface.Controls;
 using Robust.Client.UserInterface.XAML;
 using Robust.Client.Utility;
 using Robust.Shared.Configuration;
+using Robust.Shared.ContentPack;
 using Robust.Shared.Enums;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Utility;
@@ -43,6 +45,7 @@ namespace Content.Client.Lobby.UI
         private readonly IFileDialogManager _dialogManager;
         private readonly IPlayerManager _playerManager;
         private readonly IPrototypeManager _prototypeManager;
+        private readonly IResourceManager _resManager;
         private readonly MarkingManager _markingManager;
         private readonly JobRequirementsManager _requirements;
         private readonly LobbyUIController _controller;
@@ -54,6 +57,7 @@ namespace Content.Client.Lobby.UI
         private LoadoutWindow? _loadoutWindow;
 
         private bool _exporting;
+        private bool _imaging;
 
         /// <summary>
         /// If we're attempting to save.
@@ -107,6 +111,7 @@ namespace Content.Client.Lobby.UI
             ILogManager logManager,
             IPlayerManager playerManager,
             IPrototypeManager prototypeManager,
+            IResourceManager resManager,
             JobRequirementsManager requirements,
             MarkingManager markings)
         {
@@ -119,6 +124,7 @@ namespace Content.Client.Lobby.UI
             _prototypeManager = prototypeManager;
             _markingManager = markings;
             _preferencesManager = preferencesManager;
+            _resManager = resManager;
             _requirements = requirements;
             _controller = UserInterfaceManager.GetUIController<LobbyUIController>();
 
@@ -132,6 +138,16 @@ namespace Content.Client.Lobby.UI
                 ExportProfile();
             };
 
+            ExportImageButton.OnPressed += args =>
+            {
+                ExportImage();
+            };
+
+            OpenImagesButton.OnPressed += args =>
+            {
+                _resManager.UserData.OpenOsWindow(ContentSpriteSystem.Exports);
+            };
+
             ResetButton.OnPressed += args =>
             {
                 SetProfile((HumanoidCharacterProfile?) _preferencesManager.Preferences?.SelectedCharacter, _preferencesManager.Preferences?.SelectedCharacterIndex);
@@ -424,7 +440,6 @@ namespace Content.Client.Lobby.UI
             SpeciesInfoButton.OnPressed += OnSpeciesInfoButtonPressed;
 
             UpdateSpeciesGuidebookIcon();
-            ReloadPreview();
             IsDirty = false;
         }
 
@@ -697,11 +712,12 @@ namespace Content.Client.Lobby.UI
             _entManager.DeleteEntity(PreviewDummy);
             PreviewDummy = EntityUid.Invalid;
 
-            if (Profile == null || !_prototypeManager.HasIndex<SpeciesPrototype>(Profile.Species))
+            if (Profile == null || !_prototypeManager.HasIndex(Profile.Species))
                 return;
 
             PreviewDummy = _controller.LoadProfileEntity(Profile, JobOverride, ShowClothes.Pressed);
             SpriteView.SetEntity(PreviewDummy);
+            _entManager.System<MetaDataSystem>().SetEntityName(PreviewDummy, Profile.Name);
         }
 
         /// <summary>
@@ -1122,6 +1138,17 @@ namespace Content.Client.Lobby.UI
 
             _loadoutWindow?.Dispose();
             _loadoutWindow = null;
+        }
+
+        protected override void EnteredTree()
+        {
+            base.EnteredTree();
+            ReloadPreview();
+        }
+
+        protected override void ExitedTree()
+        {
+            base.ExitedTree();
             _entManager.DeleteEntity(PreviewDummy);
             PreviewDummy = EntityUid.Invalid;
         }
@@ -1182,6 +1209,11 @@ namespace Content.Client.Lobby.UI
         {
             Profile = Profile?.WithName(newName);
             SetDirty();
+
+            if (!IsDirty)
+                return;
+
+            _entManager.System<MetaDataSystem>().SetEntityName(PreviewDummy, newName);
         }
 
         private void SetSpawnPriority(SpawnPriorityPreference newSpawnPriority)
@@ -1513,6 +1545,19 @@ namespace Content.Client.Lobby.UI
             UpdateNameEdit();
         }
 
+        private async void ExportImage()
+        {
+            if (_imaging)
+                return;
+
+            var dir = SpriteView.OverrideDirection ?? Direction.South;
+
+            // I tried disabling the button but it looks sorta goofy as it only takes a frame or two to save
+            _imaging = true;
+            await _entManager.System<ContentSpriteSystem>().Export(PreviewDummy, dir, includeId: false);
+            _imaging = false;
+        }
+
         private async void ImportProfile()
         {
             if (_exporting || CharacterSlot == null || Profile == null)
diff --git a/Content.Client/Sprite/ContentSpriteSystem.cs b/Content.Client/Sprite/ContentSpriteSystem.cs
new file mode 100644 (file)
index 0000000..da05476
--- /dev/null
@@ -0,0 +1,218 @@
+using System.IO;
+using System.Numerics;
+using System.Threading;
+using System.Threading.Tasks;
+using Content.Client.Administration.Managers;
+using Content.Shared.Database;
+using Content.Shared.Verbs;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Shared.ContentPack;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.PixelFormats;
+using Color = Robust.Shared.Maths.Color;
+
+namespace Content.Client.Sprite;
+
+public sealed class ContentSpriteSystem : EntitySystem
+{
+    [Dependency] private readonly IClientAdminManager _adminManager = default!;
+    [Dependency] private readonly IClyde _clyde = default!;
+    [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly IResourceManager _resManager = default!;
+    [Dependency] private readonly IUserInterfaceManager _ui = default!;
+
+    private ContentSpriteControl _control = new();
+
+    public static readonly ResPath Exports = new ResPath("/Exports");
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        _resManager.UserData.CreateDir(Exports);
+        _ui.RootControl.AddChild(_control);
+        SubscribeLocalEvent<GetVerbsEvent<Verb>>(GetVerbs);
+    }
+
+    public override void Shutdown()
+    {
+        base.Shutdown();
+
+        foreach (var queued in _control._queuedTextures)
+        {
+            queued.Tcs.SetCanceled();
+        }
+
+        _control._queuedTextures.Clear();
+
+        _ui.RootControl.RemoveChild(_control);
+    }
+
+    /// <summary>
+    /// Exports sprites for all directions
+    /// </summary>
+    public async Task Export(EntityUid entity, bool includeId = true, CancellationToken cancelToken = default)
+    {
+        var tasks = new Task[4];
+        var i = 0;
+
+        foreach (var dir in new Direction[]
+                 {
+                     Direction.South,
+                     Direction.East,
+                     Direction.North,
+                     Direction.West,
+                 })
+        {
+            tasks[i++] = Export(entity, dir, includeId: includeId, cancelToken);
+        }
+
+        await Task.WhenAll(tasks);
+    }
+
+    /// <summary>
+    /// Exports the sprite for a particular direction.
+    /// </summary>
+    public async Task Export(EntityUid entity, Direction direction, bool includeId = true, CancellationToken cancelToken = default)
+    {
+        if (!_timing.IsFirstTimePredicted)
+            return;
+
+        if (!TryComp(entity, out SpriteComponent? spriteComp))
+            return;
+
+        // Don't want to wait for engine pr
+        var size = Vector2i.Zero;
+
+        foreach (var layer in spriteComp.AllLayers)
+        {
+            if (!layer.Visible)
+                continue;
+
+            size = Vector2i.ComponentMax(size, layer.PixelSize);
+        }
+
+        // Stop asserts
+        if (size.Equals(Vector2i.Zero))
+            return;
+
+        var texture = _clyde.CreateRenderTarget(new Vector2i(size.X, size.Y), new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "export");
+        var tcs = new TaskCompletionSource(cancelToken);
+
+        _control._queuedTextures.Enqueue((texture, direction, entity, includeId, tcs));
+
+        await tcs.Task;
+    }
+
+    private void GetVerbs(GetVerbsEvent<Verb> ev)
+    {
+        if (!_adminManager.IsAdmin())
+            return;
+
+        Verb verb = new()
+        {
+            Text = Loc.GetString("export-entity-verb-get-data-text"),
+            Category = VerbCategory.Debug,
+            Act = () =>
+            {
+                Export(ev.Target);
+            },
+        };
+
+        ev.Verbs.Add(verb);
+    }
+
+    /// <summary>
+    /// This is horrible. I asked PJB if there's an easy way to render straight to a texture outside of the render loop
+    /// and she also mentioned this as a bad possibility.
+    /// </summary>
+    private sealed class ContentSpriteControl : Control
+    {
+        [Dependency] private readonly IEntityManager _entManager = default!;
+        [Dependency] private readonly ILogManager _logMan = default!;
+        [Dependency] private readonly IResourceManager _resManager = default!;
+
+        internal Queue<(
+            IRenderTexture Texture,
+            Direction Direction,
+            EntityUid Entity,
+            bool IncludeId,
+            TaskCompletionSource Tcs)> _queuedTextures = new();
+
+        private ISawmill _sawmill;
+
+        public ContentSpriteControl()
+        {
+            IoCManager.InjectDependencies(this);
+            _sawmill = _logMan.GetSawmill("sprite.export");
+        }
+
+        protected override void Draw(DrawingHandleScreen handle)
+        {
+            base.Draw(handle);
+
+            while (_queuedTextures.TryDequeue(out var queued))
+            {
+                if (queued.Tcs.Task.IsCanceled)
+                    continue;
+
+                try
+                {
+                    if (!_entManager.TryGetComponent(queued.Entity, out MetaDataComponent? metadata))
+                        continue;
+
+                    var filename = metadata.EntityName;
+                    var result = queued;
+
+                    handle.RenderInRenderTarget(queued.Texture, () =>
+                    {
+                        handle.DrawEntity(result.Entity, result.Texture.Size / 2, Vector2.One, Angle.Zero,
+                            overrideDirection: result.Direction);
+                    }, Color.Transparent);
+
+                    ResPath fullFileName;
+
+                    if (queued.IncludeId)
+                    {
+                        fullFileName = Exports / $"{filename}-{queued.Direction}-{queued.Entity}.png";
+                    }
+                    else
+                    {
+                        fullFileName = Exports / $"{filename}-{queued.Direction}.png";
+                    }
+
+                    queued.Texture.CopyPixelsToMemory<Rgba32>(image =>
+                    {
+                        if (_resManager.UserData.Exists(fullFileName))
+                        {
+                            _sawmill.Info($"Found existing file {fullFileName} to replace.");
+                            _resManager.UserData.Delete(fullFileName);
+                        }
+
+                        using var file =
+                            _resManager.UserData.Open(fullFileName, FileMode.CreateNew, FileAccess.Write,
+                                FileShare.None);
+
+                        image.SaveAsPng(file);
+                    });
+
+                    _sawmill.Info($"Saved screenshot to {fullFileName}");
+                    queued.Tcs.SetResult();
+                }
+                catch (Exception exc)
+                {
+                    queued.Texture.Dispose();
+
+                    if (!string.IsNullOrEmpty(exc.StackTrace))
+                        _sawmill.Fatal(exc.StackTrace);
+
+                    queued.Tcs.SetException(exc);
+                }
+            }
+        }
+    }
+}
index 16715087ee4006b91d85a095c866777a022cd7c2..24294f05298a3bb3ae0e72ad97a7b24009294d4b 100644 (file)
@@ -14,3 +14,5 @@ admin-verbs-erase-description = Removes the player from the round and crew manif
     Players are shown a popup indicating them to play as if they never existed.
 toolshed-verb-mark = Mark
 toolshed-verb-mark-description = Places this entity into the $marked variable, a list of entities, replacing it's prior value.
+
+export-entity-verb-get-data-text = Export sprite
index f75a21c5ff3955b017bcd0bd8855f1215a8b2787..04ea0d9d51f49ce594d5d9c499a576fe9ead04c1 100644 (file)
@@ -18,6 +18,8 @@ humanoid-profile-editor-pronouns-epicene-text = They / Them
 humanoid-profile-editor-pronouns-neuter-text = It / It
 humanoid-profile-editor-import-button = Import
 humanoid-profile-editor-export-button = Export
+humanoid-profile-editor-export-image-button = Export image
+humanoid-profile-editor-open-image-button = Open images
 humanoid-profile-editor-save-button = Save
 humanoid-profile-editor-reset-button = Reset
 humanoid-profile-editor-spawn-priority-label = Spawn priority: