Skip to content

Commit

Permalink
Botões para exportar imagem do personagem
Browse files Browse the repository at this point in the history
  • Loading branch information
cosmosgc committed Feb 19, 2025
1 parent 1c27208 commit bbb08fd
Show file tree
Hide file tree
Showing 6 changed files with 266 additions and 0 deletions.
1 change: 1 addition & 0 deletions Content.Client/Lobby/LobbyUIController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ private void SaveProfile()
_dialogManager,
_playerManager,
_prototypeManager,
_resourceCache,
_requirements,
_markings,
_random);
Expand Down
2 changes: 2 additions & 0 deletions Content.Client/Lobby/UI/HumanoidProfileEditor.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
<Button Name="ExportButton" Text="{Loc 'humanoid-profile-editor-export-button'}"/>
<Button Name="SaveButton" Text="{Loc 'humanoid-profile-editor-save-button'}" />
<Button Name="ResetButton" Disabled="True" Text="{Loc 'humanoid-profile-editor-reset-button'}"/>
<Button Name="ExportImageButton" Text="{Loc 'humanoid-profile-editor-export-image-button'}"/>
<Button Name="OpenImagesButton" Text="{Loc 'humanoid-profile-editor-open-image-button'}"/>
</BoxContainer>
</prefUi:HighlightedContainer>
</BoxContainer>
Expand Down
42 changes: 42 additions & 0 deletions Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Content.Client.Humanoid;
using Content.Client.Message;
using Content.Client.Players.PlayTimeTracking;
using Content.Client.Sprite;
using Content.Client.UserInterface.Controls;
using Content.Client.UserInterface.Systems.Guidebook;
using Content.Shared.CCVar;
Expand All @@ -27,6 +28,7 @@
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.ContentPack;
using Robust.Client.Utility;
using Robust.Client.Player;
using Robust.Shared.Configuration;
Expand All @@ -43,6 +45,7 @@ namespace Content.Client.Lobby.UI
[GenerateTypedNameReferences]
public sealed partial class HumanoidProfileEditor : BoxContainer
{
private readonly IResourceManager _resManager;
private readonly IConfigurationManager _cfgManager;
private readonly IEntityManager _entManager;
private readonly IFileDialogManager _dialogManager;
Expand All @@ -63,6 +66,7 @@ public sealed partial class HumanoidProfileEditor : BoxContainer
/// If we're attempting to save
public event Action? Save;
private bool _exporting;
private bool _imaging;
private bool _isDirty;

/// The character slot for the current profile
Expand Down Expand Up @@ -108,6 +112,7 @@ public HumanoidProfileEditor(
IFileDialogManager dialogManager,
IPlayerManager playerManager,
IPrototypeManager prototypeManager,
IResourceManager resManager,
JobRequirementsManager requirements,
MarkingManager markings,
IRobustRandom random
Expand All @@ -121,9 +126,11 @@ IRobustRandom random
_prototypeManager = prototypeManager;
_markingManager = markings;
_preferencesManager = preferencesManager;
_resManager = resManager;
_requirements = requirements;
_random = random;


_characterRequirementsSystem = _entManager.System<CharacterRequirementsSystem>();
_controller = UserInterfaceManager.GetUIController<LobbyUIController>();

Expand All @@ -136,6 +143,15 @@ IRobustRandom random
(HumanoidCharacterProfile?) _preferencesManager.Preferences?.SelectedCharacter,
_preferencesManager.Preferences?.SelectedCharacterIndex);
};
ExportImageButton.OnPressed += args =>
{
ExportImage();
};

OpenImagesButton.OnPressed += args =>
{
_resManager.UserData.OpenOsWindow(ContentSpriteSystem.Exports);
};

#region Left

Expand Down Expand Up @@ -688,6 +704,7 @@ private void ReloadPreview()

PreviewDummy = _controller.LoadProfileEntity(Profile, ShowClothes.Pressed, ShowLoadouts.Pressed);
SpriteView.SetEntity(PreviewDummy);
_entManager.System<MetaDataSystem>().SetEntityName(PreviewDummy, Profile.Name);
}

/// Reloads the dummy entity's clothes for preview
Expand Down Expand Up @@ -1203,7 +1220,17 @@ protected override void Dispose(bool disposing)
base.Dispose(disposing);
if (!disposing)
return;
}

protected override void EnteredTree()
{
base.EnteredTree();
ReloadPreview();
}

protected override void ExitedTree()
{
base.ExitedTree();
_entManager.DeleteEntity(PreviewDummy);
PreviewDummy = EntityUid.Invalid;

Expand Down Expand Up @@ -1300,6 +1327,10 @@ private void SetName(string newName)
{
Profile = Profile?.WithName(newName);
IsDirty = true;
if (!IsDirty)
return;

_entManager.System<MetaDataSystem>().SetEntityName(PreviewDummy, newName);
}

private void SetCustomSpecieName(string customname)
Expand Down Expand Up @@ -1757,7 +1788,18 @@ private void RandomizeName()
SetName(name);
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)
Expand Down
218 changes: 218 additions & 0 deletions Content.Client/Sprite/ContentSpriteSystem.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
}
1 change: 1 addition & 0 deletions Resources/Locale/pt-BR/administration/admin-verbs.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ admin-verbs-erase-description = Remove o jogador da rodada, do manifesto da trip
Os jogadores veem um pop-up indicando que no jogo é como se nunca tivessem existido.
toolshed-verb-mark = Marcar
toolshed-verb-mark-description = Coloca essa entidade na variável $marked, uma lista de entidades, substituindo seu valor anterior.
export-entity-verb-get-data-text = Export sprite
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ humanoid-profile-editor-width-label = Largura: {$width}cm
humanoid-profile-editor-weight-label = Peso: {$weight}kg
humanoid-profile-editor-import-button = Importar
humanoid-profile-editor-export-button = Exportar
humanoid-profile-editor-export-image-button = Export img
humanoid-profile-editor-open-image-button = Open img
humanoid-profile-editor-save-button = Salvar
humanoid-profile-editor-reset-button = Resetar
humanoid-profile-editor-clothing-label = Roupas:
Expand Down

0 comments on commit bbb08fd

Please sign in to comment.