extends Node signal decor_save_succeeded(item: Dictionary) signal decor_save_failed(item: Dictionary, message: String) signal decor_revision_conflict(current_revision: int, message: String) const MAX_SAVE_RETRIES: int = 3 const RETRY_BASE_DELAY: float = 0.75 var _retry_timer: Timer var _queue: Array[Dictionary] = [] var _in_flight: Dictionary = {} var _account_generation: int = -1 var _request_serial: int = 0 var _confirmed_revision: int = 0 func _ready() -> void: _retry_timer = Timer.new() _retry_timer.name = "roomDecorSaveRetryTimer" _retry_timer.one_shot = true _retry_timer.timeout.connect(_process_next_save) add_child(_retry_timer) var auth_manager := get_node_or_null("/root/AuthManager") if auth_manager != null and auth_manager.has_signal("auth_state_changed"): _account_generation = _current_account_generation() auth_manager.auth_state_changed.connect(_on_auth_state_changed) func set_layout_revision(revision: int, clear_pending: bool = true) -> void: if clear_pending: clear_pending_saves() _confirmed_revision = maxi(0, revision) func get_layout_revision() -> int: return _confirmed_revision func clear_pending_saves() -> void: _request_serial += 1 if is_instance_valid(_retry_timer): _retry_timer.stop() _queue.clear() _in_flight.clear() func enqueue_save(item: Dictionary) -> bool: if not _is_authenticated(): decor_save_failed.emit(item.duplicate(true), "请先登录后保存摆放") return false var decor_id := str(item.get("decor_id", "")).strip_edges() if decor_id.is_empty(): decor_save_failed.emit(item.duplicate(true), "家具数据缺少 decor_id") return false for index in range(_queue.size() - 1, -1, -1): var queued_item: Dictionary = _queue[index].get("item", {}) if str(queued_item.get("decor_id", "")) == decor_id: _queue.remove_at(index) _queue.append({ "item": item.duplicate(true), "retry_count": 0, "mutation_id": _create_mutation_id(), }) _process_next_save() return true func has_pending_saves() -> bool: return not _in_flight.is_empty() or not _queue.is_empty() or (_retry_timer != null and not _retry_timer.is_stopped()) func _process_next_save() -> void: if not _in_flight.is_empty() or _queue.is_empty() or not _is_authenticated(): return if _retry_timer != null and not _retry_timer.is_stopped(): return _in_flight = _queue.pop_front() if not _in_flight.has("layout_revision"): _in_flight["layout_revision"] = _confirmed_revision _in_flight["mutation_revision"] = _confirmed_revision + 1 var item: Dictionary = _in_flight.get("item", {}) var payload := { "decor_id": str(item.get("decor_id", "")), "placed": bool(item.get("placed", false)), "position_x": float(item.get("position_x", 0.0)), "position_y": float(item.get("position_y", 0.0)), "scale": float(item.get("scale", item.get("default_scale", 1.0))), "rotation_degrees": int(item.get("rotation_degrees", item.get("default_rotation_degrees", 0))), "z_index": int(item.get("z_index", item.get("default_z_index", 0))), "layout_revision": int(_in_flight.get("layout_revision", _confirmed_revision)), "mutation_revision": int(_in_flight.get("mutation_revision", _confirmed_revision + 1)), "mutation_id": str(_in_flight.get("mutation_id", "")), } var api_client := get_node_or_null("/root/ApiClient") if api_client == null or not api_client.has_method("put_json"): _retry_or_fail("家具位置保存服务不可用") return _request_serial += 1 var serial := _request_serial var generation := _current_account_generation() var decor_id := str(item.get("decor_id", "")).uri_encode() api_client.call( "put_json", "/rooms/me/decor-placements/%s" % decor_id, payload, Callable(self, "_on_save_response").bind(serial, generation), true ) func _on_save_response( success: bool, response: Dictionary, error_info: Dictionary, serial: int, generation: int ) -> void: if serial != _request_serial or _in_flight.is_empty(): return if generation != _current_account_generation(): clear_pending_saves() return if not success: var message := str(error_info.get("message", "家具位置保存失败")) if int(error_info.get("response_code", 0)) == 409: _handle_revision_conflict(error_info, message) return _retry_or_fail(message) return var saved_item: Dictionary = (_in_flight.get("item", {}) as Dictionary).duplicate(true) var data_variant: Variant = response.get("data", {}) if data_variant is Dictionary: saved_item = (data_variant as Dictionary).duplicate(true) var accepted_revision := int(saved_item.get("layout_revision", _in_flight.get("mutation_revision", _confirmed_revision))) _confirmed_revision = maxi(_confirmed_revision, accepted_revision) _in_flight.clear() decor_save_succeeded.emit(saved_item) _process_next_save() func _retry_or_fail(message: String) -> void: if _in_flight.is_empty(): return var failed_entry := _in_flight.duplicate(true) var failed_item: Dictionary = failed_entry.get("item", {}) _in_flight.clear() var retry_count := int(failed_entry.get("retry_count", 0)) + 1 if retry_count <= MAX_SAVE_RETRIES and _is_authenticated(): failed_entry["retry_count"] = retry_count _queue.push_front(failed_entry) _retry_timer.start(RETRY_BASE_DELAY * pow(2.0, retry_count - 1)) return decor_save_failed.emit(failed_item.duplicate(true), message) _fail_queued_entries("前序装修写入失败,请重新保存") func _handle_revision_conflict(error_info: Dictionary, message: String) -> void: var current_revision := int(error_info.get("current_revision", _confirmed_revision)) var failed_item: Dictionary = (_in_flight.get("item", {}) as Dictionary).duplicate(true) _in_flight.clear() decor_save_failed.emit(failed_item, message) _fail_queued_entries(message) _confirmed_revision = maxi(_confirmed_revision, current_revision) decor_revision_conflict.emit(current_revision, message) func _fail_queued_entries(message: String) -> void: for entry in _queue: var item_variant: Variant = entry.get("item", {}) if item_variant is Dictionary: decor_save_failed.emit((item_variant as Dictionary).duplicate(true), message) _queue.clear() if is_instance_valid(_retry_timer): _retry_timer.stop() func _on_auth_state_changed(_is_authenticated_value: bool, _user: Dictionary) -> void: var current_generation := _current_account_generation() if current_generation == _account_generation: return _account_generation = current_generation clear_pending_saves() _confirmed_revision = 0 func _is_authenticated() -> bool: var auth_manager := get_node_or_null("/root/AuthManager") return auth_manager != null and auth_manager.has_method("is_authenticated") and bool(auth_manager.call("is_authenticated")) func _current_account_generation() -> int: var auth_manager := get_node_or_null("/root/AuthManager") return int(auth_manager.call("get_account_generation")) if auth_manager != null and auth_manager.has_method("get_account_generation") else -1 func _create_mutation_id() -> String: return Crypto.new().generate_random_bytes(16).hex_encode()