Compare commits

..

3 commits

Author SHA1 Message Date
NGnius (Graham) 9c38200dd7 Refactor Pixi import system as per issue #5 2020-07-23 21:09:11 -04:00
NGnius (Graham) 1afff6c583 Use geometric (pythagorean) distance for nearest colour calculations 2020-07-20 09:07:41 -04:00
NGnius (Graham) 28d362cba4 Update refs for preview version 2020-07-20 08:47:58 -04:00
22 changed files with 704 additions and 1805 deletions

View file

@ -1,37 +0,0 @@
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)
{
}
}
}

View file

@ -1,106 +0,0 @@
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 */},
};
}
}
}

View file

@ -1,207 +0,0 @@
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

@ -20,7 +20,7 @@ namespace Pixi.Common
internal ProcessedVoxelObjectNotation Process()
{
BlockIDs block = ConversionUtility.BlockIDsToEnum(name.Split('\t')[0]);
BlockIDs block = ConversionUtility.BlockIDsToEnum(name);
return new ProcessedVoxelObjectNotation
{
block = block,

View file

@ -14,18 +14,11 @@ namespace Pixi.Common
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());
StreamReader bluemap = new StreamReader(Assembly.GetExecutingAssembly().GetManifestResourceStream(name));
return JsonConvert.DeserializeObject<Dictionary<string, BlockJsonInfo[]>>(bluemap.ReadToEnd());
}
public static ProcessedVoxelObjectNotation[][] ProcessAndExpandBlocks(string name, BlockJsonInfo[] blocks, BlueprintProvider blueprints)

View file

@ -4,22 +4,15 @@ 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 const float optimal_delta = 0.2f;
private static Dictionary<BlockColor, float[]> colorMap = null;
@ -28,7 +21,7 @@ namespace Pixi.Common
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static BlockColor QuantizeToBlockColor(Color pixel)
{
//if (colorMap == null) BuildColorMap();
if (colorMap == null) BuildColorMap();
float[] closest = new float[3] { 1, 1, 1 };
BlockColor c = new BlockColor
{
@ -50,14 +43,14 @@ namespace Pixi.Common
if (geometricClosest < optimal_delta)
{
#if DEBUG
//Logging.MetaLog($"Final delta ({closest[0]},{closest[1]},{closest[2]}) t:{geometricClosest}");
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}");
Logging.MetaLog($"Final delta ({closest[0]},{closest[1]},{closest[2]}) t:{geometricClosest}");
#endif
return c;
}
@ -72,21 +65,13 @@ namespace Pixi.Common
[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();
if (colorMap == null) BuildColorMap();
return colorMap[c];
}
@ -117,16 +102,23 @@ namespace Pixi.Common
});
}
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
// TODO create actual color map
foreach (BlockColors c in Enum.GetValues(typeof(BlockColors)))
{
for (byte d = 0; d < 10; d++)
{
BlockColor colorStruct = new BlockColor
{
Color = c,
Darkness = d,
};
colorMap[colorStruct] = new float[3] { 1f, 0f, 1f };
}
}
// 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 };
@ -237,8 +229,6 @@ namespace Pixi.Common
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()
@ -281,42 +271,5 @@ namespace Pixi.Common
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();
}
}
}
}
}

View file

@ -1,9 +1,7 @@
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;
@ -12,7 +10,6 @@ using GamecraftModdingAPI;
using GamecraftModdingAPI.Blocks;
using GamecraftModdingAPI.Commands;
using GamecraftModdingAPI.Utility;
using Svelto.DataStructures;
namespace Pixi.Common
{
@ -43,8 +40,6 @@ namespace Pixi.Common
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;
@ -52,52 +47,6 @@ namespace Pixi.Common
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);
@ -157,7 +106,7 @@ namespace Pixi.Common
#endif
// import blocks
BlockJsonInfo[] blocksInfo = magicImporter.Import(name);
if (blocksInfo == null || blocksInfo.Length == 0)
if (blocksInfo.Length == 0)
{
#if DEBUG
Logging.CommandLogError($"Importer {magicImporter.Name} didn't provide any blocks to import. Mission Aborted!");
@ -195,7 +144,7 @@ namespace Pixi.Common
{
for (int pass = 0; pass < OPTIMISATION_PASSES; pass++)
{
OptimiseBlocks(ref optVONs, (pass + 1) * GROUP_SIZE);
OptimiseBlocks(ref optVONs);
#if DEBUG
Logging.MetaLog($"Optimisation pass {pass} completed");
#endif
@ -217,16 +166,7 @@ namespace Pixi.Common
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)
{
@ -239,66 +179,7 @@ namespace Pixi.Common
}
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)
private void OptimiseBlocks(ref List<ProcessedVoxelObjectNotation> optVONs)
{
// a really complicated algorithm to determine if two similar blocks are touching (before they're placed)
// the general concept:
@ -313,99 +194,89 @@ namespace Pixi.Common
// 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
// TODO multithread this expensive operation
int item = 0;
while (item < optVONs.Count)
{
#if DEBUG
Stopwatch timer = Stopwatch.StartNew();
#endif
FasterList<ProcessedVoxelObjectNotation> optVONs = new FasterList<ProcessedVoxelObjectNotation>(blocksToOptimise);
int item = 0;
while (item < optVONs.count - 1)
bool isItemUpdated = false;
ProcessedVoxelObjectNotation itemVON = optVONs[item];
if (isOptimisableBlock(itemVON.block))
{
#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)
{
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)
{
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
{
seeker++;
// 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
{
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++;
}
seeker++;
}
}
if (isItemUpdated)
{
optVONs[item] = itemVON;
//Logging.MetaLog($"Optimised block is now {itemVON}");
}
item++;
}
else
if (isItemUpdated)
{
item++;
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)
private float3[] calculateCorners(ProcessedVoxelObjectNotation von)
{
float3[] cornerMultiplicands = new float3[8]
{
new float3(1, 1, 1),
new float3(1, 1, -1),
new float3(-1, 1, 1),
new float3(-1, 1, -1),
new float3(-1, -1, 1),
new float3(-1, -1, -1),
new float3(1, -1, 1),
new float3(1, -1, -1),
};
float3[] corners = new float3[8];
Quaternion rotation = Quaternion.Euler(von.rotation);
float3 rotatedScale = rotation * von.scale;
@ -413,14 +284,24 @@ namespace Pixi.Common
// generate corners
for (int i = 0; i < corners.Length; i++)
{
corners[i] = trueCenter + BLOCK_SIZE * (cornerMultiplicands1[i] * rotatedScale / 2);
corners[i] = trueCenter + BLOCK_SIZE * (cornerMultiplicands[i] * rotatedScale / 2);
}
return corners;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void updateVonFromCorners(float3[] corners, ref ProcessedVoxelObjectNotation von)
private void updateVonFromCorners(float3[] corners, ref ProcessedVoxelObjectNotation von)
{
float3[] cornerMultiplicands = new float3[8]
{
new float3(1, 1, 1),
new float3(1, 1, -1),
new float3(1, -1, 1),
new float3(1, -1, -1),
new float3(-1, 1, 1),
new float3(-1, 1, -1),
new float3(-1, -1, 1),
new float3(-1, -1, -1),
};
float3 newCenter = sumOfFloat3Arr(corners) / corners.Length;
float3 newPosition = newCenter;
Quaternion rot = Quaternion.Euler(von.rotation);
@ -430,9 +311,26 @@ namespace Pixi.Common
//Logging.MetaLog($"Updated VON scale {von.scale} (absolute {rotatedScale})");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int[][] findMatchingCorners(float3[] corners1, float3[] corners2)
private int[][] findMatchingCorners(float3[] corners1, float3[] corners2)
{
int[][] cornerFaceMappings = new int[][]
{
new int[] {0, 1, 2, 3}, // top
new int[] {2, 3, 4, 5}, // left
new int[] {4, 5, 6, 7}, // bottom
new int[] {6, 7, 0, 1}, // right
new int[] {0, 2, 4, 6}, // back
new int[] {1, 3, 5, 7}, // front
};
int[][] oppositeFaceMappings = new int[][]
{
new int[] {6, 7, 4, 5}, // bottom
new int[] {0, 1, 6, 7}, // right
new int[] {2, 3, 0, 1}, // top
new int[] {4, 5, 2, 3}, // left
new int[] {1, 3, 5, 7}, // front
new int[] {0, 2, 4, 6}, // back
};
float3[][] faces1 = facesFromCorners(corners1);
float3[][] faces2 = facesFromCorners(corners2);
for (byte i = 0; i < faces1.Length; i++)
@ -457,8 +355,7 @@ namespace Pixi.Common
}
// this assumes the corners are in the order that calculateCorners outputs
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static float3[][] facesFromCorners(float3[] corners)
private float3[][] facesFromCorners(float3[] corners)
{
return new float3[][]
{
@ -471,8 +368,7 @@ namespace Pixi.Common
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int[] matchFace(float3[] face1, float3[] face2)
private int[] matchFace(float3[] face1, float3[] face2)
{
int[] result = new int[4];
byte count = 0;
@ -500,8 +396,7 @@ namespace Pixi.Common
return new int[0];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static float3 sumOfFloat3Arr(float3[] arr)
private float3 sumOfFloat3Arr(float3[] arr)
{
float3 total = float3.zero;
for (int i = 0; i < arr.Length; i++)
@ -513,115 +408,12 @@ namespace Pixi.Common
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool isOptimisableBlock(BlockIDs block)
private 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;
return block.ToString().EndsWith("Cube", StringComparison.InvariantCultureIgnoreCase);
}
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)
private string float3ArrToString(float3[] arr)
{
string result = "[";
foreach (float3 f in arr)
@ -642,11 +434,10 @@ namespace Pixi.Common
catch (Exception e)
{
#if DEBUG
Logging.CommandLogError("RIP Pixi\n" + e);
Logging.CommandLogError("RIP\n" + e);
#else
Logging.CommandLogError("Pixi failed (reason: " + e.Message + ")");
Logging.CommandLogError("Pixi failed (reason: " + e.Message + ")");
#endif
Logging.LogWarning("Pixi Error\n" + e);
}
}
}

View file

@ -42,7 +42,7 @@ namespace Pixi.Common
public static BlockJsonInfo JsonObject(Block block, float[] origin = null)
{
if (origin == null) origin = origin_base;
BlockJsonInfo jsonInfo = new BlockJsonInfo
return 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]},
@ -50,37 +50,6 @@ namespace Pixi.Common
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)

View file

@ -26,11 +26,6 @@ namespace Pixi.Images
public string Name { get; } = "ImageCanvas~Spell";
public BlueprintProvider BlueprintProvider { get; } = null;
public ImageCanvasImporter()
{
GamecraftModdingAPI.App.Client.EnterMenu += ColorSpaceUtility.LoadColorMenuEvent;
}
public bool Qualifies(string name)
{

View file

@ -54,11 +54,7 @@ namespace Pixi.Images
commandBlockContents[name] = text;
return new BlockJsonInfo[]
{
new BlockJsonInfo
{
color = new float[] {-1f, -1f, -1f},
name = "ConsoleBlock"
}
new BlockJsonInfo {name = "ConsoleBlock"}
};
}

View file

@ -65,7 +65,7 @@ namespace Pixi.Images
{
new BlockJsonInfo
{
color = new float[] {-1f, -1f, -1f},
color = new float[] {0f, 0f, 0f},
name = "TextBlock",
position = new float[] {0f, 0f, 0f},
rotation = new float[] {0f, 0f, 0f},

View file

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

File diff suppressed because it is too large Load diff

View file

@ -8,23 +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 : IEnhancedPlugin // the Illusion Plugin Architecture (IPA) will ignore classes that don't implement IPlugin'
public class PixiPlugin : IPlugin // the Illusion Plugin Architecture (IPA) will ignore classes that don't implement IPlugin'
{
public override string Name { get; } = Assembly.GetExecutingAssembly().GetName().Name; // Pixi
public string Name { get; } = Assembly.GetExecutingAssembly().GetName().Name; // Pixi
// To change the name, change the project's name
public override string Version { get; } = Assembly.GetExecutingAssembly().GetName().Version.ToString();
public string Version { get; } = Assembly.GetExecutingAssembly().GetName().Version.ToString();
// To change the version, change <Version>#.#.#</Version> in Pixi.csproj
// called when Gamecraft shuts down
public override void OnApplicationQuit()
public void OnApplicationQuit()
{
// Shutdown this mod
Logging.LogDebug($"{Name} has shutdown");
@ -34,7 +34,7 @@ namespace Pixi
}
// called when Gamecraft starts up
public override void OnApplicationStart()
public void OnApplicationStart()
{
// Initialize the Gamecraft modding API first
GamecraftModdingAPI.Main.Init();
@ -47,19 +47,23 @@ namespace Pixi
root.Inject(new ImageTextBlockImporter());
root.Inject(new ImageCommandImporter());
// Robot functionality
var robot = new RobotInternetImporter();
root.Inject(robot);
root.Inject(new RobotInternetImporter());
//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

@ -215,10 +215,10 @@ namespace Pixi.Robots
[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))
@ -231,34 +231,81 @@ namespace Pixi.Robots
#endif
}
string cubeName = map[cubeId];
string gcName = cubeName.Contains("glass") || cubeName.Contains("windshield")
? "Glass"
: "Aluminium";
if (cubeName.Contains("round"))
gcName += "Rounded";
if (cubeName.Contains("cube"))
gcName += "Cube";
{
result.block = BlockIDs.AluminiumCube;
result.rotation = float3.zero;
}
else if (cubeName.Contains("prism") || cubeName.Contains("edge"))
gcName += "Slope";
{
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;
}
}
else if (cubeName.Contains("inner"))
gcName += "SlicedCube";
{
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;
}
}
else if (cubeName.Contains("tetra") || cubeName.Contains("corner"))
gcName += "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;
}
}
else if (cubeName.Contains("pyramid"))
gcName += "PyramidSegment";
{
result.block = BlockIDs.AluminiumPyramidSegment;
}
else if (cubeName.Contains("cone"))
gcName += "ConeSegment";
{
result.block = BlockIDs.AluminiumConeSegment;
}
else
{
result.block = BlockIDs.TextBlock;
result.name = cubeName;
return;
}
BlockIDs id = VoxelObjectNotationUtility.NameToEnum(gcName);
result.block = id;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]

View file

@ -1,14 +1,12 @@
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
@ -35,8 +33,14 @@ namespace Pixi.Robots
if (!botprints.ContainsKey(root.name) || RobotInternetImporter.CubeSize != 3)
{
BlockJsonInfo copy = root;
copy.name = $"TextBlock\t{root.name} ({CubeUtility.CubeIdDescription(uint.Parse(root.name))})\tPixi";
if (!parent.textBlockInfo.ContainsKey(name))
{
parent.textBlockInfo[name] = new FasterList<string>();
}
BlockJsonInfo copy = root;
copy.name = "TextBlock";
Logging.MetaLog($"Parsing uint from '{root.name}'");
parent.textBlockInfo[name].Add(root.name + " (" + CubeUtility.CubeIdDescription(uint.Parse(root.name)) + ")");
return new BlockJsonInfo[1] {copy};
}
BlockJsonInfo[] blueprint = botprints[root.name];
@ -92,49 +96,5 @@ namespace Pixi.Robots
}
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

@ -31,9 +31,7 @@ namespace Pixi.Robots
{
Player local = new Player(PlayerType.Local);
Block baseBlock = local.GetBlockLookedAt();
Block[] blocks = local.GetSelectedBlocks();
if (blocks.Length == 0)
blocks = baseBlock.GetConnectedCubes();
Block[] 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)
{

View file

@ -3,7 +3,7 @@ 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;
@ -27,6 +27,8 @@ namespace Pixi.Robots
public BlueprintProvider BlueprintProvider { get; }
public static int CubeSize = 3;
internal readonly Dictionary<string, FasterList<string>> textBlockInfo = new Dictionary<string, FasterList<string>>();
public RobotInternetImporter()
{
@ -123,27 +125,23 @@ namespace Pixi.Robots
{
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)
{
int textBlockInfoIndex = 0;
for (int c = 0; c < blocks.Length; c++)
{
Block block = blocks[c];
// the goal is for this to never evaluate to true (ie all cubes are translated correctly)
if (block.Type == BlockIDs.TextBlock)
{
textBlockInfoIndex++;
block.Specialise<TextBlock>().Text = textBlockInfo[name][textBlockInfoIndex];
}
}
textBlockInfo.Remove(name);
}
}
}

View file

@ -1,52 +0,0 @@
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
}
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -99,8 +99,6 @@ Robot parsing uses information from [RobocraftAssembler](https://github.com/dddo
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.