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,
|
_logManager,
|
||||||
_playerManager,
|
_playerManager,
|
||||||
_prototypeManager,
|
_prototypeManager,
|
||||||
|
_resourceCache,
|
||||||
_requirements,
|
_requirements,
|
||||||
_markings);
|
_markings);
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,8 @@
|
|||||||
<Button Name="ResetButton" Disabled="True" Text="{Loc 'humanoid-profile-editor-reset-button'}"/>
|
<Button Name="ResetButton" Disabled="True" Text="{Loc 'humanoid-profile-editor-reset-button'}"/>
|
||||||
<Button Name="ImportButton" Text="{Loc 'humanoid-profile-editor-import-button'}"/>
|
<Button Name="ImportButton" Text="{Loc 'humanoid-profile-editor-import-button'}"/>
|
||||||
<Button Name="ExportButton" Text="{Loc 'humanoid-profile-editor-export-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>
|
</BoxContainer>
|
||||||
</ui:HighlightedContainer>
|
</ui:HighlightedContainer>
|
||||||
</BoxContainer>
|
</BoxContainer>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using Content.Client.Lobby.UI.Loadouts;
|
|||||||
using Content.Client.Lobby.UI.Roles;
|
using Content.Client.Lobby.UI.Roles;
|
||||||
using Content.Client.Message;
|
using Content.Client.Message;
|
||||||
using Content.Client.Players.PlayTimeTracking;
|
using Content.Client.Players.PlayTimeTracking;
|
||||||
|
using Content.Client.Sprite;
|
||||||
using Content.Client.Stylesheets;
|
using Content.Client.Stylesheets;
|
||||||
using Content.Client.UserInterface.Systems.Guidebook;
|
using Content.Client.UserInterface.Systems.Guidebook;
|
||||||
using Content.Shared.CCVar;
|
using Content.Shared.CCVar;
|
||||||
@@ -27,6 +28,7 @@ using Robust.Client.UserInterface.Controls;
|
|||||||
using Robust.Client.UserInterface.XAML;
|
using Robust.Client.UserInterface.XAML;
|
||||||
using Robust.Client.Utility;
|
using Robust.Client.Utility;
|
||||||
using Robust.Shared.Configuration;
|
using Robust.Shared.Configuration;
|
||||||
|
using Robust.Shared.ContentPack;
|
||||||
using Robust.Shared.Enums;
|
using Robust.Shared.Enums;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
@@ -43,6 +45,7 @@ namespace Content.Client.Lobby.UI
|
|||||||
private readonly IFileDialogManager _dialogManager;
|
private readonly IFileDialogManager _dialogManager;
|
||||||
private readonly IPlayerManager _playerManager;
|
private readonly IPlayerManager _playerManager;
|
||||||
private readonly IPrototypeManager _prototypeManager;
|
private readonly IPrototypeManager _prototypeManager;
|
||||||
|
private readonly IResourceManager _resManager;
|
||||||
private readonly MarkingManager _markingManager;
|
private readonly MarkingManager _markingManager;
|
||||||
private readonly JobRequirementsManager _requirements;
|
private readonly JobRequirementsManager _requirements;
|
||||||
private readonly LobbyUIController _controller;
|
private readonly LobbyUIController _controller;
|
||||||
@@ -54,6 +57,7 @@ namespace Content.Client.Lobby.UI
|
|||||||
private LoadoutWindow? _loadoutWindow;
|
private LoadoutWindow? _loadoutWindow;
|
||||||
|
|
||||||
private bool _exporting;
|
private bool _exporting;
|
||||||
|
private bool _imaging;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If we're attempting to save.
|
/// If we're attempting to save.
|
||||||
@@ -107,6 +111,7 @@ namespace Content.Client.Lobby.UI
|
|||||||
ILogManager logManager,
|
ILogManager logManager,
|
||||||
IPlayerManager playerManager,
|
IPlayerManager playerManager,
|
||||||
IPrototypeManager prototypeManager,
|
IPrototypeManager prototypeManager,
|
||||||
|
IResourceManager resManager,
|
||||||
JobRequirementsManager requirements,
|
JobRequirementsManager requirements,
|
||||||
MarkingManager markings)
|
MarkingManager markings)
|
||||||
{
|
{
|
||||||
@@ -119,6 +124,7 @@ namespace Content.Client.Lobby.UI
|
|||||||
_prototypeManager = prototypeManager;
|
_prototypeManager = prototypeManager;
|
||||||
_markingManager = markings;
|
_markingManager = markings;
|
||||||
_preferencesManager = preferencesManager;
|
_preferencesManager = preferencesManager;
|
||||||
|
_resManager = resManager;
|
||||||
_requirements = requirements;
|
_requirements = requirements;
|
||||||
_controller = UserInterfaceManager.GetUIController<LobbyUIController>();
|
_controller = UserInterfaceManager.GetUIController<LobbyUIController>();
|
||||||
|
|
||||||
@@ -132,6 +138,16 @@ namespace Content.Client.Lobby.UI
|
|||||||
ExportProfile();
|
ExportProfile();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ExportImageButton.OnPressed += args =>
|
||||||
|
{
|
||||||
|
ExportImage();
|
||||||
|
};
|
||||||
|
|
||||||
|
OpenImagesButton.OnPressed += args =>
|
||||||
|
{
|
||||||
|
_resManager.UserData.OpenOsWindow(ContentSpriteSystem.Exports);
|
||||||
|
};
|
||||||
|
|
||||||
ResetButton.OnPressed += args =>
|
ResetButton.OnPressed += args =>
|
||||||
{
|
{
|
||||||
SetProfile((HumanoidCharacterProfile?) _preferencesManager.Preferences?.SelectedCharacter, _preferencesManager.Preferences?.SelectedCharacterIndex);
|
SetProfile((HumanoidCharacterProfile?) _preferencesManager.Preferences?.SelectedCharacter, _preferencesManager.Preferences?.SelectedCharacterIndex);
|
||||||
@@ -424,7 +440,6 @@ namespace Content.Client.Lobby.UI
|
|||||||
SpeciesInfoButton.OnPressed += OnSpeciesInfoButtonPressed;
|
SpeciesInfoButton.OnPressed += OnSpeciesInfoButtonPressed;
|
||||||
|
|
||||||
UpdateSpeciesGuidebookIcon();
|
UpdateSpeciesGuidebookIcon();
|
||||||
ReloadPreview();
|
|
||||||
IsDirty = false;
|
IsDirty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -697,11 +712,12 @@ namespace Content.Client.Lobby.UI
|
|||||||
_entManager.DeleteEntity(PreviewDummy);
|
_entManager.DeleteEntity(PreviewDummy);
|
||||||
PreviewDummy = EntityUid.Invalid;
|
PreviewDummy = EntityUid.Invalid;
|
||||||
|
|
||||||
if (Profile == null || !_prototypeManager.HasIndex<SpeciesPrototype>(Profile.Species))
|
if (Profile == null || !_prototypeManager.HasIndex(Profile.Species))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
PreviewDummy = _controller.LoadProfileEntity(Profile, JobOverride, ShowClothes.Pressed);
|
PreviewDummy = _controller.LoadProfileEntity(Profile, JobOverride, ShowClothes.Pressed);
|
||||||
SpriteView.SetEntity(PreviewDummy);
|
SpriteView.SetEntity(PreviewDummy);
|
||||||
|
_entManager.System<MetaDataSystem>().SetEntityName(PreviewDummy, Profile.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -1122,6 +1138,17 @@ namespace Content.Client.Lobby.UI
|
|||||||
|
|
||||||
_loadoutWindow?.Dispose();
|
_loadoutWindow?.Dispose();
|
||||||
_loadoutWindow = null;
|
_loadoutWindow = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void EnteredTree()
|
||||||
|
{
|
||||||
|
base.EnteredTree();
|
||||||
|
ReloadPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ExitedTree()
|
||||||
|
{
|
||||||
|
base.ExitedTree();
|
||||||
_entManager.DeleteEntity(PreviewDummy);
|
_entManager.DeleteEntity(PreviewDummy);
|
||||||
PreviewDummy = EntityUid.Invalid;
|
PreviewDummy = EntityUid.Invalid;
|
||||||
}
|
}
|
||||||
@@ -1182,6 +1209,11 @@ namespace Content.Client.Lobby.UI
|
|||||||
{
|
{
|
||||||
Profile = Profile?.WithName(newName);
|
Profile = Profile?.WithName(newName);
|
||||||
SetDirty();
|
SetDirty();
|
||||||
|
|
||||||
|
if (!IsDirty)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_entManager.System<MetaDataSystem>().SetEntityName(PreviewDummy, newName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetSpawnPriority(SpawnPriorityPreference newSpawnPriority)
|
private void SetSpawnPriority(SpawnPriorityPreference newSpawnPriority)
|
||||||
@@ -1513,6 +1545,19 @@ namespace Content.Client.Lobby.UI
|
|||||||
UpdateNameEdit();
|
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()
|
private async void ImportProfile()
|
||||||
{
|
{
|
||||||
if (_exporting || CharacterSlot == null || Profile == null)
|
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.
|
Players are shown a popup indicating them to play as if they never existed.
|
||||||
toolshed-verb-mark = Mark
|
toolshed-verb-mark = Mark
|
||||||
toolshed-verb-mark-description = Places this entity into the $marked variable, a list of entities, replacing it's prior value.
|
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-pronouns-neuter-text = It / It
|
||||||
humanoid-profile-editor-import-button = Import
|
humanoid-profile-editor-import-button = Import
|
||||||
humanoid-profile-editor-export-button = Export
|
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-save-button = Save
|
||||||
humanoid-profile-editor-reset-button = Reset
|
humanoid-profile-editor-reset-button = Reset
|
||||||
humanoid-profile-editor-spawn-priority-label = Spawn priority:
|
humanoid-profile-editor-spawn-priority-label = Spawn priority:
|
||||||
|
|||||||
Reference in New Issue
Block a user