Fix 3 real client/server chunk-codec interop bugs, caught by a new golden-fixture test
checkpoint / checkpoint (push) Successful in 2m13s

No test ever exercised the client ChunkDataDecoder against real byte data
before this (M2 chunk streaming was never fed real content on PC2/PC3).
Added scripts/smoke_test_chunk_codec_interop.gd, decoding the backend's
exact golden fixtures (backend/tests/fixtures/chunks/*.bin) and asserting
byte-identical logical values -- the real cross-language contract this
whole persistence system depends on. It found three bugs on first run:

1. chunk_data_decoder.gd's _read_float32/_write_float32 used `bits as
   float`/`val as int`, a numeric cast in GDScript, not an IEEE-754
   bitcast -- silently corrupted every non-integer position/rotation/
   scale/bounds value (1.5 decoded as 1069547520.0, verified). Fixed with
   PackedByteArray.decode_float/encode_float (real bitcast, same
   little-endian order as the rest of this format).

2. backend/app/services/chunk_codec.py's binary codec wrote bounds as
   min+size, disagreeing with world.md §3's documented min+max wire
   format and with the client's actual (correct) decoder -- corrupted
   every chunk's bounds on the wire despite the backend's own JSON path
   using min+max correctly. Fixed the binary encode/decode to match the
   spec; regenerated the golden fixtures.

3. chunk_data_decoder.gd's object-properties parsing used
   `JSON.parse_string(props_str) or {}` -- GDScript's `or` is a boolean
   operator, not Python/JS-style short-circuit-return, so this silently
   evaluated to a bool for any object with real property data, crashing
   on assignment to the typed `properties: Dictionary` field. Fixed with
   an explicit type check.

Full backend suite (132/132) and 4 sibling client smoke tests re-verified
clean after all three fixes; no other `or {}`/`or []` instances of bug 3's
pattern found elsewhere in the codebase.
This commit is contained in:
2026-09-14 19:56:44 +00:00
parent 9c086a4b10
commit eb39e4393d
5 changed files with 139 additions and 13 deletions
+18 -5
View File
@@ -404,7 +404,15 @@ static func _parse_object_section(data: PackedByteArray, result: ChunkPayload) -
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 {}
# `or` in GDScript is a boolean operator, not Python/JS-style
# short-circuit-return -- `parsed or {}` returns a bool (true for any
# non-empty Dictionary), not `parsed` itself. Assigning that bool to
# `properties` (typed Dictionary) crashes at runtime for any object
# with real property data -- verified: a real fixture with
# {"locked": false, ...} threw "Invalid assignment of property or key
# 'properties' with value of type 'bool'".
var parsed_props: Variant = JSON.parse_string(props_str)
obj.properties = parsed_props if parsed_props is Dictionary else {}
# flags (ContentFlags)
obj.flags.violence = data[offset]
@@ -607,8 +615,11 @@ static func _read_int64(data: PackedByteArray, offset: int) -> int:
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
# `int as float` is a NUMERIC cast, not an IEEE-754 bit reinterpretation --
# it was silently corrupting every non-integer value (verified: bits for
# 1.5f decoded as 1069547520.0). decode_float() does a real bitcast and
# matches this format's little-endian byte order (§3.6).
return data.decode_float(offset)
static func _read_hex_string(data: PackedByteArray, offset: int, length: int) -> String:
var hex_chars := "0123456789abcdef"
@@ -637,8 +648,10 @@ static func _write_int64(val: int) -> PackedByteArray:
return _write_uint32(lo) + _write_uint32(hi)
static func _write_float32(val: float) -> PackedByteArray:
var bits := val as int
return _write_uint32(bits)
var out := PackedByteArray()
out.resize(4)
out.encode_float(0, val)
return out
static func _write_string(s: String) -> PackedByteArray:
var bytes := s.to_utf8_buffer()