feat(client): add MessagePack library and cross-language fixtures
Install Godot4MessagePack (pure GDScript) for MessagePack encode/decode. Add Rust fixture generator (gen_fixtures.rs) that produces canonical .msgpack files using rmp_serde::to_vec_named for cross-language testing. Fixtures cover: snapshots (empty, one NPC, multi-entity with all EntityKind variants) and player inputs (unit + data enum variants). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Executable
+21
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Luis Chirlaque Hernández
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
Executable
+368
@@ -0,0 +1,368 @@
|
|||||||
|
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
|
||||||
|
print("null size:%s"%buffer_size)
|
||||||
|
return null
|
||||||
|
|
||||||
|
elif first_byte == types["false"]: # false
|
||||||
|
print("false size:%s"%buffer_size)
|
||||||
|
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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://komeseatyar0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
うtickdヲactionゥMoveNorth
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
うtickフネヲaction�UsePerceptionModeァthermal
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,63 @@
|
|||||||
|
//! Generate MessagePack fixture files for cross-language testing (D-030 Layer 1).
|
||||||
|
//! Run with: cargo test --test gen_fixtures -- --ignored
|
||||||
|
|
||||||
|
use settled_reach_server::bridge::types::*;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
fn write_fixture(name: &str, bytes: &[u8]) {
|
||||||
|
let dir = Path::new("../tests/fixtures/msgpack");
|
||||||
|
fs::create_dir_all(dir).expect("create fixture dir");
|
||||||
|
let path = dir.join(format!("{}.msgpack", name));
|
||||||
|
fs::write(&path, bytes).expect("write fixture");
|
||||||
|
eprintln!("Wrote {} ({} bytes)", path.display(), bytes.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore] // Run manually: cargo test --test gen_fixtures -- --ignored
|
||||||
|
fn generate_msgpack_fixtures() {
|
||||||
|
// Snapshot with one NPC entity
|
||||||
|
let snapshot = ObserverSnapshot {
|
||||||
|
tick: 42,
|
||||||
|
entities: vec![VisibleEntity {
|
||||||
|
entity_id: 1,
|
||||||
|
x: 10.0,
|
||||||
|
y: 20.0,
|
||||||
|
z: 0,
|
||||||
|
kind: EntityKind::Npc,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
write_fixture("snapshot_one_npc", &rmp_serde::to_vec_named(&snapshot).unwrap());
|
||||||
|
|
||||||
|
// Empty snapshot
|
||||||
|
let empty = ObserverSnapshot {
|
||||||
|
tick: 0,
|
||||||
|
entities: vec![],
|
||||||
|
};
|
||||||
|
write_fixture("snapshot_empty", &rmp_serde::to_vec_named(&empty).unwrap());
|
||||||
|
|
||||||
|
// PlayerInput: MoveNorth
|
||||||
|
let input_north = PlayerInput {
|
||||||
|
tick: 100,
|
||||||
|
action: PlayerAction::MoveNorth,
|
||||||
|
};
|
||||||
|
write_fixture("input_move_north", &rmp_serde::to_vec_named(&input_north).unwrap());
|
||||||
|
|
||||||
|
// PlayerInput: UsePerceptionMode
|
||||||
|
let input_perception = PlayerInput {
|
||||||
|
tick: 200,
|
||||||
|
action: PlayerAction::UsePerceptionMode("thermal".to_string()),
|
||||||
|
};
|
||||||
|
write_fixture("input_perception_mode", &rmp_serde::to_vec_named(&input_perception).unwrap());
|
||||||
|
|
||||||
|
// Snapshot with multiple entities and all EntityKind variants
|
||||||
|
let snapshot_multi = ObserverSnapshot {
|
||||||
|
tick: 999,
|
||||||
|
entities: vec![
|
||||||
|
VisibleEntity { entity_id: 1, x: 5.0, y: 10.0, z: 0, kind: EntityKind::Npc },
|
||||||
|
VisibleEntity { entity_id: 2, x: 15.5, y: 3.0, z: 1, kind: EntityKind::Object },
|
||||||
|
VisibleEntity { entity_id: 3, x: 0.0, y: 0.0, z: -1, kind: EntityKind::Terrain },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
write_fixture("snapshot_multi_entity", &rmp_serde::to_vec_named(&snapshot_multi).unwrap());
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
うtickdヲactionゥMoveNorth
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
うtickフネヲaction�UsePerceptionModeァthermal
|
||||||
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Reference in New Issue
Block a user