## Generate MessagePack fixtures from GDScript encoder for Rust decoder testing. ## Run with: make fixtures-client ## (or: godot --headless --path client -s res://tests/gen_client_fixtures.gd) ## ## Reverse direction of server/tests/gen_fixtures.rs: GDScript encodes -> Rust decodes. ## Together with the Rust-generated fixtures, this closes the cross-encoder ## compatibility loop (D-030 Layer 1, #475). ## ## Loads Messagepack encoder directly (not via class_name) because -s scripts ## run before the project's class_name registry is fully populated. extends SceneTree var _msgpack: GDScript var _count := 0 var _errors := 0 var _output_dir: String func _init(): _run.call_deferred() func _run(): _msgpack = load("res://addons/messagepack/messagepack.gd") # Resolve repo root from Godot project root (client/). # Assumes client/ is one level below repo root — validated below. var project_root := ProjectSettings.globalize_path("res://") var repo_root := project_root.rstrip("/").get_base_dir() _output_dir = repo_root.path_join("server/tests/fixtures/gdscript") if not DirAccess.dir_exists_absolute(repo_root.path_join("server")): push_error("Repo root detection failed: %s/server/ does not exist" % repo_root) quit(1) return DirAccess.make_dir_recursive_absolute(_output_dir) _generate_inputs() _generate_boundary_inputs() _generate_batch() if _errors > 0: push_error("FAILED: %d encode errors encountered" % _errors) quit(1) return if _count == 0: push_error("FAILED: no fixtures generated") quit(1) return print("Generated %d GDScript fixtures at %s" % [_count, _output_dir]) quit() func _write_fixture(name: String, bytes: PackedByteArray) -> void: if bytes.is_empty(): push_error("Encode produced empty bytes for fixture: %s" % name) _errors += 1 return var path := _output_dir.path_join(name + ".msgpack") var file := FileAccess.open(path, FileAccess.WRITE) if file == null: push_error("Failed to write fixture: %s (error: %d)" % [path, FileAccess.get_open_error()]) _errors += 1 return file.store_buffer(bytes) file.flush() file.close() print(" Wrote %s (%d bytes)" % [name, bytes.size()]) _count += 1 ## Encode a single PlayerInput to MessagePack bytes. ## Mirrors Protocol.encode_player_input() from protocol.gd. func _encode_input(tick: int, action_name: String, action_data: Variant = null) -> PackedByteArray: var action: Variant if action_data != null: action = {action_name: action_data} elif action_name == "Interact": action = {"Interact": {"target_entity_id": null, "verb": null}} else: action = action_name var result = _msgpack.encode({"tick": tick, "action": action}) if result.status != null: push_error("Encode failed: %s" % result.status) return PackedByteArray() return result.value ## Encode an array of PlayerInputs to MessagePack bytes. ## Mirrors Protocol.encode_player_inputs() from protocol.gd. func _encode_inputs(inputs: Array) -> PackedByteArray: var wire_inputs: Array = [] for input in inputs: var action_name: String = input["action_name"] var action_data: Variant = input.get("action_data") var action: Variant if action_data != null: action = {action_name: action_data} elif action_name == "Interact": action = {"Interact": {"target_entity_id": null, "verb": null}} else: action = action_name wire_inputs.append({"tick": input["tick"], "action": action}) var result = _msgpack.encode(wire_inputs) if result.status != null: push_error("Batch encode failed: %s" % result.status) return PackedByteArray() return result.value func _generate_inputs() -> void: # Unit variants: all 8 movement directions (tick=100) for dir_name in [ "MoveNorth", "MoveSouth", "MoveEast", "MoveWest", "MoveNortheast", "MoveSoutheast", "MoveSouthwest", "MoveNorthwest", ]: _write_fixture("input_%s" % dir_name.to_snake_case(), _encode_input(100, dir_name)) # Data variant: UsePerceptionMode (tick=200) _write_fixture("input_perception_mode", _encode_input(200, "UsePerceptionMode", "thermal")) # Struct variant: Interact with null fields (tick=100) _write_fixture("input_interact", _encode_input(100, "Interact")) # Other unit variants _write_fixture("input_pause", _encode_input(100, "Pause")) _write_fixture("input_unpause", _encode_input(100, "Unpause")) _write_fixture("input_toggle_stance_up", _encode_input(100, "ToggleStanceUp")) _write_fixture("input_toggle_stance_down", _encode_input(100, "ToggleStanceDown")) _write_fixture("input_walk_away", _encode_input(100, "WalkAway")) func _generate_boundary_inputs() -> void: # Tick values at MessagePack encoding format boundaries. # GDScript encodes 256-32767 as int_16 (0xd1); Rust encodes as uint_16 (0xcd). # GDScript encodes 65536-2147483647 as int_32 (0xd2); Rust as uint_32 (0xce). # Both are valid MessagePack. Rust's rmp_serde must accept both. var boundary_ticks: Array = [ [0, "boundary_tick_0"], [127, "boundary_tick_127"], [128, "boundary_tick_128"], [255, "boundary_tick_255"], [256, "boundary_tick_256"], # int_16 asymmetry start [32767, "boundary_tick_32767"], # int_16 asymmetry end [32768, "boundary_tick_32768"], [65535, "boundary_tick_65535"], [65536, "boundary_tick_65536"], # int_32 asymmetry start [2147483647, "boundary_tick_2147483647"], # int_32 asymmetry end ] for pair in boundary_ticks: _write_fixture(pair[1], _encode_input(pair[0], "Pause")) func _generate_batch() -> void: # Vec with two actions (mirrors Rust input_batch_two fixture) _write_fixture("input_batch_two", _encode_inputs([ {"tick": 0, "action_name": "MoveNorth"}, {"tick": 0, "action_name": "Interact"}, ]))