Magically import images and more into Gamecraft as blocks
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.

231 lines
9.6KB

  1. using System;
  2. using System.Diagnostics;
  3. using System.IO;
  4. using System.Text;
  5. using System.Security.Cryptography;
  6. using UnityEngine;
  7. using Unity.Mathematics;
  8. using Svelto.ECS.Experimental;
  9. using Svelto.ECS;
  10. using GamecraftModdingAPI.Blocks;
  11. using GamecraftModdingAPI.Commands;
  12. using GamecraftModdingAPI.Players;
  13. using GamecraftModdingAPI.Utility;
  14. using GamecraftModdingAPI;
  15. using Pixi.Common;
  16. namespace Pixi.Images
  17. {
  18. public static class ImageCommands
  19. {
  20. public const uint PIXEL_WARNING_THRESHOLD = 25_000;
  21. // hash length to display after Pixi in text block id field
  22. public const uint HASH_LENGTH = 6;
  23. private static double blockSize = 0.2;
  24. private static uint thiccness = 1;
  25. public static float3 Rotation = float3.zero;
  26. public static void CreateThiccCommand()
  27. {
  28. CommandBuilder.Builder()
  29. .Name("PixiThicc")
  30. .Description("Set the image thickness for Pixi2D. Use this if you'd like add depth to a 2D image after importing.")
  31. .Action<int>((d) => {
  32. if (d > 0)
  33. {
  34. thiccness = (uint)d;
  35. }
  36. else Logging.CommandLogError("");
  37. })
  38. .Build();
  39. }
  40. public static void CreateImportCommand()
  41. {
  42. CommandBuilder.Builder()
  43. .Name("Pixi2D")
  44. .Description("Converts an image to blocks. Larger images will freeze your game until conversion completes.")
  45. .Action<string>(Pixelate2DFile)
  46. .Build();
  47. }
  48. public static void CreateTextCommand()
  49. {
  50. CommandBuilder.Builder()
  51. .Name("PixiText")
  52. .Description("Converts an image to coloured text in a new text block. Larger images may cause save issues.")
  53. .Action<string>(Pixelate2DFileToTextBlock)
  54. .Build();
  55. }
  56. public static void CreateTextConsoleCommand()
  57. {
  58. CommandBuilder.Builder()
  59. .Name("PixiConsole")
  60. .Description("Converts an image to a ChangeTextBlockCommand in a new console block. The first parameter is the image filepath and the second parameter is the text block id. Larger images may cause save issues.")
  61. .Action<string, string>(Pixelate2DFileToCommand)
  62. .Build();
  63. }
  64. public static void Pixelate2DFile(string filepath)
  65. {
  66. // Load image file and convert to Gamecraft blocks
  67. Texture2D img = new Texture2D(64, 64);
  68. // load file into texture
  69. try
  70. {
  71. byte[] imgData = File.ReadAllBytes(filepath);
  72. img.LoadImage(imgData);
  73. }
  74. catch (Exception e)
  75. {
  76. Logging.CommandLogError($"Failed to load picture data. Reason: {e.Message}");
  77. Logging.MetaLog(e.Message + "\n" + e.StackTrace);
  78. return;
  79. }
  80. Logging.CommandLog($"Image size: {img.width}x{img.height}");
  81. Player p = new Player(PlayerType.Local);
  82. float3 position = p.Position;
  83. BlockIDs pickedBlock = p.SelectedBlock == BlockIDs.Invalid ? BlockIDs.AluminiumCube : p.SelectedBlock;
  84. uint blockCount = 0;
  85. Quaternion imgRotation = Quaternion.Euler(Rotation);
  86. position += (float3)(imgRotation * new float3(1f, (float)blockSize, 0f));
  87. float3 basePosition = position;
  88. Stopwatch timer = Stopwatch.StartNew();
  89. // convert the image to blocks
  90. // this groups same-colored pixels in the same column into a single block to reduce the block count
  91. // any further pixel-grouping optimisations (eg 2D grouping) risk increasing conversion time higher than O(x*y)
  92. for (int x = 0; x < img.width; x++)
  93. {
  94. BlockInfo qVoxel = new BlockInfo
  95. {
  96. block = BlockIDs.AbsoluteMathsBlock, // impossible canvas block
  97. color = BlockColors.Default,
  98. darkness = 10,
  99. visible = false,
  100. };
  101. float3 scale = new float3(1, 1, thiccness);
  102. //position.x += (float)(blockSize);
  103. for (int y = 0; y < img.height; y++)
  104. {
  105. //position.y += (float)blockSize;
  106. Color pixel = img.GetPixel(x, y);
  107. BlockInfo qPixel = PixelUtility.QuantizePixel(pixel);
  108. if (qPixel.darkness != qVoxel.darkness
  109. || qPixel.color != qVoxel.color
  110. || qPixel.visible != qVoxel.visible
  111. || qPixel.block != qVoxel.block)
  112. {
  113. if (y != 0)
  114. {
  115. if (qVoxel.visible)
  116. {
  117. position = basePosition + (float3)(imgRotation * (new float3(0,1,0) * (float)((y * blockSize + (y - scale.y) * blockSize) / 2) + new float3(1, 0, 0) * (float)(x * blockSize)));
  118. BlockIDs blockType = qVoxel.block == BlockIDs.AluminiumCube ? pickedBlock : qVoxel.block;
  119. Block.PlaceNew(blockType, position, rotation: Rotation,color: qVoxel.color, darkness: qVoxel.darkness, scale: scale);
  120. blockCount++;
  121. }
  122. scale = new float3(1, 1, thiccness);
  123. }
  124. qVoxel = qPixel;
  125. }
  126. else
  127. {
  128. scale.y += 1;
  129. }
  130. }
  131. if (qVoxel.visible)
  132. {
  133. position = basePosition + (float3)(imgRotation * (new float3(0, 1, 0) * (float)((img.height * blockSize + (img.height - scale.y) * blockSize) / 2) + new float3(1, 0, 0) * (float)(x * blockSize)));
  134. BlockIDs blockType = qVoxel.block == BlockIDs.AluminiumCube ? pickedBlock : qVoxel.block;
  135. Block.PlaceNew(blockType, position, rotation: Rotation, color: qVoxel.color, darkness: qVoxel.darkness, scale: scale);
  136. blockCount++;
  137. }
  138. //position.y = zero_y;
  139. }
  140. timer.Stop();
  141. Logging.CommandLog($"Placed {img.width}x{img.height} image beside you ({blockCount} blocks total, {blockCount * 100 / (img.width * img.height)}%)");
  142. Logging.MetaLog($"Placed {blockCount} in {timer.ElapsedMilliseconds}ms (saved {(img.width * img.height) - blockCount} blocks -- {blockCount * 100 / (img.width * img.height)}% original size) for {filepath}");
  143. }
  144. public static void Pixelate2DFileToTextBlock(string filepath)
  145. {
  146. // Thanks to TheGreenGoblin for the idea (and the working Python implementation for reference)
  147. // Load image file and convert to Gamecraft blocks
  148. Texture2D img = new Texture2D(64, 64);
  149. // load file into texture
  150. try
  151. {
  152. byte[] imgData = File.ReadAllBytes(filepath);
  153. img.LoadImage(imgData);
  154. }
  155. catch (Exception e)
  156. {
  157. Logging.CommandLogError($"Failed to load picture data. Reason: {e.Message}");
  158. Logging.MetaLog(e.Message + "\n" + e.StackTrace);
  159. return;
  160. }
  161. float3 position = new Player(PlayerType.Local).Position;
  162. position.x += 1f;
  163. position.y += (float)blockSize;
  164. Stopwatch timer = Stopwatch.StartNew();
  165. string text = PixelUtility.TextureToString(img);
  166. TextBlock textBlock = TextBlock.PlaceNew(position, scale: new float3(Mathf.Ceil(img.width / 16), 1, Mathf.Ceil(img.height / 16)));
  167. textBlock.Text = text;
  168. byte[] textHash;
  169. using (HashAlgorithm hasher = SHA256.Create())
  170. textHash = hasher.ComputeHash(Encoding.UTF8.GetBytes(text));
  171. string textId = "Pixi_";
  172. // every byte converts to 2 hexadecimal characters so hash length needs to be halved
  173. for (int i = 0; i < HASH_LENGTH/2 && i < textHash.Length; i++)
  174. {
  175. textId += textHash[i].ToString("X2");
  176. }
  177. textBlock.TextBlockId = textId;
  178. timer.Stop();
  179. Logging.CommandLog($"Placed {img.width}x{img.height} image in text block named {textId} beside you ({text.Length} characters)");
  180. Logging.MetaLog($"Completed image text block {textId} synthesis in {timer.ElapsedMilliseconds}ms containing {text.Length} characters for {img.width*img.height} pixels");
  181. }
  182. public static void Pixelate2DFileToCommand(string filepath, string textBlockId)
  183. {
  184. // Thanks to Nullpersonan for the idea
  185. // Load image file and convert to Gamecraft blocks
  186. Texture2D img = new Texture2D(64, 64);
  187. // load file into texture
  188. try
  189. {
  190. byte[] imgData = File.ReadAllBytes(filepath);
  191. img.LoadImage(imgData);
  192. }
  193. catch (Exception e)
  194. {
  195. Logging.CommandLogError($"Failed to load picture data. Reason: {e.Message}");
  196. Logging.MetaLog(e.Message + "\n" + e.StackTrace);
  197. return;
  198. }
  199. float3 position = new Player(PlayerType.Local).Position;
  200. position.x += 1f;
  201. position.y += (float)blockSize;
  202. Stopwatch timer = Stopwatch.StartNew();
  203. float zero_y = position.y;
  204. string text = PixelUtility.TextureToString(img); // conversion
  205. ConsoleBlock console = ConsoleBlock.PlaceNew(position);
  206. // set console's command
  207. console.Command = "ChangeTextBlockCommand";
  208. console.Arg1 = textBlockId;
  209. console.Arg2 = text;
  210. console.Arg3 = "";
  211. Logging.CommandLog($"Placed {img.width}x{img.height} image in console block beside you ({text.Length} characters)");
  212. Logging.MetaLog($"Completed image console block {textBlockId} synthesis in {timer.ElapsedMilliseconds}ms containing {text.Length} characters for {img.width * img.height} pixels");
  213. }
  214. }
  215. }