@@ -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; | |||
} | |||
} |
@@ -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]})}}"; | |||
} | |||
} | |||
} |
@@ -0,0 +1,9 @@ | |||
namespace Pixi.Common | |||
{ | |||
public interface BlueprintProvider | |||
{ | |||
string Name { get; } | |||
BlockJsonInfo[] Blueprint(string name, BlockJsonInfo root); | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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 | |||
} | |||
} | |||
} | |||
} |
@@ -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}; | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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})"; | |||
} | |||
} | |||
} |
@@ -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) { } | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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"); | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
@@ -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> | |||
@@ -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 | |||
@@ -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) | |||
{ | |||
@@ -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; | |||
} | |||
} | |||
} |
@@ -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); | |||
@@ -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); | |||
} | |||
} | |||
} |
@@ -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. |