Browse Source

Refactor Pixi import system as per issue #5

refactor
NGnius (Graham) 3 years ago
parent
commit
9c38200dd7
20 changed files with 1233 additions and 490 deletions
  1. +0
    -17
      Pixi/Common/BlockInfo.cs
  2. +25
    -0
      Pixi/Common/BlockJsonInfo.cs
  3. +9
    -0
      Pixi/Common/BlueprintProvider.cs
  4. +65
    -0
      Pixi/Common/BlueprintUtility.cs
  5. +444
    -0
      Pixi/Common/CommandRoot.cs
  6. +46
    -0
      Pixi/Common/ConversionUtility.cs
  7. +27
    -0
      Pixi/Common/Importer.cs
  8. +40
    -0
      Pixi/Common/ProcessedVoxelObjectNotation.cs
  9. +94
    -0
      Pixi/Images/ImageCanvasImporter.cs
  10. +83
    -0
      Pixi/Images/ImageCommandImporter.cs
  11. +0
    -230
      Pixi/Images/ImageCommands.cs
  12. +97
    -0
      Pixi/Images/ImageTextBlockImporter.cs
  13. +1
    -21
      Pixi/Images/PixelUtility.cs
  14. +1
    -1
      Pixi/Pixi.csproj
  15. +10
    -10
      Pixi/PixiPlugin.cs
  16. +19
    -3
      Pixi/Robots/CubeUtility.cs
  17. +100
    -0
      Pixi/Robots/RobotBlueprintProvider.cs
  18. +1
    -176
      Pixi/Robots/RobotCommands.cs
  19. +147
    -0
      Pixi/Robots/RobotInternetImporter.cs
  20. +24
    -32
      README.md

+ 0
- 17
Pixi/Common/BlockInfo.cs View File

@@ -1,17 +0,0 @@
using System;

using GamecraftModdingAPI.Blocks;

namespace Pixi.Common
{
public struct BlockInfo
{
public BlockIDs block;

public BlockColors color;

public byte darkness;

public bool visible;
}
}

+ 25
- 0
Pixi/Common/BlockJsonInfo.cs View File

@@ -1,4 +1,9 @@
using System;

using Unity.Mathematics;

using GamecraftModdingAPI.Blocks;

namespace Pixi.Common
{
public struct BlockJsonInfo
@@ -12,5 +17,25 @@ namespace Pixi.Common
public float[] color;

public float[] scale;

internal ProcessedVoxelObjectNotation Process()
{
BlockIDs block = ConversionUtility.BlockIDsToEnum(name);
return new ProcessedVoxelObjectNotation
{
block = block,
blueprint = block == BlockIDs.Invalid,
color = ColorSpaceUtility.QuantizeToBlockColor(color),
metadata = name,
position = ConversionUtility.FloatArrayToFloat3(position),
rotation = ConversionUtility.FloatArrayToFloat3(rotation),
scale = ConversionUtility.FloatArrayToFloat3(scale),
};
}

public override string ToString()
{
return $"BlockJsonInfo {{ name:{name}, color:(r{color[0]},g{color[1]},b{color[2]}), position:({position[0]},{position[1]},{position[2]}), rotation:({rotation[0]},{rotation[1]},{rotation[2]}), scale:({scale[0]},{scale[1]},{scale[2]})}}";
}
}
}

+ 9
- 0
Pixi/Common/BlueprintProvider.cs View File

@@ -0,0 +1,9 @@
namespace Pixi.Common
{
public interface BlueprintProvider
{
string Name { get; }
BlockJsonInfo[] Blueprint(string name, BlockJsonInfo root);
}
}

+ 65
- 0
Pixi/Common/BlueprintUtility.cs View File

@@ -0,0 +1,65 @@
using System;
using System.IO;
using System.Collections.Generic;
using System.Reflection;

using Newtonsoft.Json;

namespace Pixi.Common
{
public static class BlueprintUtility
{
public static Dictionary<string, BlockJsonInfo[]> ParseBlueprintFile(string name)
{
StreamReader bluemap = new StreamReader(File.OpenRead(name));
return JsonConvert.DeserializeObject<Dictionary<string, BlockJsonInfo[]>>(bluemap.ReadToEnd());
}
public static Dictionary<string, BlockJsonInfo[]> ParseBlueprintResource(string name)
{
StreamReader bluemap = new StreamReader(Assembly.GetExecutingAssembly().GetManifestResourceStream(name));
return JsonConvert.DeserializeObject<Dictionary<string, BlockJsonInfo[]>>(bluemap.ReadToEnd());
}

public static ProcessedVoxelObjectNotation[][] ProcessAndExpandBlocks(string name, BlockJsonInfo[] blocks, BlueprintProvider blueprints)
{
List<ProcessedVoxelObjectNotation[]> expandedBlocks = new List<ProcessedVoxelObjectNotation[]>();
for (int i = 0; i < blocks.Length; i++)
{
ProcessedVoxelObjectNotation root = blocks[i].Process();
if (root.blueprint)
{
if (blueprints == null)
{
throw new NullReferenceException("Blueprint block info found but BlueprintProvider is null");
}

BlockJsonInfo[] blueprint = blueprints.Blueprint(name, blocks[i]);
ProcessedVoxelObjectNotation[] expanded = new ProcessedVoxelObjectNotation[blueprint.Length];
for (int j = 0; j < expanded.Length; j++)
{
expanded[j] = blueprint[j].Process();
}

expandedBlocks.Add(expanded);
}
else
{
expandedBlocks.Add(new ProcessedVoxelObjectNotation[]{root});
}
}
return expandedBlocks.ToArray();
}

public static ProcessedVoxelObjectNotation[] ProcessBlocks(BlockJsonInfo[] blocks)
{
ProcessedVoxelObjectNotation[] procBlocks = new ProcessedVoxelObjectNotation[blocks.Length];
for (int i = 0; i < blocks.Length; i++)
{
procBlocks[i] = blocks[i].Process();
}

return procBlocks;
}
}
}

+ 444
- 0
Pixi/Common/CommandRoot.cs View File

@@ -0,0 +1,444 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using UnityEngine;
using Unity.Mathematics;
using Svelto.ECS;

using GamecraftModdingAPI;
using GamecraftModdingAPI.Blocks;
using GamecraftModdingAPI.Commands;
using GamecraftModdingAPI.Utility;

namespace Pixi.Common
{
/// <summary>
/// Command implementation.
/// CommandRoot.Pixi is the root of all Pixi calls from the CLI
/// </summary>
public class CommandRoot : ICustomCommandEngine
{
public void Ready()
{
CommandRegistrationHelper.Register<string>(Name, (name) => tryOrCommandLogError(() => this.Pixi(null, name)), Description);
CommandRegistrationHelper.Register<string, string>(Name+"2", this.Pixi, "Import something into Gamecraft using magic. Usage: Pixi \"importer\" \"myfile.png\"");
}

public EntitiesDB entitiesDB { get; set; }

public void Dispose()
{
CommandRegistrationHelper.Unregister(Name);
CommandRegistrationHelper.Unregister(Name+"2");
}

public string Name { get; } = "Pixi";

public bool isRemovable { get; } = false;

public string Description { get; } = "Import something into Gamecraft using magic. Usage: Pixi \"myfile.png\"";
public Dictionary<int, Importer[]> importers = new Dictionary<int, Importer[]>();

public const float BLOCK_SIZE = 0.2f;
public const float DELTA = BLOCK_SIZE / 2048;

public static int OPTIMISATION_PASSES = 2;

public CommandRoot()
{
CommandManager.AddCommand(this);
}

public void Inject(Importer imp)
{
if (importers.ContainsKey(imp.Priority))
{
// extend array by 1 and place imp at the end
Importer[] oldArr = importers[imp.Priority];
Importer[] newArr = new Importer[oldArr.Length + 1];
for (int i = 0; i < oldArr.Length; i++)
{
newArr[i] = oldArr[i];
}
newArr[oldArr.Length] = imp;
importers[imp.Priority] = newArr;
}
else
{
importers[imp.Priority] = new Importer[] {imp};
}
}

private void Pixi(string importerName, string name)
{
// organise priorities
int[] priorities = importers.Keys.ToArray();
Array.Sort(priorities);
Array.Reverse(priorities); // higher priorities go first
// find relevant importer
Importer magicImporter = null;
foreach (int p in priorities)
{
Importer[] imps = importers[p];
for (int i = 0; i < imps.Length; i++)
{
//Logging.MetaLog($"Now checking importer {imps[i].Name}");
if ((importerName == null && imps[i].Qualifies(name))
|| (importerName != null && imps[i].Name.Contains(importerName)))
{
magicImporter = imps[i];
break;
}
}
if (magicImporter != null) break;
}

if (magicImporter == null)
{
Logging.CommandLogError("Unsupported file or string.");
return;
}
#if DEBUG
Logging.MetaLog($"Using '{magicImporter.Name}' to import '{name}'");
#endif
// import blocks
BlockJsonInfo[] blocksInfo = magicImporter.Import(name);
if (blocksInfo.Length == 0)
{
#if DEBUG
Logging.CommandLogError($"Importer {magicImporter.Name} didn't provide any blocks to import. Mission Aborted!");
#endif
return;
}

ProcessedVoxelObjectNotation[][] procVONs;
BlueprintProvider blueprintProvider = magicImporter.BlueprintProvider;
if (blueprintProvider == null)
{
// convert block info to API-compatible format
procVONs = new ProcessedVoxelObjectNotation[][] {BlueprintUtility.ProcessBlocks(blocksInfo)};
}
else
{
// expand blueprints and convert block info
procVONs = BlueprintUtility.ProcessAndExpandBlocks(name, blocksInfo, magicImporter.BlueprintProvider);
}
// reduce block placements by grouping neighbouring similar blocks
// (after flattening block data representation)
List<ProcessedVoxelObjectNotation> optVONs = new List<ProcessedVoxelObjectNotation>();
for (int arr = 0; arr < procVONs.Length; arr++)
{
for (int elem = 0; elem < procVONs[arr].Length; elem++)
{
optVONs.Add(procVONs[arr][elem]);
}
}
#if DEBUG
Logging.MetaLog($"Imported {optVONs.Count} blocks for '{name}'");
#endif
int blockCountPreOptimisation = optVONs.Count;
if (magicImporter.Optimisable)
{
for (int pass = 0; pass < OPTIMISATION_PASSES; pass++)
{
OptimiseBlocks(ref optVONs);
#if DEBUG
Logging.MetaLog($"Optimisation pass {pass} completed");
#endif
}
#if DEBUG
Logging.MetaLog($"Optimised down to {optVONs.Count} blocks for '{name}'");
#endif
}
ProcessedVoxelObjectNotation[] optVONsArr = optVONs.ToArray();
magicImporter.PreProcess(name, ref optVONsArr);
// place blocks
Block[] blocks = new Block[optVONsArr.Length];
for (int i = 0; i < optVONsArr.Length; i++)
{
ProcessedVoxelObjectNotation desc = optVONsArr[i];
if (desc.block != BlockIDs.Invalid)
{
Block b = Block.PlaceNew(desc.block, desc.position, desc.rotation, desc.color.Color,
desc.color.Darkness, 1, desc.scale);
blocks[i] = b;
}
}
magicImporter.PostProcess(name, ref blocks);
if (magicImporter.Optimisable && blockCountPreOptimisation > blocks.Length)
{
Logging.CommandLog($"Imported {blocks.Length} blocks using {magicImporter.Name} ({blockCountPreOptimisation/blocks.Length}x ratio)");
}
else
{
Logging.CommandLog($"Imported {blocks.Length} blocks using {magicImporter.Name}");
}
}

private void OptimiseBlocks(ref List<ProcessedVoxelObjectNotation> optVONs)
{
// a really complicated algorithm to determine if two similar blocks are touching (before they're placed)
// the general concept:
// two blocks are touching when they have a common face (equal to 4 corners on the cube, where the 4 corners aren't completely opposite each other)
// between the two blocks, the 8 corners that aren't in common are the corners for the merged block
//
// to merge the 2 blocks, switch out the 4 common corners of one block with the nearest non-common corners from the other block
// i.e. swap the common face on block A with the face opposite the common face of block B
// to prevent a nonsensical face (rotated compared to other faces), the corners of the face should be swapped out with the corresponding corner which shares an edge
//
// note: e.g. if common face on block A is its top, the common face of block B is not necessarily the bottom face because blocks can be rotated differently
// this means it's not safe to assume that block A's common face (top) can be swapped with block B's non-common opposite face (top) to get the merged block
//
// note2: this does not work with blocks which aren't cubes (i.e. any block where rotation matters)
// TODO multithread this expensive operation
int item = 0;
while (item < optVONs.Count)
{
bool isItemUpdated = false;
ProcessedVoxelObjectNotation itemVON = optVONs[item];
if (isOptimisableBlock(itemVON.block))
{
float3[] itemCorners = calculateCorners(itemVON);
int seeker = item + 1; // despite this, assume that seeker goes thru the entire list (not just blocks after item)
while (seeker < optVONs.Count)
{
if (seeker == item)
{
seeker++;
}
else
{
ProcessedVoxelObjectNotation seekerVON = optVONs[seeker];
//Logging.MetaLog($"Comparing {itemVON} and {seekerVON}");
float3[] seekerCorners = calculateCorners(seekerVON);
int[][] mapping = findMatchingCorners(itemCorners, seekerCorners);
if (mapping.Length != 0
&& itemVON.block == seekerVON.block
&& itemVON.color.Color == seekerVON.color.Color
&& itemVON.color.Darkness == seekerVON.color.Darkness
&& isOptimisableBlock(seekerVON.block)) // match found
{
// switch out corners based on mapping
//Logging.MetaLog($"Corners {float3ArrToString(itemCorners)}\nand {float3ArrToString(seekerCorners)}");
//Logging.MetaLog($"Mappings (len:{mapping[0].Length}) {mapping[0][0]} -> {mapping[1][0]}\n{mapping[0][1]} -> {mapping[1][1]}\n{mapping[0][2]} -> {mapping[1][2]}\n{mapping[0][3]} -> {mapping[1][3]}\n");
for (byte i = 0; i < 4; i++)
{
itemCorners[mapping[0][i]] = seekerCorners[mapping[1][i]];
}
// remove 2nd block, since it's now part of the 1st block
//Logging.MetaLog($"Removing {seekerVON}");
optVONs.RemoveAt(seeker);
if (seeker < item)
{
item--; // note: this will never become less than 0
}
isItemUpdated = true;
// regenerate info
//Logging.MetaLog($"Final corners {float3ArrToString(itemCorners)}");
updateVonFromCorners(itemCorners, ref itemVON);
itemCorners = calculateCorners(itemVON);
//Logging.MetaLog($"Merged block is {itemVON}");
}
else
{
seeker++;
}
}
}

if (isItemUpdated)
{
optVONs[item] = itemVON;
//Logging.MetaLog($"Optimised block is now {itemVON}");
}
item++;
}
else
{
item++;
}
}
}

private float3[] calculateCorners(ProcessedVoxelObjectNotation von)
{
float3[] cornerMultiplicands = new float3[8]
{
new float3(1, 1, 1),
new float3(1, 1, -1),
new float3(-1, 1, 1),
new float3(-1, 1, -1),
new float3(-1, -1, 1),
new float3(-1, -1, -1),
new float3(1, -1, 1),
new float3(1, -1, -1),
};
float3[] corners = new float3[8];
Quaternion rotation = Quaternion.Euler(von.rotation);
float3 rotatedScale = rotation * von.scale;
float3 trueCenter = von.position;
// generate corners
for (int i = 0; i < corners.Length; i++)
{
corners[i] = trueCenter + BLOCK_SIZE * (cornerMultiplicands[i] * rotatedScale / 2);
}
return corners;
}

private void updateVonFromCorners(float3[] corners, ref ProcessedVoxelObjectNotation von)
{
float3[] cornerMultiplicands = new float3[8]
{
new float3(1, 1, 1),
new float3(1, 1, -1),
new float3(1, -1, 1),
new float3(1, -1, -1),
new float3(-1, 1, 1),
new float3(-1, 1, -1),
new float3(-1, -1, 1),
new float3(-1, -1, -1),
};
float3 newCenter = sumOfFloat3Arr(corners) / corners.Length;
float3 newPosition = newCenter;
Quaternion rot = Quaternion.Euler(von.rotation);
float3 rotatedScale = 2 * (corners[0] - newCenter) / BLOCK_SIZE;
von.scale = Quaternion.Inverse(rot) * rotatedScale;
von.position = newPosition;
//Logging.MetaLog($"Updated VON scale {von.scale} (absolute {rotatedScale})");
}
private int[][] findMatchingCorners(float3[] corners1, float3[] corners2)
{
int[][] cornerFaceMappings = new int[][]
{
new int[] {0, 1, 2, 3}, // top
new int[] {2, 3, 4, 5}, // left
new int[] {4, 5, 6, 7}, // bottom
new int[] {6, 7, 0, 1}, // right
new int[] {0, 2, 4, 6}, // back
new int[] {1, 3, 5, 7}, // front
};
int[][] oppositeFaceMappings = new int[][]
{
new int[] {6, 7, 4, 5}, // bottom
new int[] {0, 1, 6, 7}, // right
new int[] {2, 3, 0, 1}, // top
new int[] {4, 5, 2, 3}, // left
new int[] {1, 3, 5, 7}, // front
new int[] {0, 2, 4, 6}, // back
};
float3[][] faces1 = facesFromCorners(corners1);
float3[][] faces2 = facesFromCorners(corners2);
for (byte i = 0; i < faces1.Length; i++)
{
for (byte j = 0; j < faces2.Length; j++)
{
//Logging.MetaLog($"Checking faces {float3ArrToString(faces1[i])} and {float3ArrToString(faces2[j])}");
int[] match = matchFace(faces1[i], faces2[j]);
if (match.Length != 0)
{
//Logging.MetaLog($"Matched faces {float3ArrToString(faces1[i])} and {float3ArrToString(faces2[j])}");
// translate from face mapping to corner mapping
for (byte k = 0; k < match.Length; k++)
{
match[k] = oppositeFaceMappings[j][match[k]];
}
return new int[][] {cornerFaceMappings[i], match}; // {{itemCorners index}, {seekerCorners index}}
}
}
}
return new int[0][];
}
// this assumes the corners are in the order that calculateCorners outputs
private float3[][] facesFromCorners(float3[] corners)
{
return new float3[][]
{
new float3[] {corners[0], corners[1], corners[2], corners[3]}, // top
new float3[] {corners[2], corners[3], corners[4], corners[5]}, // left
new float3[] {corners[4], corners[5], corners[6], corners[7]}, // bottom
new float3[] {corners[6], corners[7], corners[0], corners[1]}, // right
new float3[] {corners[0], corners[2], corners[4], corners[6]}, // back
new float3[] {corners[1], corners[3], corners[5], corners[7]}, // front
};
}

private int[] matchFace(float3[] face1, float3[] face2)
{
int[] result = new int[4];
byte count = 0;
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 4; j++)
{
//Logging.MetaLog($"Comparing {face1[i]} and {face1[i]} ({Mathf.Abs(face1[i].x - face2[j].x)} & {Mathf.Abs(face1[i].y - face2[j].y)} & {Mathf.Abs(face1[i].z - face2[j].z)} vs {DELTA})");
// if (face1[i] == face2[j])
if (Mathf.Abs(face1[i].x - face2[j].x) < DELTA
&& Mathf.Abs(face1[i].y - face2[j].y) < DELTA
&& Mathf.Abs(face1[i].z - face2[j].z) < DELTA)
{
count++;
result[i] = j; // map corners to each other
break;
}
}
}
//Logging.MetaLog($"matched {count}/4");
if (count == 4)
{
return result;
}
return new int[0];
}

private float3 sumOfFloat3Arr(float3[] arr)
{
float3 total = float3.zero;
for (int i = 0; i < arr.Length; i++)
{
total += arr[i];
}

return total;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool isOptimisableBlock(BlockIDs block)
{
return block.ToString().EndsWith("Cube", StringComparison.InvariantCultureIgnoreCase);
}

private string float3ArrToString(float3[] arr)
{
string result = "[";
foreach (float3 f in arr)
{
result += f.ToString() + ", ";
}

return result.Substring(0, result.Length - 2) + "]";
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void tryOrCommandLogError(Action toTry)
{
try
{
toTry();
}
catch (Exception e)
{
#if DEBUG
Logging.CommandLogError("RIP\n" + e);
#else
Logging.CommandLogError("Pixi failed (reason: " + e.Message + ")");
#endif
}
}
}
}

+ 46
- 0
Pixi/Common/ConversionUtility.cs View File

@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;

using Unity.Mathematics;

using GamecraftModdingAPI.Blocks;

namespace Pixi.Common
{
public static class ConversionUtility
{
private static Dictionary<string, BlockIDs> blockEnumMap = null;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void loadBlockEnumMap()
{
blockEnumMap = new Dictionary<string, BlockIDs>();
foreach(BlockIDs e in Enum.GetValues(typeof(BlockIDs)))
{
blockEnumMap[e.ToString()] = e;
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static BlockIDs BlockIDsToEnum(string name)
{
if (blockEnumMap == null) loadBlockEnumMap();
if (blockEnumMap.ContainsKey(name)) return blockEnumMap[name];
return BlockIDs.Invalid;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float3 FloatArrayToFloat3(float[] vec)
{
if (vec.Length < 3) return float3.zero;
return new float3(vec[0], vec[1], vec[2]);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float[] Float3ToFloatArray(float3 vec)
{
return new float[3] {vec.x, vec.y, vec.z};
}
}
}

+ 27
- 0
Pixi/Common/Importer.cs View File

@@ -0,0 +1,27 @@
using GamecraftModdingAPI;

namespace Pixi.Common
{
/// <summary>
/// Thing importer.
/// This imports the thing by converting it to a common block format that Pixi can understand.
/// </summary>
public interface Importer
{
int Priority { get; }
bool Optimisable { get; }
string Name { get; }
BlueprintProvider BlueprintProvider { get; }
bool Qualifies(string name);

BlockJsonInfo[] Import(string name);

void PreProcess(string name, ref ProcessedVoxelObjectNotation[] blocks);

void PostProcess(string name, ref Block[] blocks);
}
}

+ 40
- 0
Pixi/Common/ProcessedVoxelObjectNotation.cs View File

@@ -0,0 +1,40 @@
using Unity.Mathematics;

using GamecraftModdingAPI.Blocks;

namespace Pixi.Common
{
public struct ProcessedVoxelObjectNotation
{
public BlockIDs block;

public BlockColor color;

public bool blueprint;
public float3 position;

public float3 rotation;

public float3 scale;

public string metadata;

internal BlockJsonInfo VoxelObjectNotation()
{
return new BlockJsonInfo
{
name = block == BlockIDs.Invalid ? metadata.Split(' ')[0] : block.ToString(),
color = ColorSpaceUtility.UnquantizeToArray(color),
position = ConversionUtility.Float3ToFloatArray(position),
rotation = ConversionUtility.Float3ToFloatArray(rotation),
scale = ConversionUtility.Float3ToFloatArray(scale),
};
}

public override string ToString()
{
return $"ProcessedVoxelObjectNotation {{ block:{block}, color:{color.Color}-{color.Darkness}, blueprint:{blueprint}, position:{position}, rotation:{rotation}, scale:{scale}}} ({metadata})";
}
}
}

+ 94
- 0
Pixi/Images/ImageCanvasImporter.cs View File

@@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using System.IO;

using Unity.Mathematics;
using UnityEngine;

using GamecraftModdingAPI;
using GamecraftModdingAPI.Blocks;
using GamecraftModdingAPI.Players;
using GamecraftModdingAPI.Utility;
using Pixi.Common;

namespace Pixi.Images
{
public class ImageCanvasImporter : Importer
{
public static float3 Rotation = float3.zero;

public static uint Thiccness = 1;
public int Priority { get; } = 1;

public bool Optimisable { get; } = true;

public string Name { get; } = "ImageCanvas~Spell";

public BlueprintProvider BlueprintProvider { get; } = null;
public bool Qualifies(string name)
{
//Logging.MetaLog($"Qualifies received name {name}");
return name.EndsWith(".png", StringComparison.InvariantCultureIgnoreCase)
|| name.EndsWith(".jpg", StringComparison.InvariantCultureIgnoreCase);
}

public BlockJsonInfo[] Import(string name)
{
// Load image file and convert to Gamecraft blocks
Texture2D img = new Texture2D(64, 64);
// load file into texture
try
{
byte[] imgData = File.ReadAllBytes(name);
img.LoadImage(imgData);
}
catch (Exception e)
{
Logging.CommandLogError($"Failed to load picture data. Reason: {e.Message}");
Logging.MetaLog(e.Message + "\n" + e.StackTrace);
return new BlockJsonInfo[0];
}
//Logging.CommandLog($"Image size: {img.width}x{img.height}");
Player p = new Player(PlayerType.Local);
string pickedBlock = p.SelectedBlock == BlockIDs.Invalid ? BlockIDs.AluminiumCube.ToString() : p.SelectedBlock.ToString();
Quaternion imgRotation = Quaternion.Euler(Rotation);

BlockJsonInfo[] blocks = new BlockJsonInfo[img.width * img.height];
// convert the image to blocks
// optimisation occurs later
for (int x = 0; x < img.width; x++)
{
for (int y = 0; y < img.height; y++)
{
Color pixel = img.GetPixel(x, y);
float3 position = (imgRotation * (new float3((x * CommandRoot.BLOCK_SIZE),y * CommandRoot.BLOCK_SIZE,0)));
BlockJsonInfo qPixel = new BlockJsonInfo
{
name = pixel.a > 0.75 ? pickedBlock : BlockIDs.GlassCube.ToString(),
color = new float[] {pixel.r, pixel.g, pixel.b},
rotation = ConversionUtility.Float3ToFloatArray(Rotation),
position = ConversionUtility.Float3ToFloatArray(position),
scale = new float[] { 1, 1, Thiccness},
};
if (pixel.a < 0.5f) qPixel.name = BlockIDs.Invalid.ToString();
blocks[(x * img.height) + y] = qPixel;
}
}
return blocks;
}

public void PreProcess(string name, ref ProcessedVoxelObjectNotation[] blocks)
{
Player p = new Player(PlayerType.Local);
float3 pos = p.Position;
for (int i = 0; i < blocks.Length; i++)
{
blocks[i].position += pos;
}
}

public void PostProcess(string name, ref Block[] blocks) { }
}
}

+ 83
- 0
Pixi/Images/ImageCommandImporter.cs View File

@@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

using Unity.Mathematics;
using UnityEngine;

using GamecraftModdingAPI;
using GamecraftModdingAPI.Blocks;
using GamecraftModdingAPI.Players;
using GamecraftModdingAPI.Utility;
using Pixi.Common;

namespace Pixi.Images
{
public class ImageCommandImporter : Importer
{
public int Priority { get; } = 0;

public bool Optimisable { get; } = false;

public string Name { get; } = "ImageConsole~Spell";

public BlueprintProvider BlueprintProvider { get; } = null;
private Dictionary<string, string> commandBlockContents = new Dictionary<string, string>();
public bool Qualifies(string name)
{
return name.EndsWith(".png", StringComparison.InvariantCultureIgnoreCase)
|| name.EndsWith(".jpg", StringComparison.InvariantCultureIgnoreCase);
}

public BlockJsonInfo[] Import(string name)
{
// Thanks to Nullpersona for the idea
// Load image file and convert to Gamecraft blocks
Texture2D img = new Texture2D(64, 64);
// load file into texture
try
{
byte[] imgData = File.ReadAllBytes(name);
img.LoadImage(imgData);
}
catch (Exception e)
{
Logging.CommandLogError($"Failed to load picture data. Reason: {e.Message}");
Logging.MetaLog(e.Message + "\n" + e.StackTrace);
return new BlockJsonInfo[0];
}
string text = PixelUtility.TextureToString(img); // conversion
// save console's command
commandBlockContents[name] = text;
return new BlockJsonInfo[]
{
new BlockJsonInfo {name = "ConsoleBlock"}
};
}

public void PreProcess(string name, ref ProcessedVoxelObjectNotation[] blocks)
{
Player p = new Player(PlayerType.Local);
float3 pos = p.Position;
for (int i = 0; i < blocks.Length; i++)
{
blocks[i].position += pos;
}
}

public void PostProcess(string name, ref Block[] blocks)
{
// populate console block
AsyncUtils.WaitForSubmission(); // just in case
ConsoleBlock cb = blocks[0].Specialise<ConsoleBlock>();
cb.Command = "ChangeTextBlockCommand";
cb.Arg1 = "TextBlockID";
cb.Arg2 = commandBlockContents[name];
cb.Arg3 = "";
commandBlockContents.Remove(name);
}
}
}

+ 0
- 230
Pixi/Images/ImageCommands.cs View File

@@ -1,230 +0,0 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Security.Cryptography;

using UnityEngine;
using Unity.Mathematics;
using Svelto.ECS.Experimental;
using Svelto.ECS;

using GamecraftModdingAPI.Blocks;
using GamecraftModdingAPI.Commands;
using GamecraftModdingAPI.Players;
using GamecraftModdingAPI.Utility;
using GamecraftModdingAPI;

using Pixi.Common;

namespace Pixi.Images
{
public static class ImageCommands
{
public const uint PIXEL_WARNING_THRESHOLD = 25_000;
// hash length to display after Pixi in text block id field
public const uint HASH_LENGTH = 6;

private static double blockSize = 0.2;

private static uint thiccness = 1;

public static float3 Rotation = float3.zero;

public static void CreateThiccCommand()
{
CommandBuilder.Builder()
.Name("PixiThicc")
.Description("Set the image thickness for Pixi2D. Use this if you'd like add depth to a 2D image after importing.")
.Action<int>((d) => {
if (d > 0)
{
thiccness = (uint)d;
}
else Logging.CommandLogError("");
})
.Build();
}

public static void CreateImportCommand()
{
CommandBuilder.Builder()
.Name("Pixi2D")
.Description("Converts an image to blocks. Larger images will freeze your game until conversion completes.")
.Action<string>(Pixelate2DFile)
.Build();
}

public static void CreateTextCommand()
{
CommandBuilder.Builder()
.Name("PixiText")
.Description("Converts an image to coloured text in a new text block. Larger images may cause save issues.")
.Action<string>(Pixelate2DFileToTextBlock)
.Build();
}

public static void CreateTextConsoleCommand()
{
CommandBuilder.Builder()
.Name("PixiConsole")
.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.")
.Action<string, string>(Pixelate2DFileToCommand)
.Build();
}

public static void Pixelate2DFile(string filepath)
{
// Load image file and convert to Gamecraft blocks
Texture2D img = new Texture2D(64, 64);
// load file into texture
try
{
byte[] imgData = File.ReadAllBytes(filepath);
img.LoadImage(imgData);
}
catch (Exception e)
{
Logging.CommandLogError($"Failed to load picture data. Reason: {e.Message}");
Logging.MetaLog(e.Message + "\n" + e.StackTrace);
return;
}
Logging.CommandLog($"Image size: {img.width}x{img.height}");
Player p = new Player(PlayerType.Local);
float3 position = p.Position;
BlockIDs pickedBlock = p.SelectedBlock == BlockIDs.Invalid ? BlockIDs.AluminiumCube : p.SelectedBlock;
uint blockCount = 0;
Quaternion imgRotation = Quaternion.Euler(Rotation);
position += (float3)(imgRotation * new float3(1f, (float)blockSize, 0f));
float3 basePosition = position;
Stopwatch timer = Stopwatch.StartNew();
// convert the image to blocks
// this groups same-colored pixels in the same column into a single block to reduce the block count
// any further pixel-grouping optimisations (eg 2D grouping) risk increasing conversion time higher than O(x*y)
for (int x = 0; x < img.width; x++)
{
BlockInfo qVoxel = new BlockInfo
{
block = BlockIDs.AbsoluteMathsBlock, // impossible canvas block
color = BlockColors.Default,
darkness = 10,
visible = false,
};
float3 scale = new float3(1, 1, thiccness);
//position.x += (float)(blockSize);
for (int y = 0; y < img.height; y++)
{
//position.y += (float)blockSize;
Color pixel = img.GetPixel(x, y);
BlockInfo qPixel = PixelUtility.QuantizePixel(pixel);
if (qPixel.darkness != qVoxel.darkness
|| qPixel.color != qVoxel.color
|| qPixel.visible != qVoxel.visible
|| qPixel.block != qVoxel.block)
{
if (y != 0)
{
if (qVoxel.visible)
{
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)));
BlockIDs blockType = qVoxel.block == BlockIDs.AluminiumCube ? pickedBlock : qVoxel.block;
Block.PlaceNew(blockType, position, rotation: Rotation,color: qVoxel.color, darkness: qVoxel.darkness, scale: scale);
blockCount++;
}
scale = new float3(1, 1, thiccness);
}
qVoxel = qPixel;
}
else
{
scale.y += 1;
}

}
if (qVoxel.visible)
{
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)));
BlockIDs blockType = qVoxel.block == BlockIDs.AluminiumCube ? pickedBlock : qVoxel.block;
Block.PlaceNew(blockType, position, rotation: Rotation, color: qVoxel.color, darkness: qVoxel.darkness, scale: scale);
blockCount++;
}
//position.y = zero_y;
}
timer.Stop();
Logging.CommandLog($"Placed {img.width}x{img.height} image beside you ({blockCount} blocks total, {blockCount * 100 / (img.width * img.height)}%)");
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}");
}

public static void Pixelate2DFileToTextBlock(string filepath)
{
// Thanks to TheGreenGoblin for the idea (and the working Python implementation for reference)
// Load image file and convert to Gamecraft blocks
Texture2D img = new Texture2D(64, 64);
// load file into texture
try
{
byte[] imgData = File.ReadAllBytes(filepath);
img.LoadImage(imgData);
}
catch (Exception e)
{
Logging.CommandLogError($"Failed to load picture data. Reason: {e.Message}");
Logging.MetaLog(e.Message + "\n" + e.StackTrace);
return;
}
float3 position = new Player(PlayerType.Local).Position;
position.x += 1f;
position.y += (float)blockSize;
Stopwatch timer = Stopwatch.StartNew();
string text = PixelUtility.TextureToString(img);
TextBlock textBlock = TextBlock.PlaceNew(position, scale: new float3(Mathf.Ceil(img.width / 16), 1, Mathf.Ceil(img.height / 16)));
textBlock.Text = text;
byte[] textHash;
using (HashAlgorithm hasher = SHA256.Create())
textHash = hasher.ComputeHash(Encoding.UTF8.GetBytes(text));
string textId = "Pixi_";
// every byte converts to 2 hexadecimal characters so hash length needs to be halved
for (int i = 0; i < HASH_LENGTH/2 && i < textHash.Length; i++)
{
textId += textHash[i].ToString("X2");
}
textBlock.TextBlockId = textId;
timer.Stop();
Logging.CommandLog($"Placed {img.width}x{img.height} image in text block named {textId} beside you ({text.Length} characters)");
Logging.MetaLog($"Completed image text block {textId} synthesis in {timer.ElapsedMilliseconds}ms containing {text.Length} characters for {img.width*img.height} pixels");
}

public static void Pixelate2DFileToCommand(string filepath, string textBlockId)
{
// Thanks to Nullpersonan for the idea
// Load image file and convert to Gamecraft blocks
Texture2D img = new Texture2D(64, 64);
// load file into texture
try
{
byte[] imgData = File.ReadAllBytes(filepath);
img.LoadImage(imgData);
}
catch (Exception e)
{
Logging.CommandLogError($"Failed to load picture data. Reason: {e.Message}");
Logging.MetaLog(e.Message + "\n" + e.StackTrace);
return;
}
float3 position = new Player(PlayerType.Local).Position;
position.x += 1f;
position.y += (float)blockSize;
Stopwatch timer = Stopwatch.StartNew();
float zero_y = position.y;
string text = PixelUtility.TextureToString(img); // conversion
ConsoleBlock console = ConsoleBlock.PlaceNew(position);
// set console's command
console.Command = "ChangeTextBlockCommand";
console.Arg1 = textBlockId;
console.Arg2 = text;
console.Arg3 = "";
Logging.CommandLog($"Placed {img.width}x{img.height} image in console block beside you ({text.Length} characters)");
Logging.MetaLog($"Completed image console block {textBlockId} synthesis in {timer.ElapsedMilliseconds}ms containing {text.Length} characters for {img.width * img.height} pixels");
}
}
}

+ 97
- 0
Pixi/Images/ImageTextBlockImporter.cs View File

@@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using Unity.Mathematics;
using UnityEngine;

using GamecraftModdingAPI;
using GamecraftModdingAPI.Blocks;
using GamecraftModdingAPI.Players;
using GamecraftModdingAPI.Utility;
using Pixi.Common;

namespace Pixi.Images
{
public class ImageTextBlockImporter : Importer
{
public int Priority { get; } = 0;
public bool Optimisable { get; } = false;
public string Name { get; } = "ImageText~Spell";

public BlueprintProvider BlueprintProvider { get; } = null;
private Dictionary<string, string[]> textBlockContents = new Dictionary<string, string[]>();
public bool Qualifies(string name)
{
return name.EndsWith(".png", StringComparison.InvariantCultureIgnoreCase)
|| name.EndsWith(".jpg", StringComparison.InvariantCultureIgnoreCase);
}

public BlockJsonInfo[] Import(string name)
{
Texture2D img = new Texture2D(64, 64);
// load file into texture
try
{
byte[] imgData = File.ReadAllBytes(name);
img.LoadImage(imgData);
}
catch (Exception e)
{
Logging.CommandLogError($"Failed to load picture data. Reason: {e.Message}");
Logging.MetaLog(e.Message + "\n" + e.StackTrace);
return new BlockJsonInfo[0];
}
string text = PixelUtility.TextureToString(img);
// generate text block name
byte[] textHash;
using (HashAlgorithm hasher = SHA256.Create())
textHash = hasher.ComputeHash(Encoding.UTF8.GetBytes(text));
string textId = "Pixi_";
for (int i = 0; i < 2 && i < textHash.Length; i++)
{
textId += textHash[i].ToString("X2");
}
// save text block info for post-processing
textBlockContents[name] = new string[2] { textId, text};
return new BlockJsonInfo[1]
{
new BlockJsonInfo
{
color = new float[] {0f, 0f, 0f},
name = "TextBlock",
position = new float[] {0f, 0f, 0f},
rotation = new float[] {0f, 0f, 0f},
scale = new float[] {Mathf.Ceil(img.width / 16f), 1f, Mathf.Ceil(img.height / 16f)}
}
};
}

public void PreProcess(string name, ref ProcessedVoxelObjectNotation[] blocks)
{
Player p = new Player(PlayerType.Local);
float3 pos = p.Position;
for (int i = 0; i < blocks.Length; i++)
{
blocks[i].position += pos;
}
}

public void PostProcess(string name, ref Block[] blocks)
{
// populate text block
AsyncUtils.WaitForSubmission(); // just in case
TextBlock tb = blocks[0].Specialise<TextBlock>();
tb.TextBlockId = textBlockContents[name][0];
tb.Text = textBlockContents[name][1];
textBlockContents.Remove(name);
}
}
}

+ 1
- 21
Pixi/Images/PixelUtility.cs View File

@@ -14,27 +14,7 @@ namespace Pixi.Images
public static class PixelUtility
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static BlockInfo QuantizePixel(Color pixel)
{
#if DEBUG
Logging.MetaLog($"Color (r:{pixel.r}, g:{pixel.g}, b:{pixel.b})");
#endif
BlockColor c = ColorSpaceUtility.QuantizeToBlockColor(pixel);

BlockInfo result = new BlockInfo
{
block = pixel.a > 0.75 ? BlockIDs.AluminiumCube : BlockIDs.GlassCube,
color = c.Color,
darkness = c.Darkness,
visible = pixel.a > 0.5f,
};
#if DEBUG
Logging.MetaLog($"Quantized {result.color} (b:{result.block} d:{result.darkness} v:{result.visible})");
#endif
return result;
}

public static string HexPixel(Color pixel)
public static string HexPixel(Color pixel)
{
return "#"+ColorUtility.ToHtmlStringRGBA(pixel);
}


+ 1
- 1
Pixi/Pixi.csproj View File

@@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Version>0.4.0</Version>
<Version>1.0.0</Version>
<Authors>NGnius</Authors>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://git.exmods.org/NGnius/Pixi</PackageProjectUrl>


+ 10
- 10
Pixi/PixiPlugin.cs View File

@@ -9,6 +9,7 @@ using Unity.Mathematics; // float3
using IllusionPlugin;
using GamecraftModdingAPI.Utility;

using Pixi.Common;
using Pixi.Images;
using Pixi.Robots;

@@ -19,7 +20,7 @@ namespace Pixi
public string Name { get; } = Assembly.GetExecutingAssembly().GetName().Name; // Pixi
// To change the name, change the project's name

public string Version { get; } = Assembly.GetExecutingAssembly().GetName().Version.ToString(); // 0.1.0 (for now)
public string Version { get; } = Assembly.GetExecutingAssembly().GetName().Version.ToString();
// To change the version, change <Version>#.#.#</Version> in Pixi.csproj

// called when Gamecraft shuts down
@@ -40,20 +41,19 @@ namespace Pixi
// check out the modding API docs here: https://mod.exmods.org/

// Initialize Pixi mod
// 2D image functionality
ImageCommands.CreateThiccCommand();
ImageCommands.CreateImportCommand();
ImageCommands.CreateTextCommand();
ImageCommands.CreateTextConsoleCommand();
CommandRoot root = new CommandRoot();
// 2D Image Functionality
root.Inject(new ImageCanvasImporter());
root.Inject(new ImageTextBlockImporter());
root.Inject(new ImageCommandImporter());
// Robot functionality
RobotCommands.CreateRobotCRFCommand();
RobotCommands.CreateRobotFileCommand();
root.Inject(new RobotInternetImporter());
//RobotCommands.CreateRobotCRFCommand();
//RobotCommands.CreateRobotFileCommand();
#if DEBUG
// Development functionality
RobotCommands.CreatePartDumpCommand();
#endif

Logging.LogDebug($"{Name} has started up");
}

// unused methods


+ 19
- 3
Pixi/Robots/CubeUtility.cs View File

@@ -75,7 +75,7 @@ namespace Pixi.Robots
public static CubeInfo TranslateSpacialEnumerations(uint cubeId, byte x, byte y, byte z, byte rotation, byte colour, byte colour_x, byte colour_y, byte colour_z)
{
if (x != colour_x || z != colour_z || y != colour_y) return default;
CubeInfo result = new CubeInfo { visible = true, cubeId = cubeId };
CubeInfo result = new CubeInfo {visible = true, cubeId = cubeId, scale = new float3(1, 1, 1)};
TranslateBlockColour(colour, ref result);
TranslateBlockPosition(x, y, z, ref result);
TranslateBlockRotation(rotation, ref result);
@@ -196,6 +196,22 @@ namespace Pixi.Robots
result.darkness = c.Darkness;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string CubeIdDescription(uint cubeId)
{
if (map == null)
{
StreamReader cubemap = new StreamReader(Assembly.GetExecutingAssembly().GetManifestResourceStream("Pixi.cubes-id.json"));
map = JsonConvert.DeserializeObject<Dictionary<uint, string>>(cubemap.ReadToEnd());
}
if (!map.ContainsKey(cubeId))
{
return "Unknown cube #" + cubeId.ToString();
//result.rotation = float3.zero;
}
return map[cubeId];
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void TranslateBlockId(uint cubeId, ref CubeInfo result)
{
@@ -314,12 +330,12 @@ namespace Pixi.Robots
Logging.LogWarning($"Found empty blueprint for {cube.name} (id:{cube.cubeId}), is the blueprint correct?");
return new Block[0];
}
float3 defaultCorrectionVec = new float3((float)(0), (float)(RobotCommands.blockSize), (float)(0));
float3 defaultCorrectionVec = new float3((float)(0), (float)(CommandRoot.BLOCK_SIZE), (float)(0));
float3 baseRot = new float3(blueprint[0].rotation[0], blueprint[0].rotation[1], blueprint[0].rotation[2]);
float3 baseScale = new float3(blueprint[0].scale[0], blueprint[0].scale[1], blueprint[0].scale[2]);
Block[] placedBlocks = new Block[blueprint.Length];
bool isBaseScaled = !(blueprint[0].scale[1] > 0f && blueprint[0].scale[1] < 2f);
float3 correctionVec = isBaseScaled ? (float3)(Quaternion.Euler(baseRot) * baseScale / 2) * (float)-RobotCommands.blockSize : -defaultCorrectionVec;
float3 correctionVec = isBaseScaled ? (float3)(Quaternion.Euler(baseRot) * baseScale / 2) * (float)-CommandRoot.BLOCK_SIZE : -defaultCorrectionVec;
// FIXME scaled base blocks cause the blueprint to be placed in the wrong location (this also could be caused by a bug in DumpVON command)
if (isBaseScaled)
{


+ 100
- 0
Pixi/Robots/RobotBlueprintProvider.cs View File

@@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;

using Svelto.DataStructures;
using Unity.Mathematics;
using UnityEngine;

using GamecraftModdingAPI.Blocks;
using GamecraftModdingAPI.Utility;
using Pixi.Common;

namespace Pixi.Robots
{
public class RobotBlueprintProvider : BlueprintProvider
{
public string Name { get; } = "RobotBlueprintProvider";

private Dictionary<string, BlockJsonInfo[]> botprints = null;

private RobotInternetImporter parent;

public RobotBlueprintProvider(RobotInternetImporter rii)
{
parent = rii;
}
public BlockJsonInfo[] Blueprint(string name, BlockJsonInfo root)
{
if (botprints == null)
{
botprints = BlueprintUtility.ParseBlueprintResource("Pixi.blueprints.json");
}

if (!botprints.ContainsKey(root.name) || RobotInternetImporter.CubeSize != 3)
{
if (!parent.textBlockInfo.ContainsKey(name))
{
parent.textBlockInfo[name] = new FasterList<string>();
}
BlockJsonInfo copy = root;
copy.name = "TextBlock";
Logging.MetaLog($"Parsing uint from '{root.name}'");
parent.textBlockInfo[name].Add(root.name + " (" + CubeUtility.CubeIdDescription(uint.Parse(root.name)) + ")");
return new BlockJsonInfo[1] {copy};
}
BlockJsonInfo[] blueprint = botprints[root.name];
BlockJsonInfo[] adjustedBlueprint = new BlockJsonInfo[blueprint.Length];
Quaternion cubeQuaternion = Quaternion.Euler(ConversionUtility.FloatArrayToFloat3(root.rotation));
if (blueprint.Length == 0)
{
Logging.LogWarning($"Found empty blueprint for {root.name} (during '{name}'), is the blueprint correct?");
return new BlockJsonInfo[0];
}
// move blocks to correct position & rotation
float3 defaultCorrectionVec = new float3((float)(0), (float)(CommandRoot.BLOCK_SIZE), (float)(0));
float3 baseRot = new float3(blueprint[0].rotation[0], blueprint[0].rotation[1], blueprint[0].rotation[2]);
float3 baseScale = new float3(blueprint[0].scale[0], blueprint[0].scale[1], blueprint[0].scale[2]);
//Block[] placedBlocks = new Block[blueprint.Length];
bool isBaseScaled = !(blueprint[0].scale[1] > 0f && blueprint[0].scale[1] < 2f);
float3 correctionVec = isBaseScaled ? (float3)(Quaternion.Euler(baseRot) * baseScale / 2) * (float)-CommandRoot.BLOCK_SIZE : -defaultCorrectionVec;
// FIXME scaled base blocks cause the blueprint to be placed in the wrong location (this also could be caused by a bug in DumpVON command)
if (isBaseScaled)
{
Logging.LogWarning($"Found blueprint with scaled base block for {root.name} (during '{name}'), this is not currently supported");
}

float3 rootPos = ConversionUtility.FloatArrayToFloat3(root.position);
for (int i = 0; i < blueprint.Length; i++)
{
BlockColor blueprintBlockColor = ColorSpaceUtility.QuantizeToBlockColor(blueprint[i].color);
float[] physicalColor = blueprintBlockColor.Color == BlockColors.White && blueprintBlockColor.Darkness == 0 ? root.color : blueprint[i].color;
float3 bluePos = ConversionUtility.FloatArrayToFloat3(blueprint[i].position);
float3 blueScale = ConversionUtility.FloatArrayToFloat3(blueprint[i].scale);
float3 blueRot = ConversionUtility.FloatArrayToFloat3(blueprint[i].rotation);
float3 physicalLocation = (float3)(cubeQuaternion * bluePos) + rootPos;// + (blueprintSizeRotated / 2);
//physicalLocation.x += blueprintSize.x / 2;
physicalLocation += (float3)(cubeQuaternion * (correctionVec));
//physicalLocation.y -= (float)(RobotCommands.blockSize * scale / 2);
//float3 physicalScale = (float3)(cubeQuaternion * blueScale); // this actually over-rotates when combined with rotation
float3 physicalScale = blueScale;
float3 physicalRotation = (cubeQuaternion * Quaternion.Euler(blueRot)).eulerAngles;
#if DEBUG
Logging.MetaLog($"Placing blueprint block at {physicalLocation} rot{physicalRotation} scale{physicalScale}");
Logging.MetaLog($"Location math check original:{bluePos} rotated: {(float3)(cubeQuaternion * bluePos)} actualPos: {rootPos} result: {physicalLocation}");
Logging.MetaLog($"Scale math check original:{blueScale} rotation: {(float3)cubeQuaternion.eulerAngles} result: {physicalScale}");
Logging.MetaLog($"Rotation math check original:{blueRot} rotated: {(cubeQuaternion * Quaternion.Euler(blueRot))} result: {physicalRotation}");
#endif
adjustedBlueprint[i] = new BlockJsonInfo
{
color = physicalColor,
name = blueprint[i].name,
position = ConversionUtility.Float3ToFloatArray(physicalLocation),
rotation = ConversionUtility.Float3ToFloatArray(physicalRotation),
scale = ConversionUtility.Float3ToFloatArray(physicalScale)
};
}
return adjustedBlueprint;
}
}
}

+ 1
- 176
Pixi/Robots/RobotCommands.cs View File

@@ -18,29 +18,7 @@ namespace Pixi.Robots
{
public static class RobotCommands
{
internal const double blockSize = 0.2;

public static int CubeSize = 3;

public static void CreateRobotFileCommand()
{
CommandBuilder.Builder()
.Name("PixiBotFile")
.Description("Converts a robot file from RCBUP into Gamecraft blocks. Larger robots will freeze your game until conversion completes.")
.Action<string>(ImportRobotFile)
.Build();
}

public static void CreateRobotCRFCommand()
{
CommandBuilder.Builder()
.Name("PixiBot")
.Description("Downloads a robot from Robocraft's Factory and converts it into Gamecraft blocks. Larger robots will freeze your game until conversion completes.")
.Action<string>(ImportRobotOnline)
.Build();
}

public static void CreatePartDumpCommand()
public static void CreatePartDumpCommand()
{
CommandBuilder.Builder()
.Name("DumpVON")
@@ -49,159 +27,6 @@ namespace Pixi.Robots
.Build();
}

private static void ImportRobotFile(string filepath)
{
string file;
try
{
file = File.ReadAllText(filepath);
}
catch (Exception e)
{
Logging.CommandLogError($"Failed to load robot data. Reason: {e.Message}");
Logging.MetaLog(e);
return;
}
RobotStruct? robot = CubeUtility.ParseRobotInfo(file);
if (!robot.HasValue)
{
Logging.CommandLogError($"Failed to parse robot data. File format was not recognised.");
return;
}
float3 position = new Player(PlayerType.Local).Position;
position.y += (float)(blockSize * CubeSize * 3); // 3 is roughly the max height of any cube in RC
CubeInfo[] cubes = CubeUtility.ParseCubes(robot.Value);
// move origin closer to player (since bots are rarely built at the garage bay origin)
if (cubes.Length == 0)
{
Logging.CommandLogError($"Robot data contains no cubes");
return;
}
float3 minPosition = cubes[0].position;
for (int c = 0; c < cubes.Length; c++)
{
float3 cubePos = cubes[c].position;
if (cubePos.x < minPosition.x)
{
minPosition.x = cubePos.x;
}
if (cubePos.y < minPosition.y)
{
minPosition.y = cubePos.y;
}
if (cubePos.z < minPosition.z)
{
minPosition.z = cubePos.z;
}
}
Block[][] blocks = new Block[cubes.Length][];
for (int c = 0; c < cubes.Length; c++) // sometimes I wish this were C++
{
CubeInfo cube = cubes[c];
float3 realPosition = ((cube.position - minPosition) * (float)blockSize * CubeSize) + position;
if (cube.block == BlockIDs.TextBlock && !string.IsNullOrEmpty(cube.name))
{
// TextBlock block ID means it's a placeholder
blocks[c] = CubeUtility.BuildBlueprintOrTextBlock(cube, realPosition, CubeSize);
}
else
{
blocks[c] = new Block[] { Block.PlaceNew(cube.block, realPosition, cube.rotation, cube.color, cube.darkness, CubeSize) };
}
}
int blockCount = 0;
for (int c = 0; c < cubes.Length; c++)
{
CubeInfo cube = cubes[c];
// the goal is for this to never evaluate to true (ie all cubes are translated correctly)
if (!string.IsNullOrEmpty(cube.name) && cube.block == BlockIDs.TextBlock && blocks[c].Length == 1)
{
//Logging.MetaLog($"Block is {blocks[c][0].Type} and was placed as {cube.block}");
blocks[c][0].Specialise<TextBlock>().Text = cube.name;
}
blockCount += blocks[c].Length;
}
Logging.CommandLog($"Placed {robot?.name} by {robot?.addedByDisplayName} beside you ({cubes.Length}RC -> {blockCount}GC)");
}

private static void ImportRobotOnline(string robotName)
{
Stopwatch timer = Stopwatch.StartNew();
// download robot data
RobotStruct robot;
try
{
RobotBriefStruct[] botList = RoboAPIUtility.ListRobots(robotName);
if (botList.Length == 0)
throw new Exception("Failed to find robot");
robot = RoboAPIUtility.QueryRobotInfo(botList[0].itemId);
}
catch (Exception e)
{
Logging.CommandLogError($"Failed to download robot data. Reason: {e.Message}");
Logging.MetaLog(e);
timer.Stop();
return;
}
timer.Stop();
Logging.MetaLog($"Completed API calls in {timer.ElapsedMilliseconds}ms");
float3 position = new Player(PlayerType.Local).Position;
position.y += (float)(blockSize * CubeSize * 3); // 3 is roughly the max height of any cube in RC
CubeInfo[] cubes = CubeUtility.ParseCubes(robot);
// move origin closer to player (since bots are rarely built at the garage bay origin)
if (cubes.Length == 0)
{
Logging.CommandLogError($"Robot data contains no cubes");
return;
}
float3 minPosition = cubes[0].position;
for (int c = 0; c < cubes.Length; c++)
{
float3 cubePos = cubes[c].position;
if (cubePos.x < minPosition.x)
{
minPosition.x = cubePos.x;
}
if (cubePos.y < minPosition.y)
{
minPosition.y = cubePos.y;
}
if (cubePos.z < minPosition.z)
{
minPosition.z = cubePos.z;
}
}
Block[][] blocks = new Block[cubes.Length][];
for (int c = 0; c < cubes.Length; c++) // sometimes I wish this were C++
{
CubeInfo cube = cubes[c];
float3 realPosition = ((cube.position - minPosition) * (float)blockSize * CubeSize) + position;
if (cube.block == BlockIDs.TextBlock && !string.IsNullOrEmpty(cube.name))
{
// TextBlock block ID means it's a placeholder
blocks[c] = CubeUtility.BuildBlueprintOrTextBlock(cube, realPosition, CubeSize);
}
else
{
blocks[c] = new Block[] { Block.PlaceNew(cube.block, realPosition, cube.rotation, cube.color, cube.darkness, CubeSize) };
}
}
int blockCount = 0;
for (int c = 0; c < cubes.Length; c++)
{
CubeInfo cube = cubes[c];
// the goal is for this to never evaluate to true (ie all cubes are translated correctly)
if (!string.IsNullOrEmpty(cube.name) && cube.block == BlockIDs.TextBlock && blocks[c].Length == 1)
{
//Logging.MetaLog($"Block is {blocks[c][0].Type} and was placed as {cube.block}");
blocks[c][0].Specialise<TextBlock>().Text = cube.name;
}
blockCount += blocks[c].Length;
}
Logging.CommandLog($"Placed {robot.name} by {robot.addedByDisplayName} beside you ({cubes.Length}RC -> {blockCount}GC)");
}

private static void DumpBlockStructure(string filename)
{
Player local = new Player(PlayerType.Local);


+ 147
- 0
Pixi/Robots/RobotInternetImporter.cs View File

@@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;

using Svelto.DataStructures;
using Unity.Mathematics;
using UnityEngine;

using GamecraftModdingAPI;
using GamecraftModdingAPI.Blocks;
using GamecraftModdingAPI.Players;
using GamecraftModdingAPI.Utility;
using Pixi.Common;

namespace Pixi.Robots
{
public class RobotInternetImporter : Importer
{
public int Priority { get; } = -100;
public bool Optimisable { get; } = false;

public string Name { get; } = "RobocraftRobot~Spell";

public BlueprintProvider BlueprintProvider { get; }
public static int CubeSize = 3;
internal readonly Dictionary<string, FasterList<string>> textBlockInfo = new Dictionary<string, FasterList<string>>();

public RobotInternetImporter()
{
BlueprintProvider = new RobotBlueprintProvider(this);
}
public bool Qualifies(string name)
{
string[] extensions = name.Split('.');
return extensions.Length == 1
|| !extensions[extensions.Length - 1].Contains(" ");
}

public BlockJsonInfo[] Import(string name)
{
// download robot data
RobotStruct robot;
try
{
RobotBriefStruct[] botList = RoboAPIUtility.ListRobots(name);
if (botList.Length == 0)
throw new Exception("Failed to find robot");
robot = RoboAPIUtility.QueryRobotInfo(botList[0].itemId);
}
catch (Exception e)
{
Logging.CommandLogError($"Failed to download robot data. Reason: {e.Message}");
Logging.MetaLog(e);
return new BlockJsonInfo[0];
}
CubeInfo[] cubes = CubeUtility.ParseCubes(robot);
// move bot closer to origin (since bots are rarely built at the garage bay origin of the bottom south-west corner)
if (cubes.Length == 0)
{
Logging.CommandLogError($"Robot data contains no cubes");
return new BlockJsonInfo[0];
}
float3 minPosition = cubes[0].position;
for (int c = 0; c < cubes.Length; c++)
{
float3 cubePos = cubes[c].position;
if (cubePos.x < minPosition.x)
{
minPosition.x = cubePos.x;
}
if (cubePos.y < minPosition.y)
{
minPosition.y = cubePos.y;
}
if (cubePos.z < minPosition.z)
{
minPosition.z = cubePos.z;
}
}
BlockJsonInfo[] blocks = new BlockJsonInfo[cubes.Length];
for (int c = 0; c < cubes.Length; c++)
{
ref CubeInfo cube = ref cubes[c];
float3 realPosition = ((cube.position - minPosition) * CommandRoot.BLOCK_SIZE * CubeSize);
if (cube.block == BlockIDs.TextBlock && !string.IsNullOrEmpty(cube.name))
{
// TextBlock block ID means it's a placeholder
blocks[c] = new BlockJsonInfo
{
color = ColorSpaceUtility.UnquantizeToArray(cube.color, cube.darkness),
name = cube.cubeId.ToString(),
position = ConversionUtility.Float3ToFloatArray(realPosition),
rotation = ConversionUtility.Float3ToFloatArray(cube.rotation),
scale = ConversionUtility.Float3ToFloatArray(cube.scale)
};
}
else
{
blocks[c] = new BlockJsonInfo
{
color = ColorSpaceUtility.UnquantizeToArray(cube.color, cube.darkness),
name = cube.block.ToString(),
position = ConversionUtility.Float3ToFloatArray(realPosition),
rotation = ConversionUtility.Float3ToFloatArray(cube.rotation),
scale = ConversionUtility.Float3ToFloatArray(cube.scale * CubeSize)
};
}
}

return blocks;
}

public void PreProcess(string name, ref ProcessedVoxelObjectNotation[] blocks)
{
Player p = new Player(PlayerType.Local);
float3 pos = p.Position;
for (int i = 0; i < blocks.Length; i++)
{
blocks[i].position += pos;
}
}

public void PostProcess(string name, ref Block[] blocks)
{
int textBlockInfoIndex = 0;
for (int c = 0; c < blocks.Length; c++)
{
Block block = blocks[c];
// the goal is for this to never evaluate to true (ie all cubes are translated correctly)
if (block.Type == BlockIDs.TextBlock)
{
textBlockInfoIndex++;
block.Specialise<TextBlock>().Text = textBlockInfo[name][textBlockInfoIndex];
}
}

textBlockInfo.Remove(name);
}
}
}

+ 24
- 32
README.md View File

@@ -22,49 +22,40 @@ and `@"[dog name]"` could be a value like `@"Clifford"` or `@"doggo"`.

### Commands

`PixiText @"[image]"` converts an image to text and places a text block with that text beside you.

`PixiConsole @"[image]" "[text block id]"` converts an image to text and places a console block beside you which changes the specified text block.

`Pixi2D @"[image]"` converts an image to blocks and places it beside you.
`Pixi @"[thing]"` to import `thing` into your Gamecraft game.

For example, if you want to add an image called `pixel_art.png`, stored in Gamecraft's installation directory,
execute the command `Pixi2D @"pixel_art.png"` to load the image as blocks.
It's important to include the file extension, since Pixi isn't capable of black magic (yet).

**EXPERIMENTAL**
execute the command `Pixi @"pixel_art.png"` to load the image as blocks.
It's important to include the file extension, since that's what makes Pixi's magic work.

`PixiBot @"[bot]"` downloads a bot from Robocraft's community Factory and places it beside you.
If you know the name of the Pixi spell you want, you can also use

`PixiBotFile @"[bot]"` converts a `.bot` file from [rcbup](https://github.com/NGnius/rcbup) to blocks and places it beside you.
`Pixi2 "[spell]" @"[thing]"` to use `spell` to import `thing` into your Gamecraft game.

`PixiThicc [depth]` sets the block thickness, a positive integer value, for `Pixi2D` image conversion.
The default thickness is 1.

Some commands also have hidden features, like image rotation.
Talk to NGnius on the Exmods Discord server or read the Pixi's source code to figure that out.
Some commands also have hidden features, like image rotation and bot scaling.
Talk to NGnius on the Exmods Discord server or read Pixi's source code to figure that out.

### Behaviour

PixiText and PixiConsole share the same image conversion system.
ImageText and ImageConsole share the same image conversion system.
The conversion system converts every pixel to a [color tag](http://digitalnativestudios.com/textmeshpro/docs/rich-text/#color) followed by a square text character.
For PixiText, the resulting character string is set to the text field of the text block that the command places.
For PixiConsole, the character string is automatically set to a console block in the form `ChangeTextBlockCommand [text block id] [character string]`.
Due to limitations in Gamecraft, larger images will crash your game.
For ImageText, the resulting character string is set to the text field of the text block that the command places.
For ImageConsole, the character string is automatically set to a console block in the form `ChangeTextBlockCommand [text block id] [character string]`.
Due to limitations in Gamecraft, larger images will crash your game!!!

Pixi2D takes an image file and converts every pixel to a coloured block.
Pixi2D uses an algorithm to convert each pixel in an image into the closest paint colour, but colour accuracy will never be as good as a regular image.
ImageCanvas takes an image file and converts every pixel to a coloured block.
ImageCanvas uses an algorithm to convert each pixel in an image into the closest paint colour, but colour accuracy will never be as good as a regular image.

Pixi2D's colour-conversion algorithm also uses pixel transparency so you can cut out shapes.
ImageCanvas' colour-conversion algorithm also uses pixel transparency so you can cut out shapes.
A pixel which has opacity of less than 50% will be ignored.
A pixel which has an opacity between 75% and 50% will be converted into a glass cube.
A pixel which has an opacity greater than 75% will be converted into the block you're holding (or aluminium if you've got your hand selected).
This only works with `.PNG` image files since the `.JPG` format doesn't support image transparency.

Pixi2D also optimises block placement, since images have a lot of pixels.
ImageCanvas also optimises block placement, since images have a lot of pixels.
The blocks grouping ratio is displayed in the command line output once image importing is completed.

PixiBot and PixiBotFile convert a robot to equivalent Gamecraft blocks.
RobocraftRobot converts a robot to equivalent Gamecraft blocks.
If the conversion algorithm encounters a block it cannot convert, it will place a text block, with the block name, instead.

## Development
@@ -77,7 +68,7 @@ Show your love by offering your help!
- Report any bugs that you encounter while using Pixi.
- Report an idea for an improvement to Pixi or for a new file format.

For questions, concerns or reports, please contact NGnius in the [Exmods Discord server](https://discord.exmods.org).
For questions, concerns, or any other inquiry, please contact NGnius in the [Exmods Discord server](https://discord.exmods.org).

### Setup

@@ -96,14 +87,14 @@ Pixi also requires the [GamecraftModdingAPI](https://git.exmods.org/modtainers/G
### Building

After you've completed the setup, open the solution file `Pixi.sln` in your prefered C# .NET/Mono development environment.
I'd recommend Visual Studio Community Edition or JetBrains Rider for Windows and Monodevelop for Linux.
I'd recommend Visual Studio Community Edition for Windows or JetBrains Rider for Linux.

If you've successfully completed setup, you should be able to build the Pixi project without errors.
If it doesn't work and you can't figure out why, ask for help on the [Exmods Discord server](https://discord.gg/2CtWzZT).
If it doesn't work and you can't figure out why, ask for help on the [Exmods Discord server](https://discord.exmods.org).

# Acknowledgements

PixiBot uses the Factory to download robots, which involves a partial re-implementation of [rcbup](https://github.com/NGnius/rcbup).
RobocraftRobot uses the Factory to download robots, which involves a partial re-implementation of [rcbup](https://github.com/NGnius/rcbup).
Robot parsing uses information from [RobocraftAssembler](https://github.com/dddontshoot/RoboCraftAssembler).

Gamecraft interactions use the [GamecraftModdingAPI](https://git.exmods.org/modtainers/GamecraftModdingAPI).
@@ -114,9 +105,10 @@ Thanks to **Mr. Rotor** for all of the Robocraft blocks used in the PixiBot and

# Disclaimer

Pixi, Exmods and NGnius are not endorsed or supported by Gamecraft or FreeJam.
Pixi source code and releases are available free of charge as open-source software for the purpose of modding Gamecraft.
Modify Gamecraft at your own risk.
Read the LICENSE file for licensing information.
Read the LICENSE file for official licensing information.
Pixi, Exmods and NGnius are not endorsed or supported by Gamecraft or FreeJam.
Please don't sue this project or its contributors (that's what all disclaimers boil down to, right?).

Pixi is not magic and is actually just sufficiently advanced technology that's indistinguishable from magic.
Pixi is actually just sufficiently advanced technology that's indistinguishable from magic.