Implement basic SNBT reader and testsuite

This commit is contained in:
zontreck 2024-06-06 14:14:13 -07:00
parent 396c660113
commit a0f372693b
17 changed files with 352 additions and 2 deletions

View file

@ -5,7 +5,7 @@ import 'Tag.dart';
import 'impl/CompoundTag.dart';
class SnbtIo {
static void write(String file, CompoundTag tag) {
static void writeToFile(String file, CompoundTag tag) {
File handle = File(file);
if (handle.existsSync())
@ -15,4 +15,22 @@ class SnbtIo {
Tag.writeStringifiedNamedTag(tag, builder, 0);
handle.writeAsString(builder.toString());
}
static Future<Tag> readFromFile(String file) async {
File fi = File(file);
String data = await fi.readAsString();
StringReader reader = StringReader(data);
return Tag.readStringifiedNamedTag(reader);
}
static String writeToString(CompoundTag tag) {
StringBuilder builder = StringBuilder();
Tag.writeStringifiedNamedTag(tag, builder, 0);
return builder.toString();
}
static Future<Tag> readFromString(String data) async {
return Tag.readStringifiedNamedTag(StringReader(data));
}
}

View file

@ -408,3 +408,124 @@ class StringBuilder {
return "${_buffer}";
}
}
class StringReader {
final String _buffer;
int _position = 0;
int _lastPostion = 0;
StringReader(this._buffer);
// Check if there's more to read
bool get canRead => _position < _buffer.length;
// Read the next character
String next() {
if (canRead) {
skipWhitespace();
return _buffer[_position++];
} else {
throw Exception("End of buffer reached");
}
}
// Peek the next character without advancing the position
String peek() {
skipWhitespace();
if (canRead) {
return _buffer[_position];
} else {
throw Exception("End of buffer reached");
}
}
// Skip any whitespace characters
void skipWhitespace() {
while (canRead && isWhitespace(_buffer[_position])) {
_position++;
}
}
// Check if a character is a whitespace
bool isWhitespace(String char) {
return char.trim().isEmpty;
}
// Read until a specific character is found
String readUntil(String stopChar) {
StringBuffer result = StringBuffer();
while (canRead && peek() != stopChar) {
result.write(next());
}
return result.toString();
}
// Read a string enclosed in double quotes
String readQuotedString() {
if (next() != '"') {
throw Exception('Expected double quotes at the start of a string');
}
StringBuffer result = StringBuffer();
while (canRead) {
String char = next();
if (char == '"') {
break;
}
result.write(char);
}
return result.toString();
}
// Read a number (int or double)
String readNumber() {
StringBuffer result = StringBuffer();
while (canRead && (isDigit(peek()) || peek() == '.' || peek() == '-')) {
result.write(next());
}
return result.toString();
}
// Check if a character is a digit
bool isDigit(String char) {
return int.tryParse(char) != null;
}
// Read an unquoted string (used for keys in SNBT)
String readUnquotedString() {
StringBuffer result = StringBuffer();
while (canRead &&
!isWhitespace(peek()) &&
peek() != ':' &&
peek() != ',' &&
peek() != '{' &&
peek() != '}' &&
peek() != '[' &&
peek() != ']') {
result.write(next());
}
return result.toString();
}
String readString() {
if (peek() == "\"") {
return readQuotedString();
} else
return readUnquotedString();
}
// Read a specific character and throw an exception if it's not found
void expect(String expectedChar) {
if (next().toLowerCase() != expectedChar.toLowerCase()) {
throw Exception('Expected $expectedChar');
}
}
void startSeek() {
_lastPostion = _position;
}
void endSeek() {
_position = _lastPostion;
_lastPostion = 0;
}
}

View file

@ -40,6 +40,79 @@ enum TagType {
return TagType.End;
}
static TagType getStringifiedTagType(StringReader reader) {
reader.startSeek();
TagType ret = TagType.End;
// Start to determine the next tag type
while (reader.canRead && ret == TagType.End) {
var val = reader.next().toUpperCase();
switch (val) {
case "{":
{
ret = TagType.Compound;
break;
}
case "[":
{
// Check for a type Prefix
var X = reader.readUntil(";");
switch (X.toUpperCase()) {
case "B":
{
ret = TagType.ByteArray;
break;
}
case "I":
{
ret = TagType.IntArray;
break;
}
case "L":
{
ret = TagType.LongArray;
break;
}
default:
{
ret = TagType.List;
break;
}
}
break;
}
case "B":
{
ret = TagType.Byte;
break;
}
case "D":
{
ret = TagType.Double;
break;
}
case "F":
{
ret = TagType.Float;
break;
}
case "L":
{
ret = TagType.Long;
break;
}
case "S":
{
ret = TagType.Short;
break;
}
}
}
reader.endSeek();
return ret;
}
}
abstract class Tag {
@ -100,6 +173,23 @@ abstract class Tag {
}
}
static Tag readStringifiedNamedTag(StringReader string) {
String name = "";
if (string.peek() == "{" || string.peek() == "[") {
// No name
name = "";
} else {
name = string.readString();
string.expect(":");
}
TagType type = TagType.getStringifiedTagType(string);
Tag tag = Tag.makeTagOfType(type);
tag._key = name;
tag.readStringifiedValue(string);
return tag;
}
bool shouldQuoteName() {
if (getKey() == "") {
return false;
@ -116,6 +206,7 @@ abstract class Tag {
}
void writeStringifiedValue(StringBuilder builder, int indent, bool isList);
void readStringifiedValue(StringReader reader);
bool equals(dynamic object) {
if (object == null || object is! Tag) return false;

View file

@ -52,4 +52,16 @@ class ByteArrayTag extends Tag {
builder.append(
"${isList ? "".padLeft(indent, '\t') : ""}[B; ${value.join("B, ")}B]");
}
@override
void readStringifiedValue(StringReader reader) {
reader.expect("[");
while (reader.peek() != "]") {
value.add(int.parse(reader.readNumber()));
reader.expect("b");
if (reader.peek() == ",") reader.next();
}
reader.expect("]");
}
}

View file

@ -39,4 +39,12 @@ class ByteTag extends Tag {
void writeStringifiedValue(StringBuilder builder, int indent, bool isList) {
builder.append("${value}b");
}
@override
void readStringifiedValue(StringReader reader) {
String val = reader.readNumber();
value = int.parse(val);
reader.expect("b");
}
}

View file

@ -190,4 +190,16 @@ class CompoundTag extends Tag implements Map<String, Tag> {
@override
Iterable<Tag> get values => value.values;
@override
void readStringifiedValue(StringReader reader) {
reader.expect("{");
while (reader.peek() != "}") {
Tag tag = Tag.readStringifiedNamedTag(reader);
put(tag.getKey(), tag);
}
reader.expect("}");
}
}

View file

@ -39,4 +39,12 @@ class DoubleTag extends Tag {
void writeStringifiedValue(StringBuilder builder, int indent, bool isList) {
builder.append("${isList ? "".padLeft(indent, '\t') : ""}${value}d");
}
@override
void readStringifiedValue(StringReader reader) {
double val = double.parse(reader.readNumber());
value = val;
reader.expect("d");
}
}

View file

@ -22,4 +22,7 @@ class EndTag extends Tag {
@override
void writeStringifiedValue(StringBuilder builder, int indent, bool isList) {}
@override
void readStringifiedValue(StringReader reader) {}
}

View file

@ -38,4 +38,12 @@ class FloatTag extends Tag {
void writeStringifiedValue(StringBuilder builder, int indent, bool isList) {
builder.append("${isList ? "".padLeft(indent, '\t') : ""}${value}f");
}
@override
void readStringifiedValue(StringReader reader) {
double val = double.parse(reader.readNumber());
value = val;
reader.expect("f");
}
}

View file

@ -51,4 +51,15 @@ class IntArrayTag extends Tag {
builder.append(
"${isList ? "".padLeft(indent, '\t') : ""}[I; ${value.join('I, ')}I]");
}
@override
void readStringifiedValue(StringReader reader) {
reader.expect("[");
while (reader.peek() != "]") {
value.add(int.parse(reader.readNumber()));
if (reader.peek() == ",") reader.next();
}
reader.expect("]");
}
}

View file

@ -38,4 +38,10 @@ class IntTag extends Tag {
void writeStringifiedValue(StringBuilder builder, int indent, bool isList) {
builder.append("${isList ? "".padLeft(indent, '\t') : ""}${value}i");
}
@override
void readStringifiedValue(StringReader reader) {
String val = reader.readNumber();
value = int.parse(val);
}
}

View file

@ -103,4 +103,15 @@ class ListTag extends Tag {
}
builder.append("\n${"".padLeft(indent - 1, '\t')}]");
}
@override
void readStringifiedValue(StringReader reader) {
reader.expect("[");
while (reader.peek() != "]") {
value.add(Tag.readStringifiedNamedTag(reader));
if (reader.peek() == ",") reader.next();
}
reader.expect("]");
}
}

View file

@ -55,4 +55,16 @@ class LongArrayTag extends Tag {
builder.append(
"${isList ? "".padLeft(indent, '\t') : ""}[L; ${value.join('L, ')}L]");
}
@override
void readStringifiedValue(StringReader reader) {
reader.expect("[");
while (reader.peek() != "]") {
value.add(int.parse(reader.readNumber()));
reader.expect("l");
if (reader.peek() == ",") reader.next();
}
reader.expect("]");
}
}

View file

@ -38,4 +38,12 @@ class LongTag extends Tag {
void writeStringifiedValue(StringBuilder builder, int indent, bool isList) {
builder.append("${isList ? "".padLeft(indent, '\t') : ""}${value}L");
}
@override
void readStringifiedValue(StringReader reader) {
int val = int.parse(reader.readNumber());
value = val;
reader.expect("l");
}
}

View file

@ -38,4 +38,12 @@ class ShortTag extends Tag {
void writeStringifiedValue(StringBuilder builder, int indent, bool isList) {
builder.append("${isList ? "".padLeft(indent, '\t') : ""}${value}S");
}
@override
void readStringifiedValue(StringReader reader) {
int val = int.parse(reader.readNumber());
value = val;
reader.expect("s");
}
}

View file

@ -38,4 +38,10 @@ class StringTag extends Tag {
void writeStringifiedValue(StringBuilder builder, int indent, bool isList) {
builder.append("${isList ? "".padLeft(indent, '\t') : ""}\"${value}\"");
}
@override
void readStringifiedValue(StringReader reader) {
String str = reader.readString();
value = str;
}
}

View file

@ -82,12 +82,19 @@ void main() {
await NbtIo.read("${Directory.current.path}/test/bigtest.nbt");
String output = "${Directory.current.path}/build/bigtest.snbt";
File file = File(output);
SnbtIo.write(output, ct);
SnbtIo.writeToFile(output, ct);
// Expect that the file exists
expect(file.existsSync(), true);
});
test("Read BigTest from SNBT file", () async {
CompoundTag tag = await SnbtIo.readFromFile(
"${Directory.current.path}/build/bigtest.snbt") as CompoundTag;
expect(tag.containsKey("stringTest"), true);
});
test("Write NULL UUID to NBT", () async {
CompoundTag tag = CompoundTag();
NbtUtils.writeUUID(tag, "test", NbtUUID.fromUUID(UUID.ZERO));