Compare commits

...

56 commits

Author SHA1 Message Date
NGnius (Graham) ae069f6d93 Fix references 2020-12-28 14:08:13 -05:00
NGnius (Graham) 53564b9c56 Remove missing dependencies causing build warnings 2020-10-31 14:41:11 -04:00
NGnius (Graham) d761e4c16a Add hovers 2020-10-31 14:26:15 -04:00
NGnius (Graham) 89f5d9eb43 Add teslas, insect legs and some weapons (t1 laser, updated t5 laser msmg, t3 plasma) 2020-10-30 16:44:24 -04:00
NGnius (Graham) c3dc80fc84 Add RC propellers and fix textblock <color> replacement regex 2020-10-27 15:53:27 -04:00
NGnius (Graham) 3c00d05a3b Add custom stats dumping support, skis and minor tweaks 2020-10-27 12:00:38 -04:00
NGnius (Graham) 0e65267e88 Merge https://git.exmods.org/NorbiPeti/Pixi into master 2020-10-04 22:49:58 -04:00
NGnius (Graham) 85c1342313 Add info for common unsupported audio types 2020-10-03 17:37:03 -04:00
NGnius (Graham) 3c9c60b679 Add text block support to RC importer & general tweaks to make that work 2020-10-03 17:37:03 -04:00
NGnius (Graham) 84fc330b12 Multi-thread optimisation algorithm 2020-10-03 17:37:03 -04:00
NGnius (Graham) d2a2ce52f0 Optimise midi import block placement 2020-10-03 17:37:03 -04:00
NGnius (Graham) 742bcf25ef Create prototype MIDI importer 2020-10-03 17:37:03 -04:00
Norbi Peti 16f833ceda Fix RC glass cube and revert temp text support 2020-09-28 03:02:15 +02:00
Norbi Peti a0ab2ec9e7 Add wings, rudders & rotors, temporary text block support 2020-09-27 02:42:27 +02:00
Norbi Peti 30a3f5001f Ability to rotate blueprints, updated struts and such 2020-09-26 03:18:22 +02:00
Norbi Peti db5ff7223c Blueprint fixes, T5 laser 2020-09-23 23:35:09 +02:00
Norbi Peti 02401f39f9 Add t5 shields 2020-09-21 21:56:20 +02:00
Norbi Peti 60cf8bdd67 Actually add t3 shields and t4 ones too 2020-09-21 00:25:53 +02:00
Norbi Peti ab169fb87c (Re)load blueprints from file, t3 shields
Blueprints can be loaded and reloaded from a file in the game's directory except in a release version
2020-09-20 23:23:33 +02:00
Norbi Peti 5bc0351bd1 Fix thrusters and add some shields 2020-09-19 23:51:17 +02:00
Norbi Peti bd813d852d Allow selections when dumping, added thrusters and modules 2020-09-19 22:30:54 +02:00
NGnius (Graham) 4b35647c0b Use official paint colours for conversion algorithm 2020-09-04 16:00:12 -04:00
NGnius (Graham) 1b126b69c0 Convert to new GCIPA 2020-08-23 10:31:38 -04:00
NGnius (Graham) bb19c084d1 Add default colour quantization support 2020-08-16 11:27:46 -04:00
NGnius (Graham) 90a653b1c4 Remove mock ref 2020-07-24 11:14:57 -04:00
NGnius (Graham) 244be3468b Refactor Pixi import system as per issue #5 2020-07-24 11:05:00 -04:00
NGnius (Graham) acb0c59967 Use geometric (pythagorean) distance for nearest colour calculations 2020-07-24 11:05:00 -04:00
NGnius (Graham) df7ba96434 Update license 2020-06-26 12:51:59 -04:00
NGnius (Graham) 523baec814 Fix missing RC->GC colour mapping 2020-06-14 12:49:10 -04:00
NGnius (Graham) 5acd445361 Add wheels and some minor tweaks 2020-06-14 12:11:33 -04:00
NGnius (Graham) fe8ee1c262 Update README with more concise info and acknowledgements 2020-06-08 20:24:50 -04:00
NGnius (Graham) 34fdd0aefa Remove quotation marks around ChangeTextBlock command args because that breaks stuff 2020-06-08 13:42:25 -04:00
NGnius (Graham) 5c5e46e84b Convert bot color conversion to MrRotor values & speedup 2020-06-08 11:48:10 -04:00
NGnius (Graham) b8547c856d Add pick block support to Pixi2D 2020-06-07 13:08:59 -04:00
NGnius (Graham) d9a28cfbf7 Add image rotation support 2020-06-07 12:51:11 -04:00
NGnius (Graham) 0d25570cba Update PixiBotFile command with correct blueprints functionality 2020-06-07 11:59:45 -04:00
NGnius (Graham) 6e817bff18 Add strut straight long 2020-06-07 10:59:49 -04:00
NGnius (Graham) 76b67e7684 Fix diagonal 3d strut rotations and strut texture lines 2020-06-07 10:47:29 -04:00
NGnius (Graham) 351f668f51 Add strut blueprints 2020-06-07 10:10:59 -04:00
NGnius (Graham) abd300351f Move bot placement closer to player 2020-06-06 11:31:31 -04:00
NGnius (Graham) cdd474e1ec Add blueprint RC cube functionality (rods-only atm) 2020-06-06 11:15:30 -04:00
NGnius (Graham) d0726b9514 Reduce Sync() events 2020-05-26 10:57:59 -04:00
NGnius (Graham) e8ca1fe398 Update README install instructions 2020-05-25 09:05:49 -04:00
NGnius (Graham) 72386d2c53 Update behaviour info for new commands 2020-05-20 09:12:47 -04:00
NGnius (Graham) 1187d7896c Fix text block display issues 2020-05-19 22:25:16 -04:00
NGnius (Graham) b7cbf4486f Add rounded & glass cube support 2020-05-19 22:03:20 -04:00
NGnius (Graham) e6f0816c5f Remove Pixi from command descriptions 2020-05-19 21:38:30 -04:00
NGnius (Graham) bfe0c1972c Implement Robocraft factory importing 2020-05-19 21:36:21 -04:00
NGnius (Graham) 2e314595ac Refactor and implement img to text block importing 2020-05-17 23:37:54 -04:00
NGnius (Graham) 6acf1b7d4e Fix breaking API changes 2020-05-15 13:48:49 -04:00
NGnius (Graham) d536956e6c Version 0.2.0 2020-05-11 17:25:54 -04:00
NGnius (Graham) 93f4bd37e2 Add alpha channel support 2020-05-11 17:21:12 -04:00
NGnius c7b4f89828 Improve README information 2020-05-11 19:52:14 +00:00
NGnius (Graham) 1cf0c51e35 Improve user interactions 2020-05-10 15:27:16 -04:00
NGnius (Graham) 2d7daf0976 Fix aqua and orange 2020-05-09 20:55:20 -04:00
NGnius (Graham) 7c42f263b8 Fix some cases where darkness was over-bright 2020-05-09 19:13:35 -04:00
32 changed files with 4126 additions and 802 deletions

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) <year> <copyright holders>
Copyright (c) 2020 NGnius
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -0,0 +1,37 @@
using System;
using GamecraftModdingAPI;
using GamecraftModdingAPI.Utility;
using Pixi.Common;
namespace Pixi.Audio
{
public class AudioFakeImporter : Importer
{
public int Priority { get; } = 0;
public bool Optimisable { get; } = false;
public string Name { get; } = "AudioWarning~Spell";
public BlueprintProvider BlueprintProvider { get; } = null;
public bool Qualifies(string name)
{
return name.EndsWith(".flac", StringComparison.InvariantCultureIgnoreCase)
|| name.EndsWith(".ogg", StringComparison.InvariantCultureIgnoreCase)
|| name.EndsWith(".mp3", StringComparison.InvariantCultureIgnoreCase)
|| name.EndsWith(".wav", StringComparison.InvariantCultureIgnoreCase)
|| name.EndsWith(".aac", StringComparison.InvariantCultureIgnoreCase);
}
public BlockJsonInfo[] Import(string name)
{
Logging.CommandLogWarning($"Audio importing only works with MIDI (.mid) files, which '{name}' is not.\nThere are many converters online, but for best quality use a MIDI file made from a music transcription.\nFor example, musescore.com has lots of good transcriptions and they offer a 30-day free trial.");
return null;
}
public void PreProcess(string name, ref ProcessedVoxelObjectNotation[] blocks)
{
}
public void PostProcess(string name, ref Block[] blocks)
{
}
}
}

106
Pixi/Audio/AudioTools.cs Normal file
View file

@ -0,0 +1,106 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using GamecraftModdingAPI.Utility;
using Melanchall.DryWetMidi.Common;
namespace Pixi.Audio
{
public static class AudioTools
{
private static Dictionary<byte, byte> programMap = null;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte TrackType(FourBitNumber channel)
{
return TrackType((byte) channel);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte TrackType(byte channel)
{
if (programMap.ContainsKey(channel)) return programMap[channel];
#if DEBUG
Logging.MetaLog($"Using default value (piano) for channel number {channel}");
#endif
return 5; // Piano
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float VelocityToVolume(SevenBitNumber velocity)
{
// faster key hit means louder note
return 100f * velocity / ((float) SevenBitNumber.MaxValue + 1f);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void GenerateProgramMap()
{
programMap = new Dictionary<byte, byte>
{
{0, 5 /* Piano */},
{1, 5},
{2, 5},
{3, 5},
{4, 5},
{5, 5},
{6, 5},
{7, 5},
{8, 0 /* Kick Drum */},
{9, 0},
{10, 0},
{11, 0},
{12, 0},
{13, 0},
{14, 0},
{15, 0},
{24, 6 /* Guitar 1 (Acoustic) */},
{25, 6},
{26, 6},
{27, 6},
{28, 6},
{29, 7 /* Guitar 2 (Dirty Electric) */},
{30, 7},
{32, 6},
{33, 6},
{34, 6},
{35, 6},
{36, 6},
{37, 6},
{38, 6},
{39, 6},
{56, 8 /* Trumpet */}, // basically all brass & reeds are trumpets... that's how music works right?
{57, 8},
{58, 8},
{59, 8},
{60, 8},
{61, 8},
{62, 8},
{63, 8},
{64, 8},
{65, 8},
{66, 8},
{67, 8},
{68, 8},
{69, 8}, // Nice
{70, 8},
{71, 8},
{72, 8},
{73, 8},
{74, 8},
{75, 8},
{76, 8},
{77, 8},
{78, 8},
{79, 8},
{112, 0},
{113, 0},
{114, 0},
{115, 0},
{116, 0},
{117, 4 /* Tom Drum */},
{118, 4},
{119, 3 /* Open High Hat */},
};
}
}
}

207
Pixi/Audio/MidiImporter.cs Normal file
View file

@ -0,0 +1,207 @@
using System;
using System.Collections.Generic;
using System.Linq;
using GamecraftModdingAPI;
using GamecraftModdingAPI.Players;
using GamecraftModdingAPI.Blocks;
using GamecraftModdingAPI.Utility;
using Melanchall.DryWetMidi.Common;
using Melanchall.DryWetMidi.Core;
using Melanchall.DryWetMidi.Devices;
using Melanchall.DryWetMidi.Interaction;
using Pixi.Common;
using Unity.Mathematics;
namespace Pixi.Audio
{
public class MidiImporter : Importer
{
public int Priority { get; } = 1;
public bool Optimisable { get; } = false;
public string Name { get; } = "Midi~Spell";
public BlueprintProvider BlueprintProvider { get; } = null;
private Dictionary<string, MidiFile> openFiles = new Dictionary<string, MidiFile>();
public static bool ThreeDee = false;
public static float Spread = 1f;
public static byte Key = 0;
public static float VolumeMultiplier = 1f;
public MidiImporter()
{
AudioTools.GenerateProgramMap();
}
public bool Qualifies(string name)
{
return name.EndsWith(".mid", StringComparison.InvariantCultureIgnoreCase)
|| name.EndsWith(".midi", StringComparison.InvariantCultureIgnoreCase);
}
public BlockJsonInfo[] Import(string name)
{
MidiFile midi = MidiFile.Read(name);
openFiles[name] = midi;
Logging.MetaLog($"Found {midi.GetNotes().Count()} notes over {midi.GetDuration<MidiTimeSpan>().TimeSpan} time units");
BlockJsonInfo[] blocks = new BlockJsonInfo[(midi.GetNotes().Count() * 2) + 3];
List<BlockJsonInfo> blocksToBuild = new List<BlockJsonInfo>();
// convert Midi notes to sfx blocks
Dictionary<long, uint> breadthCache = new Dictionary<long, uint>();
Dictionary<long, uint> depthCache = new Dictionary<long, uint>();
HashSet<long> timerCache = new HashSet<long>();
//uint count = 0;
float zdepth = 0;
foreach (Note n in midi.GetNotes())
{
long microTime = n.TimeAs<MetricTimeSpan>(midi.GetTempoMap()).TotalMicroseconds;
float breadth = 0f;
if (!timerCache.Contains(microTime))
{
depthCache[microTime] = (uint)++zdepth;
breadthCache[microTime] = 1;
timerCache.Add(microTime);
blocksToBuild.Add(new BlockJsonInfo
{
name = GamecraftModdingAPI.Blocks.BlockIDs.Timer.ToString(),
position = new float[] { breadth * 0.2f * Spread, 2 * 0.2f, zdepth * 0.2f * Spread},
rotation = new float[] { 0, 0, 0},
color = new float[] { -1, -1, -1},
scale = new float[] { 1, 1, 1},
});
}
else
{
zdepth = depthCache[microTime]; // remember the z-position of notes played at the same moment (so they can be placed adjacent to each other)
breadth += breadthCache[microTime]++; // if multiple notes exist for a given time, place them beside each other on the x-axis
}
blocksToBuild.Add(new BlockJsonInfo
{
name = GamecraftModdingAPI.Blocks.BlockIDs.SFXBlockInstrument.ToString(),
position = new float[] { breadth * 0.2f * Spread, 1 * 0.2f, zdepth * 0.2f * Spread},
rotation = new float[] { 0, 0, 0},
color = new float[] { -1, -1, -1},
scale = new float[] { 1, 1, 1},
});
/*
blocks[count] = new BlockJsonInfo
{
name = GamecraftModdingAPI.Blocks.BlockIDs.Timer.ToString(),
position = new float[] { breadth * 0.2f * Spread, 2 * 0.2f, zdepth * 0.2f * Spread},
rotation = new float[] { 0, 0, 0},
color = new float[] { -1, -1, -1},
scale = new float[] { 1, 1, 1},
};
count++;
blocks[count] = new BlockJsonInfo
{
name = GamecraftModdingAPI.Blocks.BlockIDs.SFXBlockInstrument.ToString(),
position = new float[] { breadth * 0.2f * Spread, 1 * 0.2f, zdepth * 0.2f * Spread},
rotation = new float[] { 0, 0, 0},
color = new float[] { -1, -1, -1},
scale = new float[] { 1, 1, 1},
};
count++;*/
}
// playback IO (reset & play)
blocksToBuild.Add(new BlockJsonInfo
{
name = GamecraftModdingAPI.Blocks.BlockIDs.SimpleConnector.ToString(),
position = new float[] { -0.2f, 3 * 0.2f, 0},
rotation = new float[] { 0, 0, 0},
color = new float[] { -1, -1, -1},
scale = new float[] { 1, 1, 1},
}); // play is second last (placed above stop)
blocksToBuild.Add(new BlockJsonInfo
{
name = GamecraftModdingAPI.Blocks.BlockIDs.SimpleConnector.ToString(),
position = new float[] { -0.2f, 2 * 0.2f, 0},
rotation = new float[] { 0, 0, 0},
color = new float[] { -1, -1, -1},
scale = new float[] { 1, 1, 1},
}); // stop is middle (placed above reset)
blocksToBuild.Add(new BlockJsonInfo
{
name = GamecraftModdingAPI.Blocks.BlockIDs.SimpleConnector.ToString(),
position = new float[] { -0.2f, 1 * 0.2f, 0},
rotation = new float[] { 0, 0, 0},
color = new float[] { -1, -1, -1},
scale = new float[] { 1, 1, 1},
}); // reset is last (placed below stop)
return blocksToBuild.ToArray();
}
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)
{
// playback IO
LogicGate startConnector = blocks[blocks.Length - 3].Specialise<LogicGate>();
LogicGate stopConnector = blocks[blocks.Length - 2].Specialise<LogicGate>();
LogicGate resetConnector = blocks[blocks.Length - 1].Specialise<LogicGate>();
uint count = 0;
// generate channel data
byte[] channelPrograms = new byte[16];
for (byte i = 0; i < channelPrograms.Length; i++) // init array
{
channelPrograms[i] = 5; // Piano
}
foreach (TimedEvent e in openFiles[name].GetTimedEvents())
{
if (e.Event.EventType == MidiEventType.ProgramChange)
{
ProgramChangeEvent pce = (ProgramChangeEvent) e.Event;
channelPrograms[pce.Channel] = AudioTools.TrackType(pce.ProgramNumber);
#if DEBUG
Logging.MetaLog($"Detected channel {pce.Channel} as program {pce.ProgramNumber} (index {channelPrograms[pce.Channel]})");
#endif
}
}
Timer t = null;
//count = 0;
foreach (Note n in openFiles[name].GetNotes())
{
while (blocks[count].Type == BlockIDs.Timer)
{
// set timing info
#if DEBUG
Logging.Log($"Handling Timer for notes at {n.TimeAs<MetricTimeSpan>(openFiles[name].GetTempoMap()).TotalMicroseconds * 0.000001f}s");
#endif
t = blocks[count].Specialise<Timer>();
t.Start = 0;
t.End = 0.01f + n.TimeAs<MetricTimeSpan>(openFiles[name].GetTempoMap()).TotalMicroseconds * 0.000001f;
count++;
}
// set notes info
SfxBlock sfx = blocks[count].Specialise<SfxBlock>();
sfx.Pitch = n.NoteNumber - 60 + Key; // In MIDI, 60 is middle C, but GC uses 0 for middle C
sfx.TrackIndex = channelPrograms[n.Channel];
sfx.Is3D = ThreeDee;
sfx.Volume = AudioTools.VelocityToVolume(n.Velocity) * VolumeMultiplier;
count++;
// connect wires
if (t == null) continue; // this should never happen
t.Connect(0, sfx, 0);
startConnector.Connect(0, t, 0);
stopConnector.Connect(0, t, 1);
resetConnector.Connect(0, t, 2);
}
openFiles.Remove(name);
}
}
}

View file

@ -0,0 +1,41 @@
using System;
using Unity.Mathematics;
using GamecraftModdingAPI.Blocks;
namespace Pixi.Common
{
public struct BlockJsonInfo
{
public string name;
public float[] position;
public float[] rotation;
public float[] color;
public float[] scale;
internal ProcessedVoxelObjectNotation Process()
{
BlockIDs block = ConversionUtility.BlockIDsToEnum(name.Split('\t')[0]);
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]})}}";
}
}
}

View file

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

View file

@ -0,0 +1,72 @@
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;
#if DEBUG
if (File.Exists(name))
bluemap = File.OpenText(name);
else
#endif
bluemap = new StreamReader(Assembly.GetExecutingAssembly().GetManifestResourceStream(name));
using (bluemap)
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;
}
}
}

View file

@ -0,0 +1,322 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using GamecraftModdingAPI.App;
using GamecraftModdingAPI.Blocks;
using GamecraftModdingAPI.Tasks;
using GamecraftModdingAPI.Utility;
using MiniJSON;
using Svelto.Tasks;
using UnityEngine.ResourceManagement.ResourceLocations;
namespace Pixi.Common
{
public static class ColorSpaceUtility
{
private const float optimal_delta = 0.1f;
private static Dictionary<BlockColor, float[]> colorMap = null;
private static Dictionary<byte, BlockColor> botColorMap = null;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static BlockColor QuantizeToBlockColor(Color pixel)
{
//if (colorMap == null) BuildColorMap();
float[] closest = new float[3] { 1, 1, 1 };
BlockColor c = new BlockColor
{
Color = BlockColors.Default,
Darkness = 0,
};
BlockColor[] keys = colorMap.Keys.ToArray();
float geometricClosest = float.MaxValue;
for (int k = 0; k < keys.Length; k++)
{
float[] color = colorMap[keys[k]];
float[] distance = new float[3] { Math.Abs(pixel.r - color[0]), Math.Abs(pixel.g - color[1]), Math.Abs(pixel.b - color[2]) };
float dist = Mathf.Sqrt(Mathf.Pow(distance[0], 2) + Mathf.Pow(distance[1], 2) + Mathf.Pow(distance[2], 2));
if (dist < geometricClosest)
{
c = keys[k];
closest = distance;
geometricClosest = Mathf.Sqrt(Mathf.Pow(closest[0], 2) + Mathf.Pow(closest[1], 2) + Mathf.Pow(closest[2], 2));
if (geometricClosest < optimal_delta)
{
#if DEBUG
//Logging.MetaLog($"Final delta ({closest[0]},{closest[1]},{closest[2]}) t:{geometricClosest}");
#endif
return c;
}
}
}
#if DEBUG
//Logging.MetaLog($"Final delta ({closest[0]},{closest[1]},{closest[2]}) t:{geometricClosest}");
#endif
return c;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static BlockColor QuantizeToBlockColor(byte cubeColorEnum)
{
if (botColorMap == null) BuildBotColorMap();
return botColorMap[cubeColorEnum];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static BlockColor QuantizeToBlockColor(float[] pixel)
{
if (pixel.Length < 3 || pixel[0] < 0 || pixel[1] < 0 || pixel[2] < 0)
{
return new BlockColor
{
Color = BlockColors.Default,
Darkness = 0,
};
}
return QuantizeToBlockColor(new Color(pixel[0], pixel[1], pixel[2]));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float[] UnquantizeToArray(BlockColor c)
{
//if (colorMap == null) BuildColorMap();
return colorMap[c];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float[] UnquantizeToArray(BlockColors color, byte darkness = 0)
{
return UnquantizeToArray(new BlockColor
{
Color = color,
Darkness = darkness,
});
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Color UnquantizeToColor(BlockColor c)
{
float[] t = UnquantizeToArray(c);
return new Color(t[0], t[1], t[2]);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Color UnquantizeToColor(BlockColors color, byte darkness = 0)
{
return UnquantizeToColor(new BlockColor
{
Color = color,
Darkness = darkness,
});
}
public static void LoadColorMenuEvent(object caller, MenuEventArgs info)
{
Scheduler.Schedule(new AsyncRunner());
}
private static void BuildColorMap()
{
// old manual version for building color map
colorMap = new Dictionary<BlockColor, float[]>();
// this was done manually -- never again
// White
colorMap[new BlockColor { Color = BlockColors.White, Darkness = 0 }] = new float[3] { 1f, 1f, 1f};
colorMap[new BlockColor { Color = BlockColors.White, Darkness = 1 }] = new float[3] { 0.88f, 0.98f, 0.99f };
colorMap[new BlockColor { Color = BlockColors.White, Darkness = 2 }] = new float[3] { 0.80f, 0.89f, 0.99f };
colorMap[new BlockColor { Color = BlockColors.White, Darkness = 3 }] = new float[3] { 0.746f, 0.827f, 0.946f };
colorMap[new BlockColor { Color = BlockColors.White, Darkness = 4 }] = new float[3] { 0.71f, 0.789f, 0.888f };
colorMap[new BlockColor { Color = BlockColors.White, Darkness = 5 }] = new float[3] { 0.597f, 0.664f, 0.742f };
colorMap[new BlockColor { Color = BlockColors.White, Darkness = 6 }] = new float[3] { 0.484f, 0.535f, 0.61f };
colorMap[new BlockColor { Color = BlockColors.White, Darkness = 7 }] = new float[3] { 0.355f, 0.39f, 0.449f };
colorMap[new BlockColor { Color = BlockColors.White, Darkness = 8 }] = new float[3] { 0f, 0f, 0f };
colorMap[new BlockColor { Color = BlockColors.White, Darkness = 9 }] = new float[3] { 0.581f, 0.643f, 0.745f };
// Pink
colorMap[new BlockColor { Color = BlockColors.Pink, Darkness = 0 }] = new float[3] { 1f, 0.657f, 1f };
colorMap[new BlockColor { Color = BlockColors.Pink, Darkness = 1 }] = new float[3] { 0.912f, 0.98f, 0.993f };
colorMap[new BlockColor { Color = BlockColors.Pink, Darkness = 2 }] = new float[3] { 0.897f, 0.905f, 0.991f };
colorMap[new BlockColor { Color = BlockColors.Pink, Darkness = 3 }] = new float[3] { 0.892f, 0.776f, 0.988f };
colorMap[new BlockColor { Color = BlockColors.Pink, Darkness = 4 }] = new float[3] { 0.898f, 0.698f, 0.992f };
colorMap[new BlockColor { Color = BlockColors.Pink, Darkness = 5 }] = new float[3] { 0.875f, 0.267f, 0.882f };
colorMap[new BlockColor { Color = BlockColors.Pink, Darkness = 6 }] = new float[3] { 0.768f, 0.199f, 0.767f };
colorMap[new BlockColor { Color = BlockColors.Pink, Darkness = 7 }] = new float[3] { 0.628f, 0.15f, 0.637f };
colorMap[new BlockColor { Color = BlockColors.Pink, Darkness = 8 }] = new float[3] { 0.435f, 0.133f, 0.439f };
colorMap[new BlockColor { Color = BlockColors.Pink, Darkness = 9 }] = new float[3] { 0.726f, 0.659f, 0.871f };
// Purple
colorMap[new BlockColor { Color = BlockColors.Purple, Darkness = 0 }] = new float[3] { 0.764f, 0.587f, 1f };
colorMap[new BlockColor { Color = BlockColors.Purple, Darkness = 1 }] = new float[3] { 0.893f, 0.966f, 0.992f };
colorMap[new BlockColor { Color = BlockColors.Purple, Darkness = 2 }] = new float[3] { 0.842f, 0.877f, 0.991f };
colorMap[new BlockColor { Color = BlockColors.Purple, Darkness = 3 }] = new float[3] { 0.794f, 0.747f, 0.99f };
colorMap[new BlockColor { Color = BlockColors.Purple, Darkness = 4 }] = new float[3] { 0.783f, 0.669f, 0.992f };
colorMap[new BlockColor { Color = BlockColors.Purple, Darkness = 5 }] = new float[3] { 0.636f, 0.249f, 0.991f };
colorMap[new BlockColor { Color = BlockColors.Purple, Darkness = 6 }] = new float[3] { 0.548f, 0.18f, 0.896f };
colorMap[new BlockColor { Color = BlockColors.Purple, Darkness = 7 }] = new float[3] { 0.441f, 0.152f, 0.726f };
colorMap[new BlockColor { Color = BlockColors.Purple, Darkness = 8 }] = new float[3] { 0.308f, 0.135f, 0.498f };
colorMap[new BlockColor { Color = BlockColors.Purple, Darkness = 9 }] = new float[3] { 0.659f, 0.646f, 0.909f };
// Blue
colorMap[new BlockColor { Color = BlockColors.Blue, Darkness = 0 }] = new float[3] { 0.449f, 0.762f, 1f };
colorMap[new BlockColor { Color = BlockColors.Blue, Darkness = 1 }] = new float[3] { 0.856f, 0.971f, 0.992f };
colorMap[new BlockColor { Color = BlockColors.Blue, Darkness = 2 }] = new float[3] { 0.767f, 0.907f, 0.989f };
colorMap[new BlockColor { Color = BlockColors.Blue, Darkness = 3 }] = new float[3] { 0.642f, 0.836f, 0.992f };
colorMap[new BlockColor { Color = BlockColors.Blue, Darkness = 4 }] = new float[3] { 0.564f, 0.812f, 0.989f };
colorMap[new BlockColor { Color = BlockColors.Blue, Darkness = 5 }] = new float[3] { 0.211f, 0.621f, 0.989f };
colorMap[new BlockColor { Color = BlockColors.Blue, Darkness = 6 }] = new float[3] { 0.143f, 0.525f, 0.882f };
colorMap[new BlockColor { Color = BlockColors.Blue, Darkness = 7 }] = new float[3] { 0.114f, 0.410f, 0.705f };
colorMap[new BlockColor { Color = BlockColors.Blue, Darkness = 8 }] = new float[3] { 0.116f, 0.289f, 0.481f };
colorMap[new BlockColor { Color = BlockColors.Blue, Darkness = 9 }] = new float[3] { 0.571f, 0.701f, 0.901f };
// Aqua
colorMap[new BlockColor { Color = BlockColors.Aqua, Darkness = 0 }] = new float[3] { 0.408f, 0.963f, 1f };
colorMap[new BlockColor { Color = BlockColors.Aqua, Darkness = 1 }] = new float[3] { 0.838f, 0.976f, 0.990f };
colorMap[new BlockColor { Color = BlockColors.Aqua, Darkness = 2 }] = new float[3] { 0.747f, 0.961f, 0.994f };
colorMap[new BlockColor { Color = BlockColors.Aqua, Darkness = 3 }] = new float[3] { 0.605f, 0.948f, 0.990f };
colorMap[new BlockColor { Color = BlockColors.Aqua, Darkness = 4 }] = new float[3] { 0.534f, 0.954f, 0.993f };
colorMap[new BlockColor { Color = BlockColors.Aqua, Darkness = 5 }] = new float[3] { 0.179f, 0.841f, 0.991f };
colorMap[new BlockColor { Color = BlockColors.Aqua, Darkness = 6 }] = new float[3] { 0.121f, 0.719f, 0.868f };
colorMap[new BlockColor { Color = BlockColors.Aqua, Darkness = 7 }] = new float[3] { 0.117f, 0.574f, 0.687f };
colorMap[new BlockColor { Color = BlockColors.Aqua, Darkness = 8 }] = new float[3] { 0.116f, 0.399f, 0.478f };
colorMap[new BlockColor { Color = BlockColors.Aqua, Darkness = 9 }] = new float[3] { 0.556f, 0.768f, 0.901f };
// Green
colorMap[new BlockColor { Color = BlockColors.Green, Darkness = 0 }] = new float[3] { 0.344f, 1f, 0.579f };
colorMap[new BlockColor { Color = BlockColors.Green, Darkness = 1 }] = new float[3] { 0.823f, 0.977f, 0.994f };
colorMap[new BlockColor { Color = BlockColors.Green, Darkness = 2 }] = new float[3] { 0.731f, 0.966f, 0.958f };
colorMap[new BlockColor { Color = BlockColors.Green, Darkness = 3 }] = new float[3] { 0.643f, 0.964f, 0.873f };
colorMap[new BlockColor { Color = BlockColors.Green, Darkness = 4 }] = new float[3] { 0.498f, 0.961f, 0.721f };
colorMap[new BlockColor { Color = BlockColors.Green, Darkness = 5 }] = new float[3] { 0.176f, 0.853f, 0.415f };
colorMap[new BlockColor { Color = BlockColors.Green, Darkness = 6 }] = new float[3] { 0.120f, 0.728f, 0.350f };
colorMap[new BlockColor { Color = BlockColors.Green, Darkness = 7 }] = new float[3] { 0.105f, 0.560f, 0.264f };
colorMap[new BlockColor { Color = BlockColors.Green, Darkness = 8 }] = new float[3] { 0.122f, 0.392f, 0.221f };
colorMap[new BlockColor { Color = BlockColors.Green, Darkness = 9 }] = new float[3] { 0.542f, 0.771f, 0.717f };
// Lime
colorMap[new BlockColor { Color = BlockColors.Lime, Darkness = 0 }] = new float[3] { 0.705f, 1f, 0.443f };
colorMap[new BlockColor { Color = BlockColors.Lime, Darkness = 1 }] = new float[3] { 0.869f, 0.978f, 0.991f };
colorMap[new BlockColor { Color = BlockColors.Lime, Darkness = 2 }] = new float[3] { 0.815f, 0.967f, 0.932f };
colorMap[new BlockColor { Color = BlockColors.Lime, Darkness = 3 }] = new float[3] { 0.778f, 0.962f, 0.821f };
colorMap[new BlockColor { Color = BlockColors.Lime, Darkness = 4 }] = new float[3] { 0.753f, 0.964f, 0.631f };
colorMap[new BlockColor { Color = BlockColors.Lime, Darkness = 5 }] = new float[3] { 0.599f, 0.855f, 0.268f };
colorMap[new BlockColor { Color = BlockColors.Lime, Darkness = 6 }] = new float[3] { 0.505f, 0.712f, 0.201f };
colorMap[new BlockColor { Color = BlockColors.Lime, Darkness = 7 }] = new float[3] { 0.376f, 0.545f, 0.185f };
colorMap[new BlockColor { Color = BlockColors.Lime, Darkness = 8 }] = new float[3] { 0.268f, 0.379f, 0.172f };
colorMap[new BlockColor { Color = BlockColors.Lime, Darkness = 9 }] = new float[3] { 0.631f, 0.768f, 0.690f };
// Yellow
colorMap[new BlockColor { Color = BlockColors.Yellow, Darkness = 0 }] = new float[3] { 0.893f, 1f, 0.457f };
colorMap[new BlockColor { Color = BlockColors.Yellow, Darkness = 1 }] = new float[3] { 0.887f, 0.981f, 0.995f };
colorMap[new BlockColor { Color = BlockColors.Yellow, Darkness = 2 }] = new float[3] { 0.878f, 0.971f, 0.920f };
colorMap[new BlockColor { Color = BlockColors.Yellow, Darkness = 3 }] = new float[3] { 0.874f, 0.964f, 0.802f };
colorMap[new BlockColor { Color = BlockColors.Yellow, Darkness = 4 }] = new float[3] { 0.875f, 0.964f, 0.619f };
colorMap[new BlockColor { Color = BlockColors.Yellow, Darkness = 5 }] = new float[3] { 0.771f, 0.846f, 0.246f };
colorMap[new BlockColor { Color = BlockColors.Yellow, Darkness = 6 }] = new float[3] { 0.638f, 0.703f, 0.192f };
colorMap[new BlockColor { Color = BlockColors.Yellow, Darkness = 7 }] = new float[3] { 0.477f, 0.522f, 0.142f };
colorMap[new BlockColor { Color = BlockColors.Yellow, Darkness = 8 }] = new float[3] { 0.330f, 0.363f, 0.151f };
colorMap[new BlockColor { Color = BlockColors.Yellow, Darkness = 9 }] = new float[3] { 0.693f, 0.763f, 0.678f };
// Orange
colorMap[new BlockColor { Color = BlockColors.Orange, Darkness = 0 }] = new float[3] { 0.891f, 0.750f, 0.423f };
colorMap[new BlockColor { Color = BlockColors.Orange, Darkness = 1 }] = new float[3] { 0.883f, 0.948f, 0.992f };
colorMap[new BlockColor { Color = BlockColors.Orange, Darkness = 2 }] = new float[3] { 0.877f, 0.873f, 0.894f };
colorMap[new BlockColor { Color = BlockColors.Orange, Darkness = 3 }] = new float[3] { 0.878f, 0.831f, 0.771f };
colorMap[new BlockColor { Color = BlockColors.Orange, Darkness = 4 }] = new float[3] { 0.886f, 0.801f, 0.595f };
colorMap[new BlockColor { Color = BlockColors.Orange, Darkness = 5 }] = new float[3] { 0.777f, 0.621f, 0.241f };
colorMap[new BlockColor { Color = BlockColors.Orange, Darkness = 6 }] = new float[3] { 0.637f, 0.507f, 0.168f };
colorMap[new BlockColor { Color = BlockColors.Orange, Darkness = 7 }] = new float[3] { 0.466f, 0.364f, 0.123f };
colorMap[new BlockColor { Color = BlockColors.Orange, Darkness = 8 }] = new float[3] { 0.323f, 0.266f, 0.138f };
colorMap[new BlockColor { Color = BlockColors.Orange, Darkness = 9 }] = new float[3] { 0.689f, 0.672f, 0.667f };
// Red
colorMap[new BlockColor { Color = BlockColors.Red, Darkness = 0 }] = new float[3] { 0.890f, 0.323f, 0.359f };
colorMap[new BlockColor { Color = BlockColors.Red, Darkness = 1 }] = new float[3] { 0.879f, 0.863f, 0.987f };
colorMap[new BlockColor { Color = BlockColors.Red, Darkness = 2 }] = new float[3] { 0.872f, 0.758f, 0.868f };
colorMap[new BlockColor { Color = BlockColors.Red, Darkness = 3 }] = new float[3] { 0.887f, 0.663f, 0.756f };
colorMap[new BlockColor { Color = BlockColors.Red, Darkness = 4 }] = new float[3] { 0.903f, 0.546f, 0.608f };
colorMap[new BlockColor { Color = BlockColors.Red, Darkness = 5 }] = new float[3] { 0.785f, 0.222f, 0.222f };
colorMap[new BlockColor { Color = BlockColors.Red, Darkness = 6 }] = new float[3] { 0.641f, 0.155f, 0.152f };
colorMap[new BlockColor { Color = BlockColors.Red, Darkness = 7 }] = new float[3] { 0.455f, 0.105f, 0.108f };
colorMap[new BlockColor { Color = BlockColors.Red, Darkness = 8 }] = new float[3] { 0.320f, 0.121f, 0.133f };
colorMap[new BlockColor { Color = BlockColors.Red, Darkness = 9 }] = new float[3] { 0.687f, 0.571f, 0.661f };
// default
colorMap[new BlockColor { Color = BlockColors.Default, Darkness = 0 }] = new float[3] { -1f, -1f, -1f };
}
private static void BuildBotColorMap()
{
botColorMap = new Dictionary<byte, BlockColor>();
// standard colours
botColorMap[0] = new BlockColor { Color = BlockColors.White, Darkness = 0 };
botColorMap[1] = new BlockColor { Color = BlockColors.White, Darkness = 6 };
botColorMap[4] = new BlockColor { Color = BlockColors.White, Darkness = 8 };
botColorMap[5] = new BlockColor { Color = BlockColors.Red, Darkness = 5 };
botColorMap[2] = new BlockColor { Color = BlockColors.Orange, Darkness = 0 };
botColorMap[6] = new BlockColor { Color = BlockColors.Yellow, Darkness = 0 };
botColorMap[7] = new BlockColor { Color = BlockColors.Green, Darkness = 5 };
botColorMap[3] = new BlockColor { Color = BlockColors.Aqua, Darkness = 5 };
botColorMap[9] = new BlockColor { Color = BlockColors.Blue, Darkness = 5 };
botColorMap[10] = new BlockColor { Color = BlockColors.Purple, Darkness = 5 };
// premium colours
botColorMap[16] = new BlockColor { Color = BlockColors.Red, Darkness = 0 };
botColorMap[17] = new BlockColor { Color = BlockColors.Red, Darkness = 7 };
botColorMap[11] = new BlockColor { Color = BlockColors.Orange, Darkness = 6 };
botColorMap[18] = new BlockColor { Color = BlockColors.Purple, Darkness = 9 };
botColorMap[19] = new BlockColor { Color = BlockColors.Pink, Darkness = 9 };
botColorMap[20] = new BlockColor { Color = BlockColors.Orange, Darkness = 5 };
botColorMap[14] = new BlockColor { Color = BlockColors.Yellow, Darkness = 3 };
botColorMap[21] = new BlockColor { Color = BlockColors.Green, Darkness = 7 };
botColorMap[22] = new BlockColor { Color = BlockColors.Lime, Darkness = 8 };
botColorMap[13] = new BlockColor { Color = BlockColors.Green, Darkness = 6 };
botColorMap[12] = new BlockColor { Color = BlockColors.Lime, Darkness = 5 };
// blue gang
botColorMap[23] = new BlockColor { Color = BlockColors.Blue, Darkness = 8 };
botColorMap[24] = new BlockColor { Color = BlockColors.Aqua, Darkness = 8 };
botColorMap[25] = new BlockColor { Color = BlockColors.Blue, Darkness = 7 };
botColorMap[26] = new BlockColor { Color = BlockColors.White, Darkness = 5 };
botColorMap[27] = new BlockColor { Color = BlockColors.White, Darkness = 4 };
botColorMap[28] = new BlockColor { Color = BlockColors.Aqua, Darkness = 4 };
botColorMap[29] = new BlockColor { Color = BlockColors.Purple, Darkness = 8 };
// purples & pinks
botColorMap[30] = new BlockColor { Color = BlockColors.Pink, Darkness = 0 };
botColorMap[8] = new BlockColor { Color = BlockColors.Pink, Darkness = 5 };
botColorMap[31] = new BlockColor { Color = BlockColors.Pink, Darkness = 4 };
botColorMap[15] = new BlockColor { Color = BlockColors.Red, Darkness = 3 };
}
private class AsyncRunner : ISchedulable
{
public IEnumerator<TaskContract> Run()
{
AsyncOperationHandle<TextAsset> asyncHandle = Addressables.LoadAssetAsync<TextAsset>("colours");
yield return asyncHandle.Continue();
Dictionary<string, object> colourData = Json.Deserialize(asyncHandle.Result.text) as Dictionary<string, object>;
if (colourData == null) yield break;
Client.EnterMenu -= LoadColorMenuEvent;
// Logging.MetaLog((List<object>)((colourData["Colours"] as Dictionary<string, object>)["Data"] as Dictionary<string, object>)["Slots"]);
// Generate color map
List<object> hexColors =
(((colourData["Colours"] as Dictionary<string, object>)?["Data"] as Dictionary<string, object>)?
["Slots"] as List<object>);
int count = 0;
colorMap = new Dictionary<BlockColor, float[]>();
for (byte d = 0; d < 10; d++)
{
foreach (BlockColors c in Enum.GetValues(typeof(BlockColors)))
{
if (c != BlockColors.Default)
{
BlockColor colorStruct = new BlockColor
{
Color = c,
Darkness = d,
};
Color pixel = Images.PixelUtility.PixelHex((string)hexColors[count]);
colorMap[colorStruct] = new float[] {pixel.r, pixel.g, pixel.b};
count++;
}
}
yield return asyncHandle.Continue();
}
}
}
}
}

653
Pixi/Common/CommandRoot.cs Normal file
View file

@ -0,0 +1,653 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using UnityEngine;
using Unity.Mathematics;
using Svelto.ECS;
using GamecraftModdingAPI;
using GamecraftModdingAPI.Blocks;
using GamecraftModdingAPI.Commands;
using GamecraftModdingAPI.Utility;
using Svelto.DataStructures;
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 static ThreadSafeDictionary<int, bool> optimisableBlockCache = new ThreadSafeDictionary<int, bool>();
public const float BLOCK_SIZE = 0.2f;
public const float DELTA = BLOCK_SIZE / 2048;
public static int OPTIMISATION_PASSES = 2;
public static int GROUP_SIZE = 32;
// optimisation algorithm constants
private static float3[] cornerMultiplicands1 = 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),
};
private static float3[] cornerMultiplicands2 = 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),
};
private static 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
};
private static 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
};
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 == null || 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, (pass + 1) * GROUP_SIZE);
#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;
}
#if DEBUG
else
{
Logging.LogWarning($"Found invalid block at index {i}\n\t{optVONsArr[i].ToString()}");
}
#endif
}
// handle special block parameters
PostProcessSpecialBlocks(ref optVONsArr, ref blocks);
// post processing
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, int chunkSize)
{
// Reduce blocks to place to reduce lag while placing and from excessive blocks in the world.
// Blocks are reduced by grouping similar blocks that are touching (before they're placed)
// multithreaded because this is an expensive (slow) operation
int item = 0;
ProcessedVoxelObjectNotation[][] groups = new ProcessedVoxelObjectNotation[optVONs.Count / chunkSize][];
Thread[] tasks = new Thread[groups.Length];
while (item < groups.Length)
{
groups[item] = new ProcessedVoxelObjectNotation[chunkSize];
optVONs.CopyTo(item * chunkSize, groups[item], 0, chunkSize);
int tmpItem = item; // scope is dumb
tasks[item] = new Thread(() =>
{
groups[tmpItem] = groupBlocksBestEffort(groups[tmpItem], tmpItem);
});
tasks[item].Start();
item++;
}
#if DEBUG
Logging.MetaLog($"Created {groups.Length} + 1? groups");
#endif
// final group
ProcessedVoxelObjectNotation[] finalGroup = null;
Thread finalThread = null;
if (optVONs.Count > item * chunkSize)
{
//finalGroup = optVONs.GetRange(item * GROUP_SIZE, optVONs.Count - (item * GROUP_SIZE)).ToArray();
finalGroup = new ProcessedVoxelObjectNotation[optVONs.Count - (item * chunkSize)];
optVONs.CopyTo(item * chunkSize, finalGroup, 0, optVONs.Count - (item * chunkSize));
finalThread = new Thread(() =>
{
finalGroup = groupBlocksBestEffort(finalGroup, -1);
});
finalThread.Start();
}
// gather results
List<ProcessedVoxelObjectNotation> result = new List<ProcessedVoxelObjectNotation>();
for (int i = 0; i < groups.Length; i++)
{
#if DEBUG
Logging.MetaLog($"Waiting for completion of task {i}");
#endif
tasks[i].Join();
result.AddRange(groups[i]);
}
if (finalThread != null)
{
#if DEBUG
Logging.MetaLog($"Waiting for completion of final task");
#endif
finalThread.Join();
result.AddRange(finalGroup);
}
optVONs = result;
}
private static ProcessedVoxelObjectNotation[] groupBlocksBestEffort(ProcessedVoxelObjectNotation[] blocksToOptimise, int id)
{
// 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)
try
{
#if DEBUG
Stopwatch timer = Stopwatch.StartNew();
#endif
FasterList<ProcessedVoxelObjectNotation> optVONs = new FasterList<ProcessedVoxelObjectNotation>(blocksToOptimise);
int item = 0;
while (item < optVONs.count - 1)
{
#if DEBUG
Logging.MetaLog($"({id}) Now grouping item {item}/{optVONs.count} ({100f * item/(float)optVONs.count}%)");
#endif
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++;
}
}
#if DEBUG
timer.Stop();
Logging.MetaLog($"({id}) Completed best effort grouping of range in {timer.ElapsedMilliseconds}ms");
#endif
return optVONs.ToArray();
}
catch (Exception e)
{
Logging.MetaLog($"({id}) Exception occured...\n{e.ToString()}");
}
return blocksToOptimise;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static float3[] calculateCorners(ProcessedVoxelObjectNotation von)
{
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 * (cornerMultiplicands1[i] * rotatedScale / 2);
}
return corners;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void updateVonFromCorners(float3[] corners, ref ProcessedVoxelObjectNotation von)
{
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})");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int[][] findMatchingCorners(float3[] corners1, float3[] corners2)
{
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
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static 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
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static 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];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static 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 static bool isOptimisableBlock(BlockIDs block)
{
if (optimisableBlockCache.ContainsKey((int) block))
{
return optimisableBlockCache[(int) block];
}
bool result = block.ToString().EndsWith("Cube", StringComparison.InvariantCultureIgnoreCase);
optimisableBlockCache[(int) block] = result;
return result;
}
private static void PostProcessSpecialBlocks(ref ProcessedVoxelObjectNotation[] pVONs, ref Block[] blocks)
{
// populate block attributes using metadata field from ProcessedVoxelObjectNotation
for (int i = 0; i < pVONs.Length; i++)
{
switch (pVONs[i].block)
{
case BlockIDs.TextBlock:
string[] textSplit = pVONs[i].metadata.Split('\t');
if (textSplit.Length > 1)
{
TextBlock tb = blocks[i].Specialise<TextBlock>();
tb.Text = textSplit[1];
if (textSplit.Length > 2)
{
tb.TextBlockId = textSplit[2];
}
}
break;
case BlockIDs.ConsoleBlock:
string[] cmdSplit = pVONs[i].metadata.Split('\t');
if (cmdSplit.Length > 1)
{
ConsoleBlock cb = blocks[i].Specialise<ConsoleBlock>();
cb.Command = cmdSplit[1];
if (cmdSplit.Length > 2)
{
cb.Arg1 = cmdSplit[2];
if (cmdSplit.Length > 3)
{
cb.Arg1 = cmdSplit[3];
if (cmdSplit.Length > 4)
{
cb.Arg1 = cmdSplit[4];
}
}
}
}
break;
case BlockIDs.DampedSpring:
string[] springSplit = pVONs[i].metadata.Split('\t');
if (springSplit.Length > 1 && float.TryParse(springSplit[1], out float stiffness))
{
DampedSpring d = blocks[i].Specialise<DampedSpring>();
d.Stiffness = stiffness;
if (springSplit.Length > 2 && float.TryParse(springSplit[2], out float damping))
{
d.Damping = damping;
}
}
break;
case BlockIDs.ServoAxle:
case BlockIDs.ServoHinge:
case BlockIDs.PneumaticAxle:
case BlockIDs.PneumaticHinge:
string[] servoSplit = pVONs[i].metadata.Split('\t');
if (servoSplit.Length > 1 && float.TryParse(servoSplit[1], out float minAngle))
{
Servo s = blocks[i].Specialise<Servo>();
s.MinimumAngle = minAngle;
if (servoSplit.Length > 2 && float.TryParse(servoSplit[2], out float maxAngle))
{
s.MaximumAngle = maxAngle;
if (servoSplit.Length > 3 && float.TryParse(servoSplit[3], out float maxForce))
{
s.MaximumForce = maxForce;
if (servoSplit.Length > 4 && bool.TryParse(servoSplit[4], out bool reverse))
{
s.Reverse = reverse;
}
}
}
}
break;
case BlockIDs.MotorM:
case BlockIDs.MotorS:
string[] motorSplit = pVONs[i].metadata.Split('\t');
if (motorSplit.Length > 1 && float.TryParse(motorSplit[1], out float topSpeed))
{
Motor m = blocks[i].Specialise<Motor>();
m.TopSpeed = topSpeed;
if (motorSplit.Length > 2 && float.TryParse(motorSplit[2], out float torque))
{
m.Torque = torque;
if (motorSplit.Length > 3 && bool.TryParse(motorSplit[3], out bool reverse))
{
m.Reverse = reverse;
}
}
}
break;
default: break; // do nothing
}
}
}
private static 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 Pixi\n" + e);
#else
Logging.CommandLogError("Pixi failed (reason: " + e.Message + ")");
#endif
Logging.LogWarning("Pixi Error\n" + e);
}
}
}
}

View file

@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Unity.Mathematics;
using GamecraftModdingAPI.Blocks;
namespace Pixi.Common
{
public static class ConversionUtility
{
private static Dictionary<string, BlockIDs> blockEnumMap = null;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void loadBlockEnumMap()
{
blockEnumMap = new Dictionary<string, BlockIDs>();
foreach(BlockIDs e in Enum.GetValues(typeof(BlockIDs)))
{
blockEnumMap[e.ToString()] = e;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static BlockIDs BlockIDsToEnum(string name)
{
if (blockEnumMap == null) loadBlockEnumMap();
if (blockEnumMap.ContainsKey(name)) return blockEnumMap[name];
return BlockIDs.Invalid;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float3 FloatArrayToFloat3(float[] vec)
{
if (vec.Length < 3) return float3.zero;
return new float3(vec[0], vec[1], vec[2]);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float[] Float3ToFloatArray(float3 vec)
{
return new float[3] {vec.x, vec.y, vec.z};
}
}
}

27
Pixi/Common/Importer.cs Normal file
View file

@ -0,0 +1,27 @@
using GamecraftModdingAPI;
namespace Pixi.Common
{
/// <summary>
/// Thing importer.
/// This imports the thing by converting it to a common block format that Pixi can understand.
/// </summary>
public interface Importer
{
int Priority { get; }
bool Optimisable { get; }
string Name { get; }
BlueprintProvider BlueprintProvider { get; }
bool Qualifies(string name);
BlockJsonInfo[] Import(string name);
void PreProcess(string name, ref ProcessedVoxelObjectNotation[] blocks);
void PostProcess(string name, ref Block[] blocks);
}
}

View file

@ -0,0 +1,40 @@
using Unity.Mathematics;
using GamecraftModdingAPI.Blocks;
namespace Pixi.Common
{
public struct ProcessedVoxelObjectNotation
{
public BlockIDs block;
public BlockColor color;
public bool blueprint;
public float3 position;
public float3 rotation;
public float3 scale;
public string metadata;
internal BlockJsonInfo VoxelObjectNotation()
{
return new BlockJsonInfo
{
name = block == BlockIDs.Invalid ? metadata.Split(' ')[0] : block.ToString(),
color = ColorSpaceUtility.UnquantizeToArray(color),
position = ConversionUtility.Float3ToFloatArray(position),
rotation = ConversionUtility.Float3ToFloatArray(rotation),
scale = ConversionUtility.Float3ToFloatArray(scale),
};
}
public override string ToString()
{
return $"ProcessedVoxelObjectNotation {{ block:{block}, color:{color.Color}-{color.Darkness}, blueprint:{blueprint}, position:{position}, rotation:{rotation}, scale:{scale}}} ({metadata})";
}
}
}

View file

@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using System.Text;
using Newtonsoft.Json;
using GamecraftModdingAPI;
using GamecraftModdingAPI.Blocks;
namespace Pixi.Common
{
public static class VoxelObjectNotationUtility
{
private static readonly float[] origin_base = new float[3] { 0, 0, 0 };
private static Dictionary<string, BlockIDs> enumMap = null;
public static string SerializeBlocks(Block[] blocks, float[] origin = null)
{
BlockJsonInfo[] blockJsons = new BlockJsonInfo[blocks.Length];
for (int i = 0; i < blocks.Length; i++)
{
blockJsons[i] = JsonObject(blocks[i], origin);
}
return JsonConvert.SerializeObject(blockJsons);
}
public static byte[] SerializeBlocksToBytes(Block[] blocks)
{
return Encoding.UTF8.GetBytes(SerializeBlocks(blocks));
}
public static BlockJsonInfo[] DeserializeBlocks(byte[] data)
{
return DeserializeBlocks(Encoding.UTF8.GetString(data));
}
public static BlockJsonInfo[] DeserializeBlocks(string data)
{
return JsonConvert.DeserializeObject<BlockJsonInfo[]>(data);
}
public static BlockJsonInfo JsonObject(Block block, float[] origin = null)
{
if (origin == null) origin = origin_base;
BlockJsonInfo jsonInfo = new BlockJsonInfo
{
name = block.Type.ToString(),
position = new float[3] { block.Position.x - origin[0], block.Position.y - origin[1], block.Position.z - origin[2]},
rotation = new float[3] { block.Rotation.x, block.Rotation.y, block.Rotation.z },
color = ColorSpaceUtility.UnquantizeToArray(block.Color),
scale = new float[3] {block.Scale.x, block.Scale.y, block.Scale.z},
};
// custom stats for special blocks
switch (block.Type)
{
case BlockIDs.TextBlock:
TextBlock t = block.Specialise<TextBlock>();
jsonInfo.name += "\t" + t.Text + "\t" + t.TextBlockId;
break;
case BlockIDs.ConsoleBlock:
ConsoleBlock c = block.Specialise<ConsoleBlock>();
jsonInfo.name += "\t" + c.Command + "\t" + c.Arg1 + "\t" + c.Arg2 + "\t" + c.Arg3;
break;
case BlockIDs.DampedSpring:
DampedSpring d = block.Specialise<DampedSpring>();
jsonInfo.name += "\t" + d.Stiffness + "\t" + d.Damping;
break;
case BlockIDs.ServoAxle:
case BlockIDs.ServoHinge:
case BlockIDs.PneumaticAxle:
case BlockIDs.PneumaticHinge:
Servo s = block.Specialise<Servo>();
jsonInfo.name += "\t" + s.MinimumAngle + "\t" + s.MaximumAngle + "\t" + s.MaximumForce + "\t" +
s.Reverse;
break;
case BlockIDs.MotorM:
case BlockIDs.MotorS:
Motor m = block.Specialise<Motor>();
jsonInfo.name += "\t" + m.TopSpeed + "\t" + m.Torque + "\t" + m.Reverse;
break;
default: break;
}
return jsonInfo;
}
public static BlockIDs NameToEnum(BlockJsonInfo block)
{
return NameToEnum(block.name);
}
public static BlockIDs NameToEnum(string name)
{
if (enumMap == null) GenerateEnumMap();
return enumMap[name];
}
private static void GenerateEnumMap()
{
enumMap = new Dictionary<string, BlockIDs>();
foreach(BlockIDs e in Enum.GetValues(typeof(BlockIDs)))
{
enumMap[e.ToString()] = e;
}
}
}
}

View file

@ -0,0 +1,99 @@
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 ImageCanvasImporter()
{
GamecraftModdingAPI.App.Client.EnterMenu += ColorSpaceUtility.LoadColorMenuEvent;
}
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) { }
}
}

View file

@ -0,0 +1,87 @@
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
{
color = new float[] {-1f, -1f, -1f},
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);
}
}
}

View file

@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using Unity.Mathematics;
using UnityEngine;
using GamecraftModdingAPI;
using GamecraftModdingAPI.Blocks;
using GamecraftModdingAPI.Players;
using GamecraftModdingAPI.Utility;
using Pixi.Common;
namespace Pixi.Images
{
public class ImageTextBlockImporter : Importer
{
public int Priority { get; } = 0;
public bool Optimisable { get; } = false;
public string Name { get; } = "ImageText~Spell";
public BlueprintProvider BlueprintProvider { get; } = null;
private Dictionary<string, string[]> textBlockContents = new Dictionary<string, string[]>();
public bool Qualifies(string name)
{
return name.EndsWith(".png", StringComparison.InvariantCultureIgnoreCase)
|| name.EndsWith(".jpg", StringComparison.InvariantCultureIgnoreCase);
}
public BlockJsonInfo[] Import(string name)
{
Texture2D img = new Texture2D(64, 64);
// load file into texture
try
{
byte[] imgData = File.ReadAllBytes(name);
img.LoadImage(imgData);
}
catch (Exception e)
{
Logging.CommandLogError($"Failed to load picture data. Reason: {e.Message}");
Logging.MetaLog(e.Message + "\n" + e.StackTrace);
return new BlockJsonInfo[0];
}
string text = PixelUtility.TextureToString(img);
// generate text block name
byte[] textHash;
using (HashAlgorithm hasher = SHA256.Create())
textHash = hasher.ComputeHash(Encoding.UTF8.GetBytes(text));
string textId = "Pixi_";
for (int i = 0; i < 2 && i < textHash.Length; i++)
{
textId += textHash[i].ToString("X2");
}
// save text block info for post-processing
textBlockContents[name] = new string[2] { textId, text};
return new BlockJsonInfo[1]
{
new BlockJsonInfo
{
color = new float[] {-1f, -1f, -1f},
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);
}
}
}

View file

@ -0,0 +1,61 @@
using System;
using System.Runtime.CompilerServices;
using System.Text;
using UnityEngine;
using GamecraftModdingAPI.Blocks;
using GamecraftModdingAPI.Utility;
using Pixi.Common;
namespace Pixi.Images
{
public static class PixelUtility
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string HexPixel(Color pixel)
{
return "#"+ColorUtility.ToHtmlStringRGBA(pixel);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Color PixelHex(string hex)
{
if (ColorUtility.TryParseHtmlString(hex, out Color result))
{
return result;
}
return default;
}
public static string TextureToString(Texture2D img)
{
StringBuilder imgString = new StringBuilder("<cspace=-0.13em><line-height=40%>");
bool lastPixelAssigned = false;
Color lastPixel = new Color();
for (int y = img.height-1; y >= 0 ; y--) // text origin is top right, but img origin is bottom right
{
for (int x = 0; x < img.width; x++)
{
Color pixel = img.GetPixel(x, y);
if (!lastPixelAssigned || lastPixel != pixel)
{
imgString.Append("<color=");
imgString.Append(HexPixel(pixel));
imgString.Append(">");
lastPixel = pixel;
if (!lastPixelAssigned)
{
lastPixelAssigned = true;
}
}
imgString.Append("\u25a0");
}
imgString.Append("<br>");
}
imgString.Append("</color></cspace></line-height>");
return imgString.ToString();
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -7,283 +7,59 @@ using UnityEngine;
using Unity.Mathematics; // float3
using IllusionPlugin;
// using GamecraftModdingAPI;
using GamecraftModdingAPI.Commands;
using GamecraftModdingAPI.Utility;
using GamecraftModdingAPI.Blocks;
using Pixi.Audio;
using Pixi.Common;
using Pixi.Images;
using Pixi.Robots;
namespace Pixi
{
public class PixiPlugin : IPlugin // the Illusion Plugin Architecture (IPA) will ignore classes that don't implement IPlugin'
public class PixiPlugin : IEnhancedPlugin // the Illusion Plugin Architecture (IPA) will ignore classes that don't implement IPlugin'
{
public string Name { get; } = Assembly.GetExecutingAssembly().GetName().Name; // Pixi
public override 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 override string Version { get; } = Assembly.GetExecutingAssembly().GetName().Version.ToString();
// To change the version, change <Version>#.#.#</Version> in Pixi.csproj
private uint width = 32;
private uint height = 32;
private double blockSize = 0.2;
private PlayerLocationEngine playerLocationEngine = new PlayerLocationEngine();
// called when Gamecraft shuts down
public void OnApplicationQuit()
public override 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();
}
// called when Gamecraft starts up
public void OnApplicationStart()
public override void OnApplicationStart()
{
// Initialize the Gamecraft modding API first
GamecraftModdingAPI.Main.Init();
// check out the modding API docs here: https://mod.exmods.org/
// Initialize Pixi mod
// create SimpleCustomCommandEngine for 2D image importing
SimpleCustomCommandEngine<string> pixelate2DCommand = new SimpleCustomCommandEngine<string>(
pixelate2DFile, // command action
"Pixi2D", // command name (used to invoke it in the console)
"Converts an image to blocks.\nLarger images will freeze your game until conversion completes. (Pixi)" // command description (displayed when help command is executed)
);
SimpleCustomCommandEngine<string> pixelate3DCommand = new SimpleCustomCommandEngine<string>(
pixelate3DFile, // command action
"Pixi3D", // command name (used to invoke it in the console)
"Converts a 3D model to blocks.\nLarger models will freeze your game until conversion completes. (Pixi)" // command description (displayed when help command is executed)
);
SimpleCustomCommandEngine<uint, uint> scaleCommand = new SimpleCustomCommandEngine<uint, uint>(
setScale, // command action
"PixiScale", // command name (used to invoke it in the console)
"Sets the image scale factor for Pixi2D.\nBigger images take longer to convert. (Pixi)" // command description (displayed when help command is executed)
);
// register commands so the modding API knows about it
CommandManager.AddCommand(pixelate2DCommand);
CommandManager.AddCommand(scaleCommand);
GameEngineManager.AddGameEngine(playerLocationEngine);
GamecraftModdingAPI.Utility.Logging.LogDebug($"{Name} has started up");
CommandRoot root = new CommandRoot();
// 2D Image Functionality
root.Inject(new ImageCanvasImporter());
root.Inject(new ImageTextBlockImporter());
root.Inject(new ImageCommandImporter());
// Robot functionality
var robot = new RobotInternetImporter();
root.Inject(robot);
//RobotCommands.CreateRobotCRFCommand();
//RobotCommands.CreateRobotFileCommand();
#if DEBUG
// Development functionality
RobotCommands.CreatePartDumpCommand();
((RobotBlueprintProvider) robot.BlueprintProvider).AddDebugCommands();
root.Inject(new TestImporter());
#endif
// Audio functionality
root.Inject(new MidiImporter());
root.Inject(new AudioFakeImporter());
}
// unused methods
public void OnFixedUpdate() { } // called once per physics update
public void OnLevelWasInitialized(int level) { } // called after a level is initialized
public void OnLevelWasLoaded(int level) { } // called after a level is loaded
public void OnUpdate() { } // called once per rendered frame (frame update)
// pixelation methods
private void pixelate2DFile(string filepath)
{
Logging.CommandLogWarning("Large images may freeze your game for a long period");
// Load image file and convert to Gamecraft blocks
Texture2D img = new Texture2D((int)width, (int)height);
// 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.LogException(e);
return;
}
float3 position = playerLocationEngine.GetPlayerLocation(0u);
uint blockCount = 0;
position.x += 1f;
//position.y += 1f;
float zero_y = position.y;
// 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 < width; x++)
{
QuantizedPixel qVoxel = new QuantizedPixel{color = BlockColors.Default, darkness = 10};
float3 scale = new float3(1, 1, 1);
position.x += (float)(blockSize);
for (int y = 0; y < height; y++)
{
//position.y += (float)blockSize;
Color pixel = img.GetPixel(x, y);
QuantizedPixel qPixel = quantizeColor(pixel);
if (qPixel.darkness != qVoxel.darkness || qPixel.color != qVoxel.color || qPixel.visible != qVoxel.visible)
{
if (y != 0)
{
if (qVoxel.visible)
{
position.y = zero_y + (float)((y * blockSize + (y - scale.y) * blockSize) / 2);
Placement.PlaceBlock(BlockIDs.AluminiumCube, position, color: qVoxel.color, darkness: qVoxel.darkness, scale: scale);
blockCount++;
}
scale = new float3(1, 1, 1);
}
qVoxel = qPixel;
}
else
{
scale.y += 1;
}
}
if (qVoxel.visible)
{
position.y = zero_y + (float)((height * blockSize + (height - scale.y) * blockSize) / 2);
Placement.PlaceBlock(BlockIDs.AluminiumCube, position, color: qVoxel.color, darkness: qVoxel.darkness, scale: scale);
blockCount++;
}
//position.y = zero_y;
}
Logging.CommandLog($"Placed {width}x{height} image beside you ({blockCount} blocks total)");
Logging.MetaLog($"Saved {(width * height) - blockCount} blocks while placing {filepath}");
}
private void setScale(uint _width, uint _height)
{
width = _width;
height = _height;
Logging.CommandLog($"Pixi image size set to {width}x{height}");
}
private void pixelate3DFile(string filepath)
{
// TODO?
Logging.CommandLogError("Oh no you found this command!\nCommand functionality not implemented (yet)");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private QuantizedPixel quantizeColor(Color pixel)
{
BlockColors color = BlockColors.Default;
int darkness = 0;
Logging.MetaDebugLog($"Color (r:{pixel.r}, g:{pixel.g}, b:{pixel.b})");
if (Mathf.Abs(pixel.r - pixel.g) < pixel.r * 0.1f && Mathf.Abs(pixel.r - pixel.b) < pixel.r * 0.1f)
{
color = BlockColors.White;
darkness = (int)(10 - ((pixel.r + pixel.g + pixel.b) * 3.5));
//Logging.MetaDebugLog($"Color (r:{pixel.r}, g:{pixel.g}, b:{pixel.b})");
}
else if (pixel.r >= pixel.g && pixel.r >= pixel.b)
{
// Red is highest
if ((pixel.r - pixel.g) > pixel.r * 0.5 && (pixel.r - pixel.b) > pixel.r * 0.5)
{
// Red is much higher than other pixels
darkness = (int)(10 - (pixel.r * 11));
color = BlockColors.Red;
}
else if ((pixel.g - pixel.b) > pixel.g * 0.3)
{
// Green is much higher than blue
darkness = (int)(10 - ((pixel.r + pixel.g) * 5.1));
if ((pixel.r - pixel.g) > pixel.r * 0.5)
{
color = BlockColors.Orange;
}
else
{
color = BlockColors.Yellow;
}
}
else if ((pixel.b - pixel.g) > pixel.b * 0.5)
{
// Blue is much higher than green
darkness = (int)(10 - ((pixel.r + pixel.b) * 4.9));
color = BlockColors.Purple;
}
else
{
// Green is close strength to blue
darkness = (int)(10 - ((pixel.r + pixel.g + pixel.b) * 4.8));
color = BlockColors.Pink;
}
}
else if (pixel.g >= pixel.r && pixel.g >= pixel.b)
{
// Green is highest
if ((pixel.g - pixel.r) > pixel.g * 0.5 && (pixel.g - pixel.b) > pixel.g * 0.5)
{
// Green is much higher than other pixels
darkness = (int)(10 - (pixel.g * 11));
color = BlockColors.Green;
}
else if ((pixel.r - pixel.b) > pixel.r * 0.5)
{
// Red is much higher than blue
darkness = (int)(10 - ((pixel.r + pixel.g) * 5.1));
color = BlockColors.Yellow;
}
else if ((pixel.b - pixel.r) > pixel.b * 0.5)
{
// Blue is much higher than red
darkness = (int)(10 - ((pixel.g + pixel.b) * 5.1));
color = BlockColors.Aqua;
}
else
{
// Red is close strength to blue
darkness = (int)(10 - ((pixel.r + pixel.g + pixel.b) * 4.8));
color = BlockColors.Lime;
}
}
else if (pixel.b >= pixel.g && pixel.b >= pixel.r)
{
// Blue is highest
if ((pixel.b - pixel.g) > pixel.b * 0.5 && (pixel.b - pixel.r) > pixel.b * 0.5)
{
// Blue is much higher than other pixels
darkness = (int)(10 - (pixel.b * 11));
color = BlockColors.Blue;
}
else if ((pixel.g - pixel.r) > pixel.g * 0.5)
{
// Green is much higher than red
darkness = (int)(10 - ((pixel.g + pixel.b) * 5.1));
color = BlockColors.Aqua;
}
else if ((pixel.r - pixel.g) > pixel.r * 0.5)
{
// Red is much higher than green
darkness = (int)(10 - ((pixel.r + pixel.b) * 4.9));
color = BlockColors.Purple;
}
else
{
// Green is close strength to red
darkness = (int)(10 - ((pixel.r + pixel.g + pixel.b) * 4.9));
color = BlockColors.Aqua;
}
}
if (darkness > 9) darkness = 9;
if (darkness < 0) darkness = 0;
// darkness 0 is the most saturated (it's not just the lightest)
Logging.MetaDebugLog($"Quantized Color {color} d:{darkness}");
return new QuantizedPixel { color = color, darkness = (byte)darkness, visible = pixel.a > 0.5f};
}
}
internal struct QuantizedPixel
{
public BlockColors color;
public byte darkness;
public bool visible;
}
}

View file

@ -1,26 +0,0 @@
using System;
using GamecraftModdingAPI.Utility;
using Svelto.ECS;
using Unity.Mathematics;
using RobocraftX.Physics;
using RobocraftX.Character;
namespace Pixi
{
internal class PlayerLocationEngine : IApiEngine
{
public string Name => "PixiPlayerLocationGameEngine";
public EntitiesDB entitiesDB { set; private get; }
public void Dispose() {}
public void Ready() {}
public float3 GetPlayerLocation(uint playerId)
{
return entitiesDB.QueryEntity<RigidBodyEntityStruct>(playerId, CharacterExclusiveGroups.OnFootGroup).position;
}
}
}

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

@ -0,0 +1,30 @@
using System;
using Unity.Mathematics;
using GamecraftModdingAPI.Blocks;
namespace Pixi.Robots
{
public struct CubeInfo
{
// 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 name;
public uint cubeId;
}
}

338
Pixi/Robots/CubeUtility.cs Normal file
View file

@ -0,0 +1,338 @@
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 UnityEngine;
using GamecraftModdingAPI.Blocks;
using GamecraftModdingAPI.Utility;
using GamecraftModdingAPI;
using Pixi.Common;
namespace Pixi.Robots
{
public static class CubeUtility
{
private static Dictionary<uint, string> map = null;
private static Dictionary<uint, BlockJsonInfo[]> blueprintMap = 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, cubeId = cubeId, scale = new float3(1, 1, 1)};
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)
#if DEBUG
Logging.MetaLog($"Cube colour {colour}");
#endif
BlockColor c = ColorSpaceUtility.QuantizeToBlockColor(colour);
result.color = c.Color;
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)
{
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.name = "Unknown cube #" + cubeId.ToString();
//result.rotation = float3.zero;
#if DEBUG
Logging.MetaLog($"Unknown cubeId {cubeId}");
#endif
}
string cubeName = map[cubeId];
string gcName = cubeName.Contains("glass") || cubeName.Contains("windshield")
? "Glass"
: "Aluminium";
if (cubeName.Contains("round"))
gcName += "Rounded";
if (cubeName.Contains("cube"))
gcName += "Cube";
else if (cubeName.Contains("prism") || cubeName.Contains("edge"))
gcName += "Slope";
else if (cubeName.Contains("inner"))
gcName += "SlicedCube";
else if (cubeName.Contains("tetra") || cubeName.Contains("corner"))
gcName += "Corner";
else if (cubeName.Contains("pyramid"))
gcName += "PyramidSegment";
else if (cubeName.Contains("cone"))
gcName += "ConeSegment";
else
{
result.block = BlockIDs.TextBlock;
result.name = cubeName;
return;
}
BlockIDs id = VoxelObjectNotationUtility.NameToEnum(gcName);
result.block = id;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Block[] BuildBlueprintOrTextBlock(CubeInfo cube, float3 actualPosition, int scale = 3)
{
// actualPosition is the middle of the cube
if (blueprintMap == null) LoadBlueprintMap();
if (!blueprintMap.ContainsKey(cube.cubeId) || scale != 3)
{
#if DEBUG
Logging.LogWarning($"Missing blueprint for {cube.name} (id:{cube.cubeId}), substituting {cube.block}");
#endif
return new Block[] { Block.PlaceNew(cube.block, actualPosition, cube.rotation, cube.color, cube.darkness, scale: cube.scale) };
}
#if DEBUG
Logging.MetaLog($"Found blueprint for {cube.name} (id:{cube.cubeId})");
#endif
Quaternion cubeQuaternion = Quaternion.Euler(cube.rotation);
BlockJsonInfo[] blueprint = blueprintMap[cube.cubeId];
if (blueprint.Length == 0)
{
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)(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 {cube.name} (id:{cube.cubeId}), this is not currently supported");
}
for (int i = 0; i < blueprint.Length; i++)
{
BlockColor blueprintBlockColor = ColorSpaceUtility.QuantizeToBlockColor(blueprint[i].color);
BlockColors blockColor = blueprintBlockColor.Color == BlockColors.White && blueprintBlockColor.Darkness == 0 ? cube.color : blueprintBlockColor.Color;
byte blockDarkness = blueprintBlockColor.Color == BlockColors.White && blueprintBlockColor.Darkness == 0 ? cube.darkness : blueprintBlockColor.Darkness;
float3 bluePos = new float3(blueprint[i].position[0], blueprint[i].position[1], blueprint[i].position[2]);
float3 blueScale = new float3(blueprint[i].scale[0], blueprint[i].scale[1], blueprint[i].scale[2]);
float3 blueRot = new float3(blueprint[i].rotation[0], blueprint[i].rotation[1], blueprint[i].rotation[2]);
float3 physicalLocation = (float3)(cubeQuaternion * bluePos) + actualPosition;// + (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: {actualPosition} 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
placedBlocks[i] = Block.PlaceNew(VoxelObjectNotationUtility.NameToEnum(blueprint[i].name),
physicalLocation,
physicalRotation,
blockColor,
blockDarkness,
scale: physicalScale);
}
#if DEBUG
Logging.MetaLog($"Placed {placedBlocks.Length} blocks for blueprint {cube.name} (id:{cube.cubeId})");
#endif
return placedBlocks;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void LoadBlueprintMap()
{
StreamReader bluemap = new StreamReader(Assembly.GetExecutingAssembly().GetManifestResourceStream("Pixi.blueprints.json"));
blueprintMap = JsonConvert.DeserializeObject<Dictionary<uint, BlockJsonInfo[]>>(bluemap.ReadToEnd());
}
}
}

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

@ -0,0 +1,140 @@
using System;
using System.Collections.Generic;
using System.IO;
using Svelto.DataStructures;
using Unity.Mathematics;
using UnityEngine;
using GamecraftModdingAPI.Blocks;
using GamecraftModdingAPI.Commands;
using GamecraftModdingAPI.Utility;
using Newtonsoft.Json;
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)
{
BlockJsonInfo copy = root;
copy.name = $"TextBlock\t{root.name} ({CubeUtility.CubeIdDescription(uint.Parse(root.name))})\tPixi";
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;
}
#if DEBUG
public void AddDebugCommands()
{
CommandBuilder.Builder("PixiReload", "Reloads the robot blueprints")
.Action(() => botprints = null).Build();
CommandBuilder.Builder("RotateBlueprint",
"Rotates a blueprint with a given ID and dumps the result to a file. 1 means 90 degrees.")
.Action<string>(RotateBlueprint).Build();
}
private void RotateBlueprint(string parameters)
{
var p = parameters.Split(' ');
string id = p[0];
var xyz = new int[3];
for (int i = 0; i < xyz.Length; i++)
xyz[i] = int.Parse(p[i + 1]) * 90;
if (botprints == null)
{
botprints = BlueprintUtility.ParseBlueprintResource("Pixi.blueprints.json");
}
if (!botprints.ContainsKey(id))
{
Logging.CommandLogWarning("Blueprint with that ID not found.");
return;
}
var bp = botprints[id];
var rotChange = Quaternion.Euler(xyz[0], xyz[1], xyz[2]);
for (var i = 0; i < bp.Length; i++)
{
ref var info = ref bp[i];
var pos = ConversionUtility.FloatArrayToFloat3(info.position);
info.position = ConversionUtility.Float3ToFloatArray(rotChange * pos);
var rot = Quaternion.Euler(ConversionUtility.FloatArrayToFloat3(info.rotation));
info.rotation = ConversionUtility.Float3ToFloatArray((rotChange * rot).eulerAngles);
}
File.WriteAllText(id, JsonConvert.SerializeObject(bp));
Logging.CommandLog("Blueprint rotated " + rotChange.eulerAngles + " and dumped");
}
#endif
}
}

View file

@ -0,0 +1,47 @@
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;
using Pixi.Common;
namespace Pixi.Robots
{
public static class RobotCommands
{
public static void CreatePartDumpCommand()
{
CommandBuilder.Builder()
.Name("DumpVON")
.Description("Dump a block structure to a JSON file compatible with Pixi's internal VON format")
.Action<string>(DumpBlockStructure)
.Build();
}
private static void DumpBlockStructure(string filename)
{
Player local = new Player(PlayerType.Local);
Block baseBlock = local.GetBlockLookedAt();
Block[] blocks = local.GetSelectedBlocks();
if (blocks.Length == 0)
blocks = baseBlock.GetConnectedCubes();
bool isBaseScaled = !(baseBlock.Scale.x > 0 && baseBlock.Scale.x < 2 && baseBlock.Scale.y > 0 && baseBlock.Scale.y < 2 && baseBlock.Scale.z > 0 && baseBlock.Scale.z < 2);
if (isBaseScaled)
{
Logging.CommandLogWarning($"Detected scaled base block. This is not currently supported");
}
float3 basePos = baseBlock.Position;
string von = VoxelObjectNotationUtility.SerializeBlocks(blocks, new float[] { basePos.x, basePos.y, basePos.z });
File.WriteAllText(filename, von);
}
}
}

View file

@ -0,0 +1,149 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
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;
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;
}
// set textblock colors (replace <color="white"> with <color=#HEX> in textblocks)
Regex pattern = new Regex("<color=((?:\"white\")|(?:white))>", RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
for (int i = 0; i < blocks.Length; i++)
{
if (blocks[i].block == BlockIDs.TextBlock)
{
// TODO this blindly replaces color tags anywhere in metadata, not just ones that will go in the TextBlock's text field
#if DEBUG
Logging.MetaLog($"Replacing text field in block with colour {blocks[i].color} with #{ColorUtility.ToHtmlStringRGBA(ColorSpaceUtility.UnquantizeToColor(blocks[i].color))}");
#endif
blocks[i].metadata = pattern.Replace(
blocks[i].metadata,
$"<color=#{ColorUtility.ToHtmlStringRGBA(ColorSpaceUtility.UnquantizeToColor(blocks[i].color))}>");
// NOTE: Regex.Replace replaces the whole match string only when there's a capture group (it's dumb, idk why).
// The non-capturing groups may be messing with .NET or something
}
}
}
public void PostProcess(string name, ref Block[] blocks)
{
}
}
}

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>)
}
}

52
Pixi/TestImporter.cs Normal file
View file

@ -0,0 +1,52 @@
using System;
using GamecraftModdingAPI;
using GamecraftModdingAPI.Blocks;
using GamecraftModdingAPI.Players;
using Pixi.Common;
using Unity.Mathematics;
namespace Pixi
{
public class TestImporter : Importer
{
public int Priority { get; } = 0;
public bool Optimisable { get; } = false;
public string Name { get; } = "Test~Spell";
public BlueprintProvider BlueprintProvider { get; } = null;
public bool Qualifies(string name)
{
return name.Equals("test", StringComparison.InvariantCultureIgnoreCase);
}
public BlockJsonInfo[] Import(string name)
{
return new[]
{
new BlockJsonInfo
{
name = BlockIDs.TextBlock.ToString() +
"\ttext that is preserved through the whole import process and ends up in the text block\ttextblockIDs_sux",
position = new[] {0f, 0f, 0f},
rotation = new[] {0f, 0f, 0f},
color = new[] {0f, 0f, 0f},
scale = new[] {1f, 1f, 1f},
}
};
}
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)
{
// meh
}
}
}

327
Pixi/blueprints.json Normal file

File diff suppressed because one or more lines are too long

1
Pixi/cubes-id.json Normal file

File diff suppressed because one or more lines are too long

107
README.md
View file

@ -1,49 +1,116 @@
# Pixi
Gamecraft mod for converting images into coloured blocks.
Think of it like automatic pixel art.
A mod for importing images and more into Gamecraft.
Developed by NGnius.
## Installation
To install the Pixi mod, copy the build's `Pixi.dll` into the `Plugins` folder in Gamecraft's main folder.
Before installing Pixi, please patch Gamecraft with [GCIPA](https://git.exmods.org/modtainers/GCIPA/releases) and install the latest version of [GamecraftModdingAPI](https://git.exmods.org/modtainers/GamecraftModdingAPI/releases).
Once that's done, install Pixi by copying `Pixi.dll` (from the latest release) into the `Plugins` folder in Gamecraft's main folder.
Alternately, follow the install guide: https://www.exmods.org/guides/install.html (ignore the part about a zip file -- move Pixi.dll into the Plugins folder instead).
## Usage
Pixi adds new commands to Gamecraft's command line to import images into a game.
Pixi adds new commands to Gamecraft's command line to import images, and other stuff, into a game.
Since Pixi places vanilla Gamecraft blocks, imported files should be visible without Pixi installed.
`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 based to the lower left corner.
For the following section, 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"`.
`Pixi2D "[image]"` converts an image to blocks and places it as blocks beside where you're standing (along the xy-plane).
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.
### Commands
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.
`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 `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.
If you know the name of the Pixi spell you want, you can also use
`Pixi2 "[spell]" @"[thing]"` to use `spell` to import `thing` into your Gamecraft game.
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
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 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!!!
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.
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.
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.
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
Show your love by offering your time.
Show your love by offering your help!
### Ways To Contribute
- Build a Robocraft block that's not currently supported by Pixi (send it to NGnius on Discord).
- 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 any other inquiry, please contact NGnius in the [Exmods Discord server](https://discord.exmods.org).
### 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.
The project is pre-configured to look in a folder called ref in the solution's main directory or one level up from that.
You can make sure Pixi can find all of `.dll` files it needs by copying your Gamecraft folder here and renaming it to `ref`, but you'll have to re-copy it after every Gamecraft update.
You can also create a symbolic link (look it up) to your Gamecraft install folder named `ref` in this folder to avoid having to re-copy files.
You can make sure Pixi can find all `.dll` files it needs by copying your Gamecraft folder here and renaming it to `ref`, but you'll have to re-copy it after every Gamecraft update.
To avoid that, create a symbolic link (look it up) to your Gamecraft install folder named `ref` in this folder instead.
Like most mods, you will have to patch your game with [GCIPA](https://git.exmods.org/modtainers/GCIPA).
Pixi also requires the [GamecraftModdingAPI](https://git.exmods.org/modtainers/GamecraftModdingAPI) library to be installed (in `ref/Plugins/GamecraftModdingAPI.dll`).
Pixi also requires the [GamecraftModdingAPI](https://git.exmods.org/modtainers/GamecraftModdingAPI) library to be installed (in `ref/Plugins/GamecraftModdingAPI.dll`, the usual place).
## Building
### 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 [our Discord server](https://discord.gg/xjnFxQV).
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
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).
MIDI file processing uses an integrated copy of melanchall's [DryWetMidi](https://github.com/melanchall/drywetmidi) library, licensed under the [MIT License](https://github.com/melanchall/drywetmidi/blob/develop/LICENSE).
Thanks to **TheGreenGoblin** and their Python app for converting images to coloured square characters, which inspired the PixiConsole and PixiText commands.
Thanks to **Mr. Rotor** for all of the Robocraft blocks used in the PixiBot and PixiBotFile commands.
# Disclaimer
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 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 actually just sufficiently advanced technology that's indistinguishable from magic.