Add sprite exporting (#29874)
* Redo of code * Dump IDs on lobby exports
This commit is contained in:
@@ -272,6 +272,7 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
|
||||
_logManager,
|
||||
_playerManager,
|
||||
_prototypeManager,
|
||||
_resourceCache,
|
||||
_requirements,
|
||||
_markings);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
218
Content.Client/Sprite/ContentSpriteSystem.cs
Normal file
218
Content.Client/Sprite/ContentSpriteSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user