From f46d2407670ed00bc3d1d1e876a22e79a32c517a Mon Sep 17 00:00:00 2001 From: Eric Freed Date: Sat, 26 Aug 2023 23:33:08 -0400 Subject: [PATCH] Implemented a new SNBT parser that does not use regex --- SharpNBT/SNBT/Lexer.cs | 49 ---- SharpNBT/SNBT/LexerRule.cs | 35 --- SharpNBT/SNBT/Scanner.cs | 64 +++++ SharpNBT/SNBT/StringNbt.cs | 515 +++++++++++++++++++++--------------- SharpNBT/SNBT/Token.cs | 34 --- SharpNBT/SNBT/TokenType.cs | 102 ------- SharpNBT/Tags/ArrayTag.cs | 6 + SharpNBT/Tags/NumericTag.cs | 83 ++++++ 8 files changed, 456 insertions(+), 432 deletions(-) delete mode 100644 SharpNBT/SNBT/Lexer.cs delete mode 100644 SharpNBT/SNBT/LexerRule.cs create mode 100644 SharpNBT/SNBT/Scanner.cs delete mode 100644 SharpNBT/SNBT/Token.cs delete mode 100644 SharpNBT/SNBT/TokenType.cs create mode 100644 SharpNBT/Tags/ArrayTag.cs create mode 100644 SharpNBT/Tags/NumericTag.cs diff --git a/SharpNBT/SNBT/Lexer.cs b/SharpNBT/SNBT/Lexer.cs deleted file mode 100644 index 7fc40f7..0000000 --- a/SharpNBT/SNBT/Lexer.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Collections.Generic; -using System.Data; - -namespace SharpNBT.SNBT; - -internal sealed class Lexer -{ - private readonly List ruleList; - - public Lexer() - { - ruleList = new List(); - } - - public void AddRule(TokenType type, string pattern, bool skipped = false) => ruleList.Add(new LexerRule(type, pattern, null, skipped)); - - public void AddRule(TokenType type, string pattern, ResultHandler handler, bool skipped = false) - { - ruleList.Add(new LexerRule(type, pattern, handler, skipped)); - } - - public IEnumerable Tokenize(string source) - { - var index = 0; - while (index < source.Length) - { - var success = false; - - foreach (var rule in ruleList) - { - var match = rule.Pattern.Match(source, index); - if (!match.Success || match.Index - index != 0) - continue; - - if (!rule.IsSkipped) - yield return new Token(rule.Type, rule.Process(source, index, match)); - - index += match.Length; - success = true; - break; - } - - if (!success) - throw new SyntaxErrorException($"Unrecognized sequence at index {index}: '{source[index]}'"); - } - } - - -} \ No newline at end of file diff --git a/SharpNBT/SNBT/LexerRule.cs b/SharpNBT/SNBT/LexerRule.cs deleted file mode 100644 index d1d4e3f..0000000 --- a/SharpNBT/SNBT/LexerRule.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Text.RegularExpressions; - -namespace SharpNBT.SNBT; - -internal delegate string ResultHandler(Match match); - -internal class LexerRule -{ - private readonly ResultHandler processResult; - - public TokenType Type { get; } - - public Regex Pattern { get; } - - public bool IsSkipped { get; } - - - public LexerRule(TokenType type, string pattern, bool skipped = false) : this(type, pattern, null, skipped) - { - } - - public LexerRule(TokenType type, string pattern, ResultHandler handler, bool skipped = false) - { - Type = type; - Pattern = new Regex(pattern, RegexOptions.Compiled); - IsSkipped = skipped; - processResult = handler; - } - - public string Process(string source, int index, Match match) - { - return processResult is null ? source.Substring(index, match.Length) : processResult.Invoke(match); - } -} \ No newline at end of file diff --git a/SharpNBT/SNBT/Scanner.cs b/SharpNBT/SNBT/Scanner.cs new file mode 100644 index 0000000..0da765b --- /dev/null +++ b/SharpNBT/SNBT/Scanner.cs @@ -0,0 +1,64 @@ +using System; +using System.Data; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace SharpNBT.SNBT; + +internal ref struct Scanner +{ + public ReadOnlySpan Source; + public int Position; + + public char Current => Source[Position]; + + public bool IsEndOfInput => Position >= Source.Length; + + public Scanner(ReadOnlySpan utf8Bytes, Encoding encoding) + { + Position = -1; + var count = encoding.GetCharCount(utf8Bytes); + var chars = new char[count]; + encoding.GetChars(utf8Bytes, chars); + Source = new ReadOnlySpan(chars); + } + + public Scanner(string text, Encoding encoding) : this(encoding.GetBytes(text), encoding) + { + } + + public char Peek(int numChars = 1) + { + if (Position + numChars >= Source.Length) + SyntaxError("Unexpected end of input."); + return Source[Position + numChars]; + } + + public bool MoveNext(bool skipWhitespace, bool fail) + { + ReadChar: + Position++; + if (Position >= Source.Length) + { + if (fail) + SyntaxError("Unexpected end of input."); + return false; + } + + if (skipWhitespace && char.IsWhiteSpace(Current)) + goto ReadChar; + return true; + } + + public void AssertChar(char c) + { + if (Current != c) + SyntaxError($"Expected \"{c}\", got \"{Current}\"."); + } + + [DoesNotReturn] + public Exception SyntaxError(string message) + { + throw new SyntaxErrorException($"Syntax error at index {Position}: {message}"); + } +} \ No newline at end of file diff --git a/SharpNBT/SNBT/StringNbt.cs b/SharpNBT/SNBT/StringNbt.cs index 7d562ff..c35f190 100644 --- a/SharpNBT/SNBT/StringNbt.cs +++ b/SharpNBT/SNBT/StringNbt.cs @@ -1,10 +1,9 @@ using System; using System.Collections.Generic; using System.Data; -using System.IO; +using System.Globalization; +using System.Numerics; using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; using JetBrains.Annotations; namespace SharpNBT.SNBT; @@ -15,91 +14,6 @@ namespace SharpNBT.SNBT; [PublicAPI] public static class StringNbt { - private static readonly Lexer lexer; - - static StringNbt() - { - lexer = new Lexer(); - lexer.AddRule(TokenType.Whitespace, @"(\r|\t|\v|\f|\s)+?", true); - lexer.AddRule(TokenType.Separator, ",", true); - lexer.AddRule(TokenType.Compound, @"{"); - lexer.AddRule(TokenType.EndCompound, @"}"); - lexer.AddRule(TokenType.Identifier, "\"(.*?)\"\\s*(?>:)", FirstGroupValue); - lexer.AddRule(TokenType.Identifier, "'(.*?)'\\s*(?>:)", FirstGroupValue); - lexer.AddRule(TokenType.Identifier, "([A-Za-z0-9_-]+)\\s*(?>:)", FirstGroupValue); - lexer.AddRule(TokenType.String, "\"(.*?)\"", FirstGroupValue); - lexer.AddRule(TokenType.String, "'(.*?)'", FirstGroupValue); - lexer.AddRule(TokenType.ByteArray, @"\[B;"); - lexer.AddRule(TokenType.IntArray, @"\[I;"); - lexer.AddRule(TokenType.LongArray, @"\[L;"); - lexer.AddRule(TokenType.List, @"\["); - lexer.AddRule(TokenType.EndArray, @"\]"); - lexer.AddRule(TokenType.Float, @"(-?[0-9]*\.[0-9]+)[Ff]", FirstGroupValue); - lexer.AddRule(TokenType.Double, @"(-?[0-9]*\.[0-9]+)[Dd]?", FirstGroupValue); - lexer.AddRule(TokenType.Bool, "(true|false)", FirstGroupValue); - lexer.AddRule(TokenType.Byte, "(-?[0-9]+)[Bb]", FirstGroupValue); - lexer.AddRule(TokenType.Short, "(-?[0-9]+)[Ss]", FirstGroupValue); - lexer.AddRule(TokenType.Long, "(-?[0-9]+)[Ll]", FirstGroupValue); - lexer.AddRule(TokenType.Int, "(-?[0-9]+)", FirstGroupValue); - } - - /// - /// Parse the text in the given into a . - /// - /// A containing the SNBT data. - /// The number of bytes to read from the , advancing its position. - /// The instance described in the source text. - /// When is . - /// When is not opened for reading. - /// When is negative. - /// When contains invalid SNBT code. - public static CompoundTag Parse(Stream stream, int length) - { - Validate(stream, length); - if (length == 0) - return new CompoundTag(null); - - var buffer = new byte[length]; - stream.Read(buffer, 0, length); - var str = Encoding.UTF8.GetString(buffer, 0, buffer.Length); - - return Parse(str); - } - - /// - /// Asynchronously parses the text in the given into a . - /// - /// A containing the SNBT data. - /// The number of bytes to read from the , advancing its position. - /// The instance described in the source text. - /// When is . - /// When is not opened for reading. - /// When is negative. - /// When contains invalid SNBT code. - public static async Task ParseAsync(Stream stream, int length) - { - Validate(stream, length); - if (length == 0) - return new CompoundTag(null); - - var buffer = new byte[length]; - await stream.ReadAsync(buffer, 0, length); - var str = Encoding.UTF8.GetString(buffer, 0, buffer.Length); - - return Parse(str); - } - - private static void Validate(Stream stream, int length) - { - if (stream is null) - throw new ArgumentNullException(nameof(stream)); - if (!stream.CanRead) - throw new IOException("Stream is not opened for reading."); - - if (length < 0) - throw new ArgumentException(Strings.NegativeLengthSpecified, nameof(length)); - } - /// /// Parse the given text into a . /// @@ -109,137 +23,314 @@ public static class StringNbt /// When is invalid SNBT code. public static CompoundTag Parse(string source) { - if (source is null) - throw new ArgumentNullException(nameof(source)); - - if (string.IsNullOrWhiteSpace(source)) - return new CompoundTag(null); - - var queue = new Queue(lexer.Tokenize(source)); - return Parse(queue); - } - - private static T Parse(Queue queue) where T : Tag => (T)Parse(queue); + var bytes = Encoding.UTF8.GetBytes(source); + var scanner = new Scanner(bytes, Encoding.UTF8); - private static Tag Parse(Queue queue) - { - string name = null; - var token = MoveNext(queue); - - if (token.Type == TokenType.Identifier) - { - name = token.Value; - token = MoveNext(queue); - } - - return token.Type switch - { - TokenType.Compound => ParseCompound(name, queue), - TokenType.String => new StringTag(name, token.Value), - TokenType.ByteArray => ParseByteArray(name, queue), - TokenType.IntArray => ParseIntArray(name, queue), - TokenType.LongArray => ParseLongArray(name, queue), - TokenType.List => ParseList(name, queue), - TokenType.Bool => new BoolTag(name, bool.Parse(token.Value)), - TokenType.Byte => new ByteTag(name, sbyte.Parse(token.Value)), - TokenType.Short => new ShortTag(name, short.Parse(token.Value)), - TokenType.Int => new IntTag(name, int.Parse(token.Value)), - TokenType.Long => new LongTag(name, long.Parse(token.Value)), - TokenType.Float => new FloatTag(name, float.Parse(token.Value)), - TokenType.Double => new DoubleTag(name, double.Parse(token.Value)), - _ => throw new SyntaxErrorException() - }; + scanner.MoveNext(true, true); + scanner.AssertChar('{'); + return ParseCompound(null, ref scanner); } + + private static CompoundTag ParseCompound(string? name, ref Scanner scanner) + { + scanner.MoveNext(true, true); - private static Token MoveNext(Queue queue) - { - if (queue.TryDequeue(out var token)) - return token; - - throw new SyntaxErrorException("Unexpected end-of-input"); - } - - private static void MoveNext(Queue queue, TokenType assertType) - { - var token = MoveNext(queue); - if (token.Type != assertType) - throw new SyntaxErrorException($"Expected token of type {assertType}, but encountered {token.Type}."); - } + // For the case of "{}", return empty compound tag. + var result = new CompoundTag(name); + if (scanner.Current == '}') + return result; - private static CompoundTag ParseCompound(string name, Queue queue) - { - var compound = new CompoundTag(name); - while (queue.TryPeek(out var token) && token.Type != TokenType.EndCompound) - { - compound.Add(Parse(queue)); - } - MoveNext(queue, TokenType.EndCompound); - return compound; - } - - private static ListTag ParseList(string name, Queue queue) - { - var values = new List(); - while (queue.TryPeek(out var token) && token.Type != TokenType.EndArray) - { - values.Add(Parse(queue)); - } - - MoveNext(queue, TokenType.EndArray); - if (values.Count > 0) - { - var type = values[0].Type; - return new ListTag(name, type, values); - } - return new ListTag(name, TagType.End); - } - - private static ByteArrayTag ParseByteArray(string name, Queue queue) - { - var values = new List(); - foreach (var token in DequeueUntil(queue, TokenType.EndArray)) - { - if (token.Type != TokenType.Byte) - throw new SyntaxErrorException($"Invalid token type in array, expected {TokenType.Byte}, got {token.Type}."); - values.Add(unchecked((byte) sbyte.Parse(token.Value))); - } - return new ByteArrayTag(name, values); - } - - private static IntArrayTag ParseIntArray(string name, Queue queue) - { - var values = new List(); - foreach (var token in DequeueUntil(queue, TokenType.EndArray)) - { - if (token.Type != TokenType.Int) - throw new SyntaxErrorException($"Invalid token type in array, expected {TokenType.Int}, got {token.Type}."); - values.Add(int.Parse(token.Value)); - } - return new IntArrayTag(name, values); - } - - private static LongArrayTag ParseLongArray(string name, Queue queue) - { - var values = new List(); - foreach (var token in DequeueUntil(queue, TokenType.EndArray)) - { - if (token.Type != TokenType.Long) - throw new SyntaxErrorException($"Invalid token type in array, expected {TokenType.Long}, got {token.Type}."); - values.Add(long.Parse(token.Value)); - } - return new LongArrayTag(name, values); - } - - private static IEnumerable DequeueUntil(Queue queue, TokenType type) - { while (true) { - var token = MoveNext(queue); - if (token.Type == type) - yield break; - yield return token; + // Read the name of the tag + var childName = ParseString(ref scanner, out _); + + // Move to and asser the next significant character is a deliminator. + // scanner.MoveNext(true, true); + scanner.AssertChar(':'); + + // Move to and parse the tag value + scanner.MoveNext(true, true); + var tag = ParseTag(childName, ref scanner); + result.Add(tag); + // scanner.MoveNext(true, true); + + if (char.IsWhiteSpace(scanner.Current)) + scanner.MoveNext(true, true); + + // Comma encountered, read another tag. + if (scanner.Current == ',') + { + scanner.MoveNext(true, true); + continue; + } + + // Closing brace encountered, break loop. + if (scanner.Current == '}') + { + scanner.MoveNext(true, false); + break; + } + + // Invalid character + scanner.SyntaxError($"Expected ',' or '}}', got '{scanner.Current}'."); } + + return result; } + + private static string ParseString(ref Scanner scanner, out bool quoted) + { + var quote = scanner.Current; + if (quote != '"' && quote != '\'') + { + quoted = false; + return ParseUnquotedString(ref scanner); + } + + quoted = true; + var escape = false; + var closed = false; + var sb = new StringBuilder(); - private static string FirstGroupValue(Match match) => match.Groups[1].Value; + while (scanner.MoveNext(false, false)) + { + if (escape) + { + escape = false; + sb.Append(scanner.Current); + continue; + } + + if (scanner.Current == quote) + { + closed = true; + scanner.Position++; + break; + } + + if (scanner.Current == '\\') + { + // TODO: Control characters like \r \n, \t, etc. + escape = true; + continue; + } + + sb.Append(scanner.Current); + } + + if (!closed) + scanner.SyntaxError("Improperly terminated string."); + return sb.ToString(); + } + + private static string ParseUnquotedString(ref Scanner scanner) + { + var start = scanner.Position; + for (var length = 0; scanner.MoveNext(false, true); length++) + { + if (!AllowedInUnquoted(scanner.Current)) + return new string(scanner.Source.Slice(start, length + 1)); + } + + return string.Empty; + } + + private static Tag ParseTag(string? name, ref Scanner scanner) + { + return scanner.Current switch + { + '{' => ParseCompound(name, ref scanner), + '[' => ParseArray(name, ref scanner), + _ => ParseLiteral(name, ref scanner) + }; + } + + private static Tag ParseLiteral(string? name, ref Scanner scanner) + { + // Read the input as a string + var value = ParseString(ref scanner, out var quoted); + if (quoted || value.Length == 0) + return new StringTag(name, value); + + // Early out for true/false values + if (bool.TryParse(value, out var boolean)) + return new ByteTag(name, boolean); + + var suffix = value[^1]; + if (char.IsNumber(suffix)) + { + // int and double do not require a suffix + if (value.Contains('.') && double.TryParse(value, NumberStyles.Float, NumberFormatInfo.InvariantInfo, out var f64)) + return new DoubleTag(name, f64); + + if (int.TryParse(value, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out var i32)) + return new IntTag(name, i32); + } + else if (TryParseNumber(name, value, suffix, out var tag)) + { + return tag; + } + + if (value.Length > 2 && value[0] == '0' && char.ToLowerInvariant(value[1]) == 'x') + { + // TODO: The "official" spec doesn't seem to support hexadecimal numbers + if (int.TryParse(value[2..], NumberStyles.HexNumber, NumberFormatInfo.InvariantInfo, out var hex)) + return new IntTag(name, hex); + } + + // When all else fails, assume it is an unquoted string + return new StringTag(name, value); + } + + + private static bool TryParseNumber(string? name, string value, char suffix, out Tag tag) + { + // A much less complicated char.ToLower() + if (suffix >= 'a') + suffix -= (char) 32; + + switch (suffix) + { + case 'B': + if (int.TryParse(value[..^1], NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out var u8)) + { + tag = new ByteTag(name, u8); + return true; + } + break; + case 'S': + if (short.TryParse(value[..^1], NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out var i16)) + { + tag = new ShortTag(name, i16); + return true; + } + break; + case 'L': + if (long.TryParse(value[..^1], NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out var i64)) + { + tag = new LongTag(name, i64); + return true; + } + break; + case 'F': + if (float.TryParse(value[..^1], NumberStyles.Float, NumberFormatInfo.InvariantInfo, out var f32)) + { + tag = new FloatTag(name, f32); + return true; + } + break; + case 'D': + if (double.TryParse(value[..^1], NumberStyles.Float, NumberFormatInfo.InvariantInfo, out var f64)) + { + tag = new DoubleTag(name, f64); + return true; + } + break; + } + + tag = null!; + return false; + } + + private static Tag ParseArray(string? name, ref Scanner scanner) + { + scanner.MoveNext(true, true); + if (scanner.Current == ']') + return new ListTag(name, TagType.End); + + if (scanner.Peek() == ';') + { + // This is an array of integer values + var prefix = scanner.Current; + scanner.Position += 2; + return prefix switch + { + 'B' => new ByteArrayTag(name, ParseArrayValues(ref scanner)), + 'I' => new IntArrayTag(name, ParseArrayValues(ref scanner)), + 'L' => new LongArrayTag(name, ParseArrayValues(ref scanner)), + _ => throw scanner.SyntaxError($"Invalid type specifier. Expected 'B', 'I', or 'L', got '{prefix}'.") + }; + } + + // No prefix, so this must be a list of tags if valid + return ParseList(name, ref scanner); + } + + private static Tag ParseList(string? name, ref Scanner scanner) + { + var list = new List(); + while (true) + { + var child = ParseTag(null, ref scanner); + list.Add(child); + + if (char.IsWhiteSpace(scanner.Current)) + scanner.MoveNext(true, true); + + // Comma encountered, read another tag. + if (scanner.Current == ',') + { + scanner.MoveNext(true, true); + continue; + } + + // Closing brace encountered, break loop. + if (scanner.Current == ']') + { + scanner.MoveNext(true, false); + break; + } + + // Invalid character + scanner.SyntaxError($"Expected ',' or ']', got '{scanner.Current}'."); + } + + var childType = list.Count > 0 ? list[0].Type : TagType.End; + return new ListTag(name, childType, list); + } + + private static T[] ParseArrayValues(ref Scanner scanner) where T : INumber, IParsable + { + // Early-out for [] + if (scanner.Current == ']') + { + scanner.Position++; + return Array.Empty(); + } + + var start = scanner.Position; + while (scanner.MoveNext(true, true)) + { + var c = char.ToLowerInvariant(scanner.Current); + if (c == ']') + break; + if (char.IsNumber(c) || c == ',') + continue; + if (c is not ('b' or 'l')) + scanner.SyntaxError($"Invalid character '{c}' in integer array."); + } + + var span = scanner.Source.Slice(start, scanner.Position - start); + var strings = new string(span).Split(SplitSeparators, SplitOpts); + + var values = new T[strings.Length]; + for (var i = 0; i < values.Length; i++) + values[i] = T.Parse(strings[i], NumberStyles.Integer, NumberFormatInfo.InvariantInfo); + + scanner.Position++; // Consume the closing ']' + return values; + } + + private static bool AllowedInUnquoted(char c) + { + return c == '_' || c == '-' || + c == '.' || c == '+' || + c >= '0' && c <= '9' || + c >= 'A' && c <= 'Z' || + c >= 'a' && c <= 'z'; + } + + private const StringSplitOptions SplitOpts = StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries; + private static readonly char[] SplitSeparators = new[] { ',', 'b', 'B', 'l', 'L' }; } \ No newline at end of file diff --git a/SharpNBT/SNBT/Token.cs b/SharpNBT/SNBT/Token.cs deleted file mode 100644 index 53d4e03..0000000 --- a/SharpNBT/SNBT/Token.cs +++ /dev/null @@ -1,34 +0,0 @@ -using JetBrains.Annotations; - -namespace SharpNBT.SNBT; - -/// -/// An object emitted by the lexer to describe a logical fragment of code that can be parsed. -/// -[PublicAPI] -public sealed class Token -{ - /// - /// Gets a value describing the general type code fragment this represents. - /// - public TokenType Type { get; } - - /// - /// Gets a value of this fragment, which can vary depending on context and the . - /// - public string Value { get; } - - /// - /// Creates a new instance of the class. - /// - /// A value describing the general type code fragment this represents. - /// Ahe value of this code fragment. - public Token(TokenType type, string value) - { - Type = type; - Value = value; - } - - /// - public override string ToString() => $"[{Type}] \"{Value}\""; -} \ No newline at end of file diff --git a/SharpNBT/SNBT/TokenType.cs b/SharpNBT/SNBT/TokenType.cs deleted file mode 100644 index 541057d..0000000 --- a/SharpNBT/SNBT/TokenType.cs +++ /dev/null @@ -1,102 +0,0 @@ -using JetBrains.Annotations; - -namespace SharpNBT.SNBT; - -/// -/// Describes types of tokens that the SNBT lexer can emit. -/// -[PublicAPI] -public enum TokenType -{ - /// - /// Any whitespace/newline not found within a string or identifier. - /// - /// This type is not yielded during tokenization. - Whitespace, - - /// - /// A separator between objects and array elements. - /// - /// This type is not yielded during tokenization. - Separator, - - /// - /// The beginning of new object. - /// - Compound, - - /// - /// The end of a . - /// - EndCompound, - - /// - /// The name of an tag. - /// - Identifier, - - /// - /// A value, which may be contain escaped quotes. - /// - String, - - /// - /// The beginning of a . - /// - ByteArray, - - /// - /// The beginning of a . - /// - IntArray, - - /// - /// The beginning of a . - /// - LongArray, - - /// - /// The beginning of a . - /// - List, - - /// - /// The end of a , , or . - /// - EndArray, - - /// - /// A value or element of a depending on context. - /// - Byte, - - /// - /// A value. - /// - Bool, - - /// - /// A value. - /// - Short, - - /// - /// A value or element of a depending on context. - /// - Int, - - /// - /// A value or element of a depending on context. - /// - Long, - - /// - /// A value. - /// - Float, - - /// - /// A value. - /// - Double -} \ No newline at end of file diff --git a/SharpNBT/Tags/ArrayTag.cs b/SharpNBT/Tags/ArrayTag.cs new file mode 100644 index 0000000..c520689 --- /dev/null +++ b/SharpNBT/Tags/ArrayTag.cs @@ -0,0 +1,6 @@ +namespace SharpNBT; + +public class ArrayTag +{ + +} \ No newline at end of file diff --git a/SharpNBT/Tags/NumericTag.cs b/SharpNBT/Tags/NumericTag.cs new file mode 100644 index 0000000..a97f1f0 --- /dev/null +++ b/SharpNBT/Tags/NumericTag.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.Serialization; +using JetBrains.Annotations; + +namespace SharpNBT; + +/// +/// Abstract base class for types that contain a single numeric value. +/// +/// A value type that implements . +[PublicAPI][Serializable] +public abstract class NumericTag : Tag, IEquatable>, IComparable>, IComparable where T : unmanaged, INumber +{ + public T Value { get; set; } + + protected NumericTag(TagType type, string? name, T value) : base(type, name) + { + Value = value; + } + + protected NumericTag(SerializationInfo info, StreamingContext context) : base(info, context) + { + var value = info.GetValue("value", typeof(T)); + Value = value is null ? default : (T)value; + } + + public bool Equals(NumericTag? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && Value.Equals(other.Value); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((NumericTag)obj); + } + + public override int GetHashCode() => base.GetHashCode(); + + public int CompareTo(NumericTag? other) + { + if (ReferenceEquals(this, other)) return 0; + if (ReferenceEquals(null, other)) return 1; + return Value.CompareTo(other.Value); + } + + public int CompareTo(object? obj) + { + if (ReferenceEquals(null, obj)) return 1; + if (ReferenceEquals(this, obj)) return 0; + return obj is NumericTag other ? CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof(NumericTag)}"); + } + + public static bool operator ==(NumericTag? left, NumericTag? right) => Equals(left, right); + + public static bool operator !=(NumericTag? left, NumericTag? right) => !Equals(left, right); + + public static bool operator <(NumericTag? left, NumericTag? right) + { + return Comparer>.Default.Compare(left, right) < 0; + } + + public static bool operator >(NumericTag? left, NumericTag? right) + { + return Comparer>.Default.Compare(left, right) > 0; + } + + public static bool operator <=(NumericTag? left, NumericTag? right) + { + return Comparer>.Default.Compare(left, right) <= 0; + } + + public static bool operator >=(NumericTag? left, NumericTag? right) + { + return Comparer>.Default.Compare(left, right) >= 0; + } +} \ No newline at end of file