1
0

ClickMapManager.cs 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. using System.Text;
  2. using Robust.Client.Graphics;
  3. using Robust.Client.ResourceManagement;
  4. using Robust.Client.Utility;
  5. using Robust.Shared.Graphics;
  6. using Robust.Shared.Graphics.RSI;
  7. using SixLabors.ImageSharp;
  8. using SixLabors.ImageSharp.PixelFormats;
  9. namespace Content.Client.Clickable
  10. {
  11. internal sealed class ClickMapManager : IClickMapManager, IPostInjectInit
  12. {
  13. private static readonly string[] IgnoreTexturePaths =
  14. {
  15. // These will probably never need click maps so skip em.
  16. "/Textures/Interface",
  17. "/Textures/LobbyScreens",
  18. "/Textures/Parallaxes",
  19. "/Textures/Logo",
  20. };
  21. private const float Threshold = 0.1f;
  22. private const int ClickRadius = 2;
  23. [Dependency] private readonly IResourceCache _resourceCache = default!;
  24. [ViewVariables]
  25. private readonly Dictionary<Texture, ClickMap> _textureMaps = new();
  26. [ViewVariables] private readonly Dictionary<RSI, RsiClickMapData> _rsiMaps =
  27. new();
  28. public void PostInject()
  29. {
  30. _resourceCache.OnRawTextureLoaded += OnRawTextureLoaded;
  31. _resourceCache.OnRsiLoaded += OnOnRsiLoaded;
  32. }
  33. private void OnOnRsiLoaded(RsiLoadedEventArgs obj)
  34. {
  35. if (obj.Atlas is Image<Rgba32> rgba)
  36. {
  37. var clickMap = ClickMap.FromImage(rgba, Threshold);
  38. var rsiData = new RsiClickMapData(clickMap, obj.AtlasOffsets);
  39. _rsiMaps[obj.Resource.RSI] = rsiData;
  40. }
  41. }
  42. private void OnRawTextureLoaded(TextureLoadedEventArgs obj)
  43. {
  44. if (obj.Image is Image<Rgba32> rgba)
  45. {
  46. var pathStr = obj.Path.ToString();
  47. foreach (var path in IgnoreTexturePaths)
  48. {
  49. if (pathStr.StartsWith(path, StringComparison.Ordinal))
  50. return;
  51. }
  52. _textureMaps[obj.Resource] = ClickMap.FromImage(rgba, Threshold);
  53. }
  54. }
  55. public bool IsOccluding(Texture texture, Vector2i pos)
  56. {
  57. if (!_textureMaps.TryGetValue(texture, out var clickMap))
  58. {
  59. return false;
  60. }
  61. return SampleClickMap(clickMap, pos, clickMap.Size, Vector2i.Zero);
  62. }
  63. public bool IsOccluding(RSI rsi, RSI.StateId state, RsiDirection dir, int frame, Vector2i pos)
  64. {
  65. if (!_rsiMaps.TryGetValue(rsi, out var rsiData))
  66. {
  67. return false;
  68. }
  69. if (!rsiData.Offsets.TryGetValue(state, out var stateDat) || stateDat.Length <= (int) dir)
  70. {
  71. return false;
  72. }
  73. var dirDat = stateDat[(int) dir];
  74. if (dirDat.Length <= frame)
  75. {
  76. return false;
  77. }
  78. var offset = dirDat[frame];
  79. return SampleClickMap(rsiData.ClickMap, pos, rsi.Size, offset);
  80. }
  81. private static bool SampleClickMap(ClickMap map, Vector2i pos, Vector2i bounds, Vector2i offset)
  82. {
  83. var (width, height) = bounds;
  84. var (px, py) = pos;
  85. for (var x = -ClickRadius; x <= ClickRadius; x++)
  86. {
  87. var ox = px + x;
  88. if (ox < 0 || ox >= width)
  89. {
  90. continue;
  91. }
  92. for (var y = -ClickRadius; y <= ClickRadius; y++)
  93. {
  94. var oy = py + y;
  95. if (oy < 0 || oy >= height)
  96. {
  97. continue;
  98. }
  99. if (map.IsOccluded((ox, oy) + offset))
  100. {
  101. return true;
  102. }
  103. }
  104. }
  105. return false;
  106. }
  107. private sealed class RsiClickMapData
  108. {
  109. public readonly ClickMap ClickMap;
  110. public readonly Dictionary<RSI.StateId, Vector2i[][]> Offsets;
  111. public RsiClickMapData(ClickMap clickMap, Dictionary<RSI.StateId, Vector2i[][]> offsets)
  112. {
  113. ClickMap = clickMap;
  114. Offsets = offsets;
  115. }
  116. }
  117. internal sealed class ClickMap
  118. {
  119. [ViewVariables] private readonly byte[] _data;
  120. public int Width { get; }
  121. public int Height { get; }
  122. [ViewVariables] public Vector2i Size => (Width, Height);
  123. public bool IsOccluded(int x, int y)
  124. {
  125. var i = y * Width + x;
  126. return (_data[i / 8] & (1 << (i % 8))) != 0;
  127. }
  128. public bool IsOccluded(Vector2i vector)
  129. {
  130. var (x, y) = vector;
  131. return IsOccluded(x, y);
  132. }
  133. private ClickMap(byte[] data, int width, int height)
  134. {
  135. Width = width;
  136. Height = height;
  137. _data = data;
  138. }
  139. public static ClickMap FromImage<T>(Image<T> image, float threshold) where T : unmanaged, IPixel<T>
  140. {
  141. var threshByte = (byte) (threshold * 255);
  142. var width = image.Width;
  143. var height = image.Height;
  144. var dataSize = (int) Math.Ceiling(width * height / 8f);
  145. var data = new byte[dataSize];
  146. var pixelSpan = image.GetPixelSpan();
  147. for (var i = 0; i < pixelSpan.Length; i++)
  148. {
  149. Rgba32 rgba = default;
  150. pixelSpan[i].ToRgba32(ref rgba);
  151. if (rgba.A >= threshByte)
  152. {
  153. data[i / 8] |= (byte) (1 << (i % 8));
  154. }
  155. }
  156. return new ClickMap(data, width, height);
  157. }
  158. public string DumpText()
  159. {
  160. var sb = new StringBuilder();
  161. for (var y = 0; y < Height; y++)
  162. {
  163. for (var x = 0; x < Width; x++)
  164. {
  165. sb.Append(IsOccluded(x, y) ? "1" : "0");
  166. }
  167. sb.AppendLine();
  168. }
  169. return sb.ToString();
  170. }
  171. }
  172. }
  173. public interface IClickMapManager
  174. {
  175. public bool IsOccluding(Texture texture, Vector2i pos);
  176. public bool IsOccluding(RSI rsi, RSI.StateId state, RsiDirection dir, int frame, Vector2i pos);
  177. }
  178. }