Compare commits
10 commits
Author | SHA1 | Date | |
---|---|---|---|
|
21ddbf698b | ||
|
5aa13c2e82 | ||
|
03c429fa72 | ||
|
5e0d2a514c | ||
|
7f9ef818ac | ||
|
d8064db54d | ||
|
95a07a178e | ||
|
ab1cd96e93 | ||
|
1afff6c583 | ||
|
28d362cba4 |
11 changed files with 1287 additions and 662 deletions
37
Pixi/Audio/AudioFakeImporter.cs
Normal file
37
Pixi/Audio/AudioFakeImporter.cs
Normal 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
106
Pixi/Audio/AudioTools.cs
Normal 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 */},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
209
Pixi/Audio/MidiImporter.cs
Normal file
209
Pixi/Audio/MidiImporter.cs
Normal file
|
@ -0,0 +1,209 @@
|
|||
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 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>();
|
||||
#if DEBUG
|
||||
// test (for faster, but incomplete, imports)
|
||||
if (blocks.Length > 103) blocks = new BlockJsonInfo[103];
|
||||
#endif
|
||||
// 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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,7 +20,7 @@ namespace Pixi.Common
|
|||
|
||||
internal ProcessedVoxelObjectNotation Process()
|
||||
{
|
||||
BlockIDs block = ConversionUtility.BlockIDsToEnum(name);
|
||||
BlockIDs block = ConversionUtility.BlockIDsToEnum(name.Split('\t')[0]);
|
||||
return new ProcessedVoxelObjectNotation
|
||||
{
|
||||
block = block,
|
||||
|
|
|
@ -19,7 +19,7 @@ namespace Pixi.Common
|
|||
{
|
||||
public static class ColorSpaceUtility
|
||||
{
|
||||
private const float optimal_delta = 0.2f;
|
||||
private const float optimal_delta = 0.1f;
|
||||
|
||||
private static Dictionary<BlockColor, float[]> colorMap = null;
|
||||
|
||||
|
@ -50,14 +50,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;
|
||||
}
|
||||
|
@ -314,6 +314,7 @@ namespace Pixi.Common
|
|||
count++;
|
||||
}
|
||||
}
|
||||
yield return asyncHandle.Continue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
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;
|
||||
|
@ -10,6 +12,7 @@ using GamecraftModdingAPI;
|
|||
using GamecraftModdingAPI.Blocks;
|
||||
using GamecraftModdingAPI.Commands;
|
||||
using GamecraftModdingAPI.Utility;
|
||||
using Svelto.DataStructures;
|
||||
|
||||
namespace Pixi.Common
|
||||
{
|
||||
|
@ -40,6 +43,8 @@ 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;
|
||||
|
||||
|
@ -47,6 +52,52 @@ 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);
|
||||
|
@ -106,7 +157,7 @@ namespace Pixi.Common
|
|||
#endif
|
||||
// import blocks
|
||||
BlockJsonInfo[] blocksInfo = magicImporter.Import(name);
|
||||
if (blocksInfo.Length == 0)
|
||||
if (blocksInfo == null || blocksInfo.Length == 0)
|
||||
{
|
||||
#if DEBUG
|
||||
Logging.CommandLogError($"Importer {magicImporter.Name} didn't provide any blocks to import. Mission Aborted!");
|
||||
|
@ -144,7 +195,7 @@ namespace Pixi.Common
|
|||
{
|
||||
for (int pass = 0; pass < OPTIMISATION_PASSES; pass++)
|
||||
{
|
||||
OptimiseBlocks(ref optVONs);
|
||||
OptimiseBlocks(ref optVONs, (pass + 1) * GROUP_SIZE);
|
||||
#if DEBUG
|
||||
Logging.MetaLog($"Optimisation pass {pass} completed");
|
||||
#endif
|
||||
|
@ -166,7 +217,16 @@ 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)
|
||||
{
|
||||
|
@ -179,7 +239,66 @@ namespace Pixi.Common
|
|||
|
||||
}
|
||||
|
||||
private void OptimiseBlocks(ref List<ProcessedVoxelObjectNotation> optVONs)
|
||||
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:
|
||||
|
@ -194,89 +313,99 @@ 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)
|
||||
// TODO multithread this expensive operation
|
||||
int item = 0;
|
||||
while (item < optVONs.Count)
|
||||
try
|
||||
{
|
||||
bool isItemUpdated = false;
|
||||
ProcessedVoxelObjectNotation itemVON = optVONs[item];
|
||||
if (isOptimisableBlock(itemVON.block))
|
||||
#if DEBUG
|
||||
Stopwatch timer = Stopwatch.StartNew();
|
||||
#endif
|
||||
FasterList<ProcessedVoxelObjectNotation> optVONs = new FasterList<ProcessedVoxelObjectNotation>(blocksToOptimise);
|
||||
int item = 0;
|
||||
while (item < optVONs.count - 1)
|
||||
{
|
||||
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 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))
|
||||
{
|
||||
if (seeker == item)
|
||||
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)
|
||||
{
|
||||
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
|
||||
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}");
|
||||
if (isItemUpdated)
|
||||
{
|
||||
optVONs[item] = itemVON;
|
||||
//Logging.MetaLog($"Optimised block is now {itemVON}");
|
||||
}
|
||||
item++;
|
||||
}
|
||||
else
|
||||
{
|
||||
item++;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
private float3[] calculateCorners(ProcessedVoxelObjectNotation von)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static 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;
|
||||
|
@ -284,24 +413,14 @@ namespace Pixi.Common
|
|||
// generate corners
|
||||
for (int i = 0; i < corners.Length; i++)
|
||||
{
|
||||
corners[i] = trueCenter + BLOCK_SIZE * (cornerMultiplicands[i] * rotatedScale / 2);
|
||||
corners[i] = trueCenter + BLOCK_SIZE * (cornerMultiplicands1[i] * rotatedScale / 2);
|
||||
}
|
||||
return corners;
|
||||
}
|
||||
|
||||
private void updateVonFromCorners(float3[] corners, ref ProcessedVoxelObjectNotation von)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static 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);
|
||||
|
@ -311,26 +430,9 @@ namespace Pixi.Common
|
|||
//Logging.MetaLog($"Updated VON scale {von.scale} (absolute {rotatedScale})");
|
||||
}
|
||||
|
||||
private int[][] findMatchingCorners(float3[] corners1, float3[] corners2)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static 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++)
|
||||
|
@ -355,7 +457,8 @@ namespace Pixi.Common
|
|||
}
|
||||
|
||||
// this assumes the corners are in the order that calculateCorners outputs
|
||||
private float3[][] facesFromCorners(float3[] corners)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static float3[][] facesFromCorners(float3[] corners)
|
||||
{
|
||||
return new float3[][]
|
||||
{
|
||||
|
@ -368,7 +471,8 @@ namespace Pixi.Common
|
|||
};
|
||||
}
|
||||
|
||||
private int[] matchFace(float3[] face1, float3[] face2)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int[] matchFace(float3[] face1, float3[] face2)
|
||||
{
|
||||
int[] result = new int[4];
|
||||
byte count = 0;
|
||||
|
@ -396,7 +500,8 @@ namespace Pixi.Common
|
|||
return new int[0];
|
||||
}
|
||||
|
||||
private float3 sumOfFloat3Arr(float3[] arr)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static float3 sumOfFloat3Arr(float3[] arr)
|
||||
{
|
||||
float3 total = float3.zero;
|
||||
for (int i = 0; i < arr.Length; i++)
|
||||
|
@ -408,12 +513,63 @@ namespace Pixi.Common
|
|||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private bool isOptimisableBlock(BlockIDs block)
|
||||
private static bool isOptimisableBlock(BlockIDs block)
|
||||
{
|
||||
return block.ToString().EndsWith("Cube", StringComparison.InvariantCultureIgnoreCase);
|
||||
if (optimisableBlockCache.ContainsKey((int) block))
|
||||
{
|
||||
return optimisableBlockCache[(int) block];
|
||||
}
|
||||
|
||||
bool result = block.ToString().EndsWith("Cube", StringComparison.InvariantCultureIgnoreCase);
|
||||
optimisableBlockCache[(int) block] = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
private string float3ArrToString(float3[] arr)
|
||||
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;
|
||||
default: break; // do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string float3ArrToString(float3[] arr)
|
||||
{
|
||||
string result = "[";
|
||||
foreach (float3 f in arr)
|
||||
|
@ -434,10 +590,11 @@ namespace Pixi.Common
|
|||
catch (Exception e)
|
||||
{
|
||||
#if DEBUG
|
||||
Logging.CommandLogError("RIP\n" + e);
|
||||
Logging.CommandLogError("RIP Pixi\n" + e);
|
||||
#else
|
||||
Logging.CommandLogError("Pixi failed (reason: " + e.Message + ")");
|
||||
Logging.CommandLogError("Pixi failed (reason: " + e.Message + ")");
|
||||
#endif
|
||||
Logging.LogWarning("Pixi Error\n" + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
1135
Pixi/Pixi.csproj
1135
Pixi/Pixi.csproj
File diff suppressed because it is too large
Load diff
|
@ -8,7 +8,7 @@ using Unity.Mathematics; // float3
|
|||
|
||||
using IllusionPlugin;
|
||||
using GamecraftModdingAPI.Utility;
|
||||
|
||||
using Pixi.Audio;
|
||||
using Pixi.Common;
|
||||
using Pixi.Images;
|
||||
using Pixi.Robots;
|
||||
|
@ -53,7 +53,11 @@ namespace Pixi
|
|||
#if DEBUG
|
||||
// Development functionality
|
||||
RobotCommands.CreatePartDumpCommand();
|
||||
root.Inject(new TestImporter());
|
||||
#endif
|
||||
// Audio functionality
|
||||
root.Inject(new MidiImporter());
|
||||
root.Inject(new AudioFakeImporter());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
@ -125,6 +125,18 @@ 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
|
||||
blocks[i].metadata = pattern.Replace(
|
||||
blocks[i].metadata,
|
||||
$"<color=#{ColorUtility.ToHtmlStringRGBA(ColorSpaceUtility.UnquantizeToColor(blocks[i].color))}>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void PostProcess(string name, ref Block[] blocks)
|
||||
|
|
52
Pixi/TestImporter.cs
Normal file
52
Pixi/TestImporter.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -99,6 +99,8 @@ 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.
|
||||
|
|
Loading…
Reference in a new issue