""" rbxl_binreader.py — низкоуровневое чтение бинарного потока Roblox. Особенности формата: 1. Little-endian для всех целочисленных и float. 2. "Interleaved transformed" массивы: для N значений длиной L байт каждое, байты идут не AAAA BBBB CCCC, а ABCD ABCD ABCD ABCD (interleave). То есть сначала все first-byte'ы, потом все second-byte'ы, итд. Это делается потому что среди одинакового свойства часто меняется только младший байт, и interleave даёт длинные последовательности нулей и повторяющихся байт → лучше сжимается LZ4. 3. Signed int → zigzag: (n << 1) ^ (n >> 31) — для int32, аналогично для int64. 4. Float → "Roblox float encoding": сдвиг битов чтобы знаковый бит был в конце. Сделано опять же чтобы маленькие близкие к нулю float'ы выглядели похоже и сжимались. 5. String: uint32 length + UTF-8 bytes. Полная спецификация: https://dom.rojo.space/binary """ import struct import io class BinReader: """Тонкая обёртка над BytesIO с методами для Roblox-типов.""" def __init__(self, data: bytes): self.buf = data self.pos = 0 # ─── атомарные ─── def read(self, n: int) -> bytes: out = self.buf[self.pos:self.pos + n] if len(out) < n: raise IOError(f"unexpected EOF: wanted {n}, got {len(out)} at pos {self.pos}") self.pos += n return out def remaining(self) -> int: return len(self.buf) - self.pos def at_end(self) -> bool: return self.pos >= len(self.buf) def skip(self, n: int) -> None: self.pos += n # ─── простые числовые ─── def uint8(self) -> int: return self.read(1)[0] def uint16(self) -> int: return struct.unpack(' int: return struct.unpack(' int: return struct.unpack(' int: return struct.unpack(' int: return struct.unpack(' float: return struct.unpack(' float: return struct.unpack(' bool: return self.read(1)[0] != 0 def string(self) -> str: length = self.uint32() if length == 0: return '' return self.read(length).decode('utf-8', errors='replace') def bytes_with_len(self) -> bytes: length = self.uint32() return self.read(length) # ────────────────────────────────────────────────────────────────────── # Decoders для "interleaved transformed" массивов. # Используются в PROP chunks где для класса с N инстансами идёт одно # свойство, и значение лежит в interleaved виде. # ────────────────────────────────────────────────────────────────────── def deinterleave(data: bytes, count: int, element_size: int) -> bytes: """Снимает interleave. Получает count*element_size байт и возвращает нормальный массив байт где значения идут последовательно. interleaved: b0[0] b1[0] b2[0] ... b0[1] b1[1] b2[1] ... b0[3] b1[3] b2[3] нормальный: b0[0] b0[1] b0[2] b0[3] b1[0] b1[1] b1[2] b1[3] ... """ if len(data) != count * element_size: raise ValueError( f"deinterleave: expected {count}*{element_size}={count*element_size} bytes, got {len(data)}" ) if count == 0: return b'' out = bytearray(count * element_size) for byte_idx in range(element_size): for value_idx in range(count): out[value_idx * element_size + byte_idx] = data[byte_idx * count + value_idx] return bytes(out) def zigzag_decode_int32(n: int) -> int: """ZigZag decode для int32: (n >> 1) ^ -(n & 1). Используется для PROP с типом Int32 (свойство 'Size' MeshPart и т.п.). """ return (n >> 1) ^ -(n & 1) def zigzag_decode_int64(n: int) -> int: return (n >> 1) ^ -(n & 1) def roblox_float_decode(raw: int) -> float: """Roblox float encoding для PROP-массивов. Roblox перекодирует float32 чтобы знаковый бит был в самом конце (это даёт лучшее сжатие при близких к нулю значениях). raw — uint32 little-endian. """ # Развернём биты так чтобы знаковый бит вернулся в позицию 31. # Roblox: { mantissa_high(23 bits), exponent(8 bits), sign(1 bit) } → стандартный float # Просто (raw >> 1) | ((raw & 1) << 31) standard = (raw >> 1) | ((raw & 1) << 31) return struct.unpack(' list: """Читает count int32 в interleaved-zigzag форме.""" raw = reader.read(count * 4) deint = deinterleave(raw, count, 4) out = [] for i in range(count): u = struct.unpack('>I', deint[i*4:i*4+4])[0] # ВНИМАНИЕ: после deinterleave порядок big-endian! out.append(zigzag_decode_int32(u)) return out def read_interleaved_uint32_array(reader: BinReader, count: int) -> list: """uint32 array (для Referent) — interleaved, но без zigzag.""" raw = reader.read(count * 4) deint = deinterleave(raw, count, 4) out = [] for i in range(count): out.append(struct.unpack('>I', deint[i*4:i*4+4])[0]) return out def read_interleaved_int64_array(reader: BinReader, count: int) -> list: raw = reader.read(count * 8) deint = deinterleave(raw, count, 8) out = [] for i in range(count): u = struct.unpack('>Q', deint[i*8:i*8+8])[0] out.append(zigzag_decode_int64(u)) return out def read_interleaved_float_array(reader: BinReader, count: int) -> list: """Float32 array — interleaved с Roblox float encoding.""" raw = reader.read(count * 4) deint = deinterleave(raw, count, 4) out = [] for i in range(count): u = struct.unpack('>I', deint[i*4:i*4+4])[0] out.append(roblox_float_decode(u)) return out def read_referent_array(reader: BinReader, count: int) -> list: """Referent array — interleaved int32 array с накопительной разностью. Каждое следующее значение = предыдущее + delta. Это даёт длинные последовательности (1, 2, 3, 4, ...) которые после deinterleave + zigzag отлично сжимаются в нули. """ deltas = read_interleaved_int32_array(reader, count) out = [] accumulator = 0 for d in deltas: accumulator += d out.append(accumulator) return out