689 lines
21 KiB
GDScript
689 lines
21 KiB
GDScript
class_name ChunkDataDecoder
|
||
extends RefCounted
|
||
## Decodes chunk binary payloads (and JSON) into logical chunk data.
|
||
##
|
||
## Implements the binary format spec from docs/architecture/world.md §3.6.
|
||
## Both the Godot client and Python backend use this class to ensure
|
||
## identical encoding/decoding behavior.
|
||
##
|
||
## ## Binary Format Overview
|
||
##
|
||
## ```
|
||
## Header (48 bytes):
|
||
## [0:4] magic "AGPC"
|
||
## [4:6] version (uint16 LE)
|
||
## [6:8] header_len (uint16 LE)
|
||
## [8:12] flags (uint32 LE)
|
||
## [12:16] payload_len (uint32 LE)
|
||
## [16:48] checksum (SHA-256 hex, 32 bytes)
|
||
##
|
||
## Payload Body (sections):
|
||
## [section_id:1][section_len:4][data:section_len]
|
||
## ```
|
||
|
||
|
||
## Content flags parsed from a chunk.
|
||
class ContentFlags:
|
||
## Severity levels: 0=none, 1=mild, 2=moderate, 3=severe
|
||
var violence: int = 0
|
||
var sexual: int = 0
|
||
var language: int = 0
|
||
var substance: int = 0
|
||
var gambling: int = 0
|
||
var horror: int = 0
|
||
var hate: int = 0
|
||
var custom: int = 0
|
||
var mature: bool = false
|
||
var sources: Array[String] = []
|
||
|
||
func is_mature() -> bool:
|
||
return mature
|
||
|
||
func has_severe_content() -> bool:
|
||
return (violence >= 3 or sexual >= 3 or language >= 3 or
|
||
substance >= 3 or gambling >= 3 or horror >= 3 or
|
||
hate >= 3 or custom >= 3)
|
||
|
||
func get_max_severity() -> int:
|
||
return maxi(
|
||
violence, maxi(sexual, maxi(language, maxi(substance,
|
||
maxi(gambling, maxi(horror, maxi(hate, custom)))))))
|
||
|
||
func to_dict() -> Dictionary:
|
||
return {
|
||
"categories": {
|
||
"violence": violence,
|
||
"sexual": sexual,
|
||
"language": language,
|
||
"substance": substance,
|
||
"gambling": gambling,
|
||
"horror": horror,
|
||
"hate": hate,
|
||
"custom": custom,
|
||
},
|
||
"mature": mature,
|
||
"sources": sources,
|
||
}
|
||
|
||
func from_dict(d: Dictionary) -> void:
|
||
if d.has("categories"):
|
||
violence = d["categories"].get("violence", 0)
|
||
sexual = d["categories"].get("sexual", 0)
|
||
language = d["categories"].get("language", 0)
|
||
substance = d["categories"].get("substance", 0)
|
||
gambling = d["categories"].get("gambling", 0)
|
||
horror = d["categories"].get("horror", 0)
|
||
hate = d["categories"].get("hate", 0)
|
||
custom = d["categories"].get("custom", 0)
|
||
mature = d.get("mature", false)
|
||
sources = d.get("sources", [])
|
||
|
||
|
||
## A tile layer definition (palette + dense tile indices).
|
||
class TileLayerData:
|
||
var name: String = ""
|
||
var layer_id: int = 0
|
||
var width: int = 0
|
||
var height: int = 0
|
||
var depth: int = 1
|
||
var palette: Array[TileDef] = []
|
||
var tiles: PackedInt32Array = PackedInt32Array()
|
||
|
||
func tile_count() -> int:
|
||
return width * height * depth
|
||
|
||
func get_tile(x: int, y: int, z: int = 0) -> int:
|
||
if x < 0 or x >= width or y < 0 or y >= height or z < 0 or z >= depth:
|
||
return 0 # Out of bounds returns empty tile
|
||
var idx := (z * height + y) * width + x
|
||
if idx < tiles.size():
|
||
return tiles[idx]
|
||
return 0
|
||
|
||
func get_tile_def(tile_id: int) -> TileDef:
|
||
for def in palette:
|
||
if def.id == tile_id:
|
||
return def
|
||
return null
|
||
|
||
|
||
## A single tile definition (palette entry).
|
||
class TileDef:
|
||
var id: int = 0
|
||
var asset_id: String = ""
|
||
var solid: bool = false
|
||
var flags: int = 0
|
||
var variants: int = 1
|
||
|
||
func is_solid() -> bool:
|
||
return solid
|
||
|
||
func is_walkable() -> bool:
|
||
return (flags & 0x01) != 0
|
||
|
||
func is_climbable() -> bool:
|
||
return (flags & 0x02) != 0
|
||
|
||
func is_liquid() -> bool:
|
||
return (flags & 0x04) != 0
|
||
|
||
|
||
## A world object parsed from chunk data.
|
||
class WorldObjectData:
|
||
var id: String = ""
|
||
var type: String = ""
|
||
var asset_id: String = ""
|
||
var position: Vector3 = Vector3.ZERO
|
||
var rotation: Vector4 = Vector4(0, 0, 0, 1)
|
||
var scale: Vector3 = Vector3.ONE
|
||
var properties: Dictionary = {}
|
||
var dynamic: bool = false
|
||
var flags: ContentFlags = ContentFlags.new()
|
||
|
||
|
||
## The complete decoded chunk payload.
|
||
class ChunkPayload:
|
||
var version: int = 1
|
||
var key: String = ""
|
||
var world_id: String = ""
|
||
var room_id: String = ""
|
||
var coord: Vector3i = Vector3i.ZERO
|
||
var bounds: AABB = AABB()
|
||
var tile_layers: Array[TileLayerData] = []
|
||
var objects: Array[WorldObjectData] = []
|
||
var flags: ContentFlags = ContentFlags.new()
|
||
var created_at: int = 0 # Unix timestamp
|
||
var updated_at: int = 0 # Unix timestamp
|
||
var revision: int = 0
|
||
var author: String = ""
|
||
var checksum: String = ""
|
||
|
||
|
||
# ─── Binary Decoding ────────────────────────────────────────────────────────
|
||
|
||
static const MAGIC: PackedByteArray = PackedByteArray([0x41, 0x47, 0x50, 0x43]) # "AGPC"
|
||
static const HEADER_SIZE: int = 48
|
||
static const SECTION_META: int = 0x01
|
||
static const SECTION_TILE: int = 0x02
|
||
static const SECTION_OBJECT: int = 0x03
|
||
static const SECTION_FLAGS: int = 0x04
|
||
static const SECTION_REVISION_META: int = 0x05
|
||
|
||
|
||
## Decode a binary chunk payload into a ChunkPayload.
|
||
## Returns null if the data is invalid or version is unsupported.
|
||
static func decode_binary(data: PackedByteArray) -> ChunkPayload:
|
||
if data.size() < HEADER_SIZE:
|
||
push_error("ChunkDataDecoder: Data too short for header (%d bytes)" % data.size())
|
||
return null
|
||
|
||
# Verify magic
|
||
var magic := PackedByteArray(data.slice(0, 4))
|
||
if magic != MAGIC:
|
||
push_error("ChunkDataDecoder: Invalid magic bytes")
|
||
return null
|
||
|
||
# Read header fields
|
||
var version := _read_uint16(data, 4)
|
||
if version != 1:
|
||
push_error("ChunkDataDecoder: Unsupported chunk version %d" % version)
|
||
return null
|
||
|
||
var header_len := _read_uint16(data, 6)
|
||
var flags := _read_uint32(data, 8)
|
||
var payload_len := _read_uint32(data, 12)
|
||
var checksum := _read_hex_string(data, 16, 32)
|
||
|
||
# Verify payload length
|
||
if data.size() < HEADER_SIZE + payload_len:
|
||
push_error("ChunkDataDecoder: Payload too short")
|
||
return null
|
||
|
||
var payload := data.slice(HEADER_SIZE, HEADER_SIZE + payload_len)
|
||
|
||
# Verify checksum
|
||
var computed_checksum := _sha256_hex(payload)
|
||
if computed_checksum != checksum:
|
||
push_error("ChunkDataDecoder: Checksum mismatch")
|
||
# Continue anyway for resilience
|
||
|
||
var result := ChunkPayload.new()
|
||
result.version = version
|
||
result.checksum = checksum
|
||
|
||
# Parse sections
|
||
var offset := 0
|
||
while offset < payload.size():
|
||
if offset + 5 > payload.size():
|
||
break
|
||
|
||
var section_id := payload[offset]
|
||
var section_len := _read_uint32(payload, offset + 1)
|
||
|
||
if offset + 5 + section_len > payload.size():
|
||
break
|
||
|
||
var section_data := payload.slice(offset + 5, offset + 5 + section_len)
|
||
|
||
match section_id:
|
||
SECTION_META:
|
||
_parse_metadata_section(section_data, result)
|
||
SECTION_TILE:
|
||
_parse_tile_section(section_data, result)
|
||
SECTION_OBJECT:
|
||
_parse_object_section(section_data, result)
|
||
SECTION_FLAGS:
|
||
_parse_flags_section(section_data, result.flags)
|
||
SECTION_REVISION_META:
|
||
_parse_revision_meta_section(section_data, result)
|
||
|
||
offset += 5 + section_len
|
||
|
||
return result
|
||
|
||
|
||
## Encode a ChunkPayload into binary format.
|
||
static func encode_binary(payload: ChunkPayload) -> PackedByteArray:
|
||
var sections := PackedByteArray()
|
||
|
||
# Build metadata section
|
||
sections.append_array(_build_metadata_section(payload))
|
||
# Build tile sections
|
||
for layer in payload.tile_layers:
|
||
sections.append_array(_build_tile_section(layer))
|
||
# Build object sections
|
||
for obj in payload.objects:
|
||
sections.append_array(_build_object_section(obj))
|
||
# Build flags section
|
||
sections.append_array(_build_flags_section(payload.flags))
|
||
# Build revision meta section
|
||
sections.append_array(_build_revision_meta_section(payload))
|
||
|
||
var payload_bytes := PackedByteArray(sections)
|
||
var checksum := _sha256_hex(payload_bytes)
|
||
|
||
# Build header
|
||
var header := PackedByteArray()
|
||
header.append_array(MAGIC)
|
||
header.append_array(_write_uint16(1)) # version
|
||
header.append_array(_write_uint16(0)) # header_len (after this field)
|
||
header.append_array(_write_uint32(0)) # flags
|
||
header.append_array(_write_uint32(payload_bytes.size()))
|
||
header.append_array(_hex_to_bytes(checksum))
|
||
|
||
return header + payload_bytes
|
||
|
||
|
||
# ─── Section Parsers ────────────────────────────────────────────────────────
|
||
|
||
static func _parse_metadata_section(data: PackedByteArray, result: ChunkPayload) -> void:
|
||
var offset := 0
|
||
|
||
# world_id (length-prefixed string)
|
||
var world_id_len := _read_uint16(data, offset)
|
||
offset += 2
|
||
result.world_id = data.slice(offset, offset + world_id_len).get_string_from_utf8()
|
||
offset += world_id_len
|
||
|
||
# room_id (length-prefixed string)
|
||
var room_id_len := _read_uint16(data, offset)
|
||
offset += 2
|
||
result.room_id = data.slice(offset, offset + room_id_len).get_string_from_utf8()
|
||
offset += room_id_len
|
||
|
||
# coord (3 × int32)
|
||
result.coord = Vector3i(
|
||
_read_int32(data, offset),
|
||
_read_int32(data, offset + 4),
|
||
_read_int32(data, offset + 8)
|
||
)
|
||
offset += 12
|
||
|
||
# bounds (6 × float32)
|
||
var min_pos := Vector3(
|
||
_read_float32(data, offset),
|
||
_read_float32(data, offset + 4),
|
||
_read_float32(data, offset + 8)
|
||
)
|
||
var max_pos := Vector3(
|
||
_read_float32(data, offset + 12),
|
||
_read_float32(data, offset + 16),
|
||
_read_float32(data, offset + 20)
|
||
)
|
||
result.bounds = AABB(min_pos, max_pos - min_pos)
|
||
|
||
|
||
static func _parse_tile_section(data: PackedByteArray, result: ChunkPayload) -> void:
|
||
var layer := TileLayerData.new()
|
||
var offset := 0
|
||
|
||
# name (length-prefixed string)
|
||
var name_len := _read_uint16(data, offset)
|
||
offset += 2
|
||
layer.name = data.slice(offset, offset + name_len).get_string_from_utf8()
|
||
offset += name_len
|
||
|
||
# layer_id (uint16)
|
||
layer.layer_id = _read_uint16(data, offset)
|
||
offset += 2
|
||
|
||
# dimensions (3 × uint16)
|
||
layer.width = _read_uint16(data, offset)
|
||
offset += 2
|
||
layer.height = _read_uint16(data, offset)
|
||
offset += 2
|
||
layer.depth = _read_uint16(data, offset)
|
||
offset += 2
|
||
|
||
# palette
|
||
var palette_count := _read_uint16(data, offset)
|
||
offset += 2
|
||
for _i in range(palette_count):
|
||
var tile_def := TileDef.new()
|
||
tile_def.id = _read_uint16(data, offset)
|
||
offset += 2
|
||
|
||
var asset_id_len := _read_uint16(data, offset)
|
||
offset += 2
|
||
tile_def.asset_id = data.slice(offset, offset + asset_id_len).get_string_from_utf8()
|
||
offset += asset_id_len
|
||
|
||
tile_def.solid = data[offset] != 0
|
||
offset += 1
|
||
|
||
tile_def.flags = _read_uint32(data, offset)
|
||
offset += 4
|
||
|
||
tile_def.variants = data[offset]
|
||
offset += 1
|
||
|
||
layer.palette.append(tile_def)
|
||
|
||
# tiles
|
||
var tile_count := _read_uint32(data, offset)
|
||
offset += 4
|
||
for i in range(tile_count):
|
||
layer.tiles.append(_read_uint16(data, offset))
|
||
offset += 2
|
||
|
||
result.tile_layers.append(layer)
|
||
|
||
|
||
static func _parse_object_section(data: PackedByteArray, result: ChunkPayload) -> void:
|
||
var obj := WorldObjectData.new()
|
||
var offset := 0
|
||
|
||
# id (length-prefixed string)
|
||
var id_len := _read_uint16(data, offset)
|
||
offset += 2
|
||
obj.id = data.slice(offset, offset + id_len).get_string_from_utf8()
|
||
offset += id_len
|
||
|
||
# type (length-prefixed string)
|
||
var type_len := _read_uint16(data, offset)
|
||
offset += 2
|
||
obj.type = data.slice(offset, offset + type_len).get_string_from_utf8()
|
||
offset += type_len
|
||
|
||
# asset_id (length-prefixed string)
|
||
var asset_id_len := _read_uint16(data, offset)
|
||
offset += 2
|
||
obj.asset_id = data.slice(offset, offset + asset_id_len).get_string_from_utf8()
|
||
offset += asset_id_len
|
||
|
||
# position (3 × float32)
|
||
obj.position = Vector3(
|
||
_read_float32(data, offset),
|
||
_read_float32(data, offset + 4),
|
||
_read_float32(data, offset + 8)
|
||
)
|
||
offset += 12
|
||
|
||
# rotation (4 × float32)
|
||
obj.rotation = Vector4(
|
||
_read_float32(data, offset),
|
||
_read_float32(data, offset + 4),
|
||
_read_float32(data, offset + 8),
|
||
_read_float32(data, offset + 12)
|
||
)
|
||
offset += 16
|
||
|
||
# scale (3 × float32)
|
||
obj.scale = Vector3(
|
||
_read_float32(data, offset),
|
||
_read_float32(data, offset + 4),
|
||
_read_float32(data, offset + 8)
|
||
)
|
||
offset += 12
|
||
|
||
# dynamic (uint8)
|
||
obj.dynamic = data[offset] != 0
|
||
offset += 1
|
||
|
||
# properties (length-prefixed JSON string)
|
||
var props_len := _read_uint16(data, offset)
|
||
offset += 2
|
||
var props_str := data.slice(offset, offset + props_len).get_string_from_utf8()
|
||
offset += props_len
|
||
obj.properties = JSON.parse_string(props_str) or {}
|
||
|
||
# flags (ContentFlags)
|
||
obj.flags.violence = data[offset]
|
||
offset += 1
|
||
obj.flags.sexual = data[offset]
|
||
offset += 1
|
||
obj.flags.language = data[offset]
|
||
offset += 1
|
||
obj.flags.substance = data[offset]
|
||
offset += 1
|
||
obj.flags.gambling = data[offset]
|
||
offset += 1
|
||
obj.flags.horror = data[offset]
|
||
offset += 1
|
||
obj.flags.hate = data[offset]
|
||
offset += 1
|
||
obj.flags.custom = data[offset]
|
||
offset += 1
|
||
obj.flags.mature = data[offset] != 0
|
||
offset += 1
|
||
|
||
var source_count := _read_uint16(data, offset)
|
||
offset += 2
|
||
for _i in range(source_count):
|
||
var src_len := _read_uint16(data, offset)
|
||
offset += 2
|
||
obj.flags.sources.append(data.slice(offset, offset + src_len).get_string_from_utf8())
|
||
offset += src_len
|
||
|
||
result.objects.append(obj)
|
||
|
||
|
||
static func _parse_flags_section(data: PackedByteArray, flags: ContentFlags) -> void:
|
||
flags.violence = data[0]
|
||
flags.sexual = data[1]
|
||
flags.language = data[2]
|
||
flags.substance = data[3]
|
||
flags.gambling = data[4]
|
||
flags.horror = data[5]
|
||
flags.hate = data[6]
|
||
flags.custom = data[7]
|
||
flags.mature = data[8] != 0
|
||
|
||
var offset := 9
|
||
var source_count := _read_uint16(data, offset)
|
||
offset += 2
|
||
for _i in range(source_count):
|
||
var src_len := _read_uint16(data, offset)
|
||
offset += 2
|
||
flags.sources.append(data.slice(offset, offset + src_len).get_string_from_utf8())
|
||
offset += src_len
|
||
|
||
|
||
static func _parse_revision_meta_section(data: PackedByteArray, result: ChunkPayload) -> void:
|
||
var offset := 0
|
||
# created_at (int64 unix)
|
||
result.created_at = _read_int64(data, offset)
|
||
offset += 8
|
||
# updated_at (int64 unix)
|
||
result.updated_at = _read_int64(data, offset)
|
||
offset += 8
|
||
# revision (uint32)
|
||
result.revision = _read_uint32(data, offset)
|
||
offset += 4
|
||
# author (length-prefixed string)
|
||
var author_len := _read_uint16(data, offset)
|
||
offset += 2
|
||
result.author = data.slice(offset, offset + author_len).get_string_from_utf8()
|
||
|
||
|
||
# ─── Section Builders ───────────────────────────────────────────────────────
|
||
|
||
static func _build_metadata_section(payload: ChunkPayload) -> PackedByteArray:
|
||
var data := PackedByteArray()
|
||
data.append_array(_write_string(payload.world_id))
|
||
data.append_array(_write_string(payload.room_id))
|
||
data.append_array(_write_int32(payload.coord.x))
|
||
data.append_array(_write_int32(payload.coord.y))
|
||
data.append_array(_write_int32(payload.coord.z))
|
||
data.append_array(_write_float32(payload.bounds.position.x))
|
||
data.append_array(_write_float32(payload.bounds.position.y))
|
||
data.append_array(_write_float32(payload.bounds.position.z))
|
||
data.append_array(_write_float32(payload.bounds.size.x))
|
||
data.append_array(_write_float32(payload.bounds.size.y))
|
||
data.append_array(_write_float32(payload.bounds.size.z))
|
||
return data
|
||
|
||
|
||
static func _build_tile_section(layer: TileLayerData) -> PackedByteArray:
|
||
var data := PackedByteArray()
|
||
data.append_array(_write_string(layer.name))
|
||
data.append_array(_write_uint16(layer.layer_id))
|
||
data.append_array(_write_uint16(layer.width))
|
||
data.append_array(_write_uint16(layer.height))
|
||
data.append_array(_write_uint16(layer.depth))
|
||
data.append_array(_write_uint16(layer.palette.size()))
|
||
|
||
for tile_def in layer.palette:
|
||
data.append_array(_write_uint16(tile_def.id))
|
||
data.append_array(_write_string(tile_def.asset_id))
|
||
data.append(tile_def.solid as int)
|
||
data.append_array(_write_uint32(tile_def.flags))
|
||
data.append(tile_def.variants)
|
||
|
||
data.append_array(_write_uint32(layer.tiles.size()))
|
||
for tile_id in layer.tiles:
|
||
data.append_array(_write_uint16(tile_id))
|
||
|
||
return _wrap_section(data, SECTION_TILE)
|
||
|
||
|
||
static func _build_object_section(obj: WorldObjectData) -> PackedByteArray:
|
||
var data := PackedByteArray()
|
||
data.append_array(_write_string(obj.id))
|
||
data.append_array(_write_string(obj.type))
|
||
data.append_array(_write_string(obj.asset_id))
|
||
data.append_array(_write_float32(obj.position.x))
|
||
data.append_array(_write_float32(obj.position.y))
|
||
data.append_array(_write_float32(obj.position.z))
|
||
data.append_array(_write_float32(obj.rotation.x))
|
||
data.append_array(_write_float32(obj.rotation.y))
|
||
data.append_array(_write_float32(obj.rotation.z))
|
||
data.append_array(_write_float32(obj.rotation.w))
|
||
data.append_array(_write_float32(obj.scale.x))
|
||
data.append_array(_write_float32(obj.scale.y))
|
||
data.append_array(_write_float32(obj.scale.z))
|
||
data.append(obj.dynamic as int)
|
||
|
||
var props_json := JSON.stringify(obj.properties, " ")
|
||
data.append_array(_write_string(props_json))
|
||
|
||
data.append(obj.flags.violence)
|
||
data.append(obj.flags.sexual)
|
||
data.append(obj.flags.language)
|
||
data.append(obj.flags.substance)
|
||
data.append(obj.flags.gambling)
|
||
data.append(obj.flags.horror)
|
||
data.append(obj.flags.hate)
|
||
data.append(obj.flags.custom)
|
||
data.append(obj.flags.mature as int)
|
||
data.append_array(_write_uint16(obj.flags.sources.size()))
|
||
for src in obj.flags.sources:
|
||
data.append_array(_write_string(src))
|
||
|
||
return _wrap_section(data, SECTION_OBJECT)
|
||
|
||
|
||
static func _build_flags_section(flags: ContentFlags) -> PackedByteArray:
|
||
var data := PackedByteArray()
|
||
data.append(flags.violence)
|
||
data.append(flags.sexual)
|
||
data.append(flags.language)
|
||
data.append(flags.substance)
|
||
data.append(flags.gambling)
|
||
data.append(flags.horror)
|
||
data.append(flags.hate)
|
||
data.append(flags.custom)
|
||
data.append(flags.mature as int)
|
||
data.append_array(_write_uint16(flags.sources.size()))
|
||
for src in flags.sources:
|
||
data.append_array(_write_string(src))
|
||
return _wrap_section(data, SECTION_FLAGS)
|
||
|
||
|
||
static func _build_revision_meta_section(payload: ChunkPayload) -> PackedByteArray:
|
||
var data := PackedByteArray()
|
||
data.append_array(_write_int64(payload.created_at))
|
||
data.append_array(_write_int64(payload.updated_at))
|
||
data.append_array(_write_uint32(payload.revision))
|
||
data.append_array(_write_string(payload.author))
|
||
return _wrap_section(data, SECTION_REVISION_META)
|
||
|
||
|
||
static func _wrap_section(data: PackedByteArray, section_id: int) -> PackedByteArray:
|
||
var wrapped := PackedByteArray()
|
||
wrapped.append(section_id)
|
||
wrapped.append_array(_write_uint32(data.size()))
|
||
wrapped.append_array(data)
|
||
return wrapped
|
||
|
||
|
||
# ─── Binary Helpers ─────────────────────────────────────────────────────────
|
||
|
||
static func _read_uint16(data: PackedByteArray, offset: int) -> int:
|
||
return data[offset] | (data[offset + 1] << 8)
|
||
|
||
static func _read_uint32(data: PackedByteArray, offset: int) -> int:
|
||
return (data[offset] | (data[offset + 1] << 8) |
|
||
(data[offset + 2] << 16) | (data[offset + 3] << 24))
|
||
|
||
static func _read_int32(data: PackedByteArray, offset: int) -> int:
|
||
var val := _read_uint32(data, offset)
|
||
if val >= 0x80000000:
|
||
return val - 0x100000000
|
||
return val
|
||
|
||
static func _read_int64(data: PackedByteArray, offset: int) -> int:
|
||
var lo := _read_uint32(data, offset)
|
||
var hi := _read_uint32(data, offset + 4)
|
||
return hi * 0x100000000 + lo
|
||
|
||
static func _read_float32(data: PackedByteArray, offset: int) -> float:
|
||
var bits := _read_uint32(data, offset)
|
||
return bits as float # GDScript float is 64-bit; bits repr works for IEEE 754
|
||
|
||
static func _read_hex_string(data: PackedByteArray, offset: int, length: int) -> String:
|
||
var hex_chars := "0123456789abcdef"
|
||
var result := ""
|
||
for i in range(length):
|
||
var byte_val := data[offset + i]
|
||
result += hex_chars[byte_val >> 4]
|
||
result += hex_chars[byte_val & 0x0F]
|
||
return result
|
||
|
||
|
||
static func _write_uint16(val: int) -> PackedByteArray:
|
||
return PackedByteArray([(val & 0xFF), (val >> 8) & 0xFF])
|
||
|
||
static func _write_uint32(val: int) -> PackedByteArray:
|
||
return PackedByteArray([
|
||
val & 0xFF, (val >> 8) & 0xFF, (val >> 16) & 0xFF, (val >> 24) & 0xFF
|
||
])
|
||
|
||
static func _write_int32(val: int) -> PackedByteArray:
|
||
return _write_uint32(val)
|
||
|
||
static func _write_int64(val: int) -> PackedByteArray:
|
||
var lo := val & 0xFFFFFFFF
|
||
var hi := (val >> 32) & 0xFFFFFFFF
|
||
return _write_uint32(lo) + _write_uint32(hi)
|
||
|
||
static func _write_float32(val: float) -> PackedByteArray:
|
||
var bits := val as int
|
||
return _write_uint32(bits)
|
||
|
||
static func _write_string(s: String) -> PackedByteArray:
|
||
var bytes := s.to_utf8_buffer()
|
||
return _write_uint16(bytes.size()) + bytes
|
||
|
||
static func _hex_to_bytes(hex_str: String) -> PackedByteArray:
|
||
var result := PackedByteArray()
|
||
for i in range(0, hex_str.length(), 2):
|
||
var byte_val := int("0x" + hex_str.substr(i, 2), 16)
|
||
result.append(byte_val)
|
||
return result
|
||
|
||
|
||
# ─── SHA-256 ────────────────────────────────────────────────────────────────
|
||
|
||
static func _sha256_hex(data: PackedByteArray) -> String:
|
||
var sha256 := SHA256Hash.new()
|
||
sha256.start()
|
||
sha256.update(data)
|
||
var hash := sha256.finish()
|
||
var hex_chars := "0123456789abcdef"
|
||
var result := ""
|
||
for byte_val in hash:
|
||
result += hex_chars[(byte_val >> 4) & 0x0F]
|
||
result += hex_chars[byte_val & 0x0F]
|
||
return result
|