fix(client): prevent signed overflow in world_seed generation and load

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 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 23:24:46 +01:00
co-authored by Claude Opus 4.6
parent 50aba3adf6
commit 6fac224d55
+8 -4
View File
@@ -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: