713 lines
19 KiB
Dart
713 lines
19 KiB
Dart
/// Tile 格式序列化/反序列化模块
|
|
///
|
|
/// 实现移动端优化的二进制格式,支持:
|
|
/// - 字符串字典压缩
|
|
/// - ID 索引化编码
|
|
/// - 坐标差值编码
|
|
///
|
|
/// @version 0.1.0
|
|
/// @date 2026-03-07
|
|
|
|
import 'dart:convert';
|
|
import 'dart:typed_data';
|
|
|
|
// ============================================================================
|
|
// 类型定义
|
|
// ============================================================================
|
|
|
|
/// 坐标类型 (单位:纳米)
|
|
typedef Coordinate = int;
|
|
|
|
/// 唯一标识符
|
|
typedef ID = String;
|
|
|
|
// ============================================================================
|
|
// Tile 文件格式头
|
|
// ============================================================================
|
|
|
|
/// Tile 文件魔数
|
|
const TILE_MAGIC = 0x54494C45; // "TILE"
|
|
|
|
/// Tile 格式版本
|
|
const TILE_VERSION = 0x0001;
|
|
|
|
/// Tile 文件头 (16 字节)
|
|
class TileHeader {
|
|
final int magic; // 4 字节
|
|
final int version; // 4 字节
|
|
final int dataSize; // 4 字节 (压缩后数据大小)
|
|
final int flags; // 4 字节 (压缩标志位)
|
|
|
|
TileHeader({
|
|
required this.magic,
|
|
required this.version,
|
|
required this.dataSize,
|
|
this.flags = 0,
|
|
});
|
|
|
|
ByteData toBytes() {
|
|
final buffer = Uint8List(16).buffer;
|
|
final data = ByteData.view(buffer);
|
|
data.setInt32(0, magic, Endian.host);
|
|
data.setInt32(4, version, Endian.host);
|
|
data.setInt32(8, dataSize, Endian.host);
|
|
data.setInt32(12, flags, Endian.host);
|
|
return data;
|
|
}
|
|
|
|
static TileHeader fromBytes(ByteData data) {
|
|
return TileHeader(
|
|
magic: data.getInt32(0, Endian.host),
|
|
version: data.getInt32(4, Endian.host),
|
|
dataSize: data.getInt32(8, Endian.host),
|
|
flags: data.getInt32(12, Endian.host),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// 字符串字典压缩器
|
|
// ============================================================================
|
|
|
|
/// 字符串字典压缩器
|
|
///
|
|
/// 将重复出现的字符串映射到索引,减少存储空间
|
|
class StringDictionary {
|
|
final Map<String, int> _stringToIndex = {};
|
|
final List<String> _indexToString = [];
|
|
|
|
/// 添加字符串并返回索引
|
|
int add(String str) {
|
|
if (_stringToIndex.containsKey(str)) {
|
|
return _stringToIndex[str]!;
|
|
}
|
|
|
|
final index = _indexToString.length;
|
|
_stringToIndex[str] = index;
|
|
_indexToString.add(str);
|
|
return index;
|
|
}
|
|
|
|
/// 根据索引获取字符串
|
|
String get(int index) {
|
|
return _indexToString[index];
|
|
}
|
|
|
|
/// 是否包含字符串
|
|
bool contains(String str) {
|
|
return _stringToIndex.containsKey(str);
|
|
}
|
|
|
|
/// 字典大小
|
|
int get size => _indexToString.length;
|
|
|
|
/// 序列化为字节
|
|
Uint8List toBytes() {
|
|
final stringBytes = <int>[];
|
|
|
|
// 写入字典大小
|
|
stringBytes.addAll(_encodeVarInt(size));
|
|
|
|
// 写入每个字符串
|
|
for (final str in _indexToString) {
|
|
final utf8Bytes = utf8.encode(str);
|
|
stringBytes.addAll(_encodeVarInt(utf8Bytes.length));
|
|
stringBytes.addAll(utf8Bytes);
|
|
}
|
|
|
|
return Uint8List.fromList(stringBytes);
|
|
}
|
|
|
|
/// 从字节反序列化
|
|
static StringDictionary fromBytes(Uint8List bytes) {
|
|
final dict = StringDictionary();
|
|
int offset = 0;
|
|
|
|
// 读取字典大小
|
|
final size = _decodeVarInt(bytes, offset);
|
|
offset += _getVarIntLength(size);
|
|
|
|
// 读取每个字符串
|
|
for (int i = 0; i < size; i++) {
|
|
final strLen = _decodeVarInt(bytes, offset);
|
|
offset += _getVarIntLength(strLen);
|
|
|
|
final strBytes = bytes.sublist(offset, offset + strLen);
|
|
offset += strLen;
|
|
|
|
dict.add(utf8.decode(strBytes));
|
|
}
|
|
|
|
return dict;
|
|
}
|
|
|
|
// Variable-length integer encoding (类似 Protocol Buffers)
|
|
List<int> _encodeVarInt(int value) {
|
|
final result = <int>[];
|
|
while (value > 0x7F) {
|
|
result.add((value & 0x7F) | 0x80);
|
|
value >>= 7;
|
|
}
|
|
result.add(value & 0x7F);
|
|
return result;
|
|
}
|
|
|
|
static int _decodeVarInt(Uint8List bytes, int offset) {
|
|
int result = 0;
|
|
int shift = 0;
|
|
while (true) {
|
|
final byte = bytes[offset + (shift >> 3)];
|
|
result |= (byte & 0x7F) << shift;
|
|
if ((byte & 0x80) == 0) break;
|
|
shift += 7;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static int _getVarIntLength(int value) {
|
|
if (value == 0) return 1;
|
|
int length = 0;
|
|
while (value > 0) {
|
|
length++;
|
|
value >>= 7;
|
|
}
|
|
return length;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// ID 索引化编码器
|
|
// ============================================================================
|
|
|
|
/// ID 索引化编码器
|
|
///
|
|
/// 将 ID 映射到紧凑的整数索引,减少重复 ID 的存储
|
|
class IdIndexer {
|
|
final Map<ID, int> _idToIndex = {};
|
|
final List<ID> _indexToId = [];
|
|
|
|
/// 添加 ID 并返回索引
|
|
int add(ID id) {
|
|
if (_idToIndex.containsKey(id)) {
|
|
return _idToIndex[id]!;
|
|
}
|
|
|
|
final index = _indexToId.length;
|
|
_idToIndex[id] = index;
|
|
_indexToId.add(id);
|
|
return index;
|
|
}
|
|
|
|
/// 根据索引获取 ID
|
|
ID get(int index) {
|
|
return _indexToId[index];
|
|
}
|
|
|
|
/// 是否包含 ID
|
|
bool contains(ID id) {
|
|
return _idToIndex.containsKey(id);
|
|
}
|
|
|
|
/// 索引器大小
|
|
int get size => _indexToId.length;
|
|
|
|
/// 获取 ID 的索引 (如果不存在返回 -1)
|
|
int getIndex(ID id) {
|
|
return _idToIndex[id] ?? -1;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// 坐标差值编码器
|
|
// ============================================================================
|
|
|
|
/// 坐标差值编码器
|
|
///
|
|
/// 对坐标序列使用差值编码,减少存储空间
|
|
/// 特别适用于走线路径、多边形顶点等连续坐标
|
|
class CoordinateDeltaEncoder {
|
|
/// 编码坐标列表 (使用差值)
|
|
///
|
|
/// 第一个坐标使用绝对值,后续坐标使用与前一个的差值
|
|
static Uint8List encode(List<(int x, int y)> coordinates) {
|
|
if (coordinates.isEmpty) {
|
|
return Uint8List(0);
|
|
}
|
|
|
|
final buffer = BytesBuilder();
|
|
|
|
// 写入坐标数量
|
|
buffer.add(_encodeVarInt(coordinates.length));
|
|
|
|
// 第一个坐标使用绝对值
|
|
var prevX = coordinates.first.$1;
|
|
var prevY = coordinates.first.$2;
|
|
|
|
buffer.add(_encodeSignedVarInt(prevX));
|
|
buffer.add(_encodeSignedVarInt(prevY));
|
|
|
|
// 后续坐标使用差值
|
|
for (int i = 1; i < coordinates.length; i++) {
|
|
final x = coordinates[i].$1;
|
|
final y = coordinates[i].$2;
|
|
|
|
final deltaX = x - prevX;
|
|
final deltaY = y - prevY;
|
|
|
|
buffer.add(_encodeSignedVarInt(deltaX));
|
|
buffer.add(_encodeSignedVarInt(deltaY));
|
|
|
|
prevX = x;
|
|
prevY = y;
|
|
}
|
|
|
|
return buffer.toBytes();
|
|
}
|
|
|
|
/// 解码坐标列表
|
|
static List<(int x, int y)> decode(Uint8List bytes) {
|
|
if (bytes.isEmpty) {
|
|
return [];
|
|
}
|
|
|
|
final coordinates = <(int x, int y)>[];
|
|
int offset = 0;
|
|
|
|
// 读取坐标数量
|
|
final count = _decodeVarInt(bytes, offset);
|
|
offset += _getVarIntLength(count);
|
|
|
|
if (count == 0) {
|
|
return [];
|
|
}
|
|
|
|
// 读取第一个坐标 (绝对值)
|
|
var x = _decodeSignedVarInt(bytes, offset);
|
|
offset += _getSignedVarIntLength(x);
|
|
var y = _decodeSignedVarInt(bytes, offset);
|
|
offset += _getSignedVarIntLength(y);
|
|
|
|
coordinates.add((x, y));
|
|
|
|
// 读取后续坐标 (差值)
|
|
for (int i = 1; i < count; i++) {
|
|
final deltaX = _decodeSignedVarInt(bytes, offset);
|
|
offset += _getSignedVarIntLength(deltaX);
|
|
final deltaY = _decodeSignedVarInt(bytes, offset);
|
|
offset += _getSignedVarIntLength(deltaY);
|
|
|
|
x += deltaX;
|
|
y += deltaY;
|
|
|
|
coordinates.add((x, y));
|
|
}
|
|
|
|
return coordinates;
|
|
}
|
|
|
|
// Variable-length integer encoding for signed integers
|
|
static List<int> _encodeSignedVarInt(int value) {
|
|
// Zigzag encoding: (value << 1) ^ (value >> 63)
|
|
final zigzag = (value << 1) ^ (value >> 63);
|
|
return _encodeVarInt(zigzag);
|
|
}
|
|
|
|
static int _decodeSignedVarInt(Uint8List bytes, int offset) {
|
|
final zigzag = _decodeVarInt(bytes, offset);
|
|
// Reverse zigzag: (zigzag >> 1) ^ -(zigzag & 1)
|
|
return (zigzag >> 1) ^ -(zigzag & 1);
|
|
}
|
|
|
|
static List<int> _encodeVarInt(int value) {
|
|
final result = <int>[];
|
|
while (value > 0x7F) {
|
|
result.add((value & 0x7F) | 0x80);
|
|
value >>= 7;
|
|
}
|
|
result.add(value & 0x7F);
|
|
return result;
|
|
}
|
|
|
|
static int _decodeVarInt(Uint8List bytes, int offset) {
|
|
int result = 0;
|
|
int shift = 0;
|
|
while (true) {
|
|
final byte = bytes[offset + (shift >> 3)];
|
|
result |= (byte & 0x7F) << shift;
|
|
if ((byte & 0x80) == 0) break;
|
|
shift += 7;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static int _getVarIntLength(int value) {
|
|
if (value == 0) return 1;
|
|
int length = 0;
|
|
while (value > 0) {
|
|
length++;
|
|
value >>= 7;
|
|
}
|
|
return length;
|
|
}
|
|
|
|
static int _getSignedVarIntLength(int value) {
|
|
final zigzag = (value << 1) ^ (value >> 63);
|
|
return _getVarIntLength(zigzag);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Tile 序列化器
|
|
// ============================================================================
|
|
|
|
/// Tile 格式序列化器
|
|
///
|
|
/// 将 JSON 格式的设计数据转换为紧凑的 Tile 二进制格式
|
|
class TileSerializer {
|
|
final StringDictionary _stringDict = StringDictionary();
|
|
final IdIndexer _idIndexer = IdIndexer();
|
|
|
|
/// 序列化设计数据为 Tile 格式
|
|
Uint8List serialize(Map<String, dynamic> jsonData) {
|
|
// 1. 构建字符串字典和 ID 索引
|
|
_buildIndexes(jsonData);
|
|
|
|
// 2. 序列化数据体
|
|
final dataBytes = _serializeData(jsonData);
|
|
|
|
// 3. 构建文件头
|
|
final header = TileHeader(
|
|
magic: TILE_MAGIC,
|
|
version: TILE_VERSION,
|
|
dataSize: dataBytes.length,
|
|
flags: 0x01, // 标志位:使用字符串字典
|
|
);
|
|
|
|
// 4. 组合完整文件
|
|
final buffer = BytesBuilder();
|
|
buffer.add(header.toBytes().buffer.asUint8List());
|
|
buffer.add(_stringDict.toBytes());
|
|
buffer.add(_serializeIdIndex());
|
|
buffer.add(dataBytes);
|
|
|
|
return buffer.toBytes();
|
|
}
|
|
|
|
/// 构建索引
|
|
void _buildIndexes(dynamic data) {
|
|
if (data is Map) {
|
|
for (final entry in data.entries) {
|
|
if (entry.key is String) {
|
|
_stringDict.add(entry.key as String);
|
|
}
|
|
if (entry.key == 'id' && entry.value is String) {
|
|
_idIndexer.add(entry.value as String);
|
|
}
|
|
if (entry.key == 'name' && entry.value is String) {
|
|
_stringDict.add(entry.value as String);
|
|
}
|
|
if (entry.key == 'type' && entry.value is String) {
|
|
_stringDict.add(entry.value as String);
|
|
}
|
|
_buildIndexes(entry.value);
|
|
}
|
|
} else if (data is List) {
|
|
for (final item in data) {
|
|
_buildIndexes(item);
|
|
}
|
|
} else if (data is String) {
|
|
_stringDict.add(data);
|
|
}
|
|
}
|
|
|
|
/// 序列化 ID 索引表
|
|
Uint8List _serializeIdIndex() {
|
|
final buffer = BytesBuilder();
|
|
|
|
// 写入 ID 数量
|
|
buffer.add(_encodeVarInt(_idIndexer.size));
|
|
|
|
// 写入每个 ID
|
|
for (int i = 0; i < _idIndexer.size; i++) {
|
|
final id = _idIndexer.get(i);
|
|
final idBytes = utf8.encode(id);
|
|
buffer.add(_encodeVarInt(idBytes.length));
|
|
buffer.add(idBytes);
|
|
}
|
|
|
|
return buffer.toBytes();
|
|
}
|
|
|
|
/// 序列化数据体
|
|
Uint8List _serializeData(Map<String, dynamic> data) {
|
|
final buffer = BytesBuilder();
|
|
_serializeValue(data, buffer);
|
|
return buffer.toBytes();
|
|
}
|
|
|
|
void _serializeValue(dynamic value, BytesBuilder buffer) {
|
|
if (value == null) {
|
|
buffer.add([0x00]); // null 标记
|
|
} else if (value is bool) {
|
|
buffer.add([value ? 0x01 : 0x02]);
|
|
} else if (value is int) {
|
|
buffer.add([0x03]);
|
|
buffer.add(_encodeSignedVarInt(value));
|
|
} else if (value is double) {
|
|
buffer.add([0x04]);
|
|
final bytes = Uint8List(8);
|
|
ByteData.view(bytes.buffer).setFloat64(0, value, Endian.host);
|
|
buffer.add(bytes);
|
|
} else if (value is String) {
|
|
buffer.add([0x05]);
|
|
final index = _stringDict.add(value);
|
|
buffer.add(_encodeVarInt(index));
|
|
} else if (value is List) {
|
|
buffer.add([0x06]);
|
|
buffer.add(_encodeVarInt(value.length));
|
|
for (final item in value) {
|
|
_serializeValue(item, buffer);
|
|
}
|
|
} else if (value is Map) {
|
|
buffer.add([0x07]);
|
|
buffer.add(_encodeVarInt(value.length));
|
|
for (final entry in value.entries) {
|
|
final keyIndex = _stringDict.add(entry.key as String);
|
|
buffer.add(_encodeVarInt(keyIndex));
|
|
_serializeValue(entry.value, buffer);
|
|
}
|
|
}
|
|
}
|
|
|
|
List<int> _encodeVarInt(int value) {
|
|
final result = <int>[];
|
|
while (value > 0x7F) {
|
|
result.add((value & 0x7F) | 0x80);
|
|
value >>= 7;
|
|
}
|
|
result.add(value & 0x7F);
|
|
return result;
|
|
}
|
|
|
|
List<int> _encodeSignedVarInt(int value) {
|
|
final zigzag = (value << 1) ^ (value >> 63);
|
|
return _encodeVarInt(zigzag);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Tile 反序列化器
|
|
// ============================================================================
|
|
|
|
/// Tile 格式反序列化器
|
|
class TileDeserializer {
|
|
late StringDictionary _stringDict;
|
|
late IdIndexer _idIndexer;
|
|
|
|
/// 反序列化 Tile 格式为 JSON
|
|
Map<String, dynamic> deserialize(Uint8List bytes) {
|
|
if (bytes.length < 16) {
|
|
throw FormatException('Tile file too small');
|
|
}
|
|
|
|
// 1. 读取文件头
|
|
final header = TileHeader.fromBytes(ByteData.sublistView(bytes, 0, 16));
|
|
|
|
if (header.magic != TILE_MAGIC) {
|
|
throw FormatException('Invalid Tile file magic');
|
|
}
|
|
|
|
if (header.version != TILE_VERSION) {
|
|
throw FormatException('Unsupported Tile version: ${header.version}');
|
|
}
|
|
|
|
// 2. 读取字符串字典
|
|
int offset = 16;
|
|
_stringDict = StringDictionary.fromBytes(
|
|
bytes.sublist(offset)
|
|
);
|
|
offset += _estimateDictionarySize(bytes, offset);
|
|
|
|
// 3. 读取 ID 索引表
|
|
_idIndexer = _deserializeIdIndex(bytes, offset);
|
|
offset += _estimateIdIndexSize(bytes, offset);
|
|
|
|
// 4. 读取数据体
|
|
final data = _deserializeData(bytes, offset);
|
|
|
|
return data as Map<String, dynamic>;
|
|
}
|
|
|
|
int _estimateDictionarySize(Uint8List bytes, int offset) {
|
|
// 简单估计:读取第一个 varint 作为大小,然后估算
|
|
final size = _decodeVarInt(bytes, offset);
|
|
int pos = offset + _getVarIntLength(size);
|
|
|
|
for (int i = 0; i < size; i++) {
|
|
final strLen = _decodeVarInt(bytes, pos);
|
|
pos += _getVarIntLength(strLen) + strLen;
|
|
}
|
|
|
|
return pos - offset;
|
|
}
|
|
|
|
int _estimateIdIndexSize(Uint8List bytes, int offset) {
|
|
final count = _decodeVarInt(bytes, offset);
|
|
int pos = offset + _getVarIntLength(count);
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
final idLen = _decodeVarInt(bytes, pos);
|
|
pos += _getVarIntLength(idLen) + idLen;
|
|
}
|
|
|
|
return pos - offset;
|
|
}
|
|
|
|
IdIndexer _deserializeIdIndex(Uint8List bytes, int offset) {
|
|
final indexer = IdIndexer();
|
|
|
|
final count = _decodeVarInt(bytes, offset);
|
|
offset += _getVarIntLength(count);
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
final idLen = _decodeVarInt(bytes, offset);
|
|
offset += _getVarIntLength(idLen);
|
|
|
|
final idBytes = bytes.sublist(offset, offset + idLen);
|
|
offset += idLen;
|
|
|
|
indexer.add(utf8.decode(idBytes));
|
|
}
|
|
|
|
return indexer;
|
|
}
|
|
|
|
dynamic _deserializeData(Uint8List bytes, int offset) {
|
|
final type = bytes[offset];
|
|
offset++;
|
|
|
|
switch (type) {
|
|
case 0x00: // null
|
|
return null;
|
|
case 0x01: // true
|
|
return true;
|
|
case 0x02: // false
|
|
return false;
|
|
case 0x03: // int
|
|
return _decodeSignedVarInt(bytes, offset);
|
|
case 0x04: // double
|
|
return ByteData.view(bytes.buffer, offset, 8).getFloat64(0, Endian.host);
|
|
case 0x05: // string
|
|
final index = _decodeVarInt(bytes, offset);
|
|
return _stringDict.get(index);
|
|
case 0x06: // list
|
|
return _deserializeList(bytes, offset);
|
|
case 0x07: // map
|
|
return _deserializeMap(bytes, offset);
|
|
default:
|
|
throw FormatException('Unknown type marker: $type');
|
|
}
|
|
}
|
|
|
|
List<dynamic> _deserializeList(Uint8List bytes, int offset) {
|
|
final count = _decodeVarInt(bytes, offset);
|
|
offset += _getVarIntLength(count);
|
|
|
|
final list = <dynamic>[];
|
|
for (int i = 0; i < count; i++) {
|
|
final value = _deserializeData(bytes, offset);
|
|
list.add(value);
|
|
offset += _estimateValueSize(bytes, offset);
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
Map<String, dynamic> _deserializeMap(Uint8List bytes, int offset) {
|
|
final count = _decodeVarInt(bytes, offset);
|
|
offset += _getVarIntLength(count);
|
|
|
|
final map = <String, dynamic>{};
|
|
for (int i = 0; i < count; i++) {
|
|
final keyIndex = _decodeVarInt(bytes, offset);
|
|
offset += _getVarIntLength(keyIndex);
|
|
final key = _stringDict.get(keyIndex);
|
|
|
|
final value = _deserializeData(bytes, offset);
|
|
map[key] = value;
|
|
offset += _estimateValueSize(bytes, offset);
|
|
}
|
|
|
|
return map;
|
|
}
|
|
|
|
int _estimateValueSize(Uint8List bytes, int offset) {
|
|
final type = bytes[offset];
|
|
offset++;
|
|
|
|
switch (type) {
|
|
case 0x00: // null
|
|
case 0x01: // true
|
|
case 0x02: // false
|
|
return 1;
|
|
case 0x03: // int
|
|
int i = offset;
|
|
while ((bytes[i] & 0x80) != 0) i++;
|
|
return i - offset + 1;
|
|
case 0x04: // double
|
|
return 8;
|
|
case 0x05: // string
|
|
return _getVarIntLength(_decodeVarInt(bytes, offset));
|
|
case 0x06: // list
|
|
case 0x07: // map
|
|
// 递归估计 (简化处理)
|
|
return 10;
|
|
default:
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
int _decodeVarInt(Uint8List bytes, int offset) {
|
|
int result = 0;
|
|
int shift = 0;
|
|
while (true) {
|
|
final byte = bytes[offset + (shift >> 3)];
|
|
result |= (byte & 0x7F) << shift;
|
|
if ((byte & 0x80) == 0) break;
|
|
shift += 7;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
int _decodeSignedVarInt(Uint8List bytes, int offset) {
|
|
final zigzag = _decodeVarInt(bytes, offset);
|
|
return (zigzag >> 1) ^ -(zigzag & 1);
|
|
}
|
|
|
|
int _getVarIntLength(int value) {
|
|
if (value == 0) return 1;
|
|
int length = 0;
|
|
while (value > 0) {
|
|
length++;
|
|
value >>= 7;
|
|
}
|
|
return length;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// 工具函数
|
|
// ============================================================================
|
|
|
|
/// 将设计对象转换为 Tile 格式
|
|
Uint8List designToTile(Map<String, dynamic> design) {
|
|
final serializer = TileSerializer();
|
|
return serializer.serialize(design);
|
|
}
|
|
|
|
/// 将 Tile 格式转换为设计对象
|
|
Map<String, dynamic> tileToDesign(Uint8List tileBytes) {
|
|
final deserializer = TileDeserializer();
|
|
return deserializer.deserialize(tileBytes);
|
|
}
|