mirror of
https://github.com/corvax-team/ss14-wl.git
synced 2026-02-15 03:31:38 +01:00
430 lines
13 KiB
C#
430 lines
13 KiB
C#
using System.IO;
|
|
using System.Numerics;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Content.Client.Administration.Managers;
|
|
using Content.Shared.Chat.TypingIndicator;
|
|
using Content.Shared.Verbs;
|
|
using Robust.Client.GameObjects;
|
|
using Robust.Client.Graphics;
|
|
using Robust.Client.UserInterface;
|
|
using Robust.Shared.ContentPack;
|
|
using Robust.Shared.Exceptions;
|
|
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!;
|
|
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
|
|
//WL-Changes-start
|
|
[Dependency] private readonly ILogManager _logMan = default!;
|
|
[Dependency] private readonly AppearanceSystem _appearance = default!;
|
|
|
|
private ISawmill _sawmill = default!;
|
|
//WL-Changes-end
|
|
|
|
private ContentSpriteControl<Rgba32> _control = default!;
|
|
|
|
public static readonly ResPath Exports = new ResPath("/Exports");
|
|
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
|
|
//WL-Changes-start
|
|
_sawmill = _logMan.GetSawmill("sprite.export");
|
|
_control = new(_appearance);
|
|
//WL-Changes-end
|
|
|
|
_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);
|
|
}
|
|
|
|
//WL-Changes-start
|
|
/// <summary>
|
|
/// Exports the sprite for a particular direction.
|
|
/// </summary>
|
|
public async Task Export(
|
|
EntityUid entity,
|
|
Direction direction,
|
|
Action<ContentSpriteControl<Rgba32>.QueueEntry, Image<Rgba32>> action,
|
|
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, tcs, action));
|
|
|
|
await tcs.Task;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Сохраняет спрайт в директорию /Exports
|
|
/// </summary>
|
|
/// <param name="entity"></param>
|
|
/// <param name="direction"></param>
|
|
/// <param name="includeId"></param>
|
|
/// <param name="cancelToken"></param>
|
|
/// <returns></returns>
|
|
public async Task Export(
|
|
EntityUid entity,
|
|
Direction direction,
|
|
bool includeId = true,
|
|
CancellationToken cancelToken = default)
|
|
{
|
|
await Export(entity, direction, (ContentSpriteControl<Rgba32>.QueueEntry queued, Image<Rgba32> image) =>
|
|
{
|
|
var metadata = MetaData(queued.Entity);
|
|
|
|
ResPath fullFileName;
|
|
|
|
var filename = metadata.EntityName;
|
|
|
|
if (includeId)
|
|
{
|
|
fullFileName = Exports / $"{filename}-{queued.Direction}-{queued.Entity}.png";
|
|
}
|
|
else
|
|
{
|
|
fullFileName = Exports / $"{filename}-{queued.Direction}.png";
|
|
}
|
|
|
|
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}");
|
|
}, cancelToken);
|
|
}
|
|
//WL-Changes-end
|
|
|
|
private void GetVerbs(GetVerbsEvent<Verb> ev)
|
|
{
|
|
if (!_adminManager.IsAdmin())
|
|
return;
|
|
|
|
var target = ev.Target;
|
|
Verb verb = new()
|
|
{
|
|
Text = Loc.GetString("export-entity-verb-get-data-text"),
|
|
Category = VerbCategory.Debug,
|
|
Act = async () =>
|
|
{
|
|
try
|
|
{
|
|
await Export(target);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
_runtimeLog.LogException(e, $"{nameof(ContentSpriteSystem)}.{nameof(Export)}");
|
|
}
|
|
},
|
|
};
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public sealed class ContentSpriteControl<T> : Control where T : unmanaged, IPixel<T>
|
|
{
|
|
[Dependency] private readonly IEntityManager _entManager = default!;
|
|
[Dependency] private readonly ILogManager _logMan = default!;
|
|
|
|
private readonly AppearanceSystem _appearance;
|
|
|
|
internal readonly Queue<QueueEntry> _queuedTextures;
|
|
|
|
private readonly Queue<QueueEntry> _defferedTextures;
|
|
|
|
private ISawmill _sawmill;
|
|
|
|
public ContentSpriteControl(AppearanceSystem appearance)
|
|
{
|
|
IoCManager.InjectDependencies(this);
|
|
_sawmill = _logMan.GetSawmill("sprite.export");
|
|
|
|
_appearance = appearance;
|
|
_queuedTextures = new();
|
|
_defferedTextures = new();
|
|
}
|
|
|
|
protected override void Draw(DrawingHandleScreen handle)
|
|
{
|
|
base.Draw(handle);
|
|
|
|
while (_queuedTextures.TryDequeue(out var queued))
|
|
{
|
|
if (queued.Tcs.Task.IsCanceled)
|
|
continue;
|
|
|
|
if (ShouldBeDeffered(queued))
|
|
{
|
|
_defferedTextures.Enqueue(queued);
|
|
continue;
|
|
}
|
|
|
|
HandleQueue(queued, handle);
|
|
}
|
|
|
|
while (_defferedTextures.TryDequeue(out var dequeue))
|
|
{
|
|
if (dequeue.Tcs.Task.IsCanceled)
|
|
continue;
|
|
|
|
if (ShouldBeDeffered(dequeue))
|
|
{
|
|
_queuedTextures.Enqueue(dequeue);
|
|
continue;
|
|
}
|
|
|
|
HandleQueue(dequeue, handle);
|
|
}
|
|
}
|
|
|
|
private bool ShouldBeDeffered(QueueEntry entry)
|
|
{
|
|
var entity = entry.Entity;
|
|
|
|
if (_appearance.TryGetData<TypingIndicatorState>(entity, TypingIndicatorVisuals.State, out var state))
|
|
{
|
|
if (state is not TypingIndicatorState.None)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void HandleQueue(QueueEntry queued, DrawingHandleScreen handle)
|
|
{
|
|
try
|
|
{
|
|
if (!_entManager.TryGetComponent(queued.Entity, out MetaDataComponent? metadata))
|
|
return;
|
|
|
|
var result = queued;
|
|
|
|
handle.RenderInRenderTarget(queued.Texture, () =>
|
|
{
|
|
handle.DrawEntity(result.Entity, result.Texture.Size / 2, Vector2.One, Angle.Zero,
|
|
overrideDirection: result.Direction);
|
|
}, Color.Transparent);
|
|
|
|
queued.Texture.CopyPixelsToMemory<T>(image =>
|
|
{
|
|
queued.Action.Invoke(queued, image);
|
|
});
|
|
|
|
queued.Tcs.SetResult();
|
|
}
|
|
catch (Exception exc)
|
|
{
|
|
queued.Texture.Dispose();
|
|
|
|
if (!string.IsNullOrEmpty(exc.StackTrace))
|
|
_sawmill.Fatal(exc.StackTrace);
|
|
|
|
queued.Tcs.SetException(exc);
|
|
}
|
|
}
|
|
|
|
public sealed class QueueEntry
|
|
{
|
|
public readonly IRenderTexture Texture;
|
|
public readonly Direction Direction;
|
|
public readonly EntityUid Entity;
|
|
public readonly TaskCompletionSource Tcs;
|
|
public readonly Action<QueueEntry, Image<T>> Action;
|
|
|
|
public QueueEntry(
|
|
IRenderTexture texture,
|
|
Direction direction,
|
|
EntityUid entity,
|
|
TaskCompletionSource tcs,
|
|
Action<QueueEntry, Image<T>> action)
|
|
{
|
|
Texture = texture;
|
|
Direction = direction;
|
|
Entity = entity;
|
|
Tcs = tcs;
|
|
Action = action;
|
|
}
|
|
|
|
public static implicit operator QueueEntry((
|
|
IRenderTexture Texture,
|
|
Direction Direction,
|
|
EntityUid Entity,
|
|
TaskCompletionSource Tcs,
|
|
Action<QueueEntry, Image<T>> Action) param)
|
|
{
|
|
return new QueueEntry(param.Texture, param.Direction, param.Entity, param.Tcs, param.Action);
|
|
}
|
|
}
|
|
}
|
|
}
|