NEO-32: fix cooldown HUD anchor when serverTimeUtc parse fails

pull/59/head
VinPropane 2026-04-30 21:45:25 -04:00
parent f4ba4cdd92
commit 56f7ae8a81
3 changed files with 34 additions and 9 deletions

View File

@ -4,6 +4,8 @@ extends RefCounted
## Uses [code]serverTimeUtc[/code] + receive tick to approximate server "now" between polls. ## Uses [code]serverTimeUtc[/code] + receive tick to approximate server "now" between polls.
const SLOT_COUNT: int = 8 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 _slot_end_unix: PackedFloat64Array = PackedFloat64Array()
var _server_unix_at_receive: float = 0.0 var _server_unix_at_receive: float = 0.0
@ -19,11 +21,19 @@ func _init() -> void:
func apply_snapshot(snapshot: Dictionary) -> void: func apply_snapshot(snapshot: Dictionary) -> void:
for i in range(SLOT_COUNT): for i in range(SLOT_COUNT):
_slot_end_unix[i] = 0.0 _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: if st is String:
var su: String = st as String var su: String = (st as String).strip_edges()
if not su.is_empty(): 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() _ticks_msec_at_receive = Time.get_ticks_msec()
var slots: Variant = snapshot.get("slots", []) var slots: Variant = snapshot.get("slots", [])
if slots is Array: if slots is Array:
@ -36,8 +46,10 @@ func apply_snapshot(snapshot: Dictionary) -> void:
continue continue
var end_v: Variant = d.get("cooldownEndsAtUtc", null) var end_v: Variant = d.get("cooldownEndsAtUtc", null)
if end_v is String and not (end_v as String).is_empty(): if end_v is String and not (end_v as String).is_empty():
var end_s: String = end_v as String var end_s: String = (end_v as String).strip_edges()
_slot_end_unix[idx] = Time.get_unix_time_from_datetime_string(end_s) 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: func _effective_server_unix() -> float:

View File

@ -149,7 +149,7 @@ func _physics_process(_delta: float) -> void:
var p: Vector3 = _player.global_position var p: Vector3 = _player.global_position
_player_pos_label.text = _build_player_position_text(p) _player_pos_label.text = _build_player_position_text(p)
_render_target_lock_label(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")): if bool(_cooldown_state.call("any_slot_cooling")):
_render_cooldown_slots_label() _render_cooldown_slots_label()
@ -317,7 +317,7 @@ func _on_cooldown_poll_timeout() -> void:
func _on_cooldown_snapshot_received(snapshot: Dictionary) -> 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 return
_cooldown_state.call("apply_snapshot", snapshot) _cooldown_state.call("apply_snapshot", snapshot)
_render_cooldown_slots_label() _render_cooldown_slots_label()
@ -334,7 +334,7 @@ func _on_cooldown_snapshot_received(snapshot: Dictionary) -> void:
func _render_cooldown_slots_label() -> void: func _render_cooldown_slots_label() -> void:
if not is_instance_valid(_cooldown_slots_label): if not is_instance_valid(_cooldown_slots_label):
return 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 return
var line: Variant = _cooldown_state.call("build_hud_line") var line: Variant = _cooldown_state.call("build_hud_line")
if line is String: 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 ability_id: String = outcome[HotbarCastSlotResolver.KEY_ABILITY_ID] as String
var resolved_slot: int = int(outcome.get(HotbarCastSlotResolver.KEY_SLOT_INDEX, slot_index)) var resolved_slot: int = int(outcome.get(HotbarCastSlotResolver.KEY_SLOT_INDEX, slot_index))
var target_id: Variant = outcome.get(HotbarCastSlotResolver.KEY_TARGET_ID, null) 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 bool(_cooldown_state.call("is_slot_cooling", resolved_slot)):
if is_instance_valid(_cast_feedback_label): if is_instance_valid(_cast_feedback_label):
_cast_feedback_label.text = "ability_cast_denied: %s (client guard)" % COOLDOWN_REASON_ON_CD _cast_feedback_label.text = "ability_cast_denied: %s (client guard)" % COOLDOWN_REASON_ON_CD

View File

@ -34,3 +34,16 @@ func test_remaining_positive_when_end_after_server_time() -> void:
# Assert # Assert
assert_that(rem).is_greater(9.0) assert_that(rem).is_greater(9.0)
assert_that(rem).is_less(11.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()