diff --git a/.gitignore b/.gitignore index 362ff8e..2ec0cee 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,6 @@ doc/api/ .idea -*.iml \ No newline at end of file +*.iml + +out \ No newline at end of file diff --git a/bin/client_test.dart b/bin/client_test.dart new file mode 100644 index 0000000..c215f8b --- /dev/null +++ b/bin/client_test.dart @@ -0,0 +1,24 @@ +import 'package:libac_dart/nbt/Stream.dart'; +import 'package:libac_dart/nbt/Tag.dart'; +import 'package:libac_dart/nbt/impl/CompoundTag.dart'; +import 'package:libac_dart/packets/packets.dart'; + +void main() async { + PacketRegistry reg = PacketRegistry(); + reg.registerDefaults(); + + PacketClient client = PacketClient(); + await client.startConnect("127.0.0.1", 25306); + + S2CResponse response = await client.send(C2SPing(), true); + CompoundTag tag = response.contents; + StringBuilder builder = StringBuilder(); + Tag.writeStringifiedNamedTag(tag, builder, 0); + + print("Response from server: \n${builder}"); + + await client.send(StopServerPacket(), false); + + if (client.connected) await client.close(); + return; +} diff --git a/bin/server_test.dart b/bin/server_test.dart new file mode 100644 index 0000000..e8a4eac --- /dev/null +++ b/bin/server_test.dart @@ -0,0 +1,5 @@ +import 'package:libac_dart/packets/packets.dart'; + +void main() async { + await PacketServer.start(25306); +} diff --git a/compile.sh b/compile.sh new file mode 100755 index 0000000..42dd2b7 --- /dev/null +++ b/compile.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +mkdir out +rm -rf out/* +flutter pub publish -f --skip-validation +dart compile exe -o out/server_test bin/server_test.dart +dart compile exe -o out/client_test bin/client_test.dart \ No newline at end of file diff --git a/lib/nbt/NbtIo.dart b/lib/nbt/NbtIo.dart index f7ffcdd..4338584 100644 --- a/lib/nbt/NbtIo.dart +++ b/lib/nbt/NbtIo.dart @@ -1,6 +1,9 @@ -import 'package:libac_flutter/nbt/Stream.dart'; -import 'package:libac_flutter/nbt/Tag.dart'; -import 'package:libac_flutter/nbt/impl/CompoundTag.dart'; +import 'dart:typed_data'; + +import '../utils/Converter.dart'; +import 'Stream.dart'; +import 'Tag.dart'; +import 'impl/CompoundTag.dart'; class NbtIo { static ByteLayer _io = ByteLayer(); @@ -49,4 +52,44 @@ class NbtIo { static ByteLayer getStream() { return _io; } + + static Future writeBase64String(CompoundTag tag) async { + _io = ByteLayer(); + Tag.writeNamedTag(tag, _io); + _io.compress(); + return base64Encoder.encode(_io.bytes); + } + + static Future readBase64String(String encoded) async { + _io = ByteLayer(); + List bytes = base64Encoder.decode(encoded); + for (int byte in bytes) { + _io.writeByte(byte); + } + _io.decompress(); + _io.resetPosition(); + return Tag.readNamedTag(_io) as CompoundTag; + } + + static Future writeToStream(CompoundTag tag) async { + _io = ByteLayer(); + Tag.writeNamedTag(tag, _io); + _io.compress(); + + return _io.bytes; + } + + static Future readFromStream(Uint8List list) async { + _io = ByteLayer(); + try { + _io.writeBytes(list); + _io.resetPosition(); + _io.decompress(); + _io.resetPosition(); + } catch (E) { + print(E); + } finally { + return Tag.readNamedTag(_io) as CompoundTag; + } + } } diff --git a/lib/nbt/NbtUtils.dart b/lib/nbt/NbtUtils.dart index 3165fe6..d69170f 100644 --- a/lib/nbt/NbtUtils.dart +++ b/lib/nbt/NbtUtils.dart @@ -1,13 +1,12 @@ -import 'package:libac_flutter/nbt/impl/ByteTag.dart'; -import 'package:libac_flutter/nbt/impl/CompoundTag.dart'; -import 'package:libac_flutter/nbt/impl/DoubleTag.dart'; -import 'package:libac_flutter/nbt/impl/IntArrayTag.dart'; -import 'package:libac_flutter/nbt/impl/IntTag.dart'; -import 'package:libac_flutter/nbt/impl/ListTag.dart'; -import 'package:libac_flutter/utils/Vector3.dart'; - import '../utils/Vector2.dart'; -import '../utils/uuid/UUID.dart'; +import '../utils/Vector3.dart'; +import '../utils/uuid/NbtUUID.dart'; +import 'impl/ByteTag.dart'; +import 'impl/CompoundTag.dart'; +import 'impl/DoubleTag.dart'; +import 'impl/IntArrayTag.dart'; +import 'impl/IntTag.dart'; +import 'impl/ListTag.dart'; class NbtUtils { static void writeBoolean(CompoundTag tag, String name, bool b) { @@ -15,7 +14,7 @@ class NbtUtils { } static bool readBoolean(CompoundTag tag, String name) { - if (tag.contains(name)) { + if (tag.containsKey(name)) { return tag.get(name)!.asByte() == 1 ? true : false; } else { return false; @@ -30,7 +29,7 @@ class NbtUtils { } static Vector2d readVector2d(CompoundTag tag, String name) { - if (tag.contains(name)) { + if (tag.containsKey(name)) { ListTag lst = tag.get(name)! as ListTag; return Vector2d(X: lst.get(0).asDouble(), Z: lst.get(1).asDouble()); } else { @@ -46,7 +45,7 @@ class NbtUtils { } static Vector2i readVector2i(CompoundTag tag, String name) { - if (tag.contains(name)) { + if (tag.containsKey(name)) { ListTag lst = tag.get(name)! as ListTag; return Vector2i(X: lst.get(0).asInt(), Z: lst.get(1).asInt()); } else { @@ -63,7 +62,7 @@ class NbtUtils { } static Vector3d readVector3d(CompoundTag tag, String name) { - if (tag.contains(name)) { + if (tag.containsKey(name)) { ListTag lst = tag.get(name)! as ListTag; return Vector3d( X: lst.get(0).asDouble(), @@ -83,7 +82,7 @@ class NbtUtils { } static Vector3i readVector3i(CompoundTag tag, String name) { - if (tag.contains(name)) { + if (tag.containsKey(name)) { ListTag lst = tag.get(name)! as ListTag; return Vector3i( X: lst.get(0).asInt(), Y: lst.get(1).asInt(), Z: lst.get(2).asInt()); @@ -96,24 +95,25 @@ class NbtUtils { return [msb >> 32, msb, lsb >> 32, lsb]; } - static List _uuidToIntArray(UUID ID) { + static List _uuidToIntArray(NbtUUID ID) { return _msbLsbToIntArray( ID.getMostSignificantBits(), ID.getLeastSignificantBits()); } - static UUID _uuidFromIntArray(List values) { - return UUID((values[0] << 32 | values[1] & 4294967295), + static NbtUUID _uuidFromIntArray(List values) { + return NbtUUID((values[0] << 32 | values[1] & 4294967295), (values[2] << 32 | values[3] & 4294967295)); } - static void writeUUID(CompoundTag tag, String name, UUID ID) { + static void writeUUID(CompoundTag tag, String name, NbtUUID ID) { tag.put(name, IntArrayTag.valueOf(_uuidToIntArray(ID))); } - static UUID readUUID(CompoundTag tag, String name) { - if (!tag.contains(name)) - return UUID.ZERO; - else + static NbtUUID readUUID(CompoundTag tag, String name) { + if (!tag.containsKey(name)) { + return NbtUUID.ZERO; + } else { return _uuidFromIntArray(tag.get(name)!.asIntArray()); + } } } diff --git a/lib/nbt/SnbtIo.dart b/lib/nbt/SnbtIo.dart new file mode 100644 index 0000000..cb823a8 --- /dev/null +++ b/lib/nbt/SnbtIo.dart @@ -0,0 +1,36 @@ +import 'dart:io'; + +import 'Stream.dart'; +import 'Tag.dart'; +import 'impl/CompoundTag.dart'; + +class SnbtIo { + static void writeToFile(String file, CompoundTag tag) { + File handle = File(file); + + if (handle.existsSync()) + handle.deleteSync(); // Ensure we flush the file to 0 bytes + + StringBuilder builder = StringBuilder(); + Tag.writeStringifiedNamedTag(tag, builder, 0); + handle.writeAsString(builder.toString()); + } + + static Future 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 readFromString(String data) async { + return Tag.readStringifiedNamedTag(StringReader(data)); + } +} diff --git a/lib/nbt/Stream.dart b/lib/nbt/Stream.dart index c568310..d315c1a 100644 --- a/lib/nbt/Stream.dart +++ b/lib/nbt/Stream.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:io'; +import 'dart:math'; import 'dart:typed_data'; class ByteLayer { @@ -209,6 +210,8 @@ class ByteLayer { Future writeToFile(String filePath) async { final file = File(filePath); + if (file.existsSync()) + file.deleteSync(); // Ensure we flush the file to 0 bytes await file.writeAsBytes(bytes); } @@ -351,8 +354,9 @@ class ByteLayer { seek(position); int current = readUnsignedByte(); return (current & mask) == mask; - } else + } else { return false; + } } int getBit(int position) { @@ -375,4 +379,156 @@ class ByteLayer { clearBit(position, maskToClear); setBit(position, maskToSet); } + + void insertRandomBytes(int count) { + Random rng = Random(); + for (int a = 0; a < count; a++) { + writeByte(rng.nextInt(255)); + } + } +} + +class StringBuilder { + String _buffer = ""; + + StringBuilder(); + + bool get isEmpty => _buffer.isEmpty; + + void append(String value) { + _buffer += value; + } + + void clear() { + _buffer = ""; + } + + @override + String toString() { + 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; + + // Get the number of chars seeked + int get getSeeked => _lastPostion - _position; + + // 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; + } } diff --git a/lib/nbt/Tag.dart b/lib/nbt/Tag.dart index 6e92017..718a4e4 100644 --- a/lib/nbt/Tag.dart +++ b/lib/nbt/Tag.dart @@ -1,17 +1,16 @@ -import 'package:libac_flutter/nbt/impl/ByteArrayTag.dart'; -import 'package:libac_flutter/nbt/impl/CompoundTag.dart'; -import 'package:libac_flutter/nbt/impl/DoubleTag.dart'; -import 'package:libac_flutter/nbt/impl/EndTag.dart'; -import 'package:libac_flutter/nbt/impl/FloatTag.dart'; -import 'package:libac_flutter/nbt/impl/IntArrayTag.dart'; -import 'package:libac_flutter/nbt/impl/IntTag.dart'; -import 'package:libac_flutter/nbt/impl/LongTag.dart'; -import 'package:libac_flutter/nbt/impl/ShortTag.dart'; - import 'Stream.dart'; +import 'impl/ByteArrayTag.dart'; import 'impl/ByteTag.dart'; +import 'impl/CompoundTag.dart'; +import 'impl/DoubleTag.dart'; +import 'impl/EndTag.dart'; +import 'impl/FloatTag.dart'; +import 'impl/IntArrayTag.dart'; +import 'impl/IntTag.dart'; import 'impl/ListTag.dart'; import 'impl/LongArrayTag.dart'; +import 'impl/LongTag.dart'; +import 'impl/ShortTag.dart'; import 'impl/StringTag.dart'; enum TagType { @@ -41,6 +40,105 @@ enum TagType { return TagType.End; } + + static TagType getStringifiedTagType(StringReader reader) { + reader.startSeek(); + TagType ret = TagType.End; + bool isNumber = true; + + // 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 "I": + { + ret = TagType.Int; + break; + } + case "L": + { + ret = TagType.Long; + break; + } + case "S": + { + ret = TagType.Short; + break; + } + case "\"": + { + ret = TagType.String; + break; + } + case ",": + case "\n": + { + if (reader.getSeeked >= 1) ret = TagType.String; + if (isNumber) ret = TagType.Int; + + break; + } + default: + { + if (!reader.isDigit(val)) { + if (isNumber) isNumber = false; + } + break; + } + } + } + + reader.endSeek(); + return ret; + } } abstract class Tag { @@ -82,6 +180,60 @@ abstract class Tag { } } + static void writeStringifiedNamedTag( + Tag tag, StringBuilder builder, int indents) { + if (tag.getType() != 0) { + if (!builder.isEmpty) { + // Write name + if (tag._key == "") { + builder.append("${"".padLeft(indents, '\t')}"); + } else { + if (tag.shouldQuoteName()) { + builder.append("${"".padLeft(indents, "\t")}\"${tag.getKey()}\": "); + } else + builder.append("${"".padLeft(indents, '\t')}${tag.getKey()}: "); + } + } + + tag.writeStringifiedValue(builder, indents + 1, false); + } + } + + 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; + } else { + String letters = "abcdefghijklmnopqrstuvwxyz"; + for (int i = 0; i < getKey().length; i++) { + String digit = getKey().substring(i, i + 1); + if (letters.indexOf(digit) == -1) { + return true; + } + } + return false; + } + } + + void writeStringifiedValue(StringBuilder builder, int indent, bool isList); + void readStringifiedValue(StringReader reader); + bool equals(dynamic object) { if (object == null || object is! Tag) return false; @@ -232,6 +384,13 @@ abstract class Tag { } } + CompoundTag asCompoundTag() { + if (this is CompoundTag) { + return this as CompoundTag; + } else + return CompoundTag(); + } + void prettyPrint(int indent, bool recurse); static String getCanonicalName(TagType type) { diff --git a/lib/nbt/impl/ByteArrayTag.dart b/lib/nbt/impl/ByteArrayTag.dart index 4cc8dad..f94a63d 100644 --- a/lib/nbt/impl/ByteArrayTag.dart +++ b/lib/nbt/impl/ByteArrayTag.dart @@ -1,5 +1,5 @@ -import 'package:libac_flutter/nbt/Stream.dart'; -import 'package:libac_flutter/nbt/Tag.dart'; +import '../Stream.dart'; +import '../Tag.dart'; class ByteArrayTag extends Tag { final List value = []; @@ -46,4 +46,24 @@ class ByteArrayTag extends Tag { print( "${"".padLeft(indent, '\t')}${Tag.getCanonicalName(getTagType())}: [$array]"); } + + @override + void writeStringifiedValue(StringBuilder builder, int indent, bool isList) { + builder.append( + "${isList ? "".padLeft(indent, '\t') : ""}[B; ${value.join("B, ")}B]"); + } + + @override + void readStringifiedValue(StringReader reader) { + reader.expect("["); + reader.expect("B"); + reader.expect(";"); + while (reader.peek() != "]") { + value.add(int.parse(reader.readNumber())); + reader.expect("b"); + + if (reader.peek() == ",") reader.next(); + } + reader.expect("]"); + } } diff --git a/lib/nbt/impl/ByteTag.dart b/lib/nbt/impl/ByteTag.dart index dc538d8..cd56ec9 100644 --- a/lib/nbt/impl/ByteTag.dart +++ b/lib/nbt/impl/ByteTag.dart @@ -1,5 +1,5 @@ -import 'package:libac_flutter/nbt/Stream.dart'; -import 'package:libac_flutter/nbt/Tag.dart'; +import '../Stream.dart'; +import '../Tag.dart'; class ByteTag extends Tag { int value = 0; @@ -34,4 +34,17 @@ class ByteTag extends Tag { print( "${"".padLeft(indent, '\t')}${Tag.getCanonicalName(getTagType())}: $value"); } + + @override + 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"); + } } diff --git a/lib/nbt/impl/CompoundTag.dart b/lib/nbt/impl/CompoundTag.dart index 1c6a632..1a5c8ea 100644 --- a/lib/nbt/impl/CompoundTag.dart +++ b/lib/nbt/impl/CompoundTag.dart @@ -1,7 +1,7 @@ -import 'package:libac_flutter/nbt/Stream.dart'; -import 'package:libac_flutter/nbt/Tag.dart'; +import '../Stream.dart'; +import '../Tag.dart'; -class CompoundTag extends Tag { +class CompoundTag extends Tag implements Map { late final Map value = {}; CompoundTag(); @@ -48,12 +48,8 @@ class CompoundTag extends Tag { tag.setKey(name); } - bool contains(String name) { - return value.containsKey(name); - } - Tag? get(String name) { - if (contains(name)) { + if (containsKey(name)) { return value[name] as Tag; } else { // Does not exist! @@ -61,10 +57,6 @@ class CompoundTag extends Tag { } } - void remove(String name) { - value.remove(name); - } - @override TagType getTagType() { return TagType.Compound; @@ -85,4 +77,131 @@ class CompoundTag extends Tag { void endPrettyPrint(int indent) { print("${"".padLeft(indent, '\t')}}"); } + + @override + void writeStringifiedValue(StringBuilder builder, int indent, bool isList) { + Iterator it = value.values.iterator; + builder.append("${isList ? "".padLeft(indent - 1, '\t') : ""}{\n"); + + bool firstEntry = true; + while (it.moveNext()) { + Tag t = it.current; + if (firstEntry) { + firstEntry = false; + } else { + builder.append(",\n"); + } + Tag.writeStringifiedNamedTag(t, builder, indent); + } + builder.append("\n${"".padLeft(indent - 1, '\t')}}"); + } + + @override + Tag? operator [](Object? key) { + return value[key]; + } + + @override + void operator []=(String key, Tag value) { + this.value[key] = value; + } + + @override + void addAll(Map other) { + value.addAll(other); + } + + @override + void addEntries(Iterable> newEntries) { + value.addEntries(newEntries); + } + + @override + Map cast() { + return value.cast(); + } + + @override + void clear() { + value.clear(); + } + + @override + bool containsKey(Object? key) { + return value.containsKey(key); + } + + @override + bool containsValue(Object? value) { + return this.value.containsValue(value); + } + + @override + Iterable> get entries => value.entries; + + @override + void forEach(void Function(String key, Tag value) action) { + value.forEach(action); + } + + @override + bool get isEmpty => value.isEmpty; + + @override + bool get isNotEmpty => value.isNotEmpty; + + @override + Iterable get keys => value.keys; + + @override + int get length => value.length; + + @override + Map map( + MapEntry Function(String key, Tag value) convert) { + return value.map(convert); + } + + @override + Tag putIfAbsent(String key, Tag Function() ifAbsent) { + return this.value.putIfAbsent(key, ifAbsent); + } + + @override + Tag? remove(Object? key) { + return value.remove(key); + } + + @override + void removeWhere(bool Function(String key, Tag value) test) { + value.removeWhere(test); + } + + @override + Tag update(String key, Tag Function(Tag value) update, + {Tag Function()? ifAbsent}) { + return value.update(key, update); + } + + @override + void updateAll(Tag Function(String key, Tag value) update) { + value.updateAll(update); + } + + @override + Iterable get values => value.values; + + @override + void readStringifiedValue(StringReader reader) { + reader.expect("{"); + + while (reader.peek() != "}") { + Tag tag = Tag.readStringifiedNamedTag(reader); + put(tag.getKey(), tag); + + if (reader.peek() == ",") reader.next(); + } + + reader.expect("}"); + } } diff --git a/lib/nbt/impl/DoubleTag.dart b/lib/nbt/impl/DoubleTag.dart index f674968..d8b284b 100644 --- a/lib/nbt/impl/DoubleTag.dart +++ b/lib/nbt/impl/DoubleTag.dart @@ -1,5 +1,5 @@ -import 'package:libac_flutter/nbt/Stream.dart'; -import 'package:libac_flutter/nbt/Tag.dart'; +import '../Stream.dart'; +import '../Tag.dart'; class DoubleTag extends Tag { double value = 0.0; @@ -34,4 +34,17 @@ class DoubleTag extends Tag { print( "${"".padLeft(indent, '\t')}${Tag.getCanonicalName(getTagType())}: $value"); } + + @override + 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"); + } } diff --git a/lib/nbt/impl/EndTag.dart b/lib/nbt/impl/EndTag.dart index 24b1ab2..0b87918 100644 --- a/lib/nbt/impl/EndTag.dart +++ b/lib/nbt/impl/EndTag.dart @@ -1,5 +1,5 @@ -import 'package:libac_flutter/nbt/Stream.dart'; -import 'package:libac_flutter/nbt/Tag.dart'; +import '../Stream.dart'; +import '../Tag.dart'; class EndTag extends Tag { EndTag(); @@ -19,4 +19,10 @@ class EndTag extends Tag { void prettyPrint(int indent, bool recurse) { print("${"".padLeft(indent, '\t')}${Tag.getCanonicalName(getTagType())}"); } + + @override + void writeStringifiedValue(StringBuilder builder, int indent, bool isList) {} + + @override + void readStringifiedValue(StringReader reader) {} } diff --git a/lib/nbt/impl/FloatTag.dart b/lib/nbt/impl/FloatTag.dart index e446d35..717a048 100644 --- a/lib/nbt/impl/FloatTag.dart +++ b/lib/nbt/impl/FloatTag.dart @@ -1,5 +1,5 @@ -import 'package:libac_flutter/nbt/Stream.dart'; -import 'package:libac_flutter/nbt/Tag.dart'; +import '../Stream.dart'; +import '../Tag.dart'; class FloatTag extends Tag { double value = 0.0; @@ -33,4 +33,17 @@ class FloatTag extends Tag { print( "${"".padLeft(indent, '\t')}${Tag.getCanonicalName(getTagType())}: $value"); } + + @override + 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"); + } } diff --git a/lib/nbt/impl/IntArrayTag.dart b/lib/nbt/impl/IntArrayTag.dart index 7a344cf..651ac33 100644 --- a/lib/nbt/impl/IntArrayTag.dart +++ b/lib/nbt/impl/IntArrayTag.dart @@ -1,5 +1,5 @@ -import 'package:libac_flutter/nbt/Stream.dart'; -import 'package:libac_flutter/nbt/Tag.dart'; +import '../Stream.dart'; +import '../Tag.dart'; class IntArrayTag extends Tag { final List value = []; @@ -45,4 +45,23 @@ class IntArrayTag extends Tag { print( "${"".padLeft(indent, '\t')}${Tag.getCanonicalName(getTagType())}: [$array]"); } + + @override + void writeStringifiedValue(StringBuilder builder, int indent, bool isList) { + builder.append( + "${isList ? "".padLeft(indent, '\t') : ""}[I; ${value.join('I, ')}I]"); + } + + @override + void readStringifiedValue(StringReader reader) { + reader.expect("["); + reader.expect("I"); + reader.expect(";"); + while (reader.peek() != "]") { + value.add(int.parse(reader.readNumber())); + + if (reader.peek() == ",") reader.next(); + } + reader.expect("]"); + } } diff --git a/lib/nbt/impl/IntTag.dart b/lib/nbt/impl/IntTag.dart index be32a44..65d16c3 100644 --- a/lib/nbt/impl/IntTag.dart +++ b/lib/nbt/impl/IntTag.dart @@ -1,5 +1,5 @@ -import 'package:libac_flutter/nbt/Stream.dart'; -import 'package:libac_flutter/nbt/Tag.dart'; +import '../Stream.dart'; +import '../Tag.dart'; class IntTag extends Tag { int value = 0; @@ -33,4 +33,21 @@ class IntTag extends Tag { print( "${"".padLeft(indent, '\t')}${Tag.getCanonicalName(getTagType())}: $value"); } + + @override + 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); + + // Since a type indicator is optional for a int, check for a comma + if (reader.peek() == ",") + return; + else + reader.expect("i"); + } } diff --git a/lib/nbt/impl/ListTag.dart b/lib/nbt/impl/ListTag.dart index 8e47a96..60ca16c 100644 --- a/lib/nbt/impl/ListTag.dart +++ b/lib/nbt/impl/ListTag.dart @@ -1,6 +1,5 @@ -import 'package:libac_flutter/nbt/Stream.dart'; -import 'package:libac_flutter/nbt/Tag.dart'; - +import '../Stream.dart'; +import '../Tag.dart'; import 'EndTag.dart'; class ListTag extends Tag { @@ -86,4 +85,36 @@ class ListTag extends Tag { void endPrettyPrint(int indent) { print("${"".padLeft(indent, '\t')}]"); } + + @override + void writeStringifiedValue(StringBuilder builder, int indent, bool isList) { + builder.append("${isList ? "".padLeft(indent - 1, '\t') : ""}[\n"); + Iterator it = value.iterator; + bool firstTag = true; + while (it.moveNext()) { + Tag tag = it.current; + if (firstTag) + firstTag = !firstTag; + else { + builder.append(",\n"); + } + + tag.writeStringifiedValue(builder, indent + 1, true); + } + builder.append("\n${"".padLeft(indent - 1, '\t')}]"); + } + + @override + void readStringifiedValue(StringReader reader) { + reader.expect("["); + while (reader.peek() != "]") { + TagType type = TagType.getStringifiedTagType(reader); + Tag newTag = Tag.makeTagOfType(type); + newTag.readStringifiedValue(reader); + add(newTag); + + if (reader.peek() == ",") reader.next(); + } + reader.expect("]"); + } } diff --git a/lib/nbt/impl/LongArrayTag.dart b/lib/nbt/impl/LongArrayTag.dart index bc1f4ab..af10d69 100644 --- a/lib/nbt/impl/LongArrayTag.dart +++ b/lib/nbt/impl/LongArrayTag.dart @@ -1,5 +1,5 @@ -import 'package:libac_flutter/nbt/Stream.dart'; -import 'package:libac_flutter/nbt/Tag.dart'; +import '../Stream.dart'; +import '../Tag.dart'; class LongArrayTag extends Tag { final List value = []; @@ -49,4 +49,24 @@ class LongArrayTag extends Tag { print( "${"".padLeft(indent, '\t')}${Tag.getCanonicalName(getTagType())}: [$array]"); } + + @override + void writeStringifiedValue(StringBuilder builder, int indent, bool isList) { + builder.append( + "${isList ? "".padLeft(indent, '\t') : ""}[L; ${value.join('L, ')}L]"); + } + + @override + void readStringifiedValue(StringReader reader) { + reader.expect("["); + reader.expect("L"); + reader.expect(";"); + while (reader.peek() != "]") { + value.add(int.parse(reader.readNumber())); + reader.expect("l"); + + if (reader.peek() == ",") reader.next(); + } + reader.expect("]"); + } } diff --git a/lib/nbt/impl/LongTag.dart b/lib/nbt/impl/LongTag.dart index 3307cee..81f3b58 100644 --- a/lib/nbt/impl/LongTag.dart +++ b/lib/nbt/impl/LongTag.dart @@ -1,5 +1,5 @@ -import 'package:libac_flutter/nbt/Stream.dart'; -import 'package:libac_flutter/nbt/Tag.dart'; +import '../Stream.dart'; +import '../Tag.dart'; class LongTag extends Tag { int value = 0; @@ -33,4 +33,17 @@ class LongTag extends Tag { print( "${"".padLeft(indent, '\t')}${Tag.getCanonicalName(getTagType())}: $value"); } + + @override + 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"); + } } diff --git a/lib/nbt/impl/ShortTag.dart b/lib/nbt/impl/ShortTag.dart index bc68fb7..a30cb12 100644 --- a/lib/nbt/impl/ShortTag.dart +++ b/lib/nbt/impl/ShortTag.dart @@ -1,5 +1,5 @@ -import 'package:libac_flutter/nbt/Stream.dart'; -import 'package:libac_flutter/nbt/Tag.dart'; +import '../Stream.dart'; +import '../Tag.dart'; class ShortTag extends Tag { int value = 0; @@ -33,4 +33,17 @@ class ShortTag extends Tag { print( "${"".padLeft(indent, '\t')}${Tag.getCanonicalName(getTagType())}: $value"); } + + @override + 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"); + } } diff --git a/lib/nbt/impl/StringTag.dart b/lib/nbt/impl/StringTag.dart index 1e8f89f..2978e2c 100644 --- a/lib/nbt/impl/StringTag.dart +++ b/lib/nbt/impl/StringTag.dart @@ -1,5 +1,5 @@ -import 'package:libac_flutter/nbt/Stream.dart'; -import 'package:libac_flutter/nbt/Tag.dart'; +import '../Stream.dart'; +import '../Tag.dart'; class StringTag extends Tag { String value = ""; @@ -33,4 +33,15 @@ class StringTag extends Tag { print( "${"".padLeft(indent, '\t')}${Tag.getCanonicalName(getTagType())}: $value"); } + + @override + 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; + } } diff --git a/lib/packets/packets.dart b/lib/packets/packets.dart new file mode 100644 index 0000000..b259924 --- /dev/null +++ b/lib/packets/packets.dart @@ -0,0 +1,451 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import '../nbt/NbtIo.dart'; +import '../nbt/Stream.dart'; +import '../nbt/Tag.dart'; +import '../nbt/impl/CompoundTag.dart'; +import '../nbt/impl/StringTag.dart'; + +class PacketServer { + static ServerSocket? socket; + static bool shouldRestart = true; + static Future start(int port) async { + socket = await ServerSocket.bind(InternetAddress.anyIPv4, port); + print("Server now listening on port ${port}"); + + await for (var sock in socket!) { + S2CResponse response = S2CResponse(); + print( + "New connection from ${sock.remoteAddress.address}:${sock.remotePort}"); + + try { + sock.listen((data) async { + try { + CompoundTag tag = await NbtIo.readFromStream(data); + StringBuilder builder = StringBuilder(); + Tag.writeStringifiedNamedTag(tag, builder, 0); + + print("Request from client: \n${builder}"); + + C2SRequestPacket request = C2SRequestPacket(); + request.decodeTag(tag); + + PacketResponse reply = await request.handleServerPacket(); + + // Server uses NBT to communicate + builder = StringBuilder(); + Tag.writeStringifiedNamedTag(reply.replyDataTag, builder, 0); + + print("Response to client: \n${builder}"); + sock.add(await NbtIo.writeToStream(reply.replyDataTag)); + } catch (E, stack) { + response.contents + .put("error", StringTag.valueOf("Malformed request packet")); + + print( + "Something went wrong. Malformed request? \n\n${E}\n\n${stack}\n\n\n\n"); + } finally { + await sock.flush(); + sock.close(); + } + }, onDone: () { + sock.close(); + }, onError: (E) { + print("ERROR: ${E}"); + sock.close(); + }); + } catch (E) { + sock.close(); + } + } + } +} + +class PacketClient { + Socket? socket; + bool connected = false; + String lastIP = ""; + int port = 25306; + + PacketClient(); + + Future startConnect(String IPAddress, int port) async { + try { + socket = await Socket.connect(IPAddress, port); + connected = true; + lastIP = IPAddress; + this.port = port; + } catch (E, stack) { + connected = false; + socket = null; + } + } + + /// Tries to send a packet to the connected server + /// + /// On success, returns either, the decoded [S2CResponse], or on error a S2CResponse containing an error and a stacktrace as [StringTag] + Future send(IPacket packet, bool shouldReconnect) async { + if (!connected) { + return S2CResponse(); + } + C2SRequestPacket request = C2SRequestPacket(); + request.payload = packet; + request.cap = packet.getChannelID(); + + socket!.add(await NbtIo.writeToStream(request.encodeTag().asCompoundTag())); + CompoundTag ct = CompoundTag(); + Completer onCompletion = Completer(); + socket!.listen((data) async { + CompoundTag result = await NbtIo.readFromStream(data); + + StringBuilder builder = StringBuilder(); + Tag.writeStringifiedNamedTag(result, builder, 0); + + print("Response from server: \n${builder}"); + ct.put("result", result); + + onCompletion.complete(); + }, onError: (E, stack) { + print("ERROR: ${E}\n${stack}"); + if (!onCompletion.isCompleted) onCompletion.complete(); + }, onDone: () { + print("Request completed"); + }); + + await onCompletion.future; + await close(); + + await startConnect(lastIP, port); + S2CResponse reply = S2CResponse(); + try { + reply.decodeTag(ct.get("result")!.asCompoundTag()); + } catch (E, stack) { + reply.contents = CompoundTag(); // This is essentially a null response + reply.contents.put("error", StringTag.valueOf(E.toString())); + reply.contents.put("stacktrace", StringTag.valueOf(stack.toString())); + } + + return reply; + } + + Future close() async { + await socket!.close(); + connected = false; + } +} + +abstract class IPacket with NbtEncodable, JsonEncodable { + String getChannelID(); + + // This function handles the packet + Future handleServerPacket(); + Future handleClientPacket(); + NetworkDirection direction(); +} + +class StopServerPacket extends IPacket { + @override + void decodeJson(String params) {} + + @override + void decodeTag(Tag tag) {} + + @override + NetworkDirection direction() { + return NetworkDirection.ClientToServer; + } + + @override + String encodeJson() { + return json.encode({}); + } + + @override + Tag encodeTag() { + return CompoundTag(); + } + + @override + void fromJson(Map js) {} + + @override + String getChannelID() { + return "StopServer"; + } + + @override + Future handleServerPacket() async { + // We're now on the server. Handle the packet with a response to the client + PacketServer.shouldRestart = false; + + S2CResponse response = S2CResponse(); + + return PacketResponse(replyDataTag: response.encodeTag().asCompoundTag()); + } + + @override + Map toJson() { + return {}; + } + + @override + Future handleClientPacket() { + throw UnimplementedError(); + } +} + +class PacketResponse { + static final nil = PacketResponse(replyDataTag: CompoundTag()); + + PacketResponse({required this.replyDataTag}); + + CompoundTag replyDataTag = CompoundTag(); +} + +class PacketRegistry { + Map _registry = {}; + static PacketRegistry _inst = PacketRegistry._(); + + PacketRegistry._() { + registerDefaults(); + } + + factory PacketRegistry() { + return _inst; + } + + int get count => _registry.length; + + void register(IPacket packet, IPacket Function() packetResolver) { + _registry[packet.getChannelID()] = packetResolver; + } + + IPacket getPacket(String channel) { + if (_registry.containsKey(channel)) { + IPacket Function() callback = _registry[channel]!; + return callback(); + } else + throw Exception("No such channel has been registered"); + } + + void registerDefaults() { + register(S2CResponse(), () { + return S2CResponse(); + }); + register(C2SRequestPacket(), () { + return C2SRequestPacket(); + }); + register(StopServerPacket(), () { + return StopServerPacket(); + }); + register(C2SPing(), () { + return C2SPing(); + }); + } +} + +enum NetworkDirection { ClientToServer, ServerToClient } + +enum PacketOperation { Encode, Decode } + +class S2CResponse implements IPacket { + CompoundTag contents = CompoundTag(); + + @override + NetworkDirection direction() { + return NetworkDirection.ServerToClient; + } + + @override + String getChannelID() { + return "Response"; + } + + @override + Future handleServerPacket() async { + // We can't predict handling for this type, it is a data packet response with no pre-defined structure. + return PacketResponse.nil; + } + + @override + void decodeJson(String encoded) { + fromJson(json.decode(encoded)); + } + + @override + void decodeTag(Tag encoded) { + CompoundTag ct = encoded as CompoundTag; + contents = ct.get("contents")!.asCompoundTag(); + } + + @override + String encodeJson() { + return json.encode(toJson()); + } + + @override + Tag encodeTag() { + CompoundTag tag = CompoundTag(); + tag.put("contents", contents); + + return tag; + } + + @override + void fromJson(Map params) {} + + @override + Map toJson() { + return {}; // Operation is not supported at this time. + } + + @override + Future handleClientPacket() async { + // We haven't got anything to process. This is structured data + } +} + +class C2SRequestPacket implements IPacket { + String cap = ""; // Packet channel + late IPacket payload; + + @override + void decodeJson(String encoded) { + fromJson(json.decode(encoded)); + } + + @override + void decodeTag(Tag encoded) { + CompoundTag tag = encoded.asCompoundTag(); + String cap = tag.get("cap")!.asString(); + payload = PacketRegistry().getPacket(cap); + payload.decodeTag(tag.get("payload")!.asCompoundTag()); + } + + @override + NetworkDirection direction() { + return NetworkDirection.ClientToServer; + } + + @override + String encodeJson() { + return json.encode(toJson()); + } + + @override + Tag encodeTag() { + CompoundTag tag = CompoundTag(); + tag.put("cap", StringTag.valueOf(payload.getChannelID())); + tag.put("payload", payload.encodeTag()); + + return tag; + } + + @override + void fromJson(Map params) { + String cap = params['cap'] as String; + payload = PacketRegistry().getPacket(cap); + payload.fromJson(params['payload']); + } + + @override + String getChannelID() { + return "C2SRequest"; + } + + @override + Future handleServerPacket() async { + // This has no internal handling + return payload.handleServerPacket(); + } + + @override + Map toJson() { + return {"cap": payload.getChannelID(), "payload": payload.toJson()}; + } + + @override + Future handleClientPacket() { + throw UnimplementedError(); + } +} + +class C2SPing implements IPacket { + String clientVersion = ""; + + @override + void decodeJson(String params) { + fromJson(json.decode(params)); + } + + @override + void decodeTag(Tag tag) { + clientVersion = tag.asCompoundTag().get("version")!.asString(); + } + + @override + NetworkDirection direction() { + return NetworkDirection.ClientToServer; + } + + @override + String encodeJson() { + return json.encode(toJson()); + } + + @override + Tag encodeTag() { + CompoundTag tag = CompoundTag(); + tag.put("version", StringTag.valueOf(clientVersion)); + + return tag; + } + + @override + void fromJson(Map js) { + clientVersion = js['version'] as String; + } + + @override + String getChannelID() { + return "Ping"; + } + + @override + Future handleServerPacket() async { + CompoundTag tag = CompoundTag(); + tag.put("pong", StringTag.valueOf(Platform.version)); + + S2CResponse response = S2CResponse(); + response.contents = tag; + + PacketResponse reply = + PacketResponse(replyDataTag: response.encodeTag().asCompoundTag()); + return reply; + } + + @override + Map toJson() { + return {"version": clientVersion}; + } + + @override + Future handleClientPacket() { + throw UnimplementedError(); + } +} + +mixin JsonEncodable { + String encodeJson(); + void decodeJson(String params); + + Map toJson(); + void fromJson(Map js); +} + +mixin NbtEncodable { + Tag encodeTag(); + void decodeTag(Tag tag); +} diff --git a/lib/utils/Converter.dart b/lib/utils/Converter.dart new file mode 100644 index 0000000..4f873c6 --- /dev/null +++ b/lib/utils/Converter.dart @@ -0,0 +1,12 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +class base64Encoder { + static String encode(List value) { + return base64.encode(value); + } + + static Uint8List decode(String value) { + return base64.decode(value); + } +} diff --git a/lib/utils/Hashing.dart b/lib/utils/Hashing.dart new file mode 100644 index 0000000..89f44e5 --- /dev/null +++ b/lib/utils/Hashing.dart @@ -0,0 +1,35 @@ +import 'dart:convert'; + +import 'package:crypto/crypto.dart'; + +class Hashing { + static String md5Hash(String input) { + var hash = md5.convert(utf8.encode(input)).bytes; + String x = ""; + for (int byte in hash) { + x += byte.toRadixString(16).padLeft(2, '0'); + } + + return x; + } + + static String sha1Hash(String input) { + var hash = sha1.convert(utf8.encode(input)).bytes; + String x = ""; + for (int byte in hash) { + x += byte.toRadixString(16).padLeft(2, '0'); + } + + return x; + } + + static String sha256Hash(String input) { + var hash = sha256.convert(utf8.encode(input)).bytes; + String x = ""; + for (int byte in hash) { + x += byte.toRadixString(16).padLeft(2, '0'); + } + + return x; + } +} diff --git a/lib/utils/IOTools.dart b/lib/utils/IOTools.dart new file mode 100644 index 0000000..c704c16 --- /dev/null +++ b/lib/utils/IOTools.dart @@ -0,0 +1,124 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +class PathHelper { + String pth = ""; + PathHelper({required this.pth}); + + static String combine(String path1, String path2) { + return path1 + Platform.pathSeparator + path2; + } + + static PathHelper builder(String startPath) { + return PathHelper(pth: startPath); + } + + bool exists() { + File fi = File(build()); + Directory dir = Directory(build()); + + return fi.existsSync() || dir.existsSync(); + } + + PathHelper clone() { + return PathHelper.builder(build()); + } + + PathHelper resolve(String path2) { + pth += Platform.pathSeparator + path2; + return this; + } + + PathHelper conditionalResolve(bool flag, String path) { + if (flag) pth += Platform.pathSeparator + path; + return this; + } + + bool deleteDirectory({bool recursive = false}) { + Directory dir = new Directory(build()); + try { + dir.deleteSync(recursive: recursive); + + return true; + } catch (E) { + return false; + } + } + + bool deleteFile() { + File file = new File(build()); + try { + file.deleteSync(recursive: true); + + return true; + } catch (E) { + return false; + } + } + + PathHelper removeDir() { + deleteDirectory(recursive: true); + return this; + } + + PathHelper removeFile() { + deleteFile(); + return this; + } + + PathHelper mkdir() { + Directory dir = new Directory(build()); + dir.createSync(recursive: true); + + return this; + } + + String build() { + return pth; + } +} + +Stream> tail(final File file) async* { + final randomAccess = await file.open(mode: FileMode.read); + var pos = await randomAccess.position(); + var len = await randomAccess.length(); + // Increase/decrease buffer size as needed. + var buf = Uint8List(8192); + + Stream _read() async* { + while (pos < len) { + try { + final bytesRead = await randomAccess.readInto(buf); + pos += bytesRead; + + yield buf.sublist(0, bytesRead); + } catch (E) {} + } + } + + // Step 1: read whole file + yield* _read(); + + // Step 2: wait for modify events and read more bytes from file + await for (final event in file.watch(events: FileSystemEvent.modify)) { + if ((event as FileSystemModifyEvent).contentChanged) { + try { + len = await (randomAccess.length()); + yield* _read(); + } catch (E) {} + } + } +} + +void tailAndPrint(File file) { + try { + tail(file) + .transform(utf8.decoder) + .transform(LineSplitter()) + .forEach((line) { + // Do something with the line that has been read, e.g. print it out... + print(line); + }); + } catch (E) {} +} diff --git a/lib/utils/TimeUtils.dart b/lib/utils/TimeUtils.dart new file mode 100644 index 0000000..5b715b2 --- /dev/null +++ b/lib/utils/TimeUtils.dart @@ -0,0 +1,146 @@ +import '../nbt/Stream.dart'; + +class Time { + int hours; + int minutes; + int seconds; + + Time({required this.hours, required this.minutes, required this.seconds}) { + autofix(); + } + + int getTotalSeconds() { + int current = 0; + current += seconds; + + current += (minutes * 60); // 60 seconds in a minute + + current += (hours * 60 * 60); // 60 seconds, 60 minutes in an hour + + return current; + } + + void add(Time time) { + seconds += time.getTotalSeconds(); + autofix(); + } + + void subtract(Time time) { + int sec = getTotalSeconds(); + sec -= time.getTotalSeconds(); + + apply(sec); + } + + void tickDown() { + int sec = getTotalSeconds(); + sec--; + + apply(sec); + } + + void tickUp() { + int sec = getTotalSeconds(); + sec++; + + apply(sec); + } + + void apply(int seconds) { + hours = 0; + minutes = 0; + this.seconds = seconds; + if (this.seconds < 0) this.seconds = 0; + + autofix(); + } + + void autofix() { + int totalSeconds = getTotalSeconds(); + if (totalSeconds < 0) totalSeconds = 0; + + int one_hour = (1 * 60 * 60); + int one_minute = (1 * 60); + + int hours = (totalSeconds / 60 / 60).round(); + totalSeconds -= (hours * one_hour); + + int minutes = (totalSeconds / 60).round(); + totalSeconds -= (minutes * one_minute); + + int seconds = totalSeconds; + + this.hours = hours; + this.minutes = minutes; + this.seconds = seconds; + } + + Time copy() { + return Time(hours: hours, minutes: minutes, seconds: seconds); + } + + factory Time.copy(Time other) { + return Time( + hours: other.hours, minutes: other.minutes, seconds: other.seconds); + } + + factory Time.fromNotation(String notation) { + int hours = 0; + int minutes = 0; + int seconds = 0; + + List current = []; + String val = notation; + if (val.indexOf('h') == -1) { + hours = 0; + } else { + current = val.split('h'); + hours = int.parse(current[0]); + + if (current.length == 2) + val = current[1]; + else + val = ""; + } + + if (val.indexOf('m') == -1) { + minutes = 0; + } else { + current = val.split('m'); + minutes = int.parse(current[0]); + + if (current.length == 2) + val = current[1]; + else + val = ""; + } + + if (val.indexOf('s') == -1) { + seconds = 0; + } else { + current = val.split('s'); + seconds = int.parse(current[0]); + + if (current.length == 2) + val = current[1]; + else + val = ""; + } + + if (val != "") { + seconds += int.parse(val); + } + + return Time(hours: hours, minutes: minutes, seconds: seconds); + } + + @override + String toString() { + StringBuilder builder = StringBuilder(); + if (hours > 0) builder.append("${hours}h"); + if (minutes > 0) builder.append("${minutes}m"); + if (seconds > 0) builder.append("${seconds}s"); + + return "${builder}"; + } +} diff --git a/lib/utils/uuid/NbtUUID.dart b/lib/utils/uuid/NbtUUID.dart new file mode 100644 index 0000000..306b757 --- /dev/null +++ b/lib/utils/uuid/NbtUUID.dart @@ -0,0 +1,42 @@ +import '../../nbt/Stream.dart'; +import 'UUID.dart'; + +class NbtUUID { + final int MSB; + final int LSB; + + const NbtUUID(this.MSB, this.LSB); + + static final NbtUUID ZERO = NbtUUID.fromUUID(UUID.ZERO); + + factory NbtUUID.fromUUID(UUID id) { + ByteLayer layer = ByteLayer(); + layer.writeBytes(id.getBytes()); + layer.resetPosition(); + + int MSB = layer.readLong(); + int LSB = layer.readLong(); + + return NbtUUID(MSB, LSB); + } + + /// Returns the Most Significant Bits (MSB) long value of the UUID. + int getMostSignificantBits() => MSB; + + /// Returns the Least Significant Bits (LSB) long value of the UUID. + int getLeastSignificantBits() => LSB; + + UUID toUUID() { + ByteLayer layer = ByteLayer(); + layer.writeLong(MSB); + layer.writeLong(LSB); + layer.resetPosition(); + + return UUID(layer.readBytes(16)); + } + + @override + String toString() { + return toUUID().toString(); + } +} diff --git a/lib/utils/uuid/UUID.dart b/lib/utils/uuid/UUID.dart index d496e33..9858cb0 100644 --- a/lib/utils/uuid/UUID.dart +++ b/lib/utils/uuid/UUID.dart @@ -2,23 +2,14 @@ import 'dart:convert'; import 'dart:math'; import 'package:crypto/crypto.dart'; -import 'package:libac_flutter/nbt/Stream.dart'; + +import '../../nbt/Stream.dart'; class UUID { - late final int LSB; - late final int MSB; late final List _bytes; - UUID(int msb, int lsb) { - MSB = msb; - LSB = lsb; - - ByteLayer layer = ByteLayer(); - layer.writeLong(MSB); - layer.writeLong(LSB); - layer.resetPosition(); - - _bytes = layer.readBytes(16); + UUID(List bytes) { + _bytes = bytes; } static final UUID ZERO = UUID.generate(0); @@ -27,8 +18,9 @@ class UUID { static bool validate(String uuid) { if (uuid.length == ((16 * 2) + 4)) { return true; // Likely is true. This is just a surface level check - } else + } else { return false; + } } /// Parses the given [uuid] string and returns a UUID object. @@ -42,30 +34,26 @@ class UUID { final msbString = segments.sublist(0, 3).join(''); final lsbString = segments.sublist(3, 5).join(''); - int msb = 0; - int lsb = 0; - int i = 0; ByteLayer layer = ByteLayer(); for (i = 0; i < msbString.length; i += 2) { - String hex = "${msbString.substring(i, i + 2)}"; + String hex = msbString.substring(i, i + 2); int byte = int.parse(hex, radix: 16); layer.writeByte(byte); } for (i = 0; i < lsbString.length; i += 2) { - String hex = "${lsbString.substring(i, i + 2)}"; + String hex = lsbString.substring(i, i + 2); int byte = int.parse(hex, radix: 16); layer.writeByte(byte); } layer.resetPosition(); - msb = layer.readLong(); - lsb = layer.readLong(); - return UUID(msb, lsb); - } else + return UUID(layer.readBytes(16)); + } else { return UUID.ZERO; + } } @override @@ -77,29 +65,33 @@ class UUID { return '${hexBuilder.substring(0, 8)}-${hexBuilder.substring(8, 12)}-${hexBuilder.substring(12, 16)}-${hexBuilder.substring(16, 20)}-${hexBuilder.substring(20, 32)}'; } - /// Returns the Most Significant Bits (MSB) long value of the UUID. - int getMostSignificantBits() => MSB; - - /// Returns the Least Significant Bits (LSB) long value of the UUID. - int getLeastSignificantBits() => LSB; - /// Factory method to generate UUID of the specific version. factory UUID.generate(int version, {List? parameters}) { List params = []; if (parameters == null) { - if (version != 4) { + if (version != 4 && version != 0) { return UUID.generate(4); } - } else - params = parameters!; + } else { + params = parameters; + } switch (version) { case 0: - return UUID(0, 0); + { + ByteLayer layer = ByteLayer(); + layer.writeLong(0); + layer.writeLong(0); + + layer.resetPosition(); + + return UUID(layer.readBytes(16)); + } case 3: { - if (params.length != 2) + if (params.length != 2) { throw Exception( "UUID v3 requires two parameters, [namespace,name]"); + } String namespace = params[0] as String; String name = params[1] as String; @@ -118,45 +110,38 @@ class UUID { layer.unsetSetBit(8, 0xC0, 0x80); layer.resetPosition(); - - var msb = layer.readUnsignedLong(); - var lsb = layer.readUnsignedLong(); - - layer.resetPosition(); - return UUID(msb, lsb); + return UUID(layer.readBytes(16)); } case 4: { ByteLayer layer = ByteLayer(); final random = Random.secure(); - layer.writeLong( - (random.nextInt(0xFFFFFFFF) << 32) | random.nextInt(0xFFFFFFFF)); - layer.writeLong( - (random.nextInt(0xFFFFFFFF) << 32) | random.nextInt(0xFFFFFFFF)); + layer.insertRandomBytes(16); layer.unsetSetBit(6, 0xF0, 0x40); layer.unsetSetBit(8, 0xC0, 0x80); layer.resetPosition(); - return UUID(layer.readUnsignedLong(), layer.readUnsignedLong()); + return UUID(layer.readBytes(16)); } case 5: { ByteLayer layer = ByteLayer(); - if (params.length != 2) + if (params.length != 2) { throw Exception( "UUID v5 requires two parameters, [namespace,name]"); + } String namespace = params[0] as String; String name = params[1] as String; - if (!namespace.isEmpty) { + if (namespace.isNotEmpty) { final namespaceBytes = utf8.encode(namespace); layer.writeBytes(namespaceBytes); } - if (!name.isEmpty) { + if (name.isNotEmpty) { final nameBytes = utf8.encode(name); layer.writeBytes(nameBytes); } @@ -170,10 +155,14 @@ class UUID { layer.resetPosition(); - return UUID(layer.readUnsignedLong(), layer.readUnsignedLong()); + return UUID(layer.readBytes(16)); } default: throw ArgumentError('Unsupported UUID version: $version'); } } + + Iterable getBytes() { + return _bytes.take(16); + } } diff --git a/pubspec.yaml b/pubspec.yaml index 6ebce4d..6bc5c9e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,28 +1,23 @@ -name: libac_flutter +name: libac_dart description: "Aria's Creations code library" -version: 1.0.0 -homepage: +version: 1.0.32 +homepage: "https://zontreck.com" + environment: - sdk: '>=3.3.4 <4.0.0' - flutter: ">=1.17.0" + sdk: ^3.4.0 +# Add regular dependencies here. dependencies: - crypto: ^3.0.3 - flutter: - sdk: flutter +# path: ^1.8.0 dev_dependencies: - flutter_test: - sdk: flutter - flutter_lints: ^3.0.0 + lints: ^3.0.0 + test: ^1.24.0 + -# For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec -# The following section is specific to Flutter packages. -flutter: - uses-material-design: true # To add assets to your package, add an assets section, like this: # assets: diff --git a/test/hash_test.dart b/test/hash_test.dart new file mode 100644 index 0000000..9539fa7 --- /dev/null +++ b/test/hash_test.dart @@ -0,0 +1,9 @@ +import 'package:libac_dart/utils/Hashing.dart'; +import 'package:test/expect.dart'; +import 'package:test/scaffolding.dart'; + +void main() { + test("Test md5", () { + expect(Hashing.md5Hash("Hello World"), "b10a8db164e0754105b7a99be72e3fe5"); + }); +} diff --git a/test/nbt_test.dart b/test/nbt_test.dart index 182b184..4e9c23a 100644 --- a/test/nbt_test.dart +++ b/test/nbt_test.dart @@ -1,11 +1,17 @@ import 'dart:io'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:libac_flutter/nbt/NbtIo.dart'; -import 'package:libac_flutter/nbt/NbtUtils.dart'; -import 'package:libac_flutter/nbt/impl/CompoundTag.dart'; -import 'package:libac_flutter/nbt/impl/StringTag.dart'; -import 'package:libac_flutter/utils/uuid/UUID.dart'; +import 'package:libac_dart/nbt/NbtIo.dart'; +import 'package:libac_dart/nbt/NbtUtils.dart'; +import 'package:libac_dart/nbt/SnbtIo.dart'; +import 'package:libac_dart/nbt/Stream.dart'; +import 'package:libac_dart/nbt/Tag.dart'; +import 'package:libac_dart/nbt/impl/CompoundTag.dart'; +import 'package:libac_dart/nbt/impl/StringTag.dart'; +import 'package:libac_dart/utils/IOTools.dart'; +import 'package:libac_dart/utils/uuid/NbtUUID.dart'; +import 'package:libac_dart/utils/uuid/UUID.dart'; +import 'package:test/expect.dart'; +import 'package:test/scaffolding.dart'; void main() { test('read non-compressed helloworld NBT', () async { @@ -13,7 +19,7 @@ void main() { CompoundTag tag = await NbtIo.read("${Directory.current.path}/test/hello_world.nbt"); expect(tag.getKey(), "hello world"); - expect(tag.contains("name"), true); + expect(tag.containsKey("name"), true); expect(tag.get("name")!.asString(), "Bananrama"); }); @@ -33,7 +39,7 @@ void main() { CompoundTag tag = await NbtIo.read("${Directory.current.path}/build/hello_world.nbt"); expect(tag.getKey(), "hello world"); - expect(tag.contains("name"), true); + expect(tag.containsKey("name"), true); expect(tag.get("name")!.asString(), "Bananrama"); }); @@ -50,9 +56,56 @@ void main() { test("Generate a UUID v4, save to NBT, and read it back again", () async { var id = UUID.generate(4); CompoundTag tag = CompoundTag(); - NbtUtils.writeUUID(tag, "test", id); + NbtUtils.writeUUID(tag, "test", NbtUUID.fromUUID(id)); var newID = NbtUtils.readUUID(tag, "test"); expect(id.toString(), newID.toString()); }); + + test("Read HelloWorld, Output to SNBT", () async { + CompoundTag ct = + await NbtIo.read("${Directory.current.path}/test/hello_world.nbt"); + StringBuilder sb = StringBuilder(); + Tag.writeStringifiedNamedTag(ct, sb, 0); + print(sb.toString()); + }); + + test("Read BigTest, Output to SNBT", () async { + CompoundTag ct = + await NbtIo.read("${Directory.current.path}/test/bigtest.nbt"); + StringBuilder sb = StringBuilder(); + Tag.writeStringifiedNamedTag(ct, sb, 0); + print(sb.toString()); + }); + + test("Write BigTest to SNBT file", () async { + CompoundTag ct = + await NbtIo.read("${Directory.current.path}/test/bigtest.nbt"); + String output = "${Directory.current.path}/build/bigtest.snbt"; + File file = File(output); + SnbtIo.writeToFile(output, ct); + + // Expect that the file exists + PathHelper ph = PathHelper.builder(Directory.current.path) + .resolve("build") + .resolve("bigtest.snbt"); + expect(ph.exists(), 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); + expect(tag.get("doubleTest")!.asDouble(), 0.4931287132182315); + }); + + test("Write NULL UUID to NBT", () async { + CompoundTag tag = CompoundTag(); + NbtUtils.writeUUID(tag, "test", NbtUUID.fromUUID(UUID.ZERO)); + NbtUUID ID = NbtUtils.readUUID(tag, "test"); + + expect(ID.MSB, 0); + expect(ID.LSB, 0); + }); } diff --git a/test/time_test.dart b/test/time_test.dart new file mode 100644 index 0000000..ff52ff4 --- /dev/null +++ b/test/time_test.dart @@ -0,0 +1,24 @@ +import 'package:libac_dart/utils/TimeUtils.dart'; +import 'package:test/expect.dart'; +import 'package:test/scaffolding.dart'; + +void main() { + test("Parse time notation", () { + Time time = Time.fromNotation("4h9s"); + + expect(time.toString(), "4h9s"); + expect(time.hours, 4); + expect(time.minutes, 0); + expect(time.seconds, 9); + }); + + test("Add time", () { + // Depends on first test working! + Time time = Time.fromNotation("4h9s"); + time.add(Time(hours: 0, minutes: 80, seconds: 1)); + + expect(time.hours, 5); + expect(time.minutes, 20); + expect(time.seconds, 10); + }); +} diff --git a/test/uuid_test.dart b/test/uuid_test.dart index 2859a74..086563c 100644 --- a/test/uuid_test.dart +++ b/test/uuid_test.dart @@ -1,6 +1,7 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:libac_flutter/nbt/Stream.dart'; -import 'package:libac_flutter/utils/uuid/UUID.dart'; +import 'package:libac_dart/nbt/Stream.dart'; +import 'package:libac_dart/utils/uuid/UUID.dart'; +import 'package:test/expect.dart'; +import 'package:test/scaffolding.dart'; void main() { test("Store a 64bit value in int", () { @@ -20,15 +21,15 @@ void main() { } for (UUID sID in ID) { - print("ID : ${sID}}"); + print("ID : $sID}"); } }); test("Check UUIDv4 for validity", () { var ID = UUID.generate(4); ByteLayer layer = ByteLayer(); - layer.writeLong(ID.getMostSignificantBits().toInt()); - layer.writeLong(ID.getLeastSignificantBits().toInt()); + //layer.writeLong(ID.getMostSignificantBits().toInt()); + //layer.writeLong(ID.getLeastSignificantBits().toInt()); print( "Checking version bit: ${layer.checkBit(6, 0x40)} - ${layer.getBit(6)}"); @@ -38,8 +39,8 @@ void main() { test("Generate and check a UUIDv3", () { var ID3 = UUID.generate(3, parameters: ["Test", "Test2"]); ByteLayer layer = ByteLayer(); - layer.writeLong(ID3.getMostSignificantBits().toInt()); - layer.writeLong(ID3.getLeastSignificantBits().toInt()); + //layer.writeLong(ID3.getMostSignificantBits().toInt()); + //layer.writeLong(ID3.getLeastSignificantBits().toInt()); print( "Checking version bit: ${layer.checkBit(6, 0x30)} - ${layer.getBit(6)}"); @@ -70,9 +71,16 @@ void main() { var ID3 = UUID.parse(asString); var ID3X = UUID.generate(3, parameters: ["OfflinePlayer:zontreck", ""]); - expect(ID3.MSB, ID3X.MSB); - expect(ID3.LSB, ID3X.LSB); + //expect(ID3.MSB, ID3X.MSB); + //expect(ID3.LSB, ID3X.LSB); expect(ID3.toString(), asString); }); + + test("Null UUID", () { + var expected = "00000000-0000-0000-0000-000000000000"; + var actual = UUID.ZERO.toString(); + + expect(actual, expected); + }); }