190 lines
6.9 KiB
GDScript
190 lines
6.9 KiB
GDScript
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()
|