extends "res://tests/test_case.gd" const NetCodec = preload("res://scripts/net_codec.gd") const ShipAction = preload("res://scripts/ship_action.gd") const NetBodyState = preload("res://scripts/net_body_state.gd") const POS_TOL := 0.01 # well under the ~1.95mm quantisation step's rounding const VEL_TOL := 0.01 const QUAT_TOL := 0.001 const AVEL_TOL := 0.2 # BALL_AVEL_RANGE/127 half-step, scaled through the ship->ball rescale const THRUST_TOL := 1.0 / 127.0 + 0.001 func _make_action(tx: float, ty: float, tz: float, rx: float, ry: float, rz: float, turbo: bool) -> ShipAction: var a := ShipAction.new() a.thrust = Vector3(tx, ty, tz) a.rotation = Vector3(rx, ry, rz) a.turbo = turbo return a func test_input_header_size_matches_spec() -> void: assert_eq(NetCodec.INPUT_HEADER_SIZE, 12, "input header size") assert_eq(NetCodec.INPUT_ENTRY_SIZE, 7, "input entry size") func test_input_roundtrip_single_entry() -> void: var actions := [_make_action(1.0, -1.0, 0.5, -0.25, 0.0, 1.0, true)] var bytes := NetCodec.pack_input(12345, 999, 6000, actions) assert_eq(bytes.size(), NetCodec.INPUT_HEADER_SIZE + NetCodec.INPUT_ENTRY_SIZE, "1-entry payload size") var decoded := NetCodec.unpack_input(bytes) assert_eq(decoded["seq"], 12345, "seq") assert_eq(decoded["count"], 1, "count") assert_eq(decoded["ack_snapshot_tick"], 999, "ack_snapshot_tick") assert_eq(decoded["client_send_ms"], 6000, "client_send_ms") var a: ShipAction = decoded["actions"][0] assert_almost_eq(a.thrust.x, 1.0, THRUST_TOL, "thrust.x") assert_almost_eq(a.thrust.y, -1.0, THRUST_TOL, "thrust.y") assert_almost_eq(a.thrust.z, 0.5, THRUST_TOL, "thrust.z") assert_almost_eq(a.rotation.x, -0.25, THRUST_TOL, "rotation.x") assert_almost_eq(a.rotation.y, 0.0, THRUST_TOL, "rotation.y") assert_almost_eq(a.rotation.z, 1.0, THRUST_TOL, "rotation.z") assert_true(a.turbo, "turbo bit") func test_input_roundtrip_max_redundancy_newest_first() -> void: var actions := [ _make_action(1.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), _make_action(0.5, 0.0, 0.0, 0.0, 0.0, 0.0, false), _make_action(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), _make_action(-1.0, 0.0, 0.0, 0.0, 0.0, 0.0, true), ] var bytes := NetCodec.pack_input(1, 0, 0, actions) assert_eq(bytes.size(), NetCodec.INPUT_HEADER_SIZE + 4 * NetCodec.INPUT_ENTRY_SIZE, "4-entry payload size") var decoded := NetCodec.unpack_input(bytes) assert_eq(decoded["count"], 4, "count") var decoded_actions: Array = decoded["actions"] assert_almost_eq(decoded_actions[0].thrust.x, 1.0, THRUST_TOL, "entry 0 (newest) thrust.x") assert_almost_eq(decoded_actions[3].thrust.x, -1.0, THRUST_TOL, "entry 3 (oldest) thrust.x") assert_true(decoded_actions[3].turbo, "entry 3 turbo bit") assert_true(not decoded_actions[0].turbo, "entry 0 turbo bit unset") func test_input_redundancy_clamped_to_max() -> void: var actions := [] for i in 6: actions.append(_make_action(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false)) var bytes := NetCodec.pack_input(1, 0, 0, actions) assert_eq(bytes.size(), NetCodec.INPUT_HEADER_SIZE + NetCodec.MAX_REDUNDANCY * NetCodec.INPUT_ENTRY_SIZE, "clamped to MAX_REDUNDANCY entries") func test_snapshot_sizes_match_spec() -> void: assert_eq(NetCodec.SNAPSHOT_CLIENT_HEADER_SIZE, 7, "client header size") assert_eq(NetCodec.SNAPSHOT_BODY_HEADER_SIZE, 8, "body header size") assert_eq(NetCodec.SNAPSHOT_BODY_SIZE, 22, "per-body size") func test_snapshot_roundtrip_seven_bodies() -> void: var bodies: Array[NetBodyState] = [] for i in 7: var b := NetBodyState.new() b.position = Vector3(float(i) * 3.0 - 10.0, 1.0, -float(i) * 2.0) b.rotation = Quaternion(Vector3.UP, float(i) * 0.3) b.linear_velocity = Vector3(float(i), 0.0, -float(i) * 0.5) b.angular_velocity = Vector3(0.1 * i, 0.0, 0.0) b.frozen = (i % 2 == 0) b.turbo = (i == 3) b.thrust_z = -1.0 + 2.0 * float(i) / 6.0 b.stalled = (i == 5) bodies.append(b) var segment := NetCodec.pack_snapshot_body_segment(4242, 3, 7, bodies) assert_eq(segment.size(), NetCodec.SNAPSHOT_BODY_HEADER_SIZE + 7 * NetCodec.SNAPSHOT_BODY_SIZE, "7-body segment size") var packet := NetCodec.pack_snapshot(555, -2, 1234, segment) assert_eq(packet.size(), NetCodec.SNAPSHOT_CLIENT_HEADER_SIZE + segment.size(), "full packet size") assert_eq(packet.size(), 169, "matches MULTIPLAYER_SPEC.md §2.4's 169 B payload figure for 7 bodies") var decoded := NetCodec.unpack_snapshot(packet) assert_eq(decoded["last_input_seq"], 555, "last_input_seq") assert_eq(decoded["input_buffer_depth"], -2, "input_buffer_depth (negative = starved)") assert_eq(decoded["echo_client_send_ms"], 1234, "echo_client_send_ms") assert_eq(decoded["server_tick"], 4242, "server_tick") assert_eq(decoded["match_state"], 3, "match_state") assert_eq(decoded["reset_gen"], 7, "reset_gen") var decoded_bodies: Array = decoded["bodies"] assert_eq(decoded_bodies.size(), 7, "body_count") for i in 7: var original: NetBodyState = bodies[i] var b: NetBodyState = decoded_bodies[i] assert_almost_eq(b.position.x, original.position.x, POS_TOL, "body %d position.x" % i) assert_almost_eq(b.position.y, original.position.y, POS_TOL, "body %d position.y" % i) assert_almost_eq(b.position.z, original.position.z, POS_TOL, "body %d position.z" % i) assert_almost_eq(b.linear_velocity.x, original.linear_velocity.x, VEL_TOL, "body %d velocity.x" % i) assert_almost_eq(b.rotation.x, original.rotation.x, QUAT_TOL, "body %d quat.x" % i) assert_almost_eq(b.rotation.y, original.rotation.y, QUAT_TOL, "body %d quat.y" % i) assert_almost_eq(b.rotation.z, original.rotation.z, QUAT_TOL, "body %d quat.z" % i) assert_almost_eq(b.rotation.w, original.rotation.w, QUAT_TOL, "body %d quat.w (sign fold)" % i) assert_eq(b.frozen, original.frozen, "body %d frozen" % i) assert_eq(b.turbo, original.turbo, "body %d turbo" % i) assert_eq(b.stalled, original.stalled, "body %d stalled" % i) func test_snapshot_quaternion_negative_w_sign_survives() -> void: # A quaternion whose w component is negative (same rotation as its # positive-w twin, but exercises the sign-fold bit specifically). var b := NetBodyState.new() b.rotation = Quaternion(0.0, 0.0, 0.0, -1.0).normalized() var segment := NetCodec.pack_snapshot_body_segment(0, 0, 0, [b]) var decoded := NetCodec.unpack_snapshot(NetCodec.pack_snapshot(0, 0, 0, segment)) var out: NetBodyState = decoded["bodies"][0] assert_true(out.rotation.w < 0.0, "negative w sign must survive the round trip") func test_thrust_z_bin_quantisation_covers_range() -> void: assert_eq(NetCodec.quantize_thrust_z_bin(-1.0), 0, "thrust_z -1.0 -> bin 0") assert_eq(NetCodec.quantize_thrust_z_bin(1.0), NetCodec.THRUST_Z_BIN_MAX, "thrust_z 1.0 -> max bin") assert_almost_eq(NetCodec.dequantize_thrust_z_bin(0), -1.0, 0.001, "bin 0 -> -1.0") assert_almost_eq(NetCodec.dequantize_thrust_z_bin(NetCodec.THRUST_Z_BIN_MAX), 1.0, 0.001, "max bin -> 1.0") func test_ball_angular_velocity_rescale() -> void: var ball := NetBodyState.new() ball.avel_range = NetCodec.BALL_AVEL_RANGE ball.angular_velocity = Vector3(20.0, -15.0, 5.0) # within ±32 rad/s, outside ship's ±4 var segment := NetCodec.pack_snapshot_body_segment(0, 0, 0, [ball]) var decoded := NetCodec.unpack_snapshot(NetCodec.pack_snapshot(0, 0, 0, segment)) var out: NetBodyState = decoded["bodies"][0] # Decoded at the wrong (ship) range first, per unpack_snapshot's documented contract. assert_almost_eq(out.angular_velocity.x, 20.0 / NetCodec.BALL_AVEL_RANGE * NetCodec.SHIP_AVEL_RANGE, AVEL_TOL, "undecoded-scale sanity check") NetCodec.rescale_avel(out, NetCodec.BALL_AVEL_RANGE) assert_almost_eq(out.angular_velocity.x, 20.0, AVEL_TOL, "rescaled avel.x") assert_almost_eq(out.angular_velocity.y, -15.0, AVEL_TOL, "rescaled avel.y") assert_almost_eq(out.angular_velocity.z, 5.0, AVEL_TOL, "rescaled avel.z") func test_type_version_byte_roundtrip() -> void: var tv := NetCodec.type_version_byte(NetCodec.PacketType.SNAPSHOT) assert_eq(NetCodec.packet_type_of(tv), NetCodec.PacketType.SNAPSHOT, "packet type nibble") assert_eq(NetCodec.protocol_version_of(tv), NetCodec.PROTOCOL_VERSION, "protocol version nibble") func test_tick_hz_derives_from_sim_constants() -> void: var SimConstants = preload("res://scripts/sim_constants.gd") assert_eq(NetCodec.TICK_HZ, SimConstants.TICK_HZ, "NetCodec.TICK_HZ must track SimConstants.TICK_HZ")