diff --git a/client/scripts/cooldown_state.gd b/client/scripts/cooldown_state.gd index 9fc96f0..d52f93a 100644 --- a/client/scripts/cooldown_state.gd +++ b/client/scripts/cooldown_state.gd @@ -4,6 +4,8 @@ extends RefCounted ## Uses [code]serverTimeUtc[/code] + receive tick to approximate server "now" between polls. const SLOT_COUNT: int = 8 +## Reject bogus [method Time.get_unix_time_from_datetime_string] results (failed parse → 0). +const MIN_VALID_UNIX_ANCHOR: float = 1_000_000_000.0 var _slot_end_unix: PackedFloat64Array = PackedFloat64Array() var _server_unix_at_receive: float = 0.0 @@ -19,11 +21,19 @@ func _init() -> void: func apply_snapshot(snapshot: Dictionary) -> void: for i in range(SLOT_COUNT): _slot_end_unix[i] = 0.0 - var st: Variant = snapshot.get("serverTimeUtc", "") + var st: Variant = snapshot.get("serverTimeUtc", null) + var anchor: float = 0.0 if st is String: - var su: String = st as String + var su: String = (st as String).strip_edges() if not su.is_empty(): - _server_unix_at_receive = Time.get_unix_time_from_datetime_string(su) + anchor = Time.get_unix_time_from_datetime_string(su) + elif st is float: + anchor = st as float + elif st is int: + anchor = float(st as int) + if anchor < MIN_VALID_UNIX_ANCHOR: + anchor = Time.get_unix_time_from_system() + _server_unix_at_receive = anchor _ticks_msec_at_receive = Time.get_ticks_msec() var slots: Variant = snapshot.get("slots", []) if slots is Array: @@ -36,8 +46,10 @@ func apply_snapshot(snapshot: Dictionary) -> void: continue var end_v: Variant = d.get("cooldownEndsAtUtc", null) if end_v is String and not (end_v as String).is_empty(): - var end_s: String = end_v as String - _slot_end_unix[idx] = Time.get_unix_time_from_datetime_string(end_s) + var end_s: String = (end_v as String).strip_edges() + var end_unix: float = Time.get_unix_time_from_datetime_string(end_s) + if end_unix >= MIN_VALID_UNIX_ANCHOR: + _slot_end_unix[idx] = end_unix func _effective_server_unix() -> float: diff --git a/client/scripts/main.gd b/client/scripts/main.gd index 26fc43e..08ac2e4 100644 --- a/client/scripts/main.gd +++ b/client/scripts/main.gd @@ -149,7 +149,7 @@ func _physics_process(_delta: float) -> void: var p: Vector3 = _player.global_position _player_pos_label.text = _build_player_position_text(p) _render_target_lock_label(p) - if is_instance_valid(_cooldown_state) and _cooldown_state.has_method("any_slot_cooling"): + if _cooldown_state != null and _cooldown_state.has_method("any_slot_cooling"): if bool(_cooldown_state.call("any_slot_cooling")): _render_cooldown_slots_label() @@ -317,7 +317,7 @@ func _on_cooldown_poll_timeout() -> void: func _on_cooldown_snapshot_received(snapshot: Dictionary) -> void: - if not is_instance_valid(_cooldown_state) or not _cooldown_state.has_method("apply_snapshot"): + if _cooldown_state == null or not _cooldown_state.has_method("apply_snapshot"): return _cooldown_state.call("apply_snapshot", snapshot) _render_cooldown_slots_label() @@ -334,7 +334,7 @@ func _on_cooldown_snapshot_received(snapshot: Dictionary) -> void: func _render_cooldown_slots_label() -> void: if not is_instance_valid(_cooldown_slots_label): return - if not is_instance_valid(_cooldown_state) or not _cooldown_state.has_method("build_hud_line"): + if _cooldown_state == null or not _cooldown_state.has_method("build_hud_line"): return var line: Variant = _cooldown_state.call("build_hud_line") if line is String: @@ -466,7 +466,7 @@ func _request_hotbar_cast_slot(slot_index: int) -> void: var ability_id: String = outcome[HotbarCastSlotResolver.KEY_ABILITY_ID] as String var resolved_slot: int = int(outcome.get(HotbarCastSlotResolver.KEY_SLOT_INDEX, slot_index)) var target_id: Variant = outcome.get(HotbarCastSlotResolver.KEY_TARGET_ID, null) - if is_instance_valid(_cooldown_state) and _cooldown_state.has_method("is_slot_cooling"): + if _cooldown_state != null and _cooldown_state.has_method("is_slot_cooling"): if bool(_cooldown_state.call("is_slot_cooling", resolved_slot)): if is_instance_valid(_cast_feedback_label): _cast_feedback_label.text = "ability_cast_denied: %s (client guard)" % COOLDOWN_REASON_ON_CD diff --git a/client/test/cooldown_state_test.gd b/client/test/cooldown_state_test.gd index 75a901e..c23133b 100644 --- a/client/test/cooldown_state_test.gd +++ b/client/test/cooldown_state_test.gd @@ -34,3 +34,16 @@ func test_remaining_positive_when_end_after_server_time() -> void: # Assert assert_that(rem).is_greater(9.0) assert_that(rem).is_less(11.0) + + +func test_invalid_server_time_string_does_not_lock_slot_ready() -> void: + # Arrange: bogus ISO would parse as 0 in Godot; anchor must fall back so slot 0 is not stuck cooling. + var state: RefCounted = CooldownStateScript.new() + var snap := {"serverTimeUtc": "__not_iso__", "slots": [{"slotIndex": 0}]} + + # Act + state.call("apply_snapshot", snap) + var cooling: bool = bool(state.call("is_slot_cooling", 0)) + + # Assert + assert_that(cooling).is_false()