Compare commits

...

43 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
30 changed files with 3634 additions and 1273 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

@ -1,17 +0,0 @@
using System;
using GamecraftModdingAPI.Blocks;
namespace Pixi.Common
{
public struct BlockInfo
{
public BlockIDs block;
public BlockColors color;
public byte darkness;
public bool visible;
}
}

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

@ -1,214 +0,0 @@
using System;
using System.IO;
using System.Text;
using System.Security.Cryptography;
using UnityEngine;
using Unity.Mathematics;
using Svelto.ECS.Experimental;
using Svelto.ECS;
using GamecraftModdingAPI.Blocks;
using GamecraftModdingAPI.Commands;
using GamecraftModdingAPI.Players;
using GamecraftModdingAPI.Utility;
using GamecraftModdingAPI;
using Pixi.Common;
namespace Pixi.Images
{
public static class ImageCommands
{
public const uint PIXEL_WARNING_THRESHOLD = 25_000;
// hash length to display after Pixi in text block id field
public const uint HASH_LENGTH = 6;
private static double blockSize = 0.2;
private static uint thiccness = 1;
public static void CreateThiccCommand()
{
CommandBuilder.Builder()
.Name("PixiThicc")
.Description("Set the image thickness for Pixi2D. Use this if you'd like add depth to a 2D image after importing.")
.Action<int>((d) => {
if (d > 0)
{
thiccness = (uint)d;
}
else Logging.CommandLogError("");
})
.Build();
}
public static void CreateImportCommand()
{
CommandBuilder.Builder()
.Name("Pixi2D")
.Description("Converts an image to blocks. Larger images will freeze your game until conversion completes.")
.Action<string>(Pixelate2DFile)
.Build();
}
public static void CreateTextCommand()
{
CommandBuilder.Builder()
.Name("PixiText")
.Description("Converts an image to coloured text in a new text block. Larger images may cause save issues.")
.Action<string>(Pixelate2DFileToTextBlock)
.Build();
}
public static void CreateTextConsoleCommand()
{
CommandBuilder.Builder()
.Name("PixiConsole")
.Description("Converts an image to a ChangeTextBlockCommand in a new console block. The first parameter is the image filepath and the second parameter is the text block id. Larger images may cause save issues.")
.Action<string, string>(Pixelate2DFileToCommand)
.Build();
}
public static void Pixelate2DFile(string filepath)
{
// Load image file and convert to Gamecraft blocks
Texture2D img = new Texture2D(64, 64);
// load file into texture
try
{
byte[] imgData = File.ReadAllBytes(filepath);
img.LoadImage(imgData);
}
catch (Exception e)
{
Logging.CommandLogError($"Failed to load picture data. Reason: {e.Message}");
Logging.MetaLog(e.Message + "\n" + e.StackTrace);
return;
}
Logging.CommandLog($"Image size: {img.width}x{img.height}");
float3 position = new Player(PlayerType.Local).Position;
uint blockCount = 0;
position.x += 1f;
position.y += (float)blockSize;
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 < img.width; x++)
{
BlockInfo qVoxel = new BlockInfo
{
block = BlockIDs.AbsoluteMathsBlock, // impossible canvas block
color = BlockColors.Default,
darkness = 10,
visible = false,
};
float3 scale = new float3(1, 1, thiccness);
position.x += (float)(blockSize);
for (int y = 0; y < img.height; y++)
{
//position.y += (float)blockSize;
Color pixel = img.GetPixel(x, y);
BlockInfo qPixel = PixelUtility.QuantizePixel(pixel);
if (qPixel.darkness != qVoxel.darkness
|| qPixel.color != qVoxel.color
|| qPixel.visible != qVoxel.visible
|| qPixel.block != qVoxel.block)
{
if (y != 0)
{
if (qVoxel.visible)
{
position.y = zero_y + (float)((y * blockSize + (y - scale.y) * blockSize) / 2);
Block.PlaceNew(qVoxel.block, position, color: qVoxel.color, darkness: qVoxel.darkness, scale: scale);
blockCount++;
}
scale = new float3(1, 1, thiccness);
}
qVoxel = qPixel;
}
else
{
scale.y += 1;
}
}
if (qVoxel.visible)
{
position.y = zero_y + (float)((img.height * blockSize + (img.height - scale.y) * blockSize) / 2);
Block.PlaceNew(qVoxel.block, position, color: qVoxel.color, darkness: qVoxel.darkness, scale: scale);
blockCount++;
}
//position.y = zero_y;
}
Logging.CommandLog($"Placed {img.width}x{img.height} image beside you ({blockCount} blocks total)");
Logging.MetaLog($"Saved {(img.width * img.height) - blockCount} blocks ({blockCount / (img.width * img.height)}%) while placing {filepath}");
}
public static void Pixelate2DFileToTextBlock(string filepath)
{
// Thanks to TheGreenGoblin for the idea (and the working Python implementation for reference)
// Load image file and convert to Gamecraft blocks
Texture2D img = new Texture2D(64, 64);
// load file into texture
try
{
byte[] imgData = File.ReadAllBytes(filepath);
img.LoadImage(imgData);
}
catch (Exception e)
{
Logging.CommandLogError($"Failed to load picture data. Reason: {e.Message}");
Logging.MetaLog(e.Message + "\n" + e.StackTrace);
return;
}
float3 position = new Player(PlayerType.Local).Position;
position.x += 1f;
position.y += (float)blockSize;
string text = PixelUtility.TextureToString(img);
TextBlock textBlock = TextBlock.PlaceNew(position, scale: new float3(Mathf.Ceil(img.width / 16), 1, Mathf.Ceil(img.height / 16)));
textBlock.Text = text;
byte[] textHash;
using (HashAlgorithm hasher = SHA256.Create())
textHash = hasher.ComputeHash(Encoding.UTF8.GetBytes(text));
string textId = "Pixi_";
// every byte converts to 2 hexadecimal characters so hash length needs to be halved
for (int i = 0; i < HASH_LENGTH/2 && i < textHash.Length; i++)
{
textId += textHash[i].ToString("X2");
}
textBlock.TextBlockId = textId;
}
public static void Pixelate2DFileToCommand(string filepath, string textBlockId)
{
// Thanks to Nullpersonan for the idea
// Load image file and convert to Gamecraft blocks
Texture2D img = new Texture2D(64, 64);
// load file into texture
try
{
byte[] imgData = File.ReadAllBytes(filepath);
img.LoadImage(imgData);
}
catch (Exception e)
{
Logging.CommandLogError($"Failed to load picture data. Reason: {e.Message}");
Logging.MetaLog(e.Message + "\n" + e.StackTrace);
return;
}
float3 position = new Player(PlayerType.Local).Position;
position.x += 1f;
position.y += (float)blockSize;
float zero_y = position.y;
string text = PixelUtility.TextureToString(img); // conversion
ConsoleBlock console = ConsoleBlock.PlaceNew(position);
// set console's command
console.Command = "ChangeTextBlockCommand";
console.Arg1 = textBlockId;
console.Arg2 = text;
console.Arg3 = "";
}
}
}

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

@ -14,150 +14,42 @@ namespace Pixi.Images
public static class PixelUtility
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static BlockInfo QuantizePixel(Color pixel)
{
BlockColors color = BlockColors.Default;
int darkness = 0;
bool force = false;
#if DEBUG
Logging.MetaLog($"Color (r:{pixel.r}, g:{pixel.g}, b:{pixel.b})");
#endif
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.65 && (pixel.r - pixel.b) > pixel.r * 0.55)
{
// Red is much higher than other pixels
darkness = (int)(9 - (pixel.r * 8.01));
color = BlockColors.Red;
}
else if ((pixel.g - pixel.b) > pixel.g * 0.25)
{
// Green is much higher than blue
if ((pixel.r - pixel.g) < pixel.r * 0.8)
{
darkness = (int)(10 - ((pixel.r * 2.1 + pixel.g) * 2.1));
color = BlockColors.Orange;
}
else
{
darkness = (int)(10 - ((pixel.r * 2.1 + pixel.g) * 2.2));
color = BlockColors.Yellow;
}
}
else if ((pixel.b - pixel.g) > pixel.b * 0.3)
{
// Blue is much higher than green
darkness = (int)(10 - ((pixel.r + pixel.b) * 5.0));
color = BlockColors.Purple;
}
else
{
// Green is close strength to blue
darkness = (int)(10 - ((pixel.r * 2.1 + pixel.g + pixel.b) * 2.5));
color = darkness < 6 ? BlockColors.Pink : BlockColors.Orange;
force = true;
}
}
else if (pixel.g >= pixel.r && pixel.g >= pixel.b)
{
// Green is highest
if ((pixel.g - pixel.r) > pixel.g * 0.6 && (pixel.g - pixel.b) > pixel.g * 0.48)
{
// Green is much higher than other pixels
darkness = (int)(10 - (pixel.g * 10.1));
color = BlockColors.Green;
}
else if ((pixel.r - pixel.b) > pixel.r * 0.3)
{
// 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.2)
{
// Blue is much higher than red
darkness = (int)(9 - ((pixel.g + pixel.b) * 5.1));
color = BlockColors.Aqua;
}
else
{
// Red is close strength to blue
darkness = (int)(10 - ((pixel.r + pixel.g * 2.2 + pixel.b) * 2.9));
color = BlockColors.Lime;
}
}
else if (pixel.b >= pixel.g && pixel.b >= pixel.r)
{
// Blue is highest
if ((pixel.b - pixel.g) > pixel.b * 0.6 && (pixel.b - pixel.r) > pixel.b * 0.6)
{
// Blue is much higher than other pixels
darkness = (int)(10 - (pixel.b * 10.1));
color = BlockColors.Blue;
}
else if ((pixel.g - pixel.r) > pixel.g * 0.3)
{
// Green is much higher than red
darkness = (int)(10 - ((pixel.g + pixel.b) * 5.1));
if (darkness == 4 || darkness == 5) darkness = 0;
else if (darkness < 3) darkness = 4;
color = BlockColors.Aqua;
}
else if ((pixel.r - pixel.g) > pixel.r * 0.3)
{
// Red is much higher than green
darkness = (int)(10 - ((pixel.r + pixel.b) * 5.0));
color = BlockColors.Purple;
}
else
{
// Green is close strength to red
darkness = (int)(10 - ((pixel.r + pixel.g + pixel.b * 2.2) * 3.0));
color = BlockColors.Aqua;
}
}
// level 9 is not darker than lvl 8
if (darkness > 8 && !force) darkness = 8;
// darkness 0 is the most saturated (it's not just the lightest)
if (darkness < 0) darkness = 0;
BlockInfo result = new BlockInfo
{
block = pixel.a > 0.75 ? BlockIDs.AluminiumCube : BlockIDs.GlassCube,
color = color,
darkness = (byte)darkness,
visible = pixel.a > 0.5f,
};
#if DEBUG
Logging.MetaLog($"Quantized {color} (b:{result.block} d:{result.darkness} v:{result.visible})");
#endif
return result;
}
public static string HexPixel(Color pixel)
public static string HexPixel(Color pixel)
{
return "#"+ColorUtility.ToHtmlStringRGBA(pixel);
}
[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);
imgString.Append("<color=");
imgString.Append(HexPixel(pixel));
imgString.Append(">");
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>");

File diff suppressed because it is too large Load diff

View file

@ -8,22 +8,23 @@ using Unity.Mathematics; // float3
using IllusionPlugin;
using GamecraftModdingAPI.Utility;
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
// called when Gamecraft shuts down
public void OnApplicationQuit()
public override void OnApplicationQuit()
{
// Shutdown this mod
Logging.LogDebug($"{Name} has shutdown");
@ -33,33 +34,32 @@ namespace Pixi
}
// 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
// 2D image functionality
ImageCommands.CreateThiccCommand();
ImageCommands.CreateImportCommand();
ImageCommands.CreateTextCommand();
ImageCommands.CreateTextConsoleCommand();
CommandRoot root = new CommandRoot();
// 2D Image Functionality
root.Inject(new ImageCanvasImporter());
root.Inject(new ImageTextBlockImporter());
root.Inject(new ImageCommandImporter());
// Robot functionality
RobotCommands.CreateRobotCRFCommand();
RobotCommands.CreateRobotFileCommand();
Logging.LogDebug($"{Name} has started up");
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)
}
}

View file

@ -6,7 +6,7 @@ namespace Pixi.Robots
{
public struct CubeInfo
{
// so you can't inherit from structs in C#...
// you can't inherit from structs in C#...
// this is an extension of BlockInfo
public BlockIDs block;
@ -23,6 +23,8 @@ namespace Pixi.Robots
public float3 scale;
public string placeholder;
public string name;
public uint cubeId;
}
}

View file

@ -7,9 +7,13 @@ 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
{
@ -17,6 +21,8 @@ namespace Pixi.Robots
{
private static Dictionary<uint, string> map = null;
private static Dictionary<uint, BlockJsonInfo[]> blueprintMap = null;
public static RobotStruct? ParseRobotInfo(string robotInfo)
{
try
@ -69,7 +75,7 @@ namespace Pixi.Robots
public static CubeInfo TranslateSpacialEnumerations(uint cubeId, byte x, byte y, byte z, byte rotation, byte colour, byte colour_x, byte colour_y, byte colour_z)
{
if (x != colour_x || z != colour_z || y != colour_y) return default;
CubeInfo result = new CubeInfo { visible = true };
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);
@ -80,7 +86,7 @@ namespace Pixi.Robots
return result;
}
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
[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
@ -180,239 +186,153 @@ namespace Pixi.Robots
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void TranslateBlockColour(byte colour, ref CubeInfo result)
{
// I hope these colours are accurate, I just guessed
// TODO colour accuracy (lol that won't ever happen)
switch (colour)
// 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)
{
case 0:
result.color = BlockColors.White;
result.darkness = 0;
break;
case 1:
result.color = BlockColors.White;
result.darkness = 5;
break;
case 2:
result.color = BlockColors.Orange;
result.darkness = 0;
break;
case 3:
result.color = BlockColors.Blue;
result.darkness = 2;
break;
case 4:
result.color = BlockColors.White;
result.darkness = 8;
break;
case 5:
result.color = BlockColors.Red;
result.darkness = 0;
break;
case 6:
result.color = BlockColors.Yellow;
result.darkness = 0;
break;
case 7:
result.color = BlockColors.Green;
result.darkness = 0;
break;
case 8:
result.color = BlockColors.Purple;
result.darkness = 0;
break;
case 9:
result.color = BlockColors.Blue;
result.darkness = 7;
break;
case 10:
result.color = BlockColors.Purple;
result.darkness = 5;
break;
case 11:
result.color = BlockColors.Orange;
result.darkness = 7;
break;
case 12:
result.color = BlockColors.Green;
result.darkness = 3;
break;
case 13:
result.color = BlockColors.Green;
result.darkness = 2;
break;
case 14:
result.color = BlockColors.Pink;
result.darkness = 3;
break;
case 15:
result.color = BlockColors.Pink;
result.darkness = 2;
break;
case 16:
result.color = BlockColors.Red;
result.darkness = 2;
break;
case 17:
result.color = BlockColors.Orange;
result.darkness = 8;
break;
case 18:
result.color = BlockColors.Red;
result.darkness = 7;
break;
case 19:
result.color = BlockColors.Pink;
result.darkness = 0;
break;
case 20:
result.color = BlockColors.Yellow;
result.darkness = 2;
break;
case 21:
result.color = BlockColors.Green;
result.darkness = 7;
break;
case 22:
result.color = BlockColors.Green;
result.darkness = 8;
break;
case 23:
result.color = BlockColors.Blue;
result.darkness = 8;
break;
case 24:
result.color = BlockColors.Aqua;
result.darkness = 7;
break;
case 25:
result.color = BlockColors.Blue;
result.darkness = 6;
break;
case 26:
result.color = BlockColors.Aqua;
result.darkness = 5;
break;
case 27:
result.color = BlockColors.Blue;
result.darkness = 4;
break;
case 28:
result.color = BlockColors.Aqua;
result.darkness = 3;
break;
case 29:
result.color = BlockColors.Blue;
result.darkness = 5;
break;
case 30:
result.color = BlockColors.Purple;
result.darkness = 3;
break;
case 31:
result.color = BlockColors.Purple;
result.darkness = 1;
break;
default:
result.color = BlockColors.Aqua;
result.darkness = 0;
break;
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)
if (map == null)
{
StreamReader cubemap = new StreamReader(Assembly.GetExecutingAssembly().GetManifestResourceStream("Pixi.cubes-id.json"));
map = JsonConvert.DeserializeObject<Dictionary<uint, string>>(cubemap.ReadToEnd());
map = JsonConvert.DeserializeObject<Dictionary<uint, string>>(cubemap.ReadToEnd());
}
if (!map.ContainsKey(cubeId))
{
result.block = BlockIDs.TextBlock;
result.placeholder = "Unknown cube #" + cubeId.ToString();
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"))
{
result.block = BlockIDs.AluminiumCube;
result.rotation = float3.zero;
}
gcName += "Cube";
else if (cubeName.Contains("prism") || cubeName.Contains("edge"))
{
if (cubeName.Contains("round"))
{
if (cubeName.Contains("glass") || cubeName.Contains("windshield"))
{
result.block = BlockIDs.GlassRoundedSlope;
} else
result.block = BlockIDs.AluminiumRoundedSlope;
}
else
{
if (cubeName.Contains("glass") || cubeName.Contains("windshield"))
{
result.block = BlockIDs.GlassSlope;
} else
result.block = BlockIDs.AluminiumSlope;
}
}
gcName += "Slope";
else if (cubeName.Contains("inner"))
{
if (cubeName.Contains("round"))
{
if (cubeName.Contains("glass") || cubeName.Contains("windshield"))
{
result.block = BlockIDs.GlassRoundedSlicedCube;
} else
result.block = BlockIDs.AluminiumRoundedSlicedCube;
}
else
{
if (cubeName.Contains("glass") || cubeName.Contains("windshield"))
{
result.block = BlockIDs.GlassSlicedCube;
} else
result.block = BlockIDs.AluminiumSlicedCube;
}
}
gcName += "SlicedCube";
else if (cubeName.Contains("tetra") || cubeName.Contains("corner"))
{
if (cubeName.Contains("round"))
{
if (cubeName.Contains("glass") || cubeName.Contains("windshield"))
{
result.block = BlockIDs.GlassRoundedCorner;
} else
result.block = BlockIDs.AluminiumRoundedCorner;
}
else
{
if (cubeName.Contains("glass") || cubeName.Contains("windshield"))
{
result.block = BlockIDs.GlassCorner;
} else
result.block = BlockIDs.AluminiumCorner;
}
}
gcName += "Corner";
else if (cubeName.Contains("pyramid"))
{
result.block = BlockIDs.AluminiumPyramidSegment;
}
gcName += "PyramidSegment";
else if (cubeName.Contains("cone"))
{
result.block = BlockIDs.AluminiumConeSegment;
}
gcName += "ConeSegment";
else
{
result.block = BlockIDs.TextBlock;
result.placeholder = cubeName;
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,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

@ -12,103 +12,36 @@ using GamecraftModdingAPI.Commands;
using GamecraftModdingAPI.Players;
using GamecraftModdingAPI.Utility;
using Pixi.Common;
namespace Pixi.Robots
{
public static class RobotCommands
{
private static double blockSize = 0.2;
public static void CreateRobotFileCommand()
public static void CreatePartDumpCommand()
{
CommandBuilder.Builder()
.Name("PixiBotFile")
.Description("Converts a robot file from RCBUP into Gamecraft blocks. Larger robots will freeze your game until conversion completes.")
.Action<string>(ImportRobotFile)
.Name("DumpVON")
.Description("Dump a block structure to a JSON file compatible with Pixi's internal VON format")
.Action<string>(DumpBlockStructure)
.Build();
}
public static void CreateRobotCRFCommand()
private static void DumpBlockStructure(string filename)
{
CommandBuilder.Builder()
.Name("PixiBot")
.Description("Downloads a robot from Robocraft's Factory and converts it into Gamecraft blocks. Larger robots will freeze your game until conversion completes.")
.Action<string>(ImportRobotOnline)
.Build();
}
private static void ImportRobotFile(string filepath)
{
string file;
try
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)
{
file = File.ReadAllText(filepath);
Logging.CommandLogWarning($"Detected scaled base block. This is not currently supported");
}
catch (Exception e)
{
Logging.CommandLogError($"Failed to load robot data. Reason: {e.Message}");
Logging.MetaLog(e);
return;
}
RobotStruct? robot = CubeUtility.ParseRobotInfo(file);
if (!robot.HasValue)
{
Logging.CommandLogError($"Failed to parse robot data. File format was not recognised.");
return;
}
float3 position = new Player(PlayerType.Local).Position;
position.y += (float)blockSize;
CubeInfo[] cubes = CubeUtility.ParseCubes(robot.Value);
for (int c = 0; c < cubes.Length; c++) // sometimes I wish this were C++
{
CubeInfo cube = cubes[c];
float3 realPosition = (cube.position * (float)blockSize) + position;
Block newBlock = Block.PlaceNew(cube.block, realPosition, cube.rotation, cube.color, cube.darkness, scale: cube.scale);
// the goal is for this to never evaluate to true (ie all cubes are translated correctly)
if (!string.IsNullOrEmpty(cube.placeholder) && cube.block == BlockIDs.TextBlock)
{
newBlock.Specialise<TextBlock>().Text = cube.placeholder;
}
}
Logging.CommandLog($"Placed {robot.Value.name} by {robot.Value.addedByDisplayName} ({cubes.Length} cubes) beside you");
}
private static void ImportRobotOnline(string robotName)
{
Stopwatch timer = Stopwatch.StartNew();
// download robot data
RobotStruct robot;
try
{
RobotBriefStruct[] botList = RoboAPIUtility.ListRobots(robotName);
if (botList.Length == 0)
throw new Exception("Failed to find robot");
robot = RoboAPIUtility.QueryRobotInfo(botList[0].itemId);
}
catch (Exception e)
{
Logging.CommandLogError($"Failed to download robot data. Reason: {e.Message}");
Logging.MetaLog(e);
timer.Stop();
return;
}
timer.Stop();
Logging.MetaLog($"Completed API calls in {timer.ElapsedMilliseconds}ms");
float3 position = new Player(PlayerType.Local).Position;
position.y += (float)blockSize;
CubeInfo[] cubes = CubeUtility.ParseCubes(robot);
for (int c = 0; c < cubes.Length; c++) // sometimes I wish this were C++
{
CubeInfo cube = cubes[c];
float3 realPosition = (cube.position * (float)blockSize) + position;
Block newBlock = Block.PlaceNew(cube.block, realPosition, cube.rotation, cube.color, cube.darkness, scale: cube.scale);
// the goal is for this to never evaluate to true (ie all cubes are translated correctly)
if (!string.IsNullOrEmpty(cube.placeholder) && cube.block == BlockIDs.TextBlock)
{
newBlock.Specialise<TextBlock>().Text = cube.placeholder;
}
}
Logging.CommandLog($"Placed {robot.name} by {robot.addedByDisplayName} ({cubes.Length} cubes) beside you");
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)
{
}
}
}

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

File diff suppressed because one or more lines are too long

126
README.md
View file

@ -1,88 +1,75 @@
# 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 `Pixi.dll` (from the latest release) into the `Plugins` folder in Gamecraft's main folder.
You'll also need [GamecraftModdingAPI](https://git.exmods.org/modtainers/GamecraftModdingAPI) installed and Gamecraft patched with [GCIPA](https://git.exmods.org/modtainers/GCIPA/releases).
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.
Since Pixi places vanilla Gamecraft blocks, imported images should be visible without Pixi installed.
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.
### Commands
`PixiText @"[image]"` converts an image to text and places a text block with that text beside you.
`PixiConsole @"[image]" "[text block id]"` converts an image to text and places a console block beside you which changes the specified text block.
`Pixi2D @"[image]"` converts an image to blocks and places it beside you (along the xy-plane).
Anything between `[` and `]` characters is a command argument you must provide by replacing everything inside and including the square brackets.
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"`.
### Commands
`Pixi @"[thing]"` to import `thing` into your Gamecraft game.
For example, if you want to add an image called `pixel_art.png`, stored in Gamecraft's installation directory,
execute the command `Pixi2D @"pixel_art.png"` to load the image as blocks.
It's important to include the file extension, since Pixi isn't psychic (yet).
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.
**EXPERIMENTAL**
If you know the name of the Pixi spell you want, you can also use
`PixiBot @"[bot]"` downloads a bot from Robocraft's community Factory and places it beside you.
`Pixi2 "[spell]" @"[thing]"` to use `spell` to import `thing` into your Gamecraft game.
`PixiBotFile @"[bot]"` converts a `.bot` file from [rcbup](https://github.com/NGnius/rcbup) to blocks and places it beside you.
**NOTE**
For the preceeding commands, do not forget the `@"` before and `"` after the command argument, otherwise the command won't work.
If your image is not stored in the same folder as Gamecraft, you should specify the full filepath (eg `C:\path\to\image.png`) to the image.
This works best with `.PNG` images, but `.JPG` also works -- you just won't be able to use transparency-based features.
Optionally, if you know your command argument won't have a backslash `\` in it, you can omit the `@` symbol.
`PixiThicc [depth]` sets the block thickness for `Pixi2D` image conversion.
The depth should be a positive whole number, like 3 or 42, and not 3.14 or -42.
The default thickness is 1.
Some commands also have hidden features, like image rotation and bot scaling.
Talk to NGnius on the Exmods Discord server or read Pixi's source code to figure that out.
### Behaviour
PixiText and PixiConsole share the same image conversion system.
The conversion system converts every pixel to a [<color> tag](http://digitalnativestudios.com/textmeshpro/docs/rich-text/#color) followed by a square text character.
For PixiText, the resulting character string is set to the text field of the text block that the command places.
For PixiConsole, the character string is automatically set to a console block in the form `ChangeTextBlockCommand [text block id] [character string]`.
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!!!
Pixi2D takes an image file and converts every pixel to a coloured block.
Unfortunately, an image file supports over 6 million colours and Gamecraft only has 100 paint colours (and only 90 are used by Pixi2D).
Pixi2D uses an algorithm to convert each pixel an image into the closest paint colour, but colour accuracy will never be as good as a regular image.
ImageCanvas takes an image file and converts every pixel to a coloured block.
ImageCanvas uses an algorithm to convert each pixel in an image into the closest paint colour, but colour accuracy will never be as good as a regular image.
Pixi2D's colour-conversion algorithm also uses pixel transparency so you can cut out shapes.
ImageCanvas' colour-conversion algorithm also uses pixel transparency so you can cut out shapes.
A pixel which has opacity of less than 50% will be ignored.
A pixel which has an opacity between 75% and 50% will be converted into a glass cube.
A pixel which has an opacity greater than 75% will be converted into an aluminium cube.
This only works with `.PNG` image files since the `.JPG` format doesn't store transparency.
A pixel which has an opacity greater than 75% will be converted into the block you're holding (or aluminium if you've got your hand selected).
This only works with `.PNG` image files since the `.JPG` format doesn't support image transparency.
Pixi2D also groups blocks together, since images have a lot of pixels.
After the colour-conversion algorithm, Pixi groups blocks in the same column with the same paint colour together.
The grouping algorithm reduces the block count by over 75% in ideal cases, and it can reduce the block count by 50% in most cases.
Imagine a standard 1080p screen (1920x1080 pixels), which has more than 2 million pixels.
Pixi2D could import that image with less than 500K blocks, which will still hurt Gamecraft's performance even on good PCs but it won't make it completely unusable like 2M blocks will.
ImageCanvas also optimises block placement, since images have a lot of pixels.
The blocks grouping ratio is displayed in the command line output once image importing is completed.
PixiBot and PixiBotFile convert robot data to equivalent Gamecraft blocks.
If the conversion algorithm encounters a block it cannot convert, it will place a text block, with additional information, instead.
PixiBot 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).
## Suggestions and Bugs
If you find a bug or have an idea for an improvement to Pixi, please create an [issue](https://git.exmods.org/NGnius/Pixi/issues) with an in-depth description.
If you'd like to discuss your issue instead, talk to NGnius on the [Exmods Discord server](https://discord.gg/xjnFxQV).
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 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.
@ -95,22 +82,35 @@ You can make sure Pixi can find all `.dll` files it needs by copying your Gamecr
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
After you've completed the setup, open the solution file `Pixi.sln` in your prefered C# .NET/Mono development environment.
I'd recommend Visual Studio Community Edition or JetBrains Rider for Windows and Monodevelop for Linux.
I'd recommend Visual Studio Community Edition for Windows or JetBrains Rider for Linux.
If you've successfully completed setup, you should be able to build the Pixi project without errors.
If it doesn't work and you can't figure out why, ask for help on the [Exmods Discord server](https://discord.gg/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, Exmods and NGnius are not endorsed or supported by Gamecraft or FreeJam.
Pixi source code and releases are available free of charge as open-source software for the purpose of modding Gamecraft.
Modify Gamecraft at your own risk.
Read the LICENSE file for licensing information.
Please don't sue this project's contributors (that's what all disclaimers boil down to, right?).
Read the LICENSE file for official licensing information.
Pixi, Exmods and NGnius are not endorsed or supported by Gamecraft or FreeJam.
Please don't sue this project or its contributors (that's what all disclaimers boil down to, right?).
Pixi is not a psychic overlord which secretly rules the world.
Well, not this world at least.
Pixi is actually just sufficiently advanced technology that's indistinguishable from magic.