Implement Robocraft factory importing

This commit is contained in:
NGnius (Graham) 2020-05-19 21:36:21 -04:00
parent 2e314595ac
commit bfe0c1972c
12 changed files with 719 additions and 24 deletions

View file

@ -143,7 +143,7 @@ namespace Pixi.Images
//position.y = zero_y;
}
Logging.CommandLog($"Placed {img.width}x{img.height} image beside you ({blockCount} blocks total)");
Logging.MetaLog($"Saved {(img.width * img.height) - blockCount} blocks ({img.width * img.height / blockCount}x) while placing {filepath}");
Logging.MetaLog($"Saved {(img.width * img.height) - blockCount} blocks ({blockCount / (img.width * img.height)}%) while placing {filepath}");
}
public static void Pixelate2DFileToTextBlock(string filepath)

View file

@ -157,11 +157,12 @@ namespace Pixi.Images
Color pixel = img.GetPixel(x, y);
imgString.Append("<color=");
imgString.Append(HexPixel(pixel));
imgString.Append(">\u25a0</color>");
imgString.Append(">");
imgString.Append("\u25a0");
}
imgString.Append("<br>");
}
imgString.Append("</cspace></line-height>");
imgString.Append("</color></cspace></line-height>");
return imgString.ToString();
}
}

View file

@ -804,6 +804,15 @@
<Reference Include="GamecraftModdingAPI">
<HintPath>..\..\ref\Plugins\GamecraftModdingAPI.dll</HintPath>
</Reference>
<Reference Include="GamecraftModdingAPI">
<HintPath>..\..\ref\Plugins\GamecraftModdingAPI.dll</HintPath>
</Reference>
</ItemGroup>
<!--End Dependencies-->
<ItemGroup>
<None Remove="cubes-id.json" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="cubes-id.json" />
</ItemGroup>
</Project>

View file

@ -7,13 +7,10 @@ using UnityEngine;
using Unity.Mathematics; // float3
using IllusionPlugin;
using GamecraftModdingAPI;
using GamecraftModdingAPI.Commands;
using GamecraftModdingAPI.Utility;
using GamecraftModdingAPI.Blocks;
using GamecraftModdingAPI.Players;
using Pixi.Images;
using Pixi.Robots;
namespace Pixi
{
@ -29,7 +26,7 @@ namespace Pixi
public void OnApplicationQuit()
{
// Shutdown this mod
GamecraftModdingAPI.Utility.Logging.LogDebug($"{Name} has shutdown");
Logging.LogDebug($"{Name} has shutdown");
// Shutdown the Gamecraft modding API last
GamecraftModdingAPI.Main.Shutdown();
@ -48,8 +45,11 @@ namespace Pixi
ImageCommands.CreateImportCommand();
ImageCommands.CreateTextCommand();
ImageCommands.CreateTextConsoleCommand();
// Robot functionality
RobotCommands.CreateRobotCRFCommand();
RobotCommands.CreateRobotFileCommand();
GamecraftModdingAPI.Utility.Logging.LogDebug($"{Name} has started up");
Logging.LogDebug($"{Name} has started up");
}
// unused methods

28
Pixi/Robots/CubeInfo.cs Normal file
View file

@ -0,0 +1,28 @@
using System;
using Unity.Mathematics;
using GamecraftModdingAPI.Blocks;
namespace Pixi.Robots
{
public struct CubeInfo
{
// so you can't inherit from structs in C#...
// this is an extension of BlockInfo
public BlockIDs block;
public BlockColors color;
public byte darkness;
public bool visible;
// additions
public float3 rotation;
public float3 position;
public float3 scale;
public string placeholder;
}
}

View file

@ -1,7 +1,373 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using RobocraftX.Common;
using Newtonsoft.Json;
using Unity.Mathematics;
using GamecraftModdingAPI.Blocks;
using GamecraftModdingAPI.Utility;
namespace Pixi.Robots
{
public static class CubeUtility
{
private static Dictionary<uint, string> map = null;
public static RobotStruct? ParseRobotInfo(string robotInfo)
{
try
{
return JsonConvert.DeserializeObject<RobotStruct>(robotInfo);
}
catch (Exception e)
{
Logging.MetaLog(e);
return null;
}
}
public static CubeInfo[] ParseCubes(RobotStruct robot)
{
return ParseCubes(robot.cubeData, robot.colourData);
}
public static CubeInfo[] ParseCubes(string cubeData, string colourData)
{
BinaryBufferReader cubes = new BinaryBufferReader(Convert.FromBase64String(cubeData), 0);
BinaryBufferReader colours = new BinaryBufferReader(Convert.FromBase64String(colourData), 0);
uint cubeCount = cubes.ReadUint();
uint colourCount = colours.ReadUint();
if (cubeCount != colourCount)
{
Logging.MetaLog("Something is fucking broken");
return null;
}
Logging.MetaLog($"Detected {cubeCount} cubes");
CubeInfo[] result = new CubeInfo[cubeCount];
for (int cube = 0; cube < cubeCount; cube++)
{
result[cube] = TranslateSpacialEnumerations(
cubes.ReadUint(),
cubes.ReadByte(),
cubes.ReadByte(),
cubes.ReadByte(),
cubes.ReadByte(),
colours.ReadByte(),
colours.ReadByte(),
colours.ReadByte(),
colours.ReadByte()
);
}
return result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
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 };
TranslateBlockColour(colour, ref result);
TranslateBlockPosition(x, y, z, ref result);
TranslateBlockRotation(rotation, ref result);
TranslateBlockId(cubeId, ref result);
#if DEBUG
Logging.MetaLog($"Cube {cubeId} ({x}, {y}, {z}) rot:{rotation} decoded as {result.block} {result.position} rot: {result.rotation} color: {result.color} {result.darkness}");
#endif
return result;
}
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void TranslateBlockRotation(byte rotation, ref CubeInfo result)
{
// face refers to the face of the block connected to the bottom of the current one
// nvm, they're all incorrect
switch (rotation)
{
case 0:
result.rotation = new float3(0, 0, 0); // top face, forwards
break;
case 1:
result.rotation = new float3(0, 0, 90); // left face, forwards
break;
case 2:
result.rotation = new float3(0, 0, 180); // bottom face, forwards
break;
case 3:
result.rotation = new float3(0, 0, -90); // front face, down
break;
case 4:
result.rotation = new float3(0, 90, 0); // top face, right
break;
case 5:
result.rotation = new float3(0, 90, 90); // front face, right
break;
case 6:
result.rotation = new float3(-90, -90, 0); // right face, backwards
break;
case 7:
result.rotation = new float3(0, 90, -90); // back face, right
break;
case 8:
result.rotation = new float3(0, -90, 90); // back face, left
break;
case 9:
result.rotation = new float3(0, -90, -90); // front face, left
break;
case 10:
result.rotation = new float3(90, -90, 0); // left face, down
break;
case 11:
result.rotation = new float3(90, 90, 0); // right face, forwards
break;
case 12:
result.rotation = new float3(-90, 90, 0); // left face, up
break;
case 13:
result.rotation = new float3(0, 90, 180); // bottom face, right
break;
case 14:
result.rotation = new float3(0, 180, 0); // top face, backwards
break;
case 15:
result.rotation = new float3(0, 180, 90); // right face, up
break;
case 16:
result.rotation = new float3(0, 180, 180); // bottom face, backwards
break;
case 17:
result.rotation = new float3(0, 180, -90); // left face, backwards
break;
case 18:
result.rotation = new float3(0, -90, 0); // top face, left
break;
case 19:
result.rotation = new float3(0, -90, 180); // bottom face, left
break;
case 20:
result.rotation = new float3(90, 0, 0); // front face, down
break;
case 21:
result.rotation = new float3(90, 180, 0); // back face, down
break;
case 22:
result.rotation = new float3(-90, 0, 0); // back face, up
break;
case 23:
result.rotation = new float3(-90, 180, 0); // front face, up
break;
default:
#if DEBUG
Logging.MetaLog($"Unknown rotation {rotation.ToString("X2")}");
#endif
result.rotation = float3.zero;
break;
}
// my brain hurts after figuring out all of those rotations
// I wouldn't recommend trying to redo this
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void TranslateBlockPosition(byte x, byte y, byte z, ref CubeInfo result)
{
// for some reason, z is forwards in garage bays
result.position = new float3(x, y, z);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void TranslateBlockColour(byte colour, ref CubeInfo result)
{
// I hope these colours are accurate, I just guessed
// TODO colour accuracy (lol that won't ever happen)
switch (colour)
{
case 0:
result.color = BlockColors.White;
result.darkness = 0;
break;
case 1:
result.color = BlockColors.White;
result.darkness = 5;
break;
case 2:
result.color = BlockColors.Orange;
result.darkness = 0;
break;
case 3:
result.color = BlockColors.Blue;
result.darkness = 2;
break;
case 4:
result.color = BlockColors.White;
result.darkness = 8;
break;
case 5:
result.color = BlockColors.Red;
result.darkness = 0;
break;
case 6:
result.color = BlockColors.Yellow;
result.darkness = 0;
break;
case 7:
result.color = BlockColors.Green;
result.darkness = 0;
break;
case 8:
result.color = BlockColors.Purple;
result.darkness = 0;
break;
case 9:
result.color = BlockColors.Blue;
result.darkness = 7;
break;
case 10:
result.color = BlockColors.Purple;
result.darkness = 5;
break;
case 11:
result.color = BlockColors.Orange;
result.darkness = 7;
break;
case 12:
result.color = BlockColors.Green;
result.darkness = 3;
break;
case 13:
result.color = BlockColors.Green;
result.darkness = 2;
break;
case 14:
result.color = BlockColors.Pink;
result.darkness = 3;
break;
case 15:
result.color = BlockColors.Pink;
result.darkness = 2;
break;
case 16:
result.color = BlockColors.Red;
result.darkness = 2;
break;
case 17:
result.color = BlockColors.Orange;
result.darkness = 8;
break;
case 18:
result.color = BlockColors.Red;
result.darkness = 7;
break;
case 19:
result.color = BlockColors.Pink;
result.darkness = 0;
break;
case 20:
result.color = BlockColors.Yellow;
result.darkness = 2;
break;
case 21:
result.color = BlockColors.Green;
result.darkness = 7;
break;
case 22:
result.color = BlockColors.Green;
result.darkness = 8;
break;
case 23:
result.color = BlockColors.Blue;
result.darkness = 8;
break;
case 24:
result.color = BlockColors.Aqua;
result.darkness = 7;
break;
case 25:
result.color = BlockColors.Blue;
result.darkness = 6;
break;
case 26:
result.color = BlockColors.Aqua;
result.darkness = 5;
break;
case 27:
result.color = BlockColors.Blue;
result.darkness = 4;
break;
case 28:
result.color = BlockColors.Aqua;
result.darkness = 3;
break;
case 29:
result.color = BlockColors.Blue;
result.darkness = 5;
break;
case 30:
result.color = BlockColors.Purple;
result.darkness = 3;
break;
case 31:
result.color = BlockColors.Purple;
result.darkness = 1;
break;
default:
result.color = BlockColors.Aqua;
result.darkness = 0;
break;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void TranslateBlockId(uint cubeId, ref CubeInfo result)
{
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))
{
result.block = BlockIDs.TextBlock;
result.placeholder = "Unknown cube #" + cubeId.ToString();
//result.rotation = float3.zero;
#if DEBUG
Logging.MetaLog($"Unknown cubeId {cubeId}");
#endif
}
string cubeName = map[cubeId];
if (cubeName.Contains("cube"))
{
result.block = BlockIDs.AluminiumCube;
result.rotation = float3.zero;
}
else if (cubeName.Contains("prism") || cubeName.Contains("edge"))
{
result.block = BlockIDs.AluminiumSlope;
}
else if (cubeName.Contains("inner"))
{
result.block = BlockIDs.AluminiumSlicedCube;
}
else if (cubeName.Contains("tetra") || cubeName.Contains("corner"))
{
result.block = BlockIDs.AluminiumCorner;
}
else if (cubeName.Contains("pyramid"))
{
result.block = BlockIDs.AluminiumPyramidSegment;
}
else if (cubeName.Contains("cone"))
{
result.block = BlockIDs.AluminiumConeSegment;
}
else
{
result.block = BlockIDs.TextBlock;
result.placeholder = cubeName;
}
}
}
}

View file

@ -0,0 +1,75 @@
using System;
using System.IO;
using System.Net;
using System.Text;
using Newtonsoft.Json;
using GamecraftModdingAPI.Utility;
namespace Pixi.Robots
{
public static class RoboAPIUtility
{
private const string ROBOT_API_LIST_URL = "https://factory.robocraftgame.com/api/roboShopItems/list";
private const string ROBOT_API_GET_URL = "https://factory.robocraftgame.com/api/roboShopItems/get/";
private const string ROBOT_API_TOKEN = "Web eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJQdWJsaWNJZCI6IjEyMyIsIkRpc3BsYXlOYW1lIjoiVGVzdCIsIlJvYm9jcmFmdE5hbWUiOiJGYWtlQ1JGVXNlciIsIkZsYWdzIjpbXSwiaXNzIjoiRnJlZWphbSIsInN1YiI6IldlYiIsImlhdCI6MTU0NTIyMzczMiwiZXhwIjoyNTQ1MjIzNzkyfQ.ralLmxdMK9rVKPZxGng8luRIdbTflJ4YMJcd25dKlqg";
public static RobotBriefStruct[] ListRobots(string searchFilter, int pageSize = 10, bool playerFilter = false)
{
// pageSize <= 2 seems to retrieve items unreliably
string bodyJson = $"{{\"page\": 1, \"pageSize\": {pageSize}, \"order\": 0, \"playerFilter\": {playerFilter.ToString().ToLower()}, \"movementFilter\": \"100000,200000,300000,400000,500000,600000,700000,800000,900000,1000000,1100000,1200000\", \"movementCategoryFilter\": \"100000,200000,300000,400000,500000,600000,700000,800000,900000,1000000,1100000,1200000\", \"weaponFilter\": \"10000000,20000000,25000000,30000000,40000000,50000000,60000000,65000000,70100000,75000000\", \"weaponCategoryFilter\": \"10000000,20000000,25000000,30000000,40000000,50000000,60000000,65000000,70100000,75000000\", \"minimumCpu\": -1, \"maximumCpu\": -1, \"minimumRobotRanking\": 0, \"maximumRobotRanking\": 1000000000, \"textFilter\": \"{searchFilter}\", \"textSearchField\": 0, \"buyable\": true, \"prependFeaturedRobot\": false, \"featuredOnly\": false, \"defaultPage\": false}}";
byte[] reqBody = Encoding.UTF8.GetBytes(bodyJson);
#if DEBUG
Logging.MetaLog($"POST body\n{bodyJson}");
#endif
// download robot list
// FIXME this blocks main thread
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(ROBOT_API_LIST_URL);
// request
request.Method = "POST";
request.ContentLength = reqBody.Length;
request.ContentType = "application/json";
request.Headers.Add(HttpRequestHeader.Authorization, ROBOT_API_TOKEN);
request.Accept = "application/json; charset=utf-8"; // HTTP Status 500 without
Stream body;
body = request.GetRequestStream();
body.Write(reqBody, 0, reqBody.Length);
body.Close();
// response
HttpWebResponse response;
response = (HttpWebResponse)request.GetResponse();
// regular Stream was unreliable
// because they could read everything before everything was availabe
StreamReader respReader = new StreamReader(response.GetResponseStream());
string bodyStr = respReader.ReadToEnd();
RobotListResponse rlr = JsonConvert.DeserializeObject<RobotListResponse>(bodyStr);
return rlr.response.roboShopItems;
}
public static RobotStruct QueryRobotInfo(int robotId)
{
// download robot info
// FIXME this blocks main thread
string url = ROBOT_API_GET_URL + robotId.ToString();
Logging.MetaLog(url);
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
// request
request.Method = "GET";
request.ContentType = "application/json";
request.Accept = "application/json; charset=utf-8"; // HTTP Status 500 without
request.Headers.Add(HttpRequestHeader.Authorization, ROBOT_API_TOKEN);
// response
HttpWebResponse response;
response = (HttpWebResponse)request.GetResponse();
// regular Stream was unreliable
// because they could read everything before everything was availabe
StreamReader body = new StreamReader(response.GetResponseStream());
string bodyStr = body.ReadToEnd();
response.Close();
RobotInfoResponse rir = JsonConvert.DeserializeObject<RobotInfoResponse>(bodyStr);
return rir.response;
}
}
}

View file

@ -0,0 +1,49 @@
using System;
namespace Pixi.Robots
{
public struct RobotBriefStruct
{
public int itemId;
public string itemName;
public string itemDescription;
public string thumbnail;
public string addedBy;
public string addedByDisplayName;
public int cpu;
public int totalRobotRanking;
public string cubeData;
public string colourData;
public bool featured;
public string cubeAmounts; // this is sent incorrectly by the API server (it's actually a Dictionary<string, int>)
}
public struct RobotList
{
public RobotBriefStruct[] roboShopItems;
}
public struct RobotListResponse
{
public RobotList response;
public int statusCode;
}
public struct RobotInfoResponse
{
public RobotStruct response;
public int statusCode;
}
}

View file

@ -1,7 +1,114 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Text;
using Unity.Mathematics;
using GamecraftModdingAPI;
using GamecraftModdingAPI.Blocks;
using GamecraftModdingAPI.Commands;
using GamecraftModdingAPI.Players;
using GamecraftModdingAPI.Utility;
namespace Pixi.Robots
{
public static class RobotCommands
{
private static double blockSize = 0.2;
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. (Pixi)")
.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. (Pixi)")
.Action<string>(ImportRobotOnline)
.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;
CubeInfo[] cubes = CubeUtility.ParseCubes(robot.Value);
for (int c = 0; c < cubes.Length; c++) // sometimes I wish this were C++
{
CubeInfo cube = cubes[c];
float3 realPosition = (cube.position * (float)blockSize) + position;
Block newBlock = Block.PlaceNew(cube.block, realPosition, cube.rotation, cube.color, cube.darkness, scale: cube.scale);
// the goal is for this to never evaluate to true (ie all cubes are translated correctly)
if (!string.IsNullOrEmpty(cube.placeholder) && cube.block == BlockIDs.TextBlock)
{
newBlock.Specialise<TextBlock>().Text = cube.placeholder;
}
}
Logging.CommandLog($"Placed {robot.Value.name} by {robot.Value.addedByDisplayName} ({cubes.Length} cubes) beside you");
}
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;
CubeInfo[] cubes = CubeUtility.ParseCubes(robot);
for (int c = 0; c < cubes.Length; c++) // sometimes I wish this were C++
{
CubeInfo cube = cubes[c];
float3 realPosition = (cube.position * (float)blockSize) + position;
Block newBlock = Block.PlaceNew(cube.block, realPosition, cube.rotation, cube.color, cube.darkness, scale: cube.scale);
// the goal is for this to never evaluate to true (ie all cubes are translated correctly)
if (!string.IsNullOrEmpty(cube.placeholder) && cube.block == BlockIDs.TextBlock)
{
newBlock.Specialise<TextBlock>().Text = cube.placeholder;
}
}
Logging.CommandLog($"Placed {robot.name} by {robot.addedByDisplayName} ({cubes.Length} cubes) beside you");
}
}
}

View file

@ -0,0 +1,30 @@
using System;
namespace Pixi.Robots
{
public struct RobotStruct
{
public int id;
public string name;
public string description;
public string thumbnail;
public string addedBy;
public string addedByDisplayName;
public int cpu;
public int totalRobotRanking;
public string cubeData;
public string colourData;
public bool featured;
public string cubeAmounts; // this is sent incorrectly by the API server (it's actually a Dictionary<string, int>)
}
}

1
Pixi/cubes-id.json Normal file

File diff suppressed because one or more lines are too long

View file

@ -15,17 +15,36 @@ Since Pixi places vanilla Gamecraft blocks, imported images should be visible wi
### Commands
`PixiScale [width] [height]` sets the block canvas size (usually you'll want this to be the same size as your image).
When conversion using `Pixi2D` is done, if the canvas is larger than your image the image will be repeated.
If the canvas is smaller than your image, the image will be cropped.
`PixiText @"[image]"` converts an image to text and places a text block with that text beside you.
`Pixi2D "[image]"` converts an image to blocks and places it beside where you're standing (along the xy-plane).
`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 (along the xy-plane).
Anything between `[` and `]` characters is a command argument you must provide by replacing everything inside and including the square brackets.
An argument like `[dog name]` is an argument named "dog name" and could be a value like `Clifford` or `doggo`,
and `@"[dog name]"` could be a value like `@"Clifford"` or `@"doggo"`.
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 psychic (yet).
**EXPERIMENTAL**
`PixiBot @"[bot]"` downloads a bot from Robocraft's community Factory and places it beside you.
`PixiBotFile @"[bot]"` converts a `.bot` file from [rcbup](https://github.com/NGnius/rcbup) to blocks and places it beside you.
**NOTE**
Do not forget the `@"` before and `"` after the command argument, otherwise the command won't work.
If your image is not stored in the same folder as Gamecraft, you should specify the full filepath (eg `C:\path\to\image.png`) to the image.
This works best with `.PNG` images, but `.JPG` also works -- you just won't be able to use transparency-based features.
Optionally, if you know your command argument won't have a backslash `\` in it, you can omit the `@` symbol.
For example, if you want to add an image called `pixel_art.png`,
with a resolution of 1920x1080, stored in Gamecraft's installation directory,
execute the command `PixiScale 1920 1080` to set the size and then `Pixi2D "pixel_art.png"` to load the image.
`PixiThicc [depth]` sets the block thickness for `Pixi2D` image conversion.
The depth should be a positive whole number, like 3 or 42, and not 3.14 or -42.
The default thickness is 1.
### Behaviour
@ -75,3 +94,13 @@ I'd recommend Visual Studio Community Edition or JetBrains Rider for Windows and
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/xjnFxQV).
# Disclaimer
Pixi, Exmods and NGnius are not endorsed or supported by Gamecraft or FreeJam.
Modify Gamecraft at your own risk.
Read the LICENSE file for licensing information.
Please don't sue this project's contributors (that's what all disclaimers boil down to, right?).
Pixi is not a psychic overlord which secretly rules the world.
Well, not this world at least.