Add sprite exporting (#29874)

* Redo of code

* Dump IDs on lobby exports
This commit is contained in:
metalgearsloth
2024-08-01 01:14:19 +10:00
committed by GitHub
parent 5bc9c04a02
commit 0432f21110
6 changed files with 272 additions and 2 deletions

View File

@@ -272,6 +272,7 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
_logManager,
_playerManager,
_prototypeManager,
_resourceCache,
_requirements,
_markings);

View 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>

View 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)

View File

@@ -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);
}
}
}
}
}

View 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

View 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: