Removed SerializableAttribute and ISerializable interface from tags. Improved JSON output.
This commit is contained in:
parent
5d974eeeb4
commit
11f3c4d70c
|
|
@ -2,8 +2,6 @@ using System;
|
|||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
|
|
@ -13,7 +11,7 @@ namespace SharpNBT;
|
|||
/// Base class for NBT tags that contain a fixed-size array of numeric types.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">A value type that implements <see cref="INumber{TSelf}"/>.</typeparam>
|
||||
[PublicAPI][Serializable]
|
||||
[PublicAPI]
|
||||
public abstract class ArrayTag<T> : Tag, IReadOnlyList<T> where T : unmanaged, INumber<T>
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -35,22 +33,6 @@ public abstract class ArrayTag<T> : Tag, IReadOnlyList<T> where T : unmanaged, I
|
|||
array = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected ArrayTag(SerializationInfo info, StreamingContext context) : base(info, context)
|
||||
{
|
||||
var _ = info.GetInt32("count");
|
||||
var value = info.GetValue("values", typeof(T[])) as T[];
|
||||
array = value ?? Array.Empty<T>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void GetObjectData(SerializationInfo info, StreamingContext context)
|
||||
{
|
||||
base.GetObjectData(info, context);
|
||||
info.AddValue("count", array.Length);
|
||||
info.AddValue("values", array);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerator<T> GetEnumerator()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.Json;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace SharpNBT;
|
||||
|
|
@ -11,7 +11,7 @@ namespace SharpNBT;
|
|||
/// This tag type does not exist in the NBT specification, and is included for convenience to differentiate it from the <see cref="ByteTag"/> that it is
|
||||
/// actually serialized as.
|
||||
/// </remarks>
|
||||
[PublicAPI][Serializable]
|
||||
[PublicAPI]
|
||||
[Obsolete("Use the IsBool and Bool properties of ByteTag. This class will be removed in a future version.")]
|
||||
public class BoolTag : Tag
|
||||
{
|
||||
|
|
@ -26,16 +26,20 @@ public class BoolTag : Tag
|
|||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Required constructor for ISerializable implementation.
|
||||
/// </summary>
|
||||
/// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo" /> to describing this instance.</param>
|
||||
/// <param name="context">The destination (see <see cref="T:System.Runtime.Serialization.StreamingContext" />) for this serialization.</param>
|
||||
protected BoolTag(SerializationInfo info, StreamingContext context) : base(info, context)
|
||||
|
||||
/// <inheritdoc />
|
||||
protected internal override void WriteJson(Utf8JsonWriter writer, bool named = true)
|
||||
{
|
||||
if (named && Name != null)
|
||||
{
|
||||
writer.WriteBoolean(Name, Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteBooleanValue(Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc cref="object.ToString"/>
|
||||
public override string ToString() => $"TAG_Byte({PrettyName}): {(Value ? "true" : "false")}";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.Json;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace SharpNBT;
|
||||
|
|
@ -13,7 +13,7 @@ namespace SharpNBT;
|
|||
/// While this class uses the CLS compliant <see cref="byte"/> (0..255), the NBT specification uses a signed value with a range of -128..127, so ensure
|
||||
/// the bits are equivalent for your values.
|
||||
/// </remarks>
|
||||
[PublicAPI][Serializable]
|
||||
[PublicAPI]
|
||||
public class ByteArrayTag : ArrayTag<byte>
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -51,16 +51,24 @@ public class ByteArrayTag : ArrayTag<byte>
|
|||
public ByteArrayTag(string? name, ReadOnlySpan<byte> values) : base(TagType.ByteArray, name, values.ToArray())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Required constructor for ISerializable implementation.
|
||||
/// </summary>
|
||||
/// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo" /> to describing this instance.</param>
|
||||
/// <param name="context">The destination (see <see cref="T:System.Runtime.Serialization.StreamingContext" />) for this serialization.</param>
|
||||
protected ByteArrayTag(SerializationInfo info, StreamingContext context) : base(info, context)
|
||||
|
||||
/// <inheritdoc />
|
||||
protected internal override void WriteJson(Utf8JsonWriter writer, bool named = true)
|
||||
{
|
||||
if (named && Name != null)
|
||||
{
|
||||
writer.WriteStartArray(Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteStartArray();
|
||||
}
|
||||
|
||||
for (var i = 0; i < Count; i++)
|
||||
writer.WriteNumberValue(this[i]);
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc cref="object.ToString"/>
|
||||
public override string ToString()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.Json;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace SharpNBT;
|
||||
|
|
@ -86,15 +86,6 @@ public class ByteTag : NumericTag<byte>
|
|||
public ByteTag(string? name, sbyte value) : base(TagType.Byte, name, unchecked((byte) value))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Required constructor for ISerializable implementation.
|
||||
/// </summary>
|
||||
/// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo" /> to describing this instance.</param>
|
||||
/// <param name="context">The destination (see <see cref="T:System.Runtime.Serialization.StreamingContext" />) for this serialization.</param>
|
||||
protected ByteTag(SerializationInfo info, StreamingContext context) : base(info, context)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="object.ToString"/>
|
||||
public override string ToString()
|
||||
|
|
@ -102,6 +93,22 @@ public class ByteTag : NumericTag<byte>
|
|||
object obj = IsBool ? Bool : Value;
|
||||
return $"TAG_Byte({PrettyName}): {obj}";
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected internal override void WriteJson(Utf8JsonWriter writer, bool named = true)
|
||||
{
|
||||
if (named && Name != null)
|
||||
{
|
||||
if (IsBool)
|
||||
writer.WriteBoolean(Name, Bool);
|
||||
else
|
||||
writer.WriteNumber(Name, Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteNumberValue(Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicit conversion of this tag to a <see cref="byte"/>.
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ using System.Collections;
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace SharpNBT;
|
||||
|
|
@ -16,7 +16,7 @@ namespace SharpNBT;
|
|||
/// This along with the <see cref="ListTag"/> class define the structure of the NBT format. Children are not order-dependent, nor is order guaranteed. The
|
||||
/// closing <see cref="EndTag"/> does not require to be explicitly added, it will be added automatically during serialization.
|
||||
/// </remarks>
|
||||
[PublicAPI][Serializable]
|
||||
[PublicAPI]
|
||||
public class CompoundTag : Tag, IDictionary<string, Tag>, ICollection<Tag>
|
||||
{
|
||||
private readonly Dictionary<string, Tag> dict;
|
||||
|
|
@ -42,25 +42,7 @@ public class CompoundTag : Tag, IDictionary<string, Tag>, ICollection<Tag>
|
|||
dict.Add(value.Name!, AssertName(value));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Required constructor for ISerializable implementation.
|
||||
/// </summary>
|
||||
/// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo" /> to describing this instance.</param>
|
||||
/// <param name="context">The destination (see <see cref="T:System.Runtime.Serialization.StreamingContext" />) for this serialization.</param>
|
||||
protected CompoundTag(SerializationInfo info, StreamingContext context) : base(info, context)
|
||||
{
|
||||
var result = info.GetValue("children", typeof(Dictionary<string, Tag>)) as Dictionary<string, Tag>;
|
||||
dict = result ?? new Dictionary<string, Tag>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void GetObjectData(SerializationInfo info, StreamingContext context)
|
||||
{
|
||||
base.GetObjectData(info, context);
|
||||
info.AddValue("children", dict);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
void ICollection<KeyValuePair<string, Tag>>.Add(KeyValuePair<string, Tag> item) => dict.Add(item.Key, item.Value);
|
||||
|
||||
|
|
@ -157,6 +139,23 @@ public class CompoundTag : Tag, IDictionary<string, Tag>, ICollection<Tag>
|
|||
return (TTag)dict[name];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected internal override void WriteJson(Utf8JsonWriter writer, bool named = true)
|
||||
{
|
||||
if (named && Name != null)
|
||||
{
|
||||
writer.WriteStartObject(Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
}
|
||||
|
||||
foreach (var child in dict.Values)
|
||||
child.WriteJson(writer, true);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
/// <summary>Returns a string that represents the current object.</summary>
|
||||
/// <returns>A string that represents the current object.</returns>
|
||||
/// <footer><a href="https://docs.microsoft.com/en-us/dotnet/api/System.Object.ToString?view=netcore-5.0">`Object.ToString` on docs.microsoft.com</a></footer>
|
||||
|
|
@ -184,18 +183,18 @@ public class CompoundTag : Tag, IDictionary<string, Tag>, ICollection<Tag>
|
|||
/// <param name="name">The name of the tag to search for.</param>
|
||||
/// <param name="recursive"><see langword="true"/> to recursively search children, otherwise <see langword="false"/> to only search direct descendants.</param>
|
||||
/// <returns>The first tag found with <paramref name="name"/>, otherwise <see langword="null"/> if none was found.</returns>
|
||||
public Tag? Find(string name, bool recursive = false)
|
||||
public TTag? Find<TTag>(string name, bool recursive = false) where TTag : Tag
|
||||
{
|
||||
foreach (var tag in dict.Values)
|
||||
foreach (var (key, value) in dict)
|
||||
{
|
||||
if (string.CompareOrdinal(name, tag.Name) == 0)
|
||||
return tag;
|
||||
if (string.CompareOrdinal(name, key) == 0 && value is TTag result)
|
||||
return result;
|
||||
|
||||
if (recursive && tag is CompoundTag child)
|
||||
if (recursive && value is CompoundTag child)
|
||||
{
|
||||
var result = child.Find(name, true);
|
||||
if (result != null)
|
||||
return result;
|
||||
var nested = child.Find<TTag>(name, recursive);
|
||||
if (nested != null)
|
||||
return nested;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.Json;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace SharpNBT;
|
||||
|
|
@ -7,7 +6,7 @@ namespace SharpNBT;
|
|||
/// <summary>
|
||||
/// A tag that contains a single IEEE-754 double-precision floating point number.
|
||||
/// </summary>
|
||||
[PublicAPI][Serializable]
|
||||
[PublicAPI]
|
||||
public class DoubleTag : NumericTag<double>
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -18,14 +17,18 @@ public class DoubleTag : NumericTag<double>
|
|||
public DoubleTag(string? name, double value) : base(TagType.Double, name, value)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Required constructor for ISerializable implementation.
|
||||
/// </summary>
|
||||
/// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo" /> to describing this instance.</param>
|
||||
/// <param name="context">The destination (see <see cref="T:System.Runtime.Serialization.StreamingContext" />) for this serialization.</param>
|
||||
protected DoubleTag(SerializationInfo info, StreamingContext context) : base(info, context)
|
||||
|
||||
/// <inheritdoc />
|
||||
protected internal override void WriteJson(Utf8JsonWriter writer, bool named = true)
|
||||
{
|
||||
if (named && Name != null)
|
||||
{
|
||||
writer.WriteNumber(Name, Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteNumberValue(Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="object.ToString"/>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace SharpNBT;
|
||||
|
|
@ -15,7 +16,13 @@ public sealed class EndTag : Tag
|
|||
public EndTag() : base(TagType.End, null)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
protected internal override void WriteJson(Utf8JsonWriter writer, bool named = true)
|
||||
{
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="object.ToString"/>
|
||||
public override string ToString() => $"TAG_End";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.Json;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace SharpNBT;
|
||||
|
|
@ -7,7 +6,7 @@ namespace SharpNBT;
|
|||
/// <summary>
|
||||
/// A tag that contains a single IEEE-754 single-precision floating point number.
|
||||
/// </summary>
|
||||
[PublicAPI][Serializable]
|
||||
[PublicAPI]
|
||||
public class FloatTag : NumericTag<float>
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -18,13 +17,18 @@ public class FloatTag : NumericTag<float>
|
|||
public FloatTag(string? name, float value) : base(TagType.Float, name, value)
|
||||
{
|
||||
}
|
||||
/// <summary>
|
||||
/// Required constructor for ISerializable implementation.
|
||||
/// </summary>
|
||||
/// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo" /> to describing this instance.</param>
|
||||
/// <param name="context">The destination (see <see cref="T:System.Runtime.Serialization.StreamingContext" />) for this serialization.</param>
|
||||
protected FloatTag(SerializationInfo info, StreamingContext context) : base(info, context)
|
||||
|
||||
/// <inheritdoc />
|
||||
protected internal override void WriteJson(Utf8JsonWriter writer, bool named = true)
|
||||
{
|
||||
if (named && Name != null)
|
||||
{
|
||||
writer.WriteNumber(Name, Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteNumberValue(Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="object.ToString"/>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.Json;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace SharpNBT;
|
||||
|
|
@ -9,7 +10,7 @@ namespace SharpNBT;
|
|||
/// <summary>
|
||||
/// A tag that whose value is a contiguous sequence of 32-bit integers.
|
||||
/// </summary>
|
||||
[PublicAPI][Serializable]
|
||||
[PublicAPI]
|
||||
public class IntArrayTag : ArrayTag<int>
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -48,14 +49,22 @@ public class IntArrayTag : ArrayTag<int>
|
|||
public IntArrayTag(string? name, ReadOnlySpan<int> values) : base(TagType.IntArray, name, values.ToArray())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Required constructor for ISerializable implementation.
|
||||
/// </summary>
|
||||
/// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo" /> to describing this instance.</param>
|
||||
/// <param name="context">The destination (see <see cref="T:System.Runtime.Serialization.StreamingContext" />) for this serialization.</param>
|
||||
protected IntArrayTag(SerializationInfo info, StreamingContext context) : base(info, context)
|
||||
|
||||
/// <inheritdoc />
|
||||
protected internal override void WriteJson(Utf8JsonWriter writer, bool named = true)
|
||||
{
|
||||
if (named && Name != null)
|
||||
{
|
||||
writer.WriteStartArray(Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteStartArray();
|
||||
}
|
||||
|
||||
for (var i = 0; i < Count; i++)
|
||||
writer.WriteNumberValue(this[i]);
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="object.ToString"/>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.Json;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace SharpNBT;
|
||||
|
|
@ -7,7 +7,7 @@ namespace SharpNBT;
|
|||
/// <summary>
|
||||
/// A tag that contains a single 32-bit integer value.
|
||||
/// </summary>
|
||||
[PublicAPI][Serializable]
|
||||
[PublicAPI]
|
||||
public class IntTag : NumericTag<int>
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -37,14 +37,18 @@ public class IntTag : NumericTag<int>
|
|||
public IntTag(Tag? parent, string? name, uint value) : base(TagType.Int, name, unchecked((int) value))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Required constructor for ISerializable implementation.
|
||||
/// </summary>
|
||||
/// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo" /> to describing this instance.</param>
|
||||
/// <param name="context">The destination (see <see cref="T:System.Runtime.Serialization.StreamingContext" />) for this serialization.</param>
|
||||
protected IntTag(SerializationInfo info, StreamingContext context) : base(info, context)
|
||||
|
||||
/// <inheritdoc />
|
||||
protected internal override void WriteJson(Utf8JsonWriter writer, bool named = true)
|
||||
{
|
||||
if (named && Name != null)
|
||||
{
|
||||
writer.WriteNumber(Name, Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteNumberValue(Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="object.ToString"/>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ using System;
|
|||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace SharpNBT;
|
||||
|
|
@ -14,7 +14,7 @@ namespace SharpNBT;
|
|||
/// <remarks>
|
||||
/// All child tags <b>must</b> be have the same <see cref="Tag.Type"/> value, and their <see cref="Tag.Name"/> value will be omitted during serialization.
|
||||
/// </remarks>
|
||||
[PublicAPI][Serializable]
|
||||
[PublicAPI]
|
||||
public class ListTag : Tag, IList<Tag>
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -56,28 +56,6 @@ public class ListTag : Tag, IList<Tag>
|
|||
foreach (var item in children)
|
||||
list.Add(AssertType(item));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Required constructor for ISerializable implementation.
|
||||
/// </summary>
|
||||
/// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo" /> to describing this instance.</param>
|
||||
/// <param name="context">The destination (see <see cref="T:System.Runtime.Serialization.StreamingContext" />) for this serialization.</param>
|
||||
protected ListTag(SerializationInfo info, StreamingContext context) : base(info, context)
|
||||
{
|
||||
ChildType = (TagType)info.GetByte("child_type");
|
||||
var count = info.GetInt32("count");
|
||||
list = new List<Tag>(count);
|
||||
if (info.GetValue("values", typeof(Tag[])) is Tag[] ary)
|
||||
AddRange(ary);
|
||||
}
|
||||
|
||||
public override void GetObjectData(SerializationInfo info, StreamingContext context)
|
||||
{
|
||||
base.GetObjectData(info, context);
|
||||
info.AddValue("child_type", (byte) ChildType);
|
||||
info.AddValue("count", list.Count);
|
||||
info.AddValue("values", list.ToArray());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerator<Tag> GetEnumerator() => list.GetEnumerator();
|
||||
|
|
@ -128,24 +106,22 @@ public class ListTag : Tag, IList<Tag>
|
|||
set => list[index] = AssertType(value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
protected internal override void WriteJson(Utf8JsonWriter writer, bool named = true)
|
||||
{
|
||||
if (named && Name != null)
|
||||
{
|
||||
writer.WriteStartArray(Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteStartArray();
|
||||
}
|
||||
|
||||
for (var i = 0; i < Count; i++)
|
||||
list[i].WriteJson(writer, false);
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="object.ToString"/>
|
||||
public override string ToString()
|
||||
|
|
@ -153,9 +129,7 @@ public class ListTag : Tag, IList<Tag>
|
|||
var word = Count == 1 ? Strings.WordEntry : Strings.WordEntries;
|
||||
return $"TAG_List({PrettyName}): [{Count} {word}]";
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// <inheritdoc cref="Tag.PrettyPrinted(StringBuilder,int,string)"/>
|
||||
protected internal override void PrettyPrinted(StringBuilder buffer, int level, string indent)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.Json;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace SharpNBT;
|
||||
|
|
@ -9,7 +9,7 @@ namespace SharpNBT;
|
|||
/// <summary>
|
||||
/// A tag that whose value is a contiguous sequence of 64-bit integers.
|
||||
/// </summary>
|
||||
[PublicAPI][Serializable]
|
||||
[PublicAPI]
|
||||
public class LongArrayTag : ArrayTag<long>
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -29,15 +29,6 @@ public class LongArrayTag : ArrayTag<long>
|
|||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Required constructor for ISerializable implementation.
|
||||
/// </summary>
|
||||
/// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo" /> to describing this instance.</param>
|
||||
/// <param name="context">The destination (see <see cref="T:System.Runtime.Serialization.StreamingContext" />) for this serialization.</param>
|
||||
protected LongArrayTag(SerializationInfo info, StreamingContext context) : base(info, context)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LongArrayTag"/> with the specified <paramref name="values"/>.
|
||||
/// </summary>
|
||||
|
|
@ -55,6 +46,23 @@ public class LongArrayTag : ArrayTag<long>
|
|||
public LongArrayTag(string? name, ReadOnlySpan<long> values) : base(TagType.LongArray, name, values.ToArray())
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected internal override void WriteJson(Utf8JsonWriter writer, bool named = true)
|
||||
{
|
||||
if (named && Name != null)
|
||||
{
|
||||
writer.WriteStartArray(Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteStartArray();
|
||||
}
|
||||
|
||||
for (var i = 0; i < Count; i++)
|
||||
writer.WriteNumberValue(this[i]);
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="object.ToString"/>
|
||||
public override string ToString()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.Json;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace SharpNBT;
|
||||
|
|
@ -7,7 +7,7 @@ namespace SharpNBT;
|
|||
/// <summary>
|
||||
/// A tag that contains a single 64-bit integer value.
|
||||
/// </summary>
|
||||
[PublicAPI][Serializable]
|
||||
[PublicAPI]
|
||||
public class LongTag : NumericTag<long>
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -37,14 +37,18 @@ public class LongTag : NumericTag<long>
|
|||
public LongTag(string? name, ulong value) : base(TagType.Long, name, unchecked((long) value))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Required constructor for ISerializable implementation.
|
||||
/// </summary>
|
||||
/// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo" /> to describing this instance.</param>
|
||||
/// <param name="context">The destination (see <see cref="T:System.Runtime.Serialization.StreamingContext" />) for this serialization.</param>
|
||||
protected LongTag(SerializationInfo info, StreamingContext context) : base(info, context)
|
||||
|
||||
/// <inheritdoc />
|
||||
protected internal override void WriteJson(Utf8JsonWriter writer, bool named = true)
|
||||
{
|
||||
if (named && Name != null)
|
||||
{
|
||||
writer.WriteNumber(Name, Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteNumberValue(Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="object.ToString"/>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.Json;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace SharpNBT;
|
||||
|
|
@ -10,7 +10,7 @@ namespace SharpNBT;
|
|||
/// Abstract base class for <see cref="Tag"/> types that contain a single numeric value.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">A value type that implements <see cref="INumber{TSelf}"/>.</typeparam>
|
||||
[PublicAPI][Serializable]
|
||||
[PublicAPI]
|
||||
public abstract class NumericTag<T> : Tag, IEquatable<NumericTag<T>>, IComparable<NumericTag<T>>, IComparable where T : unmanaged, INumber<T>
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -23,21 +23,7 @@ public abstract class NumericTag<T> : Tag, IEquatable<NumericTag<T>>, IComparabl
|
|||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected NumericTag(SerializationInfo info, StreamingContext context) : base(info, context)
|
||||
{
|
||||
var value = info.GetValue("value", typeof(T));
|
||||
Value = value is null ? default : (T)value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void GetObjectData(SerializationInfo info, StreamingContext context)
|
||||
{
|
||||
base.GetObjectData(info, context);
|
||||
info.AddValue("value", Value, typeof(T));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Equals(NumericTag<T>? other)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.Json;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace SharpNBT;
|
||||
|
|
@ -7,7 +7,7 @@ namespace SharpNBT;
|
|||
/// <summary>
|
||||
/// A tag that contains a single 16-bit integer value.
|
||||
/// </summary>
|
||||
[PublicAPI][Serializable]
|
||||
[PublicAPI]
|
||||
public class ShortTag : NumericTag<short>
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -43,14 +43,18 @@ public class ShortTag : NumericTag<short>
|
|||
public ShortTag(string? name, ushort value) : base(TagType.Short, name, unchecked((short) value))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Required constructor for ISerializable implementation.
|
||||
/// </summary>
|
||||
/// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo" /> to describing this instance.</param>
|
||||
/// <param name="context">The destination (see <see cref="T:System.Runtime.Serialization.StreamingContext" />) for this serialization.</param>
|
||||
protected ShortTag(SerializationInfo info, StreamingContext context) : base(info, context)
|
||||
|
||||
/// <inheritdoc />
|
||||
protected internal override void WriteJson(Utf8JsonWriter writer, bool named = true)
|
||||
{
|
||||
if (named && Name != null)
|
||||
{
|
||||
writer.WriteNumber(Name, Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteNumberValue(Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="object.ToString"/>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using System.Numerics;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.Json;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace SharpNBT;
|
||||
|
|
@ -8,7 +7,7 @@ namespace SharpNBT;
|
|||
/// <summary>
|
||||
/// A tag the contains a UTF-8 string.
|
||||
/// </summary>
|
||||
[PublicAPI][Serializable]
|
||||
[PublicAPI]
|
||||
public class StringTag : Tag, IEquatable<StringTag>
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -25,18 +24,18 @@ public class StringTag : Tag, IEquatable<StringTag>
|
|||
{
|
||||
Value = value ?? string.Empty;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
protected StringTag(SerializationInfo info, StreamingContext context) : base(info, context)
|
||||
protected internal override void WriteJson(Utf8JsonWriter writer, bool named = true)
|
||||
{
|
||||
Value = info.GetString("value") ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void GetObjectData(SerializationInfo info, StreamingContext context)
|
||||
{
|
||||
base.GetObjectData(info, context);
|
||||
info.AddValue("value", Value);
|
||||
if (named && Name != null)
|
||||
{
|
||||
writer.WriteString(Name, Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteStringValue(Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="object.ToString"/>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Runtime.Serialization.Json;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
[assembly: CLSCompliant(true)]
|
||||
|
|
@ -16,40 +15,9 @@ namespace SharpNBT;
|
|||
/// <summary>
|
||||
/// Abstract base class that all NBT tags inherit from.
|
||||
/// </summary>
|
||||
[PublicAPI][Serializable]
|
||||
public abstract class Tag : IEquatable<Tag>, ISerializable, ICloneable
|
||||
[PublicAPI]
|
||||
public abstract class Tag : IEquatable<Tag>, ICloneable
|
||||
{
|
||||
private static Regex simpleNameMatcher;
|
||||
|
||||
static Tag()
|
||||
{
|
||||
simpleNameMatcher = new Regex(@"^[A-Ba-z0-9_-]+$", RegexOptions.Compiled);
|
||||
}
|
||||
|
||||
|
||||
private static IEnumerable<Type> GetKnownTypes()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
typeof(TagType),
|
||||
typeof(NumericTag<>),
|
||||
typeof(ArrayTag<>),
|
||||
typeof(Tag[]),
|
||||
typeof(ByteTag),
|
||||
typeof(ShortTag),
|
||||
typeof(IntTag),
|
||||
typeof(LongTag),
|
||||
typeof(FloatTag),
|
||||
typeof(DoubleTag),
|
||||
typeof(StringTag),
|
||||
typeof(ByteArrayTag),
|
||||
typeof(IntArrayTag),
|
||||
typeof(LongArrayTag),
|
||||
typeof(ListTag),
|
||||
typeof(CompoundTag)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Text applied in a pretty-print sting when a tag has no defined <see cref="Name"/> value.
|
||||
/// </summary>
|
||||
|
|
@ -58,17 +26,18 @@ public abstract class Tag : IEquatable<Tag>, ISerializable, ICloneable
|
|||
/// <summary>
|
||||
/// Gets a constant describing the NBT type this object represents.
|
||||
/// </summary>
|
||||
public TagType Type { get; private set; }
|
||||
public TagType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parent <see cref="Tag"/> this object is a child of.
|
||||
/// </summary>
|
||||
[Obsolete("Parent property will be removed in a future version.")]
|
||||
public Tag? Parent { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name assigned to this <see cref="Tag"/>.
|
||||
/// </summary>
|
||||
public string? Name { get; set; }
|
||||
public string? Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initialized a new instance of the <see cref="Tag"/> class.
|
||||
|
|
@ -98,77 +67,115 @@ public abstract class Tag : IEquatable<Tag>, ISerializable, ICloneable
|
|||
/// Gets the name of the object as a human-readable quoted string, or a default name to indicate it has no name when applicable.
|
||||
/// </summary>
|
||||
protected internal string PrettyName => Name is null ? "None" : $"\"{Name}\"";
|
||||
|
||||
/// <summary>
|
||||
/// Uses the provided <paramref name="writer"/> to write the NBT tag in JSON format.
|
||||
/// </summary>
|
||||
/// <param name="writer">A JSON writer instance.</param>
|
||||
/// <param name="named">
|
||||
/// Flag indicating if this object's name should be written as a property name, or <see langword="false"/> when it
|
||||
/// is a child of <see cref="ListTag"/>, in which case it should be written as a JSON array element.
|
||||
/// </param>
|
||||
protected internal abstract void WriteJson(Utf8JsonWriter writer, bool named = true);
|
||||
|
||||
/// <summary>
|
||||
/// Writes the tag to the specified <paramref name="stream"/> in JSON format.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream instance to write to.</param>
|
||||
/// <param name="options">Options that will be passed to the JSON writer.</param>
|
||||
/// <exception cref="IOException">The stream is no opened for writing.</exception>
|
||||
public void WriteJson(Stream stream, JsonWriterOptions? options = null)
|
||||
{
|
||||
using var json = new Utf8JsonWriter(stream, options ?? new JsonWriterOptions());
|
||||
|
||||
if (string.IsNullOrEmpty(Name))
|
||||
{
|
||||
json.WriteStartArray();
|
||||
WriteJson(json, false);
|
||||
json.WriteEndArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
json.WriteStartObject();
|
||||
WriteJson(json, true);
|
||||
json.WriteEndObject();
|
||||
}
|
||||
|
||||
json.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously writes the tag to the specified <paramref name="stream"/> in JSON format.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream instance to write to.</param>
|
||||
/// <param name="options">Options that will be passed to the JSON writer.</param>
|
||||
/// <exception cref="IOException">The stream is no opened for writing.</exception>
|
||||
public async Task WriteJsonAsync(Stream stream, JsonWriterOptions? options = null)
|
||||
{
|
||||
await using var json = new Utf8JsonWriter(stream, options ?? new JsonWriterOptions());
|
||||
|
||||
if (string.IsNullOrEmpty(Name))
|
||||
{
|
||||
json.WriteStartArray();
|
||||
WriteJson(json, false);
|
||||
json.WriteEndArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
json.WriteStartObject();
|
||||
WriteJson(json, true);
|
||||
json.WriteEndObject();
|
||||
}
|
||||
|
||||
await json.FlushAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the NBT to an equivalent JSON representation, and returns it as a string.
|
||||
/// </summary>
|
||||
/// <param name="options">Options that will be passed to the JSON writer.</param>
|
||||
/// <returns>The JSON-encoded string representing describing the tag.</returns>
|
||||
public string ToJson(JsonWriterOptions? options = null)
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
WriteJson(stream, options);
|
||||
stream.Flush();
|
||||
return Encoding.UTF8.GetString(stream.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a representation of this <see cref="Tag"/> as a JSON string.
|
||||
/// </summary>
|
||||
/// <param name="pretty">Flag indicating if formatting should be applied to make the string human-readable.</param>
|
||||
/// <param name="indent">When <paramref name="pretty"/> is <see lawnword="true"/>, indicates the indent characters(s) to use.</param>
|
||||
/// <param name="indent">Ignored</param>
|
||||
/// <returns>A JSON string describing this object.</returns>
|
||||
[Obsolete("Use WriteJson and ToJson instead.")]
|
||||
public string ToJsonString(bool pretty = false, string indent = " ")
|
||||
{
|
||||
var settings = new DataContractJsonSerializerSettings
|
||||
{
|
||||
UseSimpleDictionaryFormat = true,
|
||||
EmitTypeInformation = EmitTypeInformation.Never,
|
||||
KnownTypes = GetKnownTypes()
|
||||
};
|
||||
var serializer = new DataContractJsonSerializer(typeof(Tag), settings);
|
||||
using var stream = new MemoryStream();
|
||||
if (pretty)
|
||||
{
|
||||
using var writer = JsonReaderWriterFactory.CreateJsonWriter(stream, Encoding.UTF8, false, true, indent);
|
||||
serializer.WriteObject(writer, this);
|
||||
writer.Flush();
|
||||
}
|
||||
else
|
||||
{
|
||||
serializer.WriteObject(stream, this);
|
||||
}
|
||||
stream.Flush();
|
||||
return Encoding.UTF8.GetString(stream.ToArray());
|
||||
var options = new JsonWriterOptions { Indented = pretty };
|
||||
return ToJson(options);
|
||||
}
|
||||
|
||||
/// <summary>Indicates whether the current object is equal to another object of the same type.</summary>
|
||||
/// <param name="other">An object to compare with this object.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true" /> if the current object is equal to the <paramref name="other" /> parameter; otherwise, <see langword="false" />.</returns>
|
||||
/// <footer><a href="https://docs.microsoft.com/en-us/dotnet/api/System.IEquatable-1.Equals?view=netstandard-2.1">`IEquatable.Equals` on docs.microsoft.com</a></footer>
|
||||
/// <inheritdoc />
|
||||
public bool Equals(Tag? other)
|
||||
{
|
||||
if (ReferenceEquals(null, other)) return false;
|
||||
if (ReferenceEquals(this, other)) return true;
|
||||
return Type == other.Type && Name == other.Name;
|
||||
return Type == other.Type && string.CompareOrdinal(Name, other.Name) == 0;
|
||||
}
|
||||
|
||||
/// <summary>Determines whether the specified object is equal to the current object.</summary>
|
||||
/// <param name="obj">The object to compare with the current object.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true" /> if the specified object is equal to the current object; otherwise, <see langword="false" />.</returns>
|
||||
/// <footer><a href="https://docs.microsoft.com/en-us/dotnet/api/System.Object.Equals?view=netstandard-2.1">`Object.Equals` on docs.microsoft.com</a></footer>
|
||||
/// <inheritdoc />
|
||||
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((Tag)obj);
|
||||
return obj.GetType() == GetType() && Equals((Tag)obj);
|
||||
}
|
||||
|
||||
/// <summary>Serves as the default hash function.</summary>
|
||||
/// <returns>A hash code for the current object.</returns>
|
||||
/// <footer><a href="https://docs.microsoft.com/en-us/dotnet/api/System.Object.GetHashCode?view=netstandard-2.1">`Object.GetHashCode` on docs.microsoft.com</a></footer>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
// ReSharper disable NonReadonlyMemberInGetHashCode
|
||||
return ((int)Type * 373) ^ (Name != null ? Name.GetHashCode() : 0);
|
||||
// ReSharper restore NonReadonlyMemberInGetHashCode
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Creates a new object that is a copy of the current instance.</summary>
|
||||
/// <returns>A new object that is a copy of this instance.</returns>
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode() => HashCode.Combine((int)Type, Name);
|
||||
|
||||
/// <inheritdoc />
|
||||
public object Clone()
|
||||
{
|
||||
// Serialize then deserialize to make a deep-copy
|
||||
|
|
@ -182,28 +189,7 @@ public abstract class Tag : IEquatable<Tag>, ISerializable, ICloneable
|
|||
writer.WriteTag(this);
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
return reader.ReadTag(!(Parent is ListTag));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Required constructor for ISerializable implementation.
|
||||
/// </summary>
|
||||
/// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo" /> to describing this instance.</param>
|
||||
/// <param name="context">The destination (see <see cref="T:System.Runtime.Serialization.StreamingContext" />) for this serialization.</param>
|
||||
protected Tag(SerializationInfo info, StreamingContext context)
|
||||
{
|
||||
Type = (TagType) info.GetByte("type");
|
||||
Name = info.GetString("name");
|
||||
}
|
||||
|
||||
/// <summary>Populates a <see cref="T:System.Runtime.Serialization.SerializationInfo" /> with the data needed to serialize the target object.</summary>
|
||||
/// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo" /> to populate with data.</param>
|
||||
/// <param name="context">The destination (see <see cref="T:System.Runtime.Serialization.StreamingContext" />) for this serialization.</param>
|
||||
/// <exception cref="T:System.Security.SecurityException">The caller does not have the required permission.</exception>
|
||||
public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
|
||||
{
|
||||
info.AddValue("type", (byte) Type);
|
||||
info.AddValue("name", Name);
|
||||
return reader.ReadTag(!string.IsNullOrWhiteSpace(Name));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -238,7 +224,7 @@ public abstract class Tag : IEquatable<Tag>, ISerializable, ICloneable
|
|||
{
|
||||
if (string.IsNullOrEmpty(Name))
|
||||
return string.Empty;
|
||||
return simpleNameMatcher.IsMatch(Name) ? Name : $"\"{Name}\"";
|
||||
return Name.All(c => c.IsValidUnquoted()) ? Name : $"\"{Name}\"";
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue