Implement basic SNBT reader and testsuite
This commit is contained in:
parent
396c660113
commit
a0f372693b
17 changed files with 352 additions and 2 deletions
|
@ -5,7 +5,7 @@ import 'Tag.dart';
|
||||||
import 'impl/CompoundTag.dart';
|
import 'impl/CompoundTag.dart';
|
||||||
|
|
||||||
class SnbtIo {
|
class SnbtIo {
|
||||||
static void write(String file, CompoundTag tag) {
|
static void writeToFile(String file, CompoundTag tag) {
|
||||||
File handle = File(file);
|
File handle = File(file);
|
||||||
|
|
||||||
if (handle.existsSync())
|
if (handle.existsSync())
|
||||||
|
@ -15,4 +15,22 @@ class SnbtIo {
|
||||||
Tag.writeStringifiedNamedTag(tag, builder, 0);
|
Tag.writeStringifiedNamedTag(tag, builder, 0);
|
||||||
handle.writeAsString(builder.toString());
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -408,3 +408,124 @@ class StringBuilder {
|
||||||
return "${_buffer}";
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -40,6 +40,79 @@ enum TagType {
|
||||||
|
|
||||||
return TagType.End;
|
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 {
|
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() {
|
bool shouldQuoteName() {
|
||||||
if (getKey() == "") {
|
if (getKey() == "") {
|
||||||
return false;
|
return false;
|
||||||
|
@ -116,6 +206,7 @@ abstract class Tag {
|
||||||
}
|
}
|
||||||
|
|
||||||
void writeStringifiedValue(StringBuilder builder, int indent, bool isList);
|
void writeStringifiedValue(StringBuilder builder, int indent, bool isList);
|
||||||
|
void readStringifiedValue(StringReader reader);
|
||||||
|
|
||||||
bool equals(dynamic object) {
|
bool equals(dynamic object) {
|
||||||
if (object == null || object is! Tag) return false;
|
if (object == null || object is! Tag) return false;
|
||||||
|
|
|
@ -52,4 +52,16 @@ class ByteArrayTag extends Tag {
|
||||||
builder.append(
|
builder.append(
|
||||||
"${isList ? "".padLeft(indent, '\t') : ""}[B; ${value.join("B, ")}B]");
|
"${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("]");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,4 +39,12 @@ class ByteTag extends Tag {
|
||||||
void writeStringifiedValue(StringBuilder builder, int indent, bool isList) {
|
void writeStringifiedValue(StringBuilder builder, int indent, bool isList) {
|
||||||
builder.append("${value}b");
|
builder.append("${value}b");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void readStringifiedValue(StringReader reader) {
|
||||||
|
String val = reader.readNumber();
|
||||||
|
value = int.parse(val);
|
||||||
|
|
||||||
|
reader.expect("b");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -190,4 +190,16 @@ class CompoundTag extends Tag implements Map<String, Tag> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Iterable<Tag> get values => value.values;
|
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("}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,4 +39,12 @@ class DoubleTag extends Tag {
|
||||||
void writeStringifiedValue(StringBuilder builder, int indent, bool isList) {
|
void writeStringifiedValue(StringBuilder builder, int indent, bool isList) {
|
||||||
builder.append("${isList ? "".padLeft(indent, '\t') : ""}${value}d");
|
builder.append("${isList ? "".padLeft(indent, '\t') : ""}${value}d");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void readStringifiedValue(StringReader reader) {
|
||||||
|
double val = double.parse(reader.readNumber());
|
||||||
|
value = val;
|
||||||
|
|
||||||
|
reader.expect("d");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,4 +22,7 @@ class EndTag extends Tag {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void writeStringifiedValue(StringBuilder builder, int indent, bool isList) {}
|
void writeStringifiedValue(StringBuilder builder, int indent, bool isList) {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void readStringifiedValue(StringReader reader) {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,4 +38,12 @@ class FloatTag extends Tag {
|
||||||
void writeStringifiedValue(StringBuilder builder, int indent, bool isList) {
|
void writeStringifiedValue(StringBuilder builder, int indent, bool isList) {
|
||||||
builder.append("${isList ? "".padLeft(indent, '\t') : ""}${value}f");
|
builder.append("${isList ? "".padLeft(indent, '\t') : ""}${value}f");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void readStringifiedValue(StringReader reader) {
|
||||||
|
double val = double.parse(reader.readNumber());
|
||||||
|
value = val;
|
||||||
|
|
||||||
|
reader.expect("f");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,4 +51,15 @@ class IntArrayTag extends Tag {
|
||||||
builder.append(
|
builder.append(
|
||||||
"${isList ? "".padLeft(indent, '\t') : ""}[I; ${value.join('I, ')}I]");
|
"${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("]");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,4 +38,10 @@ class IntTag extends Tag {
|
||||||
void writeStringifiedValue(StringBuilder builder, int indent, bool isList) {
|
void writeStringifiedValue(StringBuilder builder, int indent, bool isList) {
|
||||||
builder.append("${isList ? "".padLeft(indent, '\t') : ""}${value}i");
|
builder.append("${isList ? "".padLeft(indent, '\t') : ""}${value}i");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void readStringifiedValue(StringReader reader) {
|
||||||
|
String val = reader.readNumber();
|
||||||
|
value = int.parse(val);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,4 +103,15 @@ class ListTag extends Tag {
|
||||||
}
|
}
|
||||||
builder.append("\n${"".padLeft(indent - 1, '\t')}]");
|
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("]");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,4 +55,16 @@ class LongArrayTag extends Tag {
|
||||||
builder.append(
|
builder.append(
|
||||||
"${isList ? "".padLeft(indent, '\t') : ""}[L; ${value.join('L, ')}L]");
|
"${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("]");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,4 +38,12 @@ class LongTag extends Tag {
|
||||||
void writeStringifiedValue(StringBuilder builder, int indent, bool isList) {
|
void writeStringifiedValue(StringBuilder builder, int indent, bool isList) {
|
||||||
builder.append("${isList ? "".padLeft(indent, '\t') : ""}${value}L");
|
builder.append("${isList ? "".padLeft(indent, '\t') : ""}${value}L");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void readStringifiedValue(StringReader reader) {
|
||||||
|
int val = int.parse(reader.readNumber());
|
||||||
|
value = val;
|
||||||
|
|
||||||
|
reader.expect("l");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,4 +38,12 @@ class ShortTag extends Tag {
|
||||||
void writeStringifiedValue(StringBuilder builder, int indent, bool isList) {
|
void writeStringifiedValue(StringBuilder builder, int indent, bool isList) {
|
||||||
builder.append("${isList ? "".padLeft(indent, '\t') : ""}${value}S");
|
builder.append("${isList ? "".padLeft(indent, '\t') : ""}${value}S");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void readStringifiedValue(StringReader reader) {
|
||||||
|
int val = int.parse(reader.readNumber());
|
||||||
|
value = val;
|
||||||
|
|
||||||
|
reader.expect("s");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,4 +38,10 @@ class StringTag extends Tag {
|
||||||
void writeStringifiedValue(StringBuilder builder, int indent, bool isList) {
|
void writeStringifiedValue(StringBuilder builder, int indent, bool isList) {
|
||||||
builder.append("${isList ? "".padLeft(indent, '\t') : ""}\"${value}\"");
|
builder.append("${isList ? "".padLeft(indent, '\t') : ""}\"${value}\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void readStringifiedValue(StringReader reader) {
|
||||||
|
String str = reader.readString();
|
||||||
|
value = str;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,12 +82,19 @@ void main() {
|
||||||
await NbtIo.read("${Directory.current.path}/test/bigtest.nbt");
|
await NbtIo.read("${Directory.current.path}/test/bigtest.nbt");
|
||||||
String output = "${Directory.current.path}/build/bigtest.snbt";
|
String output = "${Directory.current.path}/build/bigtest.snbt";
|
||||||
File file = File(output);
|
File file = File(output);
|
||||||
SnbtIo.write(output, ct);
|
SnbtIo.writeToFile(output, ct);
|
||||||
|
|
||||||
// Expect that the file exists
|
// Expect that the file exists
|
||||||
expect(file.existsSync(), true);
|
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 {
|
test("Write NULL UUID to NBT", () async {
|
||||||
CompoundTag tag = CompoundTag();
|
CompoundTag tag = CompoundTag();
|
||||||
NbtUtils.writeUUID(tag, "test", NbtUUID.fromUUID(UUID.ZERO));
|
NbtUtils.writeUUID(tag, "test", NbtUUID.fromUUID(UUID.ZERO));
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue