From 6fac224d55c057810516d7500bb3b9c1d177bfb4 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sat, 28 Feb 2026 23:24:46 +0100 Subject: [PATCH] fix(client): prevent signed overflow in world_seed generation and load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GDScript int is i64 — when randi() returns a value with bit 31 set, left-shifting by 32 sets bit 63, producing a negative i64. MessagePack encodes this as a negative integer, which Rust rmp_serde rejects when deserializing as u64, causing ~50% startup failure rate. Fix: mask bit 31 before shifting in new_game() to cap entropy at 63 bits. Also mask the sign bit in _read_seed_file() to handle save files written before this fix. Co-Authored-By: Claude Opus 4.6 --- client/scripts/autoloads/session_manager.gd | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/client/scripts/autoloads/session_manager.gd b/client/scripts/autoloads/session_manager.gd index cba63a5d9..d480ae0ce 100644 --- a/client/scripts/autoloads/session_manager.gd +++ b/client/scripts/autoloads/session_manager.gd @@ -33,9 +33,11 @@ func new_game() -> String: GameState.current_game_id = game_id # #175: Generate world_seed for deterministic simulation (D-010, D-029). - # Combines two randi() calls (u32 each) into full u64 entropy range. - # Without this, upper 32 bits are always zero — halving the seed space. - GameState.world_seed = (rng.randi() << 32) | rng.randi() + # Combines two randi() calls (u32 each) into 63-bit entropy range. + # Mask bit 31 of the upper word before shifting to prevent signed overflow: + # GDScript int is i64 — if bit 63 is set, MessagePack encodes as negative, + # and Rust rmp_serde rejects negative values when deserializing as u64. + GameState.world_seed = ((rng.randi() & 0x7FFFFFFF) << 32) | rng.randi() # Persist world_seed to save directory so resume_game() can restore it. # Without this, loaded sessions would send seed=0, breaking D-010 determinism. @@ -134,12 +136,14 @@ func _write_seed_file(save_path: String, seed: int) -> void: ## Read world_seed from save directory. Returns 0 if file missing (legacy saves). +## Masks the sign bit on read: save files written before the signed-overflow fix +## may contain negative i64 values that Rust rmp_serde rejects as u64. func _read_seed_file(save_path: String) -> int: var file := FileAccess.open(save_path + "world_seed", FileAccess.READ) if file == null: push_warning("SessionManager: no seed file in %s — using seed=0 (legacy save)" % save_path) return 0 - return file.get_64() + return file.get_64() & 0x7FFFFFFFFFFFFFFF func _find_newest_save(dir_path: String) -> String: