@@ -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) | |||
@@ -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(); | |||
} | |||
} | |||
@@ -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> |
@@ -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(); | |||
GamecraftModdingAPI.Utility.Logging.LogDebug($"{Name} has started up"); | |||
// Robot functionality | |||
RobotCommands.CreateRobotCRFCommand(); | |||
RobotCommands.CreateRobotFileCommand(); | |||
Logging.LogDebug($"{Name} has started up"); | |||
} | |||
// unused methods | |||
@@ -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; | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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"); | |||
} | |||
} | |||
} |
@@ -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>) | |||
} | |||
} |
@@ -5,33 +5,52 @@ Think of it like automatic pixel art. | |||
## Installation | |||
To install the Pixi mod, copy `Pixi.dll` (from the latest release) into the `Plugins` folder in Gamecraft's main folder. | |||
You'll also need [GamecraftModdingAPI](https://git.exmods.org/modtainers/GamecraftModdingAPI) installed and Gamecraft patched with [GCIPA](https://git.exmods.org/modtainers/GCIPA/releases). | |||
To install the Pixi mod, copy `Pixi.dll` (from the latest release) into the `Plugins` folder in Gamecraft's main folder. | |||
You'll also need [GamecraftModdingAPI](https://git.exmods.org/modtainers/GamecraftModdingAPI) installed and Gamecraft patched with [GCIPA](https://git.exmods.org/modtainers/GCIPA/releases). | |||
## Usage | |||
Pixi adds new commands to Gamecraft's command line to import images into a game. | |||
Since Pixi places vanilla Gamecraft blocks, imported images should be visible without Pixi installed. | |||
Since Pixi places vanilla Gamecraft blocks, imported images should be visible without Pixi installed. | |||
### 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 | |||
Pixi takes an image file and converts every pixel to a coloured block. | |||
Unfortunately, an image file supports over 6 million colours and Gamecraft only has 100 paint colours (and only 90 are used by Pixi). | |||
Pixi uses an algorithm to convert each pixel an image into the closest paint colour, but colour accuracy will never be as good as a regular image. | |||
Pixi uses an algorithm to convert each pixel an image into the closest paint colour, but colour accuracy will never be as good as a regular image. | |||
Pixi's colour-conversion algorithm also uses pixel transparency to you can cut out shapes. | |||
A pixel which has opacity of less than 75% will be not be converted into a solid block. | |||
@@ -48,7 +67,7 @@ Pixi could import that image with less than 500K blocks, which will still hurt G | |||
## Suggestions and Bugs | |||
If you find a bug or have an idea for an improvement to Pixi, please create an [issue](https://git.exmods.org/NGnius/Pixi/issues) with an in-depth description. | |||
If you'd like to discuss your issue instead, talk to NGnius on the [Exmods Discord server](https://discord.gg/xjnFxQV). | |||
If you'd like to discuss your issue instead, talk to NGnius on the [Exmods Discord server](https://discord.gg/xjnFxQV). | |||
## Development | |||
@@ -56,7 +75,7 @@ Show your love by offering your help! | |||
### Setup | |||
Pixi's development environment is similar to most Gamecraft mods, since it's based on HelloModdingWorld's configuration. | |||
Pixi's development environment is similar to most Gamecraft mods, since it's based on HelloModdingWorld's configuration. | |||
This project requires most of Gamecraft's `.dll` files to function correctly. | |||
Most, but not all, of these files are stored in Gamecraft's `Gamecraft_Data\Managed` folder. | |||
@@ -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. |