Compare commits
56 commits
Author | SHA1 | Date | |
---|---|---|---|
|
ae069f6d93 | ||
|
53564b9c56 | ||
|
d761e4c16a | ||
|
89f5d9eb43 | ||
|
c3dc80fc84 | ||
|
3c00d05a3b | ||
|
0e65267e88 | ||
|
85c1342313 | ||
|
3c9c60b679 | ||
|
84fc330b12 | ||
|
d2a2ce52f0 | ||
|
742bcf25ef | ||
16f833ceda | |||
a0ab2ec9e7 | |||
30a3f5001f | |||
db5ff7223c | |||
02401f39f9 | |||
60cf8bdd67 | |||
ab169fb87c | |||
5bc0351bd1 | |||
bd813d852d | |||
|
4b35647c0b | ||
|
1b126b69c0 | ||
|
bb19c084d1 | ||
|
90a653b1c4 | ||
|
244be3468b | ||
|
acb0c59967 | ||
|
df7ba96434 | ||
|
523baec814 | ||
|
5acd445361 | ||
|
fe8ee1c262 | ||
|
34fdd0aefa | ||
|
5c5e46e84b | ||
|
b8547c856d | ||
|
d9a28cfbf7 | ||
|
0d25570cba | ||
|
6e817bff18 | ||
|
76b67e7684 | ||
|
351f668f51 | ||
|
abd300351f | ||
|
cdd474e1ec | ||
|
d0726b9514 | ||
|
e8ca1fe398 | ||
|
72386d2c53 | ||
|
1187d7896c | ||
|
b7cbf4486f | ||
|
e6f0816c5f | ||
|
bfe0c1972c | ||
|
2e314595ac | ||
|
6acf1b7d4e | ||
|
d536956e6c | ||
|
93f4bd37e2 | ||
|
c7b4f89828 | ||
|
1cf0c51e35 | ||
|
2d7daf0976 | ||
|
7c42f263b8 |
32 changed files with 4126 additions and 802 deletions
2
LICENSE
2
LICENSE
|
@ -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
|
||||
|
|
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 */},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
207
Pixi/Audio/MidiImporter.cs
Normal file
207
Pixi/Audio/MidiImporter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
41
Pixi/Common/BlockJsonInfo.cs
Normal file
41
Pixi/Common/BlockJsonInfo.cs
Normal 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]})}}";
|
||||
}
|
||||
}
|
||||
}
|
9
Pixi/Common/BlueprintProvider.cs
Normal file
9
Pixi/Common/BlueprintProvider.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace Pixi.Common
|
||||
{
|
||||
public interface BlueprintProvider
|
||||
{
|
||||
string Name { get; }
|
||||
|
||||
BlockJsonInfo[] Blueprint(string name, BlockJsonInfo root);
|
||||
}
|
||||
}
|
72
Pixi/Common/BlueprintUtility.cs
Normal file
72
Pixi/Common/BlueprintUtility.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
322
Pixi/Common/ColorSpaceUtility.cs
Normal file
322
Pixi/Common/ColorSpaceUtility.cs
Normal 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
653
Pixi/Common/CommandRoot.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
46
Pixi/Common/ConversionUtility.cs
Normal file
46
Pixi/Common/ConversionUtility.cs
Normal 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
27
Pixi/Common/Importer.cs
Normal 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);
|
||||
}
|
||||
}
|
40
Pixi/Common/ProcessedVoxelObjectNotation.cs
Normal file
40
Pixi/Common/ProcessedVoxelObjectNotation.cs
Normal 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})";
|
||||
}
|
||||
}
|
||||
}
|
106
Pixi/Common/VoxelObjectNotationUtility.cs
Normal file
106
Pixi/Common/VoxelObjectNotationUtility.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
99
Pixi/Images/ImageCanvasImporter.cs
Normal file
99
Pixi/Images/ImageCanvasImporter.cs
Normal 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) { }
|
||||
}
|
||||
}
|
87
Pixi/Images/ImageCommandImporter.cs
Normal file
87
Pixi/Images/ImageCommandImporter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
97
Pixi/Images/ImageTextBlockImporter.cs
Normal file
97
Pixi/Images/ImageTextBlockImporter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
61
Pixi/Images/PixelUtility.cs
Normal file
61
Pixi/Images/PixelUtility.cs
Normal file
|
@ -0,0 +1,61 @@
|
|||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
using GamecraftModdingAPI.Blocks;
|
||||
using GamecraftModdingAPI.Utility;
|
||||
|
||||
using Pixi.Common;
|
||||
|
||||
namespace Pixi.Images
|
||||
{
|
||||
public static class PixelUtility
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static string HexPixel(Color pixel)
|
||||
{
|
||||
return "#"+ColorUtility.ToHtmlStringRGBA(pixel);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Color PixelHex(string hex)
|
||||
{
|
||||
if (ColorUtility.TryParseHtmlString(hex, out Color result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
return default;
|
||||
}
|
||||
|
||||
public static string TextureToString(Texture2D img)
|
||||
{
|
||||
StringBuilder imgString = new StringBuilder("<cspace=-0.13em><line-height=40%>");
|
||||
bool lastPixelAssigned = false;
|
||||
Color lastPixel = new Color();
|
||||
for (int y = img.height-1; y >= 0 ; y--) // text origin is top right, but img origin is bottom right
|
||||
{
|
||||
for (int x = 0; x < img.width; x++)
|
||||
{
|
||||
Color pixel = img.GetPixel(x, y);
|
||||
if (!lastPixelAssigned || lastPixel != pixel)
|
||||
{
|
||||
imgString.Append("<color=");
|
||||
imgString.Append(HexPixel(pixel));
|
||||
imgString.Append(">");
|
||||
lastPixel = pixel;
|
||||
if (!lastPixelAssigned)
|
||||
{
|
||||
lastPixelAssigned = true;
|
||||
}
|
||||
}
|
||||
imgString.Append("\u25a0");
|
||||
}
|
||||
imgString.Append("<br>");
|
||||
}
|
||||
imgString.Append("</color></cspace></line-height>");
|
||||
return imgString.ToString();
|
||||
}
|
||||
}
|
||||
}
|
1263
Pixi/Pixi.csproj
1263
Pixi/Pixi.csproj
File diff suppressed because it is too large
Load diff
|
@ -7,283 +7,59 @@ using UnityEngine;
|
|||
using Unity.Mathematics; // float3
|
||||
|
||||
using IllusionPlugin;
|
||||
// using GamecraftModdingAPI;
|
||||
using GamecraftModdingAPI.Commands;
|
||||
using GamecraftModdingAPI.Utility;
|
||||
using GamecraftModdingAPI.Blocks;
|
||||
using Pixi.Audio;
|
||||
using Pixi.Common;
|
||||
using Pixi.Images;
|
||||
using Pixi.Robots;
|
||||
|
||||
namespace Pixi
|
||||
{
|
||||
public class PixiPlugin : IPlugin // the Illusion Plugin Architecture (IPA) will ignore classes that don't implement IPlugin'
|
||||
public class PixiPlugin : IEnhancedPlugin // the Illusion Plugin Architecture (IPA) will ignore classes that don't implement IPlugin'
|
||||
{
|
||||
public string Name { get; } = Assembly.GetExecutingAssembly().GetName().Name; // Pixi
|
||||
public override string Name { get; } = Assembly.GetExecutingAssembly().GetName().Name; // Pixi
|
||||
// To change the name, change the project's name
|
||||
|
||||
public string Version { get; } = Assembly.GetExecutingAssembly().GetName().Version.ToString(); // 0.1.0 (for now)
|
||||
public override string Version { get; } = Assembly.GetExecutingAssembly().GetName().Version.ToString();
|
||||
// To change the version, change <Version>#.#.#</Version> in Pixi.csproj
|
||||
|
||||
private uint width = 32;
|
||||
private uint height = 32;
|
||||
|
||||
private double blockSize = 0.2;
|
||||
|
||||
private PlayerLocationEngine playerLocationEngine = new PlayerLocationEngine();
|
||||
|
||||
// called when Gamecraft shuts down
|
||||
public void OnApplicationQuit()
|
||||
public override void OnApplicationQuit()
|
||||
{
|
||||
// Shutdown this mod
|
||||
GamecraftModdingAPI.Utility.Logging.LogDebug($"{Name} has shutdown");
|
||||
Logging.LogDebug($"{Name} has shutdown");
|
||||
|
||||
// Shutdown the Gamecraft modding API last
|
||||
GamecraftModdingAPI.Main.Shutdown();
|
||||
}
|
||||
|
||||
// called when Gamecraft starts up
|
||||
public void OnApplicationStart()
|
||||
public override void OnApplicationStart()
|
||||
{
|
||||
// Initialize the Gamecraft modding API first
|
||||
GamecraftModdingAPI.Main.Init();
|
||||
// check out the modding API docs here: https://mod.exmods.org/
|
||||
|
||||
// Initialize Pixi mod
|
||||
// create SimpleCustomCommandEngine for 2D image importing
|
||||
SimpleCustomCommandEngine<string> pixelate2DCommand = new SimpleCustomCommandEngine<string>(
|
||||
pixelate2DFile, // command action
|
||||
"Pixi2D", // command name (used to invoke it in the console)
|
||||
"Converts an image to blocks.\nLarger images will freeze your game until conversion completes. (Pixi)" // command description (displayed when help command is executed)
|
||||
);
|
||||
|
||||
SimpleCustomCommandEngine<string> pixelate3DCommand = new SimpleCustomCommandEngine<string>(
|
||||
pixelate3DFile, // command action
|
||||
"Pixi3D", // command name (used to invoke it in the console)
|
||||
"Converts a 3D model to blocks.\nLarger models will freeze your game until conversion completes. (Pixi)" // command description (displayed when help command is executed)
|
||||
);
|
||||
|
||||
SimpleCustomCommandEngine<uint, uint> scaleCommand = new SimpleCustomCommandEngine<uint, uint>(
|
||||
setScale, // command action
|
||||
"PixiScale", // command name (used to invoke it in the console)
|
||||
"Sets the image scale factor for Pixi2D.\nBigger images take longer to convert. (Pixi)" // command description (displayed when help command is executed)
|
||||
);
|
||||
|
||||
// register commands so the modding API knows about it
|
||||
CommandManager.AddCommand(pixelate2DCommand);
|
||||
CommandManager.AddCommand(scaleCommand);
|
||||
GameEngineManager.AddGameEngine(playerLocationEngine);
|
||||
|
||||
GamecraftModdingAPI.Utility.Logging.LogDebug($"{Name} has started up");
|
||||
CommandRoot root = new CommandRoot();
|
||||
// 2D Image Functionality
|
||||
root.Inject(new ImageCanvasImporter());
|
||||
root.Inject(new ImageTextBlockImporter());
|
||||
root.Inject(new ImageCommandImporter());
|
||||
// Robot functionality
|
||||
var robot = new RobotInternetImporter();
|
||||
root.Inject(robot);
|
||||
//RobotCommands.CreateRobotCRFCommand();
|
||||
//RobotCommands.CreateRobotFileCommand();
|
||||
#if DEBUG
|
||||
// Development functionality
|
||||
RobotCommands.CreatePartDumpCommand();
|
||||
((RobotBlueprintProvider) robot.BlueprintProvider).AddDebugCommands();
|
||||
root.Inject(new TestImporter());
|
||||
#endif
|
||||
// Audio functionality
|
||||
root.Inject(new MidiImporter());
|
||||
root.Inject(new AudioFakeImporter());
|
||||
}
|
||||
|
||||
// unused methods
|
||||
|
||||
public void OnFixedUpdate() { } // called once per physics update
|
||||
|
||||
public void OnLevelWasInitialized(int level) { } // called after a level is initialized
|
||||
|
||||
public void OnLevelWasLoaded(int level) { } // called after a level is loaded
|
||||
|
||||
public void OnUpdate() { } // called once per rendered frame (frame update)
|
||||
|
||||
// pixelation methods
|
||||
|
||||
private void pixelate2DFile(string filepath)
|
||||
{
|
||||
Logging.CommandLogWarning("Large images may freeze your game for a long period");
|
||||
// Load image file and convert to Gamecraft blocks
|
||||
Texture2D img = new Texture2D((int)width, (int)height);
|
||||
// load file into texture
|
||||
try
|
||||
{
|
||||
byte[] imgData = File.ReadAllBytes(filepath);
|
||||
img.LoadImage(imgData);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logging.CommandLogError($"Failed to load picture data. Reason: {e.Message}");
|
||||
Logging.LogException(e);
|
||||
return;
|
||||
}
|
||||
float3 position = playerLocationEngine.GetPlayerLocation(0u);
|
||||
uint blockCount = 0;
|
||||
position.x += 1f;
|
||||
//position.y += 1f;
|
||||
float zero_y = position.y;
|
||||
// convert the image to blocks
|
||||
// this groups same-colored pixels in the same column into a single block to reduce the block count
|
||||
// any further pixel-grouping optimisations (eg 2D grouping) risk increasing conversion time higher than O(x*y)
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
QuantizedPixel qVoxel = new QuantizedPixel{color = BlockColors.Default, darkness = 10};
|
||||
float3 scale = new float3(1, 1, 1);
|
||||
position.x += (float)(blockSize);
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
//position.y += (float)blockSize;
|
||||
Color pixel = img.GetPixel(x, y);
|
||||
QuantizedPixel qPixel = quantizeColor(pixel);
|
||||
if (qPixel.darkness != qVoxel.darkness || qPixel.color != qVoxel.color || qPixel.visible != qVoxel.visible)
|
||||
{
|
||||
if (y != 0)
|
||||
{
|
||||
if (qVoxel.visible)
|
||||
{
|
||||
position.y = zero_y + (float)((y * blockSize + (y - scale.y) * blockSize) / 2);
|
||||
Placement.PlaceBlock(BlockIDs.AluminiumCube, position, color: qVoxel.color, darkness: qVoxel.darkness, scale: scale);
|
||||
blockCount++;
|
||||
}
|
||||
scale = new float3(1, 1, 1);
|
||||
}
|
||||
qVoxel = qPixel;
|
||||
}
|
||||
else
|
||||
{
|
||||
scale.y += 1;
|
||||
}
|
||||
|
||||
}
|
||||
if (qVoxel.visible)
|
||||
{
|
||||
position.y = zero_y + (float)((height * blockSize + (height - scale.y) * blockSize) / 2);
|
||||
Placement.PlaceBlock(BlockIDs.AluminiumCube, position, color: qVoxel.color, darkness: qVoxel.darkness, scale: scale);
|
||||
blockCount++;
|
||||
}
|
||||
//position.y = zero_y;
|
||||
}
|
||||
Logging.CommandLog($"Placed {width}x{height} image beside you ({blockCount} blocks total)");
|
||||
Logging.MetaLog($"Saved {(width * height) - blockCount} blocks while placing {filepath}");
|
||||
}
|
||||
|
||||
private void setScale(uint _width, uint _height)
|
||||
{
|
||||
width = _width;
|
||||
height = _height;
|
||||
Logging.CommandLog($"Pixi image size set to {width}x{height}");
|
||||
}
|
||||
|
||||
private void pixelate3DFile(string filepath)
|
||||
{
|
||||
// TODO?
|
||||
Logging.CommandLogError("Oh no you found this command!\nCommand functionality not implemented (yet)");
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private QuantizedPixel quantizeColor(Color pixel)
|
||||
{
|
||||
BlockColors color = BlockColors.Default;
|
||||
int darkness = 0;
|
||||
Logging.MetaDebugLog($"Color (r:{pixel.r}, g:{pixel.g}, b:{pixel.b})");
|
||||
if (Mathf.Abs(pixel.r - pixel.g) < pixel.r * 0.1f && Mathf.Abs(pixel.r - pixel.b) < pixel.r * 0.1f)
|
||||
{
|
||||
color = BlockColors.White;
|
||||
darkness = (int)(10 - ((pixel.r + pixel.g + pixel.b) * 3.5));
|
||||
//Logging.MetaDebugLog($"Color (r:{pixel.r}, g:{pixel.g}, b:{pixel.b})");
|
||||
}
|
||||
else if (pixel.r >= pixel.g && pixel.r >= pixel.b)
|
||||
{
|
||||
// Red is highest
|
||||
if ((pixel.r - pixel.g) > pixel.r * 0.5 && (pixel.r - pixel.b) > pixel.r * 0.5)
|
||||
{
|
||||
// Red is much higher than other pixels
|
||||
darkness = (int)(10 - (pixel.r * 11));
|
||||
color = BlockColors.Red;
|
||||
}
|
||||
else if ((pixel.g - pixel.b) > pixel.g * 0.3)
|
||||
{
|
||||
// Green is much higher than blue
|
||||
darkness = (int)(10 - ((pixel.r + pixel.g) * 5.1));
|
||||
if ((pixel.r - pixel.g) > pixel.r * 0.5)
|
||||
{
|
||||
color = BlockColors.Orange;
|
||||
}
|
||||
else
|
||||
{
|
||||
color = BlockColors.Yellow;
|
||||
}
|
||||
|
||||
}
|
||||
else if ((pixel.b - pixel.g) > pixel.b * 0.5)
|
||||
{
|
||||
// Blue is much higher than green
|
||||
darkness = (int)(10 - ((pixel.r + pixel.b) * 4.9));
|
||||
color = BlockColors.Purple;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Green is close strength to blue
|
||||
darkness = (int)(10 - ((pixel.r + pixel.g + pixel.b) * 4.8));
|
||||
color = BlockColors.Pink;
|
||||
}
|
||||
}
|
||||
else if (pixel.g >= pixel.r && pixel.g >= pixel.b)
|
||||
{
|
||||
// Green is highest
|
||||
if ((pixel.g - pixel.r) > pixel.g * 0.5 && (pixel.g - pixel.b) > pixel.g * 0.5)
|
||||
{
|
||||
// Green is much higher than other pixels
|
||||
darkness = (int)(10 - (pixel.g * 11));
|
||||
color = BlockColors.Green;
|
||||
}
|
||||
else if ((pixel.r - pixel.b) > pixel.r * 0.5)
|
||||
{
|
||||
// Red is much higher than blue
|
||||
darkness = (int)(10 - ((pixel.r + pixel.g) * 5.1));
|
||||
color = BlockColors.Yellow;
|
||||
}
|
||||
else if ((pixel.b - pixel.r) > pixel.b * 0.5)
|
||||
{
|
||||
// Blue is much higher than red
|
||||
darkness = (int)(10 - ((pixel.g + pixel.b) * 5.1));
|
||||
color = BlockColors.Aqua;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Red is close strength to blue
|
||||
darkness = (int)(10 - ((pixel.r + pixel.g + pixel.b) * 4.8));
|
||||
color = BlockColors.Lime;
|
||||
}
|
||||
}
|
||||
else if (pixel.b >= pixel.g && pixel.b >= pixel.r)
|
||||
{
|
||||
// Blue is highest
|
||||
if ((pixel.b - pixel.g) > pixel.b * 0.5 && (pixel.b - pixel.r) > pixel.b * 0.5)
|
||||
{
|
||||
// Blue is much higher than other pixels
|
||||
darkness = (int)(10 - (pixel.b * 11));
|
||||
color = BlockColors.Blue;
|
||||
}
|
||||
else if ((pixel.g - pixel.r) > pixel.g * 0.5)
|
||||
{
|
||||
// Green is much higher than red
|
||||
darkness = (int)(10 - ((pixel.g + pixel.b) * 5.1));
|
||||
color = BlockColors.Aqua;
|
||||
}
|
||||
else if ((pixel.r - pixel.g) > pixel.r * 0.5)
|
||||
{
|
||||
// Red is much higher than green
|
||||
darkness = (int)(10 - ((pixel.r + pixel.b) * 4.9));
|
||||
color = BlockColors.Purple;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Green is close strength to red
|
||||
darkness = (int)(10 - ((pixel.r + pixel.g + pixel.b) * 4.9));
|
||||
color = BlockColors.Aqua;
|
||||
}
|
||||
}
|
||||
if (darkness > 9) darkness = 9;
|
||||
if (darkness < 0) darkness = 0;
|
||||
// darkness 0 is the most saturated (it's not just the lightest)
|
||||
Logging.MetaDebugLog($"Quantized Color {color} d:{darkness}");
|
||||
return new QuantizedPixel { color = color, darkness = (byte)darkness, visible = pixel.a > 0.5f};
|
||||
}
|
||||
}
|
||||
|
||||
internal struct QuantizedPixel
|
||||
{
|
||||
public BlockColors color;
|
||||
|
||||
public byte darkness;
|
||||
|
||||
public bool visible;
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
using System;
|
||||
|
||||
using GamecraftModdingAPI.Utility;
|
||||
using Svelto.ECS;
|
||||
using Unity.Mathematics;
|
||||
using RobocraftX.Physics;
|
||||
using RobocraftX.Character;
|
||||
|
||||
namespace Pixi
|
||||
{
|
||||
internal class PlayerLocationEngine : IApiEngine
|
||||
{
|
||||
public string Name => "PixiPlayerLocationGameEngine";
|
||||
|
||||
public EntitiesDB entitiesDB { set; private get; }
|
||||
|
||||
public void Dispose() {}
|
||||
|
||||
public void Ready() {}
|
||||
|
||||
public float3 GetPlayerLocation(uint playerId)
|
||||
{
|
||||
return entitiesDB.QueryEntity<RigidBodyEntityStruct>(playerId, CharacterExclusiveGroups.OnFootGroup).position;
|
||||
}
|
||||
}
|
||||
}
|
30
Pixi/Robots/CubeInfo.cs
Normal file
30
Pixi/Robots/CubeInfo.cs
Normal file
|
@ -0,0 +1,30 @@
|
|||
using System;
|
||||
using Unity.Mathematics;
|
||||
using GamecraftModdingAPI.Blocks;
|
||||
|
||||
namespace Pixi.Robots
|
||||
{
|
||||
public struct CubeInfo
|
||||
{
|
||||
// you can't inherit from structs in C#...
|
||||
// this is an extension of BlockInfo
|
||||
public BlockIDs block;
|
||||
|
||||
public BlockColors color;
|
||||
|
||||
public byte darkness;
|
||||
|
||||
public bool visible;
|
||||
|
||||
// additions
|
||||
public float3 rotation;
|
||||
|
||||
public float3 position;
|
||||
|
||||
public float3 scale;
|
||||
|
||||
public string name;
|
||||
|
||||
public uint cubeId;
|
||||
}
|
||||
}
|
338
Pixi/Robots/CubeUtility.cs
Normal file
338
Pixi/Robots/CubeUtility.cs
Normal file
|
@ -0,0 +1,338 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
using RobocraftX.Common;
|
||||
using Newtonsoft.Json;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
|
||||
using GamecraftModdingAPI.Blocks;
|
||||
using GamecraftModdingAPI.Utility;
|
||||
using GamecraftModdingAPI;
|
||||
|
||||
using Pixi.Common;
|
||||
|
||||
namespace Pixi.Robots
|
||||
{
|
||||
public static class CubeUtility
|
||||
{
|
||||
private static Dictionary<uint, string> map = null;
|
||||
|
||||
private static Dictionary<uint, BlockJsonInfo[]> blueprintMap = null;
|
||||
|
||||
public static RobotStruct? ParseRobotInfo(string robotInfo)
|
||||
{
|
||||
try
|
||||
{
|
||||
return JsonConvert.DeserializeObject<RobotStruct>(robotInfo);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logging.MetaLog(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static CubeInfo[] ParseCubes(RobotStruct robot)
|
||||
{
|
||||
return ParseCubes(robot.cubeData, robot.colourData);
|
||||
}
|
||||
|
||||
public static CubeInfo[] ParseCubes(string cubeData, string colourData)
|
||||
{
|
||||
BinaryBufferReader cubes = new BinaryBufferReader(Convert.FromBase64String(cubeData), 0);
|
||||
BinaryBufferReader colours = new BinaryBufferReader(Convert.FromBase64String(colourData), 0);
|
||||
uint cubeCount = cubes.ReadUint();
|
||||
uint colourCount = colours.ReadUint();
|
||||
if (cubeCount != colourCount)
|
||||
{
|
||||
Logging.MetaLog("Something is fucking broken");
|
||||
return null;
|
||||
}
|
||||
Logging.MetaLog($"Detected {cubeCount} cubes");
|
||||
CubeInfo[] result = new CubeInfo[cubeCount];
|
||||
for (int cube = 0; cube < cubeCount; cube++)
|
||||
{
|
||||
result[cube] = TranslateSpacialEnumerations(
|
||||
cubes.ReadUint(),
|
||||
cubes.ReadByte(),
|
||||
cubes.ReadByte(),
|
||||
cubes.ReadByte(),
|
||||
cubes.ReadByte(),
|
||||
colours.ReadByte(),
|
||||
colours.ReadByte(),
|
||||
colours.ReadByte(),
|
||||
colours.ReadByte()
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static CubeInfo TranslateSpacialEnumerations(uint cubeId, byte x, byte y, byte z, byte rotation, byte colour, byte colour_x, byte colour_y, byte colour_z)
|
||||
{
|
||||
if (x != colour_x || z != colour_z || y != colour_y) return default;
|
||||
CubeInfo result = new CubeInfo {visible = true, cubeId = cubeId, scale = new float3(1, 1, 1)};
|
||||
TranslateBlockColour(colour, ref result);
|
||||
TranslateBlockPosition(x, y, z, ref result);
|
||||
TranslateBlockRotation(rotation, ref result);
|
||||
TranslateBlockId(cubeId, ref result);
|
||||
#if DEBUG
|
||||
Logging.MetaLog($"Cube {cubeId} ({x}, {y}, {z}) rot:{rotation} decoded as {result.block} {result.position} rot: {result.rotation} color: {result.color} {result.darkness}");
|
||||
#endif
|
||||
return result;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void TranslateBlockRotation(byte rotation, ref CubeInfo result)
|
||||
{
|
||||
// face refers to the face of the block connected to the bottom of the current one
|
||||
// nvm, they're all incorrect
|
||||
switch (rotation)
|
||||
{
|
||||
case 0:
|
||||
result.rotation = new float3(0, 0, 0); // top face, forwards
|
||||
break;
|
||||
case 1:
|
||||
result.rotation = new float3(0, 0, 90); // left face, forwards
|
||||
break;
|
||||
case 2:
|
||||
result.rotation = new float3(0, 0, 180); // bottom face, forwards
|
||||
break;
|
||||
case 3:
|
||||
result.rotation = new float3(0, 0, -90); // front face, down
|
||||
break;
|
||||
case 4:
|
||||
result.rotation = new float3(0, 90, 0); // top face, right
|
||||
break;
|
||||
case 5:
|
||||
result.rotation = new float3(0, 90, 90); // front face, right
|
||||
break;
|
||||
case 6:
|
||||
result.rotation = new float3(-90, -90, 0); // right face, backwards
|
||||
break;
|
||||
case 7:
|
||||
result.rotation = new float3(0, 90, -90); // back face, right
|
||||
break;
|
||||
case 8:
|
||||
result.rotation = new float3(0, -90, 90); // back face, left
|
||||
break;
|
||||
case 9:
|
||||
result.rotation = new float3(0, -90, -90); // front face, left
|
||||
break;
|
||||
case 10:
|
||||
result.rotation = new float3(90, -90, 0); // left face, down
|
||||
break;
|
||||
case 11:
|
||||
result.rotation = new float3(90, 90, 0); // right face, forwards
|
||||
break;
|
||||
case 12:
|
||||
result.rotation = new float3(-90, 90, 0); // left face, up
|
||||
break;
|
||||
case 13:
|
||||
result.rotation = new float3(0, 90, 180); // bottom face, right
|
||||
break;
|
||||
case 14:
|
||||
result.rotation = new float3(0, 180, 0); // top face, backwards
|
||||
break;
|
||||
case 15:
|
||||
result.rotation = new float3(0, 180, 90); // right face, up
|
||||
break;
|
||||
case 16:
|
||||
result.rotation = new float3(0, 180, 180); // bottom face, backwards
|
||||
break;
|
||||
case 17:
|
||||
result.rotation = new float3(0, 180, -90); // left face, backwards
|
||||
break;
|
||||
case 18:
|
||||
result.rotation = new float3(0, -90, 0); // top face, left
|
||||
break;
|
||||
case 19:
|
||||
result.rotation = new float3(0, -90, 180); // bottom face, left
|
||||
break;
|
||||
case 20:
|
||||
result.rotation = new float3(90, 0, 0); // front face, down
|
||||
break;
|
||||
case 21:
|
||||
result.rotation = new float3(90, 180, 0); // back face, down
|
||||
break;
|
||||
case 22:
|
||||
result.rotation = new float3(-90, 0, 0); // back face, up
|
||||
break;
|
||||
case 23:
|
||||
result.rotation = new float3(-90, 180, 0); // front face, up
|
||||
break;
|
||||
default:
|
||||
#if DEBUG
|
||||
Logging.MetaLog($"Unknown rotation {rotation.ToString("X2")}");
|
||||
#endif
|
||||
result.rotation = float3.zero;
|
||||
break;
|
||||
}
|
||||
// my brain hurts after figuring out all of those rotations
|
||||
// I wouldn't recommend trying to redo this
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void TranslateBlockPosition(byte x, byte y, byte z, ref CubeInfo result)
|
||||
{
|
||||
// for some reason, z is forwards in garage bays
|
||||
result.position = new float3(x, y, z);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void TranslateBlockColour(byte colour, ref CubeInfo result)
|
||||
{
|
||||
// I hope these colours are accurate, I just guessed
|
||||
// TODO colour accuracy (lol that won't ever happen)
|
||||
#if DEBUG
|
||||
Logging.MetaLog($"Cube colour {colour}");
|
||||
#endif
|
||||
BlockColor c = ColorSpaceUtility.QuantizeToBlockColor(colour);
|
||||
result.color = c.Color;
|
||||
result.darkness = c.Darkness;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static string CubeIdDescription(uint cubeId)
|
||||
{
|
||||
if (map == null)
|
||||
{
|
||||
StreamReader cubemap = new StreamReader(Assembly.GetExecutingAssembly().GetManifestResourceStream("Pixi.cubes-id.json"));
|
||||
map = JsonConvert.DeserializeObject<Dictionary<uint, string>>(cubemap.ReadToEnd());
|
||||
}
|
||||
if (!map.ContainsKey(cubeId))
|
||||
{
|
||||
return "Unknown cube #" + cubeId.ToString();
|
||||
//result.rotation = float3.zero;
|
||||
}
|
||||
return map[cubeId];
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void TranslateBlockId(uint cubeId, ref CubeInfo result)
|
||||
{
|
||||
if (map == null)
|
||||
{
|
||||
StreamReader cubemap = new StreamReader(Assembly.GetExecutingAssembly().GetManifestResourceStream("Pixi.cubes-id.json"));
|
||||
map = JsonConvert.DeserializeObject<Dictionary<uint, string>>(cubemap.ReadToEnd());
|
||||
}
|
||||
|
||||
if (!map.ContainsKey(cubeId))
|
||||
{
|
||||
result.block = BlockIDs.TextBlock;
|
||||
result.name = "Unknown cube #" + cubeId.ToString();
|
||||
//result.rotation = float3.zero;
|
||||
#if DEBUG
|
||||
Logging.MetaLog($"Unknown cubeId {cubeId}");
|
||||
#endif
|
||||
}
|
||||
string cubeName = map[cubeId];
|
||||
|
||||
string gcName = cubeName.Contains("glass") || cubeName.Contains("windshield")
|
||||
? "Glass"
|
||||
: "Aluminium";
|
||||
if (cubeName.Contains("round"))
|
||||
gcName += "Rounded";
|
||||
|
||||
if (cubeName.Contains("cube"))
|
||||
gcName += "Cube";
|
||||
else if (cubeName.Contains("prism") || cubeName.Contains("edge"))
|
||||
gcName += "Slope";
|
||||
else if (cubeName.Contains("inner"))
|
||||
gcName += "SlicedCube";
|
||||
else if (cubeName.Contains("tetra") || cubeName.Contains("corner"))
|
||||
gcName += "Corner";
|
||||
else if (cubeName.Contains("pyramid"))
|
||||
gcName += "PyramidSegment";
|
||||
else if (cubeName.Contains("cone"))
|
||||
gcName += "ConeSegment";
|
||||
else
|
||||
{
|
||||
result.block = BlockIDs.TextBlock;
|
||||
result.name = cubeName;
|
||||
return;
|
||||
}
|
||||
|
||||
BlockIDs id = VoxelObjectNotationUtility.NameToEnum(gcName);
|
||||
result.block = id;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Block[] BuildBlueprintOrTextBlock(CubeInfo cube, float3 actualPosition, int scale = 3)
|
||||
{
|
||||
// actualPosition is the middle of the cube
|
||||
if (blueprintMap == null) LoadBlueprintMap();
|
||||
if (!blueprintMap.ContainsKey(cube.cubeId) || scale != 3)
|
||||
{
|
||||
#if DEBUG
|
||||
Logging.LogWarning($"Missing blueprint for {cube.name} (id:{cube.cubeId}), substituting {cube.block}");
|
||||
#endif
|
||||
return new Block[] { Block.PlaceNew(cube.block, actualPosition, cube.rotation, cube.color, cube.darkness, scale: cube.scale) };
|
||||
}
|
||||
#if DEBUG
|
||||
Logging.MetaLog($"Found blueprint for {cube.name} (id:{cube.cubeId})");
|
||||
#endif
|
||||
Quaternion cubeQuaternion = Quaternion.Euler(cube.rotation);
|
||||
BlockJsonInfo[] blueprint = blueprintMap[cube.cubeId];
|
||||
if (blueprint.Length == 0)
|
||||
{
|
||||
Logging.LogWarning($"Found empty blueprint for {cube.name} (id:{cube.cubeId}), is the blueprint correct?");
|
||||
return new Block[0];
|
||||
}
|
||||
float3 defaultCorrectionVec = new float3((float)(0), (float)(CommandRoot.BLOCK_SIZE), (float)(0));
|
||||
float3 baseRot = new float3(blueprint[0].rotation[0], blueprint[0].rotation[1], blueprint[0].rotation[2]);
|
||||
float3 baseScale = new float3(blueprint[0].scale[0], blueprint[0].scale[1], blueprint[0].scale[2]);
|
||||
Block[] placedBlocks = new Block[blueprint.Length];
|
||||
bool isBaseScaled = !(blueprint[0].scale[1] > 0f && blueprint[0].scale[1] < 2f);
|
||||
float3 correctionVec = isBaseScaled ? (float3)(Quaternion.Euler(baseRot) * baseScale / 2) * (float)-CommandRoot.BLOCK_SIZE : -defaultCorrectionVec;
|
||||
// FIXME scaled base blocks cause the blueprint to be placed in the wrong location (this also could be caused by a bug in DumpVON command)
|
||||
if (isBaseScaled)
|
||||
{
|
||||
Logging.LogWarning($"Found blueprint with scaled base block for {cube.name} (id:{cube.cubeId}), this is not currently supported");
|
||||
}
|
||||
for (int i = 0; i < blueprint.Length; i++)
|
||||
{
|
||||
BlockColor blueprintBlockColor = ColorSpaceUtility.QuantizeToBlockColor(blueprint[i].color);
|
||||
BlockColors blockColor = blueprintBlockColor.Color == BlockColors.White && blueprintBlockColor.Darkness == 0 ? cube.color : blueprintBlockColor.Color;
|
||||
byte blockDarkness = blueprintBlockColor.Color == BlockColors.White && blueprintBlockColor.Darkness == 0 ? cube.darkness : blueprintBlockColor.Darkness;
|
||||
float3 bluePos = new float3(blueprint[i].position[0], blueprint[i].position[1], blueprint[i].position[2]);
|
||||
float3 blueScale = new float3(blueprint[i].scale[0], blueprint[i].scale[1], blueprint[i].scale[2]);
|
||||
float3 blueRot = new float3(blueprint[i].rotation[0], blueprint[i].rotation[1], blueprint[i].rotation[2]);
|
||||
float3 physicalLocation = (float3)(cubeQuaternion * bluePos) + actualPosition;// + (blueprintSizeRotated / 2);
|
||||
//physicalLocation.x += blueprintSize.x / 2;
|
||||
physicalLocation += (float3)(cubeQuaternion * (correctionVec));
|
||||
//physicalLocation.y -= (float)(RobotCommands.blockSize * scale / 2);
|
||||
//float3 physicalScale = (float3)(cubeQuaternion * blueScale); // this actually over-rotates when combined with rotation
|
||||
float3 physicalScale = blueScale;
|
||||
float3 physicalRotation = (cubeQuaternion * Quaternion.Euler(blueRot)).eulerAngles;
|
||||
#if DEBUG
|
||||
Logging.MetaLog($"Placing blueprint block at {physicalLocation} rot{physicalRotation} scale{physicalScale}");
|
||||
Logging.MetaLog($"Location math check original:{bluePos} rotated: {(float3)(cubeQuaternion * bluePos)} actualPos: {actualPosition} result: {physicalLocation}");
|
||||
Logging.MetaLog($"Scale math check original:{blueScale} rotation: {(float3)cubeQuaternion.eulerAngles} result: {physicalScale}");
|
||||
Logging.MetaLog($"Rotation math check original:{blueRot} rotated: {(cubeQuaternion * Quaternion.Euler(blueRot))} result: {physicalRotation}");
|
||||
#endif
|
||||
placedBlocks[i] = Block.PlaceNew(VoxelObjectNotationUtility.NameToEnum(blueprint[i].name),
|
||||
physicalLocation,
|
||||
physicalRotation,
|
||||
blockColor,
|
||||
blockDarkness,
|
||||
scale: physicalScale);
|
||||
}
|
||||
#if DEBUG
|
||||
Logging.MetaLog($"Placed {placedBlocks.Length} blocks for blueprint {cube.name} (id:{cube.cubeId})");
|
||||
#endif
|
||||
return placedBlocks;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void LoadBlueprintMap()
|
||||
{
|
||||
StreamReader bluemap = new StreamReader(Assembly.GetExecutingAssembly().GetManifestResourceStream("Pixi.blueprints.json"));
|
||||
blueprintMap = JsonConvert.DeserializeObject<Dictionary<uint, BlockJsonInfo[]>>(bluemap.ReadToEnd());
|
||||
}
|
||||
}
|
||||
}
|
75
Pixi/Robots/RoboAPIUtility.cs
Normal file
75
Pixi/Robots/RoboAPIUtility.cs
Normal file
|
@ -0,0 +1,75 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using GamecraftModdingAPI.Utility;
|
||||
|
||||
namespace Pixi.Robots
|
||||
{
|
||||
public static class RoboAPIUtility
|
||||
{
|
||||
private const string ROBOT_API_LIST_URL = "https://factory.robocraftgame.com/api/roboShopItems/list";
|
||||
|
||||
private const string ROBOT_API_GET_URL = "https://factory.robocraftgame.com/api/roboShopItems/get/";
|
||||
|
||||
private const string ROBOT_API_TOKEN = "Web eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJQdWJsaWNJZCI6IjEyMyIsIkRpc3BsYXlOYW1lIjoiVGVzdCIsIlJvYm9jcmFmdE5hbWUiOiJGYWtlQ1JGVXNlciIsIkZsYWdzIjpbXSwiaXNzIjoiRnJlZWphbSIsInN1YiI6IldlYiIsImlhdCI6MTU0NTIyMzczMiwiZXhwIjoyNTQ1MjIzNzkyfQ.ralLmxdMK9rVKPZxGng8luRIdbTflJ4YMJcd25dKlqg";
|
||||
|
||||
public static RobotBriefStruct[] ListRobots(string searchFilter, int pageSize = 10, bool playerFilter = false)
|
||||
{
|
||||
// pageSize <= 2 seems to retrieve items unreliably
|
||||
string bodyJson = $"{{\"page\": 1, \"pageSize\": {pageSize}, \"order\": 0, \"playerFilter\": {playerFilter.ToString().ToLower()}, \"movementFilter\": \"100000,200000,300000,400000,500000,600000,700000,800000,900000,1000000,1100000,1200000\", \"movementCategoryFilter\": \"100000,200000,300000,400000,500000,600000,700000,800000,900000,1000000,1100000,1200000\", \"weaponFilter\": \"10000000,20000000,25000000,30000000,40000000,50000000,60000000,65000000,70100000,75000000\", \"weaponCategoryFilter\": \"10000000,20000000,25000000,30000000,40000000,50000000,60000000,65000000,70100000,75000000\", \"minimumCpu\": -1, \"maximumCpu\": -1, \"minimumRobotRanking\": 0, \"maximumRobotRanking\": 1000000000, \"textFilter\": \"{searchFilter}\", \"textSearchField\": 0, \"buyable\": true, \"prependFeaturedRobot\": false, \"featuredOnly\": false, \"defaultPage\": false}}";
|
||||
byte[] reqBody = Encoding.UTF8.GetBytes(bodyJson);
|
||||
#if DEBUG
|
||||
Logging.MetaLog($"POST body\n{bodyJson}");
|
||||
#endif
|
||||
// download robot list
|
||||
// FIXME this blocks main thread
|
||||
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(ROBOT_API_LIST_URL);
|
||||
// request
|
||||
request.Method = "POST";
|
||||
request.ContentLength = reqBody.Length;
|
||||
request.ContentType = "application/json";
|
||||
request.Headers.Add(HttpRequestHeader.Authorization, ROBOT_API_TOKEN);
|
||||
request.Accept = "application/json; charset=utf-8"; // HTTP Status 500 without
|
||||
Stream body;
|
||||
body = request.GetRequestStream();
|
||||
body.Write(reqBody, 0, reqBody.Length);
|
||||
body.Close();
|
||||
// response
|
||||
HttpWebResponse response;
|
||||
response = (HttpWebResponse)request.GetResponse();
|
||||
// regular Stream was unreliable
|
||||
// because they could read everything before everything was availabe
|
||||
StreamReader respReader = new StreamReader(response.GetResponseStream());
|
||||
string bodyStr = respReader.ReadToEnd();
|
||||
RobotListResponse rlr = JsonConvert.DeserializeObject<RobotListResponse>(bodyStr);
|
||||
return rlr.response.roboShopItems;
|
||||
}
|
||||
|
||||
public static RobotStruct QueryRobotInfo(int robotId)
|
||||
{
|
||||
// download robot info
|
||||
// FIXME this blocks main thread
|
||||
string url = ROBOT_API_GET_URL + robotId.ToString();
|
||||
Logging.MetaLog(url);
|
||||
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
|
||||
// request
|
||||
request.Method = "GET";
|
||||
request.ContentType = "application/json";
|
||||
request.Accept = "application/json; charset=utf-8"; // HTTP Status 500 without
|
||||
request.Headers.Add(HttpRequestHeader.Authorization, ROBOT_API_TOKEN);
|
||||
// response
|
||||
HttpWebResponse response;
|
||||
response = (HttpWebResponse)request.GetResponse();
|
||||
// regular Stream was unreliable
|
||||
// because they could read everything before everything was availabe
|
||||
StreamReader body = new StreamReader(response.GetResponseStream());
|
||||
string bodyStr = body.ReadToEnd();
|
||||
response.Close();
|
||||
RobotInfoResponse rir = JsonConvert.DeserializeObject<RobotInfoResponse>(bodyStr);
|
||||
return rir.response;
|
||||
}
|
||||
}
|
||||
}
|
49
Pixi/Robots/RobotAPIStructs.cs
Normal file
49
Pixi/Robots/RobotAPIStructs.cs
Normal file
|
@ -0,0 +1,49 @@
|
|||
using System;
|
||||
namespace Pixi.Robots
|
||||
{
|
||||
public struct RobotBriefStruct
|
||||
{
|
||||
public int itemId;
|
||||
|
||||
public string itemName;
|
||||
|
||||
public string itemDescription;
|
||||
|
||||
public string thumbnail;
|
||||
|
||||
public string addedBy;
|
||||
|
||||
public string addedByDisplayName;
|
||||
|
||||
public int cpu;
|
||||
|
||||
public int totalRobotRanking;
|
||||
|
||||
public string cubeData;
|
||||
|
||||
public string colourData;
|
||||
|
||||
public bool featured;
|
||||
|
||||
public string cubeAmounts; // this is sent incorrectly by the API server (it's actually a Dictionary<string, int>)
|
||||
}
|
||||
|
||||
public struct RobotList
|
||||
{
|
||||
public RobotBriefStruct[] roboShopItems;
|
||||
}
|
||||
|
||||
public struct RobotListResponse
|
||||
{
|
||||
public RobotList response;
|
||||
|
||||
public int statusCode;
|
||||
}
|
||||
|
||||
public struct RobotInfoResponse
|
||||
{
|
||||
public RobotStruct response;
|
||||
|
||||
public int statusCode;
|
||||
}
|
||||
}
|
140
Pixi/Robots/RobotBlueprintProvider.cs
Normal file
140
Pixi/Robots/RobotBlueprintProvider.cs
Normal 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
|
||||
}
|
||||
}
|
47
Pixi/Robots/RobotCommands.cs
Normal file
47
Pixi/Robots/RobotCommands.cs
Normal file
|
@ -0,0 +1,47 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
|
||||
using Unity.Mathematics;
|
||||
|
||||
using GamecraftModdingAPI;
|
||||
using GamecraftModdingAPI.Blocks;
|
||||
using GamecraftModdingAPI.Commands;
|
||||
using GamecraftModdingAPI.Players;
|
||||
using GamecraftModdingAPI.Utility;
|
||||
|
||||
using Pixi.Common;
|
||||
|
||||
namespace Pixi.Robots
|
||||
{
|
||||
public static class RobotCommands
|
||||
{
|
||||
public static void CreatePartDumpCommand()
|
||||
{
|
||||
CommandBuilder.Builder()
|
||||
.Name("DumpVON")
|
||||
.Description("Dump a block structure to a JSON file compatible with Pixi's internal VON format")
|
||||
.Action<string>(DumpBlockStructure)
|
||||
.Build();
|
||||
}
|
||||
|
||||
private static void DumpBlockStructure(string filename)
|
||||
{
|
||||
Player local = new Player(PlayerType.Local);
|
||||
Block baseBlock = local.GetBlockLookedAt();
|
||||
Block[] blocks = local.GetSelectedBlocks();
|
||||
if (blocks.Length == 0)
|
||||
blocks = baseBlock.GetConnectedCubes();
|
||||
bool isBaseScaled = !(baseBlock.Scale.x > 0 && baseBlock.Scale.x < 2 && baseBlock.Scale.y > 0 && baseBlock.Scale.y < 2 && baseBlock.Scale.z > 0 && baseBlock.Scale.z < 2);
|
||||
if (isBaseScaled)
|
||||
{
|
||||
Logging.CommandLogWarning($"Detected scaled base block. This is not currently supported");
|
||||
}
|
||||
float3 basePos = baseBlock.Position;
|
||||
string von = VoxelObjectNotationUtility.SerializeBlocks(blocks, new float[] { basePos.x, basePos.y, basePos.z });
|
||||
File.WriteAllText(filename, von);
|
||||
}
|
||||
}
|
||||
}
|
149
Pixi/Robots/RobotInternetImporter.cs
Normal file
149
Pixi/Robots/RobotInternetImporter.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
30
Pixi/Robots/RobotStruct.cs
Normal file
30
Pixi/Robots/RobotStruct.cs
Normal file
|
@ -0,0 +1,30 @@
|
|||
using System;
|
||||
namespace Pixi.Robots
|
||||
{
|
||||
public struct RobotStruct
|
||||
{
|
||||
public int id;
|
||||
|
||||
public string name;
|
||||
|
||||
public string description;
|
||||
|
||||
public string thumbnail;
|
||||
|
||||
public string addedBy;
|
||||
|
||||
public string addedByDisplayName;
|
||||
|
||||
public int cpu;
|
||||
|
||||
public int totalRobotRanking;
|
||||
|
||||
public string cubeData;
|
||||
|
||||
public string colourData;
|
||||
|
||||
public bool featured;
|
||||
|
||||
public string cubeAmounts; // this is sent incorrectly by the API server (it's actually a Dictionary<string, int>)
|
||||
}
|
||||
}
|
52
Pixi/TestImporter.cs
Normal file
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
|
||||
}
|
||||
}
|
||||
}
|
327
Pixi/blueprints.json
Normal file
327
Pixi/blueprints.json
Normal file
File diff suppressed because one or more lines are too long
1
Pixi/cubes-id.json
Normal file
1
Pixi/cubes-id.json
Normal file
File diff suppressed because one or more lines are too long
107
README.md
107
README.md
|
@ -1,49 +1,116 @@
|
|||
# Pixi
|
||||
|
||||
Gamecraft mod for converting images into coloured blocks.
|
||||
Think of it like automatic pixel art.
|
||||
A mod for importing images and more into Gamecraft.
|
||||
|
||||
Developed by NGnius.
|
||||
|
||||
## Installation
|
||||
|
||||
To install the Pixi mod, copy the build's `Pixi.dll` into the `Plugins` folder in Gamecraft's main folder.
|
||||
Before installing Pixi, please patch Gamecraft with [GCIPA](https://git.exmods.org/modtainers/GCIPA/releases) and install the latest version of [GamecraftModdingAPI](https://git.exmods.org/modtainers/GamecraftModdingAPI/releases).
|
||||
|
||||
Once that's done, install Pixi by copying `Pixi.dll` (from the latest release) into the `Plugins` folder in Gamecraft's main folder.
|
||||
Alternately, follow the install guide: https://www.exmods.org/guides/install.html (ignore the part about a zip file -- move Pixi.dll into the Plugins folder instead).
|
||||
|
||||
## Usage
|
||||
|
||||
Pixi adds new commands to Gamecraft's command line to import images into a game.
|
||||
Pixi adds new commands to Gamecraft's command line to import images, and other stuff, into a game.
|
||||
Since Pixi places vanilla Gamecraft blocks, imported files should be visible without Pixi installed.
|
||||
|
||||
`PixiScale [width] [height]` sets the block canvas size (usually you'll want this to be the same size as your image).
|
||||
When conversion using `Pixi2D` is done, if the canvas is larger than your image the image will be repeated.
|
||||
If the canvas is smaller than your image, the image will be cropped based to the lower left corner.
|
||||
For the following section, anything between `[` and `]` characters is a command argument you must provide by replacing everything inside and including the square brackets.
|
||||
An argument like `[dog name]` is an argument named "dog name" and could be a value like `Clifford` or `doggo`,
|
||||
and `@"[dog name]"` could be a value like `@"Clifford"` or `@"doggo"`.
|
||||
|
||||
`Pixi2D "[image]"` converts an image to blocks and places it as blocks beside where you're standing (along the xy-plane).
|
||||
If your image is not stored in the same folder as Gamecraft, you should specify the full filepath (eg `C:\path\to\image.png`) to the image.
|
||||
### Commands
|
||||
|
||||
For example, if you want to add an image called `pixel_art.png`,
|
||||
with a resolution of 1920x1080, stored in Gamecraft's installation directory,
|
||||
execute the command `PixiScale 1920 1080` to set the size and then `Pixi2D "pixel_art.png` to load the image.
|
||||
`Pixi @"[thing]"` to import `thing` into your Gamecraft game.
|
||||
|
||||
For example, if you want to add an image called `pixel_art.png`, stored in Gamecraft's installation directory,
|
||||
execute the command `Pixi @"pixel_art.png"` to load the image as blocks.
|
||||
It's important to include the file extension, since that's what makes Pixi's magic work.
|
||||
|
||||
If you know the name of the Pixi spell you want, you can also use
|
||||
|
||||
`Pixi2 "[spell]" @"[thing]"` to use `spell` to import `thing` into your Gamecraft game.
|
||||
|
||||
Some commands also have hidden features, like image rotation and bot scaling.
|
||||
Talk to NGnius on the Exmods Discord server or read Pixi's source code to figure that out.
|
||||
|
||||
### Behaviour
|
||||
|
||||
ImageText and ImageConsole share the same image conversion system.
|
||||
The conversion system converts every pixel to a [color tag](http://digitalnativestudios.com/textmeshpro/docs/rich-text/#color) followed by a square text character.
|
||||
For ImageText, the resulting character string is set to the text field of the text block that the command places.
|
||||
For ImageConsole, the character string is automatically set to a console block in the form `ChangeTextBlockCommand [text block id] [character string]`.
|
||||
Due to limitations in Gamecraft, larger images will crash your game!!!
|
||||
|
||||
ImageCanvas takes an image file and converts every pixel to a coloured block.
|
||||
ImageCanvas uses an algorithm to convert each pixel in an image into the closest paint colour, but colour accuracy will never be as good as a regular image.
|
||||
|
||||
ImageCanvas' colour-conversion algorithm also uses pixel transparency so you can cut out shapes.
|
||||
A pixel which has opacity of less than 50% will be ignored.
|
||||
A pixel which has an opacity between 75% and 50% will be converted into a glass cube.
|
||||
A pixel which has an opacity greater than 75% will be converted into the block you're holding (or aluminium if you've got your hand selected).
|
||||
This only works with `.PNG` image files since the `.JPG` format doesn't support image transparency.
|
||||
|
||||
ImageCanvas also optimises block placement, since images have a lot of pixels.
|
||||
The blocks grouping ratio is displayed in the command line output once image importing is completed.
|
||||
|
||||
RobocraftRobot converts a robot to equivalent Gamecraft blocks.
|
||||
If the conversion algorithm encounters a block it cannot convert, it will place a text block, with the block name, instead.
|
||||
|
||||
## Development
|
||||
|
||||
Show your love by offering your time.
|
||||
Show your love by offering your help!
|
||||
|
||||
### Ways To Contribute
|
||||
|
||||
- Build a Robocraft block that's not currently supported by Pixi (send it to NGnius on Discord).
|
||||
- Report any bugs that you encounter while using Pixi.
|
||||
- Report an idea for an improvement to Pixi or for a new file format.
|
||||
|
||||
For questions, concerns, or any other inquiry, please contact NGnius in the [Exmods Discord server](https://discord.exmods.org).
|
||||
|
||||
### Setup
|
||||
|
||||
Pixi's development environment is similar to most Gamecraft mods, since it's based on HelloModdingWorld's configuration.
|
||||
Pixi's development environment is similar to most Gamecraft mods, since it's based on HelloModdingWorld's configuration.
|
||||
|
||||
This project requires most of Gamecraft's `.dll` files to function correctly.
|
||||
Most, but not all, of these files are stored in Gamecraft's `Gamecraft_Data\Managed` folder.
|
||||
The project is pre-configured to look in a folder called ref in the solution's main directory or one level up from that.
|
||||
|
||||
You can make sure Pixi can find all of `.dll` files it needs by copying your Gamecraft folder here and renaming it to `ref`, but you'll have to re-copy it after every Gamecraft update.
|
||||
You can also create a symbolic link (look it up) to your Gamecraft install folder named `ref` in this folder to avoid having to re-copy files.
|
||||
You can make sure Pixi can find all `.dll` files it needs by copying your Gamecraft folder here and renaming it to `ref`, but you'll have to re-copy it after every Gamecraft update.
|
||||
To avoid that, create a symbolic link (look it up) to your Gamecraft install folder named `ref` in this folder instead.
|
||||
|
||||
Like most mods, you will have to patch your game with [GCIPA](https://git.exmods.org/modtainers/GCIPA).
|
||||
Pixi also requires the [GamecraftModdingAPI](https://git.exmods.org/modtainers/GamecraftModdingAPI) library to be installed (in `ref/Plugins/GamecraftModdingAPI.dll`).
|
||||
Pixi also requires the [GamecraftModdingAPI](https://git.exmods.org/modtainers/GamecraftModdingAPI) library to be installed (in `ref/Plugins/GamecraftModdingAPI.dll`, the usual place).
|
||||
|
||||
## Building
|
||||
### Building
|
||||
|
||||
After you've completed the setup, open the solution file `Pixi.sln` in your prefered C# .NET/Mono development environment.
|
||||
I'd recommend Visual Studio Community Edition or JetBrains Rider for Windows and Monodevelop for Linux.
|
||||
I'd recommend Visual Studio Community Edition for Windows or JetBrains Rider for Linux.
|
||||
|
||||
If you've successfully completed setup, you should be able to build the Pixi project without errors.
|
||||
If it doesn't work and you can't figure out why, ask for help on [our Discord server](https://discord.gg/xjnFxQV).
|
||||
If it doesn't work and you can't figure out why, ask for help on the [Exmods Discord server](https://discord.exmods.org).
|
||||
|
||||
# Acknowledgements
|
||||
|
||||
RobocraftRobot uses the Factory to download robots, which involves a partial re-implementation of [rcbup](https://github.com/NGnius/rcbup).
|
||||
Robot parsing uses information from [RobocraftAssembler](https://github.com/dddontshoot/RoboCraftAssembler).
|
||||
|
||||
Gamecraft interactions use the [GamecraftModdingAPI](https://git.exmods.org/modtainers/GamecraftModdingAPI).
|
||||
|
||||
MIDI file processing uses an integrated copy of melanchall's [DryWetMidi](https://github.com/melanchall/drywetmidi) library, licensed under the [MIT License](https://github.com/melanchall/drywetmidi/blob/develop/LICENSE).
|
||||
|
||||
Thanks to **TheGreenGoblin** and their Python app for converting images to coloured square characters, which inspired the PixiConsole and PixiText commands.
|
||||
|
||||
Thanks to **Mr. Rotor** for all of the Robocraft blocks used in the PixiBot and PixiBotFile commands.
|
||||
|
||||
# Disclaimer
|
||||
|
||||
Pixi source code and releases are available free of charge as open-source software for the purpose of modding Gamecraft.
|
||||
Modify Gamecraft at your own risk.
|
||||
Read the LICENSE file for official licensing information.
|
||||
Pixi, Exmods and NGnius are not endorsed or supported by Gamecraft or FreeJam.
|
||||
Please don't sue this project or its contributors (that's what all disclaimers boil down to, right?).
|
||||
|
||||
Pixi is actually just sufficiently advanced technology that's indistinguishable from magic.
|
||||
|
|
Loading…
Reference in a new issue