184 lines
6.7 KiB
GDScript
184 lines
6.7 KiB
GDScript
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))),
|
|
"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
|