extends Node signal decor_save_succeeded(item: Dictionary) signal decor_save_failed(item: Dictionary, message: String) const NetworkConfig = preload("res://_Core/utils/NetworkConfig.gd") const REQUEST_TIMEOUT: float = 12.0 const MAX_SAVE_RETRIES: int = 3 const RETRY_BASE_DELAY: float = 0.75 var _request: HTTPRequest var _retryTimer: Timer var _queue: Array[Dictionary] = [] var _inFlight: Dictionary = {} var _requestGeneration: int = -1 var _accountGeneration: int = -1 func _ready() -> void: _request = HTTPRequest.new() _request.name = "roomDecorSaveRequest" _request.timeout = REQUEST_TIMEOUT _request.request_completed.connect(_on_request_completed) add_child(_request) _retryTimer = Timer.new() _retryTimer.name = "roomDecorSaveRetryTimer" _retryTimer.one_shot = true _retryTimer.timeout.connect(_process_next_save) add_child(_retryTimer) var authManager := get_node_or_null("/root/AuthManager") if authManager != null and authManager.has_signal("auth_state_changed"): _accountGeneration = _current_account_generation() authManager.auth_state_changed.connect(_on_auth_state_changed) func enqueue_save(item: Dictionary) -> bool: if not _is_authenticated(): decor_save_failed.emit(item.duplicate(true), "请先登录后保存摆放") return false var decorId := str(item.get("decor_id", "")).strip_edges() if decorId.is_empty(): decor_save_failed.emit(item.duplicate(true), "家具数据缺少 decor_id") return false # Only the newest queued placement for a decor matters. for index in range(_queue.size() - 1, -1, -1): var queuedItem: Dictionary = _queue[index].get("item", {}) if str(queuedItem.get("decor_id", "")) == decorId: _queue.remove_at(index) _queue.append({"item": item.duplicate(true), "retry_count": 0}) _process_next_save() return true func has_pending_saves() -> bool: return not _inFlight.is_empty() or not _queue.is_empty() or (_retryTimer != null and not _retryTimer.is_stopped()) func _process_next_save() -> void: if not _inFlight.is_empty() or _queue.is_empty() or not _is_authenticated(): return if _retryTimer != null and not _retryTimer.is_stopped(): return _inFlight = _queue.pop_front() _requestGeneration = _current_account_generation() var item: Dictionary = _inFlight.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))), } var decorId := str(item.get("decor_id", "")).uri_encode() var err := _request.request( "%s/rooms/me/decor-placements/%s" % [NetworkConfig.get_api_base_url(), decorId], _auth_headers(), HTTPClient.METHOD_PUT, JSON.stringify(payload) ) if err != OK: _retry_or_fail("家具位置保存请求发送失败") func _on_request_completed(result: int, responseCode: int, _headers: PackedStringArray, body: PackedByteArray) -> void: if _inFlight.is_empty(): return if _requestGeneration != _current_account_generation(): _inFlight.clear() _requestGeneration = -1 _process_next_save() return if result != HTTPRequest.RESULT_SUCCESS or responseCode < 200 or responseCode >= 300: _retry_or_fail(_read_error_message(body, "家具位置保存失败")) return var savedItem: Dictionary = (_inFlight.get("item", {}) as Dictionary).duplicate(true) var json := JSON.new() if json.parse(body.get_string_from_utf8()) == OK and json.data is Dictionary: var response: Dictionary = json.data if not bool(response.get("success", true)): _retry_or_fail(str(response.get("message", "家具位置保存失败"))) return var dataVariant: Variant = response.get("data", {}) if dataVariant is Dictionary: savedItem = (dataVariant as Dictionary).duplicate(true) _inFlight.clear() _requestGeneration = -1 decor_save_succeeded.emit(savedItem) _process_next_save() func _retry_or_fail(message: String) -> void: if _inFlight.is_empty(): return var failedEntry := _inFlight.duplicate(true) var failedItem: Dictionary = failedEntry.get("item", {}) var decorId := str(failedItem.get("decor_id", "")) _inFlight.clear() _requestGeneration = -1 if _queue_has_decor(decorId): _process_next_save() return var retryCount := int(failedEntry.get("retry_count", 0)) + 1 if retryCount <= MAX_SAVE_RETRIES and _is_authenticated(): failedEntry["retry_count"] = retryCount _queue.push_front(failedEntry) _retryTimer.start(RETRY_BASE_DELAY * pow(2.0, retryCount - 1)) return decor_save_failed.emit(failedItem.duplicate(true), message) _process_next_save() func _queue_has_decor(decorId: String) -> bool: for entryVariant in _queue: if entryVariant is Dictionary: var item: Dictionary = (entryVariant as Dictionary).get("item", {}) if str(item.get("decor_id", "")) == decorId: return true return false func _on_auth_state_changed(_isAuthenticated: bool, _user: Dictionary) -> void: var currentGeneration := _current_account_generation() if currentGeneration == _accountGeneration: return _accountGeneration = currentGeneration if is_instance_valid(_request): _request.cancel_request() if is_instance_valid(_retryTimer): _retryTimer.stop() _queue.clear() _inFlight.clear() _requestGeneration = -1 func _auth_headers() -> PackedStringArray: var authManager := get_node_or_null("/root/AuthManager") var accessToken := str(authManager.call("get_access_token")).strip_edges() if authManager != null and authManager.has_method("get_access_token") else "" return PackedStringArray([ "Content-Type: application/json", "Authorization: Bearer %s" % accessToken, ]) func _is_authenticated() -> bool: var authManager := get_node_or_null("/root/AuthManager") return authManager != null and authManager.has_method("is_authenticated") and bool(authManager.call("is_authenticated")) func _current_account_generation() -> int: var authManager := get_node_or_null("/root/AuthManager") return int(authManager.call("get_account_generation")) if authManager != null and authManager.has_method("get_account_generation") else -1 func _read_error_message(body: PackedByteArray, fallback: String) -> String: var json := JSON.new() if json.parse(body.get_string_from_utf8()) != OK or not (json.data is Dictionary): return fallback var response: Dictionary = json.data var messageVariant: Variant = response.get("message", fallback) if messageVariant is Array: var parts: Array[String] = [] for part in messageVariant: parts.append(str(part)) return "; ".join(parts) if not parts.is_empty() else fallback var message := str(messageVariant).strip_edges() return message if not message.is_empty() else fallback