From e7ee1f202793c9ee854e54283c4c285eb0de3091 Mon Sep 17 00:00:00 2001 From: miku-666 <74728189+NessieHax@users.noreply.github.com> Date: Thu, 30 Jun 2022 17:53:40 +0200 Subject: [PATCH] Add GRFFile class and GRFFileReader --- .../Classes/FileTypes/GRFFile.cs | 246 ++++++++++++++++++ .../Classes/IO/GRF/GRFFileReader.cs | 168 ++++++++++++ .../Classes/IO/GRF/GRFFileWriter.cs | 92 +++++++ MinecraftUSkinEditor/Classes/Utils/RLE.cs | 215 +++++++++++++++ 4 files changed, 721 insertions(+) create mode 100644 MinecraftUSkinEditor/Classes/FileTypes/GRFFile.cs create mode 100644 MinecraftUSkinEditor/Classes/IO/GRF/GRFFileReader.cs create mode 100644 MinecraftUSkinEditor/Classes/IO/GRF/GRFFileWriter.cs create mode 100644 MinecraftUSkinEditor/Classes/Utils/RLE.cs diff --git a/MinecraftUSkinEditor/Classes/FileTypes/GRFFile.cs b/MinecraftUSkinEditor/Classes/FileTypes/GRFFile.cs new file mode 100644 index 00000000..7034aadb --- /dev/null +++ b/MinecraftUSkinEditor/Classes/FileTypes/GRFFile.cs @@ -0,0 +1,246 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PckStudio.Classes.FileTypes +{ + public class GRFFile + { + + public GRFTag RootTag = null; + public int Crc => _crc; + public eCompressionType CompressionType => _compressionType; + + private int _crc = 0; + private eCompressionType _compressionType = eCompressionType.None; + + [Flags] + public enum eCompressionType : byte + { + None = 0, + Zlib = 1, + ZlibRle = 2, + /// + /// custom enum value + /// + IsWolrdGrf = 0x80, + } + + public class GRFTag + { + private GRFTag _parent = null; + private SortedDictionary _parameters = new SortedDictionary(); + + /// + /// Contains all valid Parameter names + /// + public static readonly string[] ValidParameters = new string[] + { + "plus_x", + "minus_x", + "plus_z", + "minus_z", + "omni_plus_x", + "omni_minus_x", + "omni_plus_z", + "omni_minus_z", + "none", + "plus_y", + "minus_y", + "plus_x", + "minus_x", + "plus_z", + "minus_z", + "descriptionName", + "promptName", + "dataTag", + "enchantmentId", + "enchantmentLevel", + "itemId", + "quantity", + "auxValue", + "slot", + "name", + "food", + "health", + "blockId", + "useCoords", + "seed", + "flatworld", + "filename", + "rot", + "data", + "block", + "entity", + "facing", + "edgeBlock", + "fillBlock", + "skipAir", + "x", + "x0", + "x1", + "y", + "y0", + "y1", + "z", + "z0", + "z1", + "chunkX", + "chunkZ", + "yRot", + "xRot", + "spawnX", + "spawnY", + "spawnZ", + "orientation", + "dimension", + "topblockId", + "biomeId", + "feature", + "minCount", + "maxCount", + "weight", + "id", + "probability", + "method", + "hasBeenInCreative", + "cloudHeight", + "fogDistance", + "dayTime", + "target", + "speed", + "dir", + "type", + "pass", + "for", + "random", + "blockAux", + "size", + "scale", + "freq", + "func", + "upper", + "lower", + "dY", + "thickness", + "points", + "holeSize", + "variant", + "startHeight", + "pattern", + "colour", + "primary", + "laps", + "liftForceModifier", + "staticLift", + "targetHeight", + "speedBoost", + "boostDirection", + "condition_type", + "condition_value_0", + "condition_value_1", + "beam_length", + }; + + public static readonly string[] ValidGameRules = new string[] + { + "MapOptions", + "ApplySchematic", + "GenerateStructure", + "GenerateBox", + "PlaceBlock", + "PlaceContainer", + "PlaceSpawner", + "BiomeOverride", + "StartFeature", + "AddItem", + "AddEnchantment", + "WeighedTreasureItem", + "RandomItemSet", + "DistributeItems", + "WorldPosition", + "LevelRules", + "NamedArea", + "ActiveChunkArea", + "TargetArea", + "ScoreRing", + "ThermalArea", + "PlayerBoundsVolume", + "Killbox", + "BlockLayer", + "UseBlock", + "CollectItem", + "CompleteAll", + "UpdatePlayer", + "OnGameStartSpawnPositions", + "OnInitialiseWorld", + "SpawnPositionSet", + "PopulateContainer", + "DegradationSequence", + "RandomDissolveDegrade", + "DirectionalDegrade", + "GrantPermissions", + "AllowIn", + "LayerGeneration", + "LayerAsset", + "AnyCombinationOf", + "CombinationDefinition", + "Variations", + "BlockDef", + "LayerSize", + "UniformSize", + "RandomizeSize", + "LinearBlendSize", + "LayerShape", + "BasicShape", + "StarShape", + "PatchyShape", + "RingShape", + "SpiralShape", + "LayerFill", + "BasicLayerFill", + "CurvedLayerFill", + "WarpedLayerFill", + "LayerTheme", + "NullTheme", + "FilterTheme", + "ShaftsTheme", + "BasicPatchesTheme", + "BlockStackTheme", + "RainbowTheme", + "TerracottaTheme", + "FunctionPatchesTheme", + "SimplePatchesTheme", + "CarpetTrapTheme", + "MushroomBlockTheme", + "TextureTheme", + "SchematicTheme", + "BlockCollisionException", + "Powerup", + "Checkpoint", + "CustomBeacon", + "ActiveViewArea", + }; + + public string Name { get; set; } = string.Empty; + public GRFTag Parent => _parent; + public SortedDictionary Parameters => _parameters; + public List Tags { get; set; } = new List(); + + public GRFTag(string name, GRFTag parent) + { + Name = name; + _parent = parent; + } + } + + public GRFFile(eCompressionType compressionType, int crc) + { + _compressionType = compressionType; + _crc = crc; + } + + } +} diff --git a/MinecraftUSkinEditor/Classes/IO/GRF/GRFFileReader.cs b/MinecraftUSkinEditor/Classes/IO/GRF/GRFFileReader.cs new file mode 100644 index 00000000..3283254c --- /dev/null +++ b/MinecraftUSkinEditor/Classes/IO/GRF/GRFFileReader.cs @@ -0,0 +1,168 @@ +using PckStudio.Classes.FileTypes; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using ICSharpCode.SharpZipLib.Zip.Compression.Streams; +using PckStudio.Classes.Utils; + +namespace PckStudio.Classes.IO.GRF +{ + internal class GRFFileReader + { + internal List TagNames; + internal GRFFile _file; + public static GRFFile Read(Stream stream) + { + return new GRFFileReader().read(stream); + } + + + private GRFFile read(Stream stream) + { + stream = ReadHeader(stream); + ReadTagNames(stream); + ReadGRFTags(stream); + return _file; + } + + + private Stream ReadHeader(Stream stream) + { + int x = ReadShort(stream); + if (((x >> 31) | x) == 0) + { + ReadBytes(stream, 14); // 14 bools ?... + return stream; + } + + GRFFile.eCompressionType compression_type = (GRFFile.eCompressionType)stream.ReadByte(); + int crc = ReadInt(stream); + int byte1 = stream.ReadByte(); + int byte2 = stream.ReadByte(); + int byte3 = stream.ReadByte(); + int byte4 = stream.ReadByte(); + if (byte4 > 0) + { + compression_type = (GRFFile.eCompressionType)byte4; + // Custom enum + compression_type &= GRFFile.eCompressionType.IsWolrdGrf; + } + _file = new GRFFile(compression_type, crc); + + if (compression_type == GRFFile.eCompressionType.None && byte4 == 0) + return stream; + + int buf_size = ReadInt(stream); + if (byte4 != 0) + { + stream = new MemoryStream(ReadBytes(stream, buf_size)); + buf_size = ReadInt(stream); + } + else + { + ReadInt(stream); // ignored cuz rest of data is compressed + } + stream = DecompressZLX(stream); + if (compression_type > GRFFile.eCompressionType.Zlib) + { + byte[] data = RLE.Decode(ReadBytes(stream, buf_size)).ToArray(); + stream = new MemoryStream(data); + } + + if (byte4 != 0) + ReadBytes(stream, 23); + + return stream; + } + + private Stream DecompressZLX(Stream compressedStream) + { + Stream outputstream = new MemoryStream(); + using (var inputStream = new InflaterInputStream(compressedStream)) + { + inputStream.CopyTo(outputstream); + outputstream.Position = 0; + }; + return outputstream; + } + + private void ReadTagNames(Stream stream) + { + int name_count = ReadInt(stream); + TagNames = new List(name_count); + for (int i = 0; i < name_count; i++) + TagNames.Add(ReadString(stream)); + } + + private void ReadGRFTags(Stream stream) + { + var NameAndCount = GetTagNameAndDetailCount(stream); + _file.RootTag = new GRFFile.GRFTag(NameAndCount.Item1, null); + _file.RootTag.Tags = ReadItemList(stream, NameAndCount.Item2, _file.RootTag); + } + + internal List ReadItemList(Stream stream, int count, GRFFile.GRFTag parent) + { + List tags = new List(); + for (int i = 0; i < count; i++) + { + var valuePair = GetTagNameAndDetailCount(stream); + var tag = new GRFFile.GRFTag(valuePair.Item1, parent); + for (var j = 0; j < valuePair.Item2; j++) + { + var tuple = GetTagNameAndValue(stream); + tag.Parameters.Add(tuple.Item1, tuple.Item2); + } + tag.Tags = ReadItemList(stream, ReadInt(stream), tag); + tags.Add(tag); + } + return tags; + } + + internal string GetTagName(Stream stream) => TagNames[ReadInt(stream)]; + + internal (string, int) GetTagNameAndDetailCount(Stream stream) + { + return new ValueTuple(GetTagName(stream), ReadInt(stream)); + } + + internal (string, string) GetTagNameAndValue(Stream stream) + { + return new ValueTuple(GetTagName(stream), ReadString(stream)); + } + + internal byte[] ReadBytes(Stream stream, int count) + { + byte[] buffer = new byte[count]; + stream.Read(buffer, 0, count); + return buffer; + } + + internal int ReadInt(Stream stream) + { + byte[] buffer = ReadBytes(stream, 4); + if (BitConverter.IsLittleEndian) + Array.Reverse(buffer); + return BitConverter.ToInt32(buffer, 0); + } + + internal short ReadShort(Stream stream) + { + byte[] buffer = ReadBytes(stream, 2); + if (BitConverter.IsLittleEndian) + Array.Reverse(buffer); + return BitConverter.ToInt16(buffer, 0); + } + + internal string ReadString(Stream stream) + { + short stringLength = ReadShort(stream); + byte[] buffer = ReadBytes(stream, stringLength); + return Encoding.UTF8.GetString(buffer); + } + + } +} diff --git a/MinecraftUSkinEditor/Classes/IO/GRF/GRFFileWriter.cs b/MinecraftUSkinEditor/Classes/IO/GRF/GRFFileWriter.cs new file mode 100644 index 00000000..99708f17 --- /dev/null +++ b/MinecraftUSkinEditor/Classes/IO/GRF/GRFFileWriter.cs @@ -0,0 +1,92 @@ +using PckStudio.Classes.FileTypes; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PckStudio.Classes.IO.GRF +{ + public class GRFFileWriter + { + internal GRFFile _grfFile; + public static void Write(Stream stream, GRFFile grfFile) + { + new GRFFileWriter(grfFile).write(stream); + } + + private GRFFileWriter(GRFFile grfFile) + { + _grfFile = grfFile; + } + + private void write(Stream stream) + { + BuildHeader(stream); + WriteTagNames(stream); + } + + + private void BuildHeader(Stream stream) + { + WriteShort(stream, 1); // (x >> 31 | x) == 1 + stream.WriteByte((byte)_grfFile.CompressionType); + WriteInt(stream, _grfFile.Crc); + stream.WriteByte(0); + stream.WriteByte(0); + stream.WriteByte(0); + stream.WriteByte(0); + } + + private void WriteTagNames(Stream stream) + { + List tagNames = new List(); + GatherTagNames(_grfFile.RootTag, tagNames); + WriteInt(stream, tagNames.Count); + foreach (var s in tagNames) + { + WriteString(stream, s); + Console.WriteLine(s); + } + } + + private void GatherTagNames(GRFFile.GRFTag tag, List l) + { + if (!l.Contains(tag.Name)) l.Add(tag.Name); + foreach (var subTag in tag.Tags) + GatherTagNames(subTag, l); + foreach (var subTag in tag.Parameters) + if (!l.Contains(subTag.Key)) l.Add(subTag.Key); + } + + internal void WriteInt(Stream stream, int value) + { + byte[] bytes = BitConverter.GetBytes(value); + if (BitConverter.IsLittleEndian) + Array.Reverse(bytes); + WriteBytes(stream, bytes); + } + + internal void WriteShort(Stream stream, short value) + { + byte[] bytes = BitConverter.GetBytes(value); + if (BitConverter.IsLittleEndian) + Array.Reverse(bytes); + WriteBytes(stream, bytes); + } + + internal void WriteString(Stream stream, string s) + { + WriteShort(stream, (short)s.Length); + WriteBytes(stream, Encoding.UTF8.GetBytes(s)); + } + + internal void WriteBytes(Stream stream, byte[] bytes) + { + stream.Write(bytes, 0, bytes.Length); + } + + + } +} diff --git a/MinecraftUSkinEditor/Classes/Utils/RLE.cs b/MinecraftUSkinEditor/Classes/Utils/RLE.cs new file mode 100644 index 00000000..fd890bc9 --- /dev/null +++ b/MinecraftUSkinEditor/Classes/Utils/RLE.cs @@ -0,0 +1,215 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +namespace PckStudio.Classes.Utils; + +///! Credits +///! + +/// +/// Provides the RLE codec for any integer data type. +/// +/// The data's type. Must be an integer type or an ArgumentException will be thrown +static class RLE where T : struct, IConvertible +{ + /// + /// This is the marker that identifies a compressed run + /// + private static T rleMarker; + + /// + /// A run can be at most as long as the marker - 1 + /// + private static ulong maxLength; + + static RLE() + { + GetMaxValues(); + } + + /// + /// RLE-Encodes a data set. + /// + /// The data to encode + /// Encoded data + public static IEnumerable Encode(IEnumerable data) + { + var enumerator = data.GetEnumerator(); + + if (!enumerator.MoveNext()) + yield break; + + var firstRunValue = enumerator.Current; + ulong runLength = 1; + while (enumerator.MoveNext()) + { + var currentValue = enumerator.Current; + // if the current value is the value of the current run, don't yield anything, + // just extend the run + if (currentValue.Equals(firstRunValue)) + runLength++; + else + { + // the current value is different from the current run + // yield what we have so far + foreach (var item in MakeRun(firstRunValue, runLength)) + yield return item; + + // and reset the run + firstRunValue = currentValue; + runLength = 1; + } + // if there are very many identical values, don't exceed the max length + if (runLength > maxLength) + { + foreach (var item in MakeRun(firstRunValue, maxLength)) + yield return item; + runLength -= maxLength; + } + } + //yield everything that has been buffered + foreach (var item in MakeRun(firstRunValue, runLength)) + yield return item; + } + + /// + /// Decodes RLE-encoded data + /// + /// RLE-encoded data + /// The original data + public static IEnumerable Decode(IEnumerable data) + { + var enumerator = data.GetEnumerator(); + if (!enumerator.MoveNext()) + yield break; + + do + { + var value = enumerator.Current; + if (!value.Equals(rleMarker)) + { + //an ordinary value + yield return value; + } + else + { + //might be flag or escape + //examine the next value + if (!enumerator.MoveNext()) + throw new ArgumentException("The provided data is not properly encoded."); + if (enumerator.Current.Equals(rleMarker)) + { + //escaped value + yield return value; + } + else + { + //rle marker + var length = enumerator.Current.ToInt64(CultureInfo.InvariantCulture); + if (!enumerator.MoveNext()) + throw new ArgumentException("The provided data is not properly encoded."); + var val = enumerator.Current; + for (var j = 0; j < length+1; ++j) + yield return val; + } + } + } + while (enumerator.MoveNext()); + } + + private static IEnumerable MakeRun(T value, ulong length) + { + if ((length <= 3 && !value.Equals(rleMarker)) || length <= 1) + { + //don't compress this run, it is just too small + for (ulong i = 0; i < length; ++i) + { + if (value.Equals(rleMarker)) + yield return rleMarker; + yield return value; + } + } + else + { + //compressed run + yield return rleMarker; + yield return (T)(dynamic)length; + yield return value; + } + } + + + private static void GetMaxValues() + { + object maxValue = default(T); + TypeCode typeCode = Type.GetTypeCode(typeof(T)); + switch (typeCode) + { + case TypeCode.Byte: + { + var limit = byte.MaxValue; + rleMarker = __refvalue(__makeref(limit), T); + maxLength = (ulong)(limit - 1); + break; + } + case TypeCode.Char: + { + var limit = char.MaxValue; + rleMarker = __refvalue(__makeref(limit), T); + maxLength = (ulong)(limit - 1); + break; + } + case TypeCode.Int16: + { + var limit = short.MaxValue; + rleMarker = __refvalue(__makeref(limit), T); + maxLength = (ulong)(limit - 1); + break; + } + case TypeCode.Int32: + { + var limit = int.MaxValue; + rleMarker = __refvalue(__makeref(limit), T); + maxLength = (ulong)(limit - 1); + break; + } + case TypeCode.Int64: + { + var limit = long.MaxValue; + rleMarker = __refvalue(__makeref(limit), T); + maxLength = (ulong)(limit - 1); + break; + } + case TypeCode.SByte: + { + var limit = sbyte.MaxValue; + rleMarker = __refvalue(__makeref(limit), T); + maxLength = (ulong)(limit - 1); + break; + } + case TypeCode.UInt16: + { + var limit = ushort.MaxValue; + rleMarker = __refvalue(__makeref(limit), T); + maxLength = (ulong)(limit - 1); + break; + } + case TypeCode.UInt32: + { + var limit = uint.MaxValue; + rleMarker = __refvalue(__makeref(limit), T); + maxLength = (ulong)(limit - 1); + break; + } + case TypeCode.UInt64: + { + var limit = ulong.MaxValue; + rleMarker = __refvalue(__makeref(limit), T); + maxLength = (ulong)(limit - 1); + break; + } + default: + throw new ArgumentException("The provided type parameter is not an integer type"); + } + } +} \ No newline at end of file