ContentSpriteSystem.cs 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. using System.IO;
  2. using System.Numerics;
  3. using System.Threading;
  4. using System.Threading.Tasks;
  5. using Content.Client.Administration.Managers;
  6. using Content.Shared.Verbs;
  7. using Robust.Client.GameObjects;
  8. using Robust.Client.Graphics;
  9. using Robust.Client.UserInterface;
  10. using Robust.Shared.ContentPack;
  11. using Robust.Shared.Exceptions;
  12. using Robust.Shared.Timing;
  13. using Robust.Shared.Utility;
  14. using SixLabors.ImageSharp;
  15. using SixLabors.ImageSharp.PixelFormats;
  16. using Color = Robust.Shared.Maths.Color;
  17. namespace Content.Client.Sprite;
  18. public sealed class ContentSpriteSystem : EntitySystem
  19. {
  20. [Dependency] private readonly IClientAdminManager _adminManager = default!;
  21. [Dependency] private readonly IClyde _clyde = default!;
  22. [Dependency] private readonly IGameTiming _timing = default!;
  23. [Dependency] private readonly IResourceManager _resManager = default!;
  24. [Dependency] private readonly IUserInterfaceManager _ui = default!;
  25. [Dependency] private readonly IRuntimeLog _runtimeLog = default!;
  26. private ContentSpriteControl _control = new();
  27. public static readonly ResPath Exports = new ResPath("/Exports");
  28. public override void Initialize()
  29. {
  30. base.Initialize();
  31. _resManager.UserData.CreateDir(Exports);
  32. _ui.RootControl.AddChild(_control);
  33. SubscribeLocalEvent<GetVerbsEvent<Verb>>(GetVerbs);
  34. }
  35. public override void Shutdown()
  36. {
  37. base.Shutdown();
  38. foreach (var queued in _control.QueuedTextures)
  39. {
  40. queued.Tcs.SetCanceled();
  41. }
  42. _control.QueuedTextures.Clear();
  43. _ui.RootControl.RemoveChild(_control);
  44. }
  45. /// <summary>
  46. /// Exports sprites for all directions
  47. /// </summary>
  48. public async Task Export(EntityUid entity, bool includeId = true, CancellationToken cancelToken = default)
  49. {
  50. var tasks = new Task[4];
  51. var i = 0;
  52. foreach (var dir in new Direction[]
  53. {
  54. Direction.South,
  55. Direction.East,
  56. Direction.North,
  57. Direction.West,
  58. })
  59. {
  60. tasks[i++] = Export(entity, dir, includeId: includeId, cancelToken);
  61. }
  62. await Task.WhenAll(tasks);
  63. }
  64. /// <summary>
  65. /// Exports the sprite for a particular direction.
  66. /// </summary>
  67. public async Task Export(EntityUid entity, Direction direction, bool includeId = true, CancellationToken cancelToken = default)
  68. {
  69. if (!_timing.IsFirstTimePredicted)
  70. return;
  71. if (!TryComp(entity, out SpriteComponent? spriteComp))
  72. return;
  73. // Don't want to wait for engine pr
  74. var size = Vector2i.Zero;
  75. foreach (var layer in spriteComp.AllLayers)
  76. {
  77. if (!layer.Visible)
  78. continue;
  79. size = Vector2i.ComponentMax(size, layer.PixelSize);
  80. }
  81. // Stop asserts
  82. if (size.Equals(Vector2i.Zero))
  83. return;
  84. var texture = _clyde.CreateRenderTarget(new Vector2i(size.X, size.Y), new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "export");
  85. var tcs = new TaskCompletionSource(cancelToken);
  86. _control.QueuedTextures.Enqueue((texture, direction, entity, includeId, tcs));
  87. await tcs.Task;
  88. }
  89. private void GetVerbs(GetVerbsEvent<Verb> ev)
  90. {
  91. if (!_adminManager.IsAdmin())
  92. return;
  93. var target = ev.Target;
  94. Verb verb = new()
  95. {
  96. Text = Loc.GetString("export-entity-verb-get-data-text"),
  97. Category = VerbCategory.Debug,
  98. Act = async () =>
  99. {
  100. try
  101. {
  102. await Export(target);
  103. }
  104. catch (Exception e)
  105. {
  106. _runtimeLog.LogException(e, $"{nameof(ContentSpriteSystem)}.{nameof(Export)}");
  107. }
  108. },
  109. };
  110. ev.Verbs.Add(verb);
  111. }
  112. /// <summary>
  113. /// This is horrible. I asked PJB if there's an easy way to render straight to a texture outside of the render loop
  114. /// and she also mentioned this as a bad possibility.
  115. /// </summary>
  116. private sealed class ContentSpriteControl : Control
  117. {
  118. [Dependency] private readonly IEntityManager _entManager = default!;
  119. [Dependency] private readonly ILogManager _logMan = default!;
  120. [Dependency] private readonly IResourceManager _resManager = default!;
  121. internal Queue<(
  122. IRenderTexture Texture,
  123. Direction Direction,
  124. EntityUid Entity,
  125. bool IncludeId,
  126. TaskCompletionSource Tcs)> QueuedTextures = new();
  127. private ISawmill _sawmill;
  128. public ContentSpriteControl()
  129. {
  130. IoCManager.InjectDependencies(this);
  131. _sawmill = _logMan.GetSawmill("sprite.export");
  132. }
  133. protected override void Draw(DrawingHandleScreen handle)
  134. {
  135. base.Draw(handle);
  136. while (QueuedTextures.TryDequeue(out var queued))
  137. {
  138. if (queued.Tcs.Task.IsCanceled)
  139. continue;
  140. try
  141. {
  142. if (!_entManager.TryGetComponent(queued.Entity, out MetaDataComponent? metadata))
  143. continue;
  144. var filename = metadata.EntityName;
  145. var result = queued;
  146. handle.RenderInRenderTarget(queued.Texture, () =>
  147. {
  148. handle.DrawEntity(result.Entity, result.Texture.Size / 2, Vector2.One, Angle.Zero,
  149. overrideDirection: result.Direction);
  150. }, Color.Transparent);
  151. ResPath fullFileName;
  152. if (queued.IncludeId)
  153. {
  154. fullFileName = Exports / $"{filename}-{queued.Direction}-{queued.Entity}.png";
  155. }
  156. else
  157. {
  158. fullFileName = Exports / $"{filename}-{queued.Direction}.png";
  159. }
  160. queued.Texture.CopyPixelsToMemory<Rgba32>(image =>
  161. {
  162. if (_resManager.UserData.Exists(fullFileName))
  163. {
  164. _sawmill.Info($"Found existing file {fullFileName} to replace.");
  165. _resManager.UserData.Delete(fullFileName);
  166. }
  167. using var file =
  168. _resManager.UserData.Open(fullFileName, FileMode.CreateNew, FileAccess.Write,
  169. FileShare.None);
  170. image.SaveAsPng(file);
  171. });
  172. _sawmill.Info($"Saved screenshot to {fullFileName}");
  173. queued.Tcs.SetResult();
  174. }
  175. catch (Exception exc)
  176. {
  177. queued.Texture.Dispose();
  178. if (!string.IsNullOrEmpty(exc.StackTrace))
  179. _sawmill.Fatal(exc.StackTrace);
  180. queued.Tcs.SetException(exc);
  181. }
  182. }
  183. }
  184. }
  185. }