Files
settled-reach/client/addons/messagepack/messagepack.gd
T
jpmschweitzerandClaude Opus 4.6 bc875d607f fix(client): correct messagepack signed int boundary encoding
Off-by-one in all four signed int branches: upper bound used <= instead
of < (e.g., value <= 128 matched int8 instead of falling through to
uint8). Tick 128 was encoded as int8 -128, causing server deserialization
failure. Also remove stray debug prints from nil/false decode paths.

Fixes bug #4 (MessagePack encodes tick 128 as -128).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:26:13 +01:00

367 lines
9.2 KiB
GDScript
Executable File

class_name Messagepack
## Messagepack implementation for Godot 4 in GDScript
##
## You can find the full spec at: https://github.com/msgpack/msgpack/blob/master/spec.md
const FIRST3 = 0xe0
const FIRST4 = 0xf0
const LAST4 = 0x0f
const LAST5 = 0x1f
const types = {
"nil": 0xc0,
"false": 0xc2,
"true": 0xc3,
"positive_fixint": [0x00, 0x7f],
"negative_fixint": [0xe0, 0xff],
"uint_8": 0xcc,
"uint_16": 0xcd,
"uint_32": 0xce,
"uint_64": 0xcf,
"int_8": 0xd0,
"int_16": 0xd1,
"int_32": 0xd2,
"int_64": 0xd3,
"float_32": 0xca,
"float_64": 0xcb,
"fixstr": [0xa0, 0xbf],
"str_8": 0xd9,
"str_16": 0xda,
"str_32": 0xdb,
"fixarray": [0x90, 0x9f],
"array_16": 0xdc,
"array_32": 0xdd,
"fixmap": [0x80, 0x8f],
"map_16": 0xde,
"map_32": 0xdf,
"bin_8": 0xc4,
"bin_16": 0xc5,
"bin_32": 0xc6
}
## This function takes a Variant and encodes it according to the Messagepack spec
##
## Parameters:
## - value: Variant to be encoded
##
## Returns:
## A dictionary containing the status of the encoding and the value as a PackedByteArray
static func encode(value) -> Dictionary:
var buffer = StreamPeerBuffer.new()
buffer.set_big_endian(true)
var err = _encode_message(buffer, value)
return {
value = buffer.data_array,
status = err
}
static func _encode_message(buffer: StreamPeerBuffer, value):
match typeof(value):
TYPE_NIL:
buffer.put_u8(types["nil"])
TYPE_BOOL:
if value == true:
buffer.put_u8(types["true"])
else:
buffer.put_u8(types["false"])
TYPE_INT:
if - (1 << 5) <= value and value <= (1 << 7) - 1:
buffer.put_8(value)
elif - (1 << 7) <= value and value < (1 << 7):
buffer.put_u8(types["int_8"])
buffer.put_8(value)
elif 0 <= value and value <= (1 << 8) - 1:
buffer.put_u8(types["uint_8"])
buffer.put_u8(value)
elif - (1 << 15) <= value and value < (1 << 15):
buffer.put_u8(types["int_16"])
buffer.put_16(value)
elif 0 <= value and value <= (1 << 16) - 1:
buffer.put_u8(types["uint_16"])
buffer.put_u16(value)
elif - (1 << 31) <= value and value < (1 << 31):
buffer.put_u8(types["int_32"])
buffer.put_32(value)
elif 0 <= value and value <= (1 << 32) - 1:
buffer.put_u8(types["uint_32"])
buffer.put_u32(value)
elif - (1 << 63) <= value and value < (1 << 63):
buffer.put_u8(types["int_64"])
buffer.put_64(value)
else:
buffer.put_u8(types["uint_64"])
buffer.put_u64(value)
TYPE_FLOAT:
buffer.put_u8(types["float_32"])
buffer.put_float(value)
TYPE_STRING:
var bytes = value.to_utf8_buffer()
var size = bytes.size()
if size <= (1 << 5) - 1:
buffer.put_u8(types["fixstr"][0]|size)
elif size <= (1 << 8) - 1:
buffer.put_u8(types["str_8"])
buffer.put_u8(size)
elif size <= (1 << 16) - 1:
buffer.put_u8(types["str_16"])
buffer.put_u16(size)
elif size <= (1 << 32) - 1:
buffer.put_u8(types["str_32"])
buffer.put_u32(size)
else:
printerr("Unsupported string: string is too big")
return ERR_INVALID_DATA
buffer.put_data(bytes)
TYPE_ARRAY:
var size = value.size()
if size <= 15:
buffer.put_u8(types["fixarray"][0]|size)
elif size <= (1 << 16) - 1:
buffer.put_u8(types["array_16"])
buffer.put_u16(size)
elif size <= (1 << 32) - 1:
buffer.put_u8(types["array_32"])
buffer.put_u32(size)
else:
printerr("Unsupported array: array is too long")
return ERR_INVALID_DATA
for obj in value:
_encode_message(buffer, obj)
TYPE_DICTIONARY:
var size = value.size()
if size <= 15:
buffer.put_u8(types["fixmap"][0]|size)
elif size <= (1 << 16) - 1:
buffer.put_u8(types["map_16"])
buffer.put_u16(size)
elif size <= (1 << 32) - 1:
buffer.put_u8(types["map_32"])
buffer.put_u32(size)
else:
printerr("Unsupported dictionary: dictionary is too big")
return ERR_INVALID_DATA
for key in value:
_encode_message(buffer, key)
_encode_message(buffer, value[key])
TYPE_PACKED_BYTE_ARRAY:
var size = value.size()
if size <= (1 << 8) - 1:
buffer.put_u8(types["bin_8"])
buffer.put_u8(size)
elif size <= (1 << 16) - 1:
buffer.put_u8(types["bin_16"])
buffer.put_u16(size)
elif size <= (1 << 32) - 1:
buffer.put_u8(types["bin_32"])
buffer.put_u32(size)
else:
printerr("Unsupported packed byte array: packed byte array is too big")
return ERR_INVALID_DATA
buffer.put_data(value)
_:
printerr("Unsupported data type: %s" % typeof(value))
return ERR_UNAVAILABLE
## This function takes a PackedByteArray and decodes it according to the Messagepack spec
##
## Parameters:
## - bytes: PackedByteArray to be decoded
##
## Returns:
## A dictionary containing the status of the decoding and the value as Godot Variants
static func decode(bytes: PackedByteArray):
var buffer = StreamPeerBuffer.new()
buffer.set_big_endian(true)
buffer.set_data_array(bytes)
var err = {
error = null
}
var message = _decode_message(buffer, err)
return {
value = message,
status = err.error
}
static func _decode_message(buffer: StreamPeerBuffer, err: Dictionary):
var buffer_size = buffer.get_size()
var first_byte = buffer.get_u8()
if first_byte & 0x80 == 0: # positive fixint
return first_byte
elif first_byte & FIRST4 == 0x80: # fixmap
var size = first_byte & 0x0f
var dict = {}
for _x in range(size):
var key = _decode_message(buffer, err)
var val = _decode_message(buffer, err)
dict[key] = val
return dict
elif first_byte & FIRST4 == 0x90: # fixarray
var size = first_byte & 0x0f
var array = []
for _x in range(size):
var val = _decode_message(buffer, err)
array.append(val)
return array
elif first_byte & FIRST3 == 0xa0: # fixstr
var size = first_byte & 0x1f
return buffer.get_utf8_string(size)
elif first_byte == types["nil"]: # nil
return null
elif first_byte == types["false"]: # false
return false
elif first_byte == types["true"]: # true
return true
elif first_byte == types["bin_8"]: # bin 8
var length = buffer.get_u8()
return buffer.get_partial_data(length)
elif first_byte == types["bin_16"]: # bin 16
var length = buffer.get_u16()
return buffer.get_partial_data(length)
elif first_byte == types["bin_32"]: # bin 32
var length = buffer.get_u32()
return buffer.get_partial_data(length)
elif first_byte == 0xc7: # ext 8
print("Ext 8 type not implemented")
return null
elif first_byte == 0xc8: # ext 16
print("Ext 16 type not implemented")
return null
elif first_byte == 0xc9: # ext 32
print("Ext 32 type not implemented")
return null
elif first_byte == types["float_32"]: # float 32
return buffer.get_float()
elif first_byte == types["float_64"]: # float 64
return buffer.get_double()
elif first_byte == types["uint_8"]: # uint 8
return buffer.get_u8()
elif first_byte == types["uint_16"]: # uint 16
return buffer.get_u16()
elif first_byte == types["uint_32"]: # uint 32
return buffer.get_u32()
elif first_byte == types["uint_64"]: # uint 64
return buffer.get_u64()
elif first_byte == types["int_8"]: # int 8
return buffer.get_8()
elif first_byte == types["int_16"]: # int 16
return buffer.get_16()
elif first_byte == types["int_32"]: # int 32
return buffer.get_32()
elif first_byte == types["int_64"]: # int 64
return buffer.get_64()
elif first_byte == 0xd4: # fixext 1
print("Fixext 1 type not implemented")
err.error = ERR_UNAVAILABLE
return null
elif first_byte == 0xd5: # fixext 2
print("Fixext 2 type not implemented")
err.error = ERR_UNAVAILABLE
return null
elif first_byte == 0xd6: # fixext 4
print("Fixext 4 type not implemented")
err.error = ERR_UNAVAILABLE
return null
elif first_byte == 0xd7: # fixext 8
print("Fixext 8 type not implemented")
err.error = ERR_UNAVAILABLE
return null
elif first_byte == 0xd8: # fixext 16
print("Fixext 16 type not implemented")
err.error = ERR_UNAVAILABLE
return null
elif first_byte == types["str_8"]: # str 8
var size = buffer.get_u8()
return buffer.get_utf8_string(size)
elif first_byte == types["str_16"]: # str 16
var size = buffer.get_u16()
return buffer.get_utf8_string(size)
elif first_byte == types["str_32"]: # str 32
var size = buffer.get_u32()
return buffer.get_utf8_string(size)
elif first_byte == types["array_16"]: # array 16
var length = buffer.get_u16()
var array = []
for _x in range(length):
var val = _decode_message(buffer, err)
array.append(val)
return array
elif first_byte == types["array_32"]: # array 32
var length = buffer.get_u32()
var array = []
for _x in range(length):
var val = _decode_message(buffer, err)
array.append(val)
return array
elif first_byte == types["map_16"]: # map 16
var length = buffer.get_u16()
var dict = {}
for _x in range(length):
var key = _decode_message(buffer, err)
var val = _decode_message(buffer, err)
dict[key] = val
return dict
elif first_byte == types["map_32"]: # map 32
var length = buffer.get_u32()
var dict = {}
for _x in range(length):
var key = _decode_message(buffer, err)
var val = _decode_message(buffer, err)
dict[key] = val
return dict
elif first_byte & FIRST3 == 0xe0: # negative fixint
return first_byte - 256
else:
printerr("Unknown header")
err.error = ERR_UNAVAILABLE
return null