forked from xiangwang25/whale-town-front-v2
- add network NPC synchronization and dialogue interactions - add world bulletin publishing and display - improve authentication, session refresh, and appearance sync - synchronize player direction and movement animations - improve input focus and progressive Web loading - add macOS/Windows builds and deployment configuration - include required fonts, shaders, and runtime assets
216 lines
8.2 KiB
GDScript
216 lines
8.2 KiB
GDScript
extends Node
|
|
|
|
# ============================================================================
|
|
# ApiClient.gd - 后端 JSON API 客户端
|
|
# ============================================================================
|
|
# 统一处理 WhaleTown 后端 HTTP JSON 请求、认证头和响应信封解析。
|
|
# ============================================================================
|
|
|
|
signal request_failed(endpoint: String, message: String)
|
|
|
|
const NetworkConfig = preload("res://_Core/utils/NetworkConfig.gd")
|
|
|
|
const REQUEST_TIMEOUT: float = 12.0
|
|
|
|
var _activeRequests: Array[HTTPRequest] = []
|
|
var _pendingAuthRetries: Array[Dictionary] = []
|
|
var _awaitingSessionRefresh: bool = false
|
|
|
|
func _ready() -> void:
|
|
call_deferred("_connect_auth_manager")
|
|
|
|
func _connect_auth_manager() -> void:
|
|
var authManager := get_node_or_null("/root/AuthManager")
|
|
if authManager != null and authManager.has_signal("session_refresh_completed") and not authManager.is_connected("session_refresh_completed", _on_session_refresh_completed):
|
|
authManager.connect("session_refresh_completed", _on_session_refresh_completed)
|
|
|
|
func _exit_tree() -> void:
|
|
for request in _activeRequests:
|
|
if is_instance_valid(request):
|
|
request.cancel_request()
|
|
request.queue_free()
|
|
_activeRequests.clear()
|
|
_pendingAuthRetries.clear()
|
|
|
|
func get_json(endpoint: String, callback: Callable, authenticated: bool = true) -> void:
|
|
request_json(endpoint, {}, callback, HTTPClient.METHOD_GET, authenticated)
|
|
|
|
func post_json(endpoint: String, payload: Dictionary, callback: Callable, authenticated: bool = true) -> void:
|
|
request_json(endpoint, payload, callback, HTTPClient.METHOD_POST, authenticated)
|
|
|
|
func patch_json(endpoint: String, payload: Dictionary, callback: Callable, authenticated: bool = true) -> void:
|
|
request_json(endpoint, payload, callback, HTTPClient.METHOD_PATCH, authenticated)
|
|
|
|
func put_json(endpoint: String, payload: Dictionary, callback: Callable, authenticated: bool = true) -> void:
|
|
request_json(endpoint, payload, callback, HTTPClient.METHOD_PUT, authenticated)
|
|
|
|
func request_json(endpoint: String, payload: Dictionary, callback: Callable, method: int = HTTPClient.METHOD_GET, authenticated: bool = true) -> void:
|
|
_request_json(endpoint, payload, callback, method, authenticated, 0)
|
|
|
|
func _request_json(endpoint: String, payload: Dictionary, callback: Callable, method: int, authenticated: bool, authRetryCount: int) -> void:
|
|
var request := HTTPRequest.new()
|
|
request.timeout = REQUEST_TIMEOUT
|
|
add_child(request)
|
|
_activeRequests.append(request)
|
|
|
|
request.request_completed.connect(func(result: int, responseCode: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
|
_activeRequests.erase(request)
|
|
request.queue_free()
|
|
_handle_response(endpoint, result, responseCode, body, callback, payload, method, authenticated, authRetryCount)
|
|
)
|
|
|
|
var url := "%s%s" % [NetworkConfig.get_api_base_url(), endpoint]
|
|
var body := "" if method == HTTPClient.METHOD_GET else JSON.stringify(payload)
|
|
var err := request.request(url, _headers(authenticated), method, body)
|
|
if err != OK:
|
|
_activeRequests.erase(request)
|
|
request.queue_free()
|
|
var message := "网络请求发送失败: %s" % error_string(err)
|
|
request_failed.emit(endpoint, message)
|
|
callback.call(false, {}, {"message": message})
|
|
|
|
func _headers(authenticated: bool) -> PackedStringArray:
|
|
var headers := PackedStringArray(["Content-Type: application/json"])
|
|
if authenticated:
|
|
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 ""
|
|
if not accessToken.is_empty():
|
|
headers.append("Authorization: Bearer %s" % accessToken)
|
|
return headers
|
|
|
|
func _handle_response(
|
|
endpoint: String,
|
|
result: int,
|
|
responseCode: int,
|
|
body: PackedByteArray,
|
|
callback: Callable,
|
|
payload: Dictionary,
|
|
method: int,
|
|
authenticated: bool,
|
|
authRetryCount: int
|
|
) -> void:
|
|
if result != HTTPRequest.RESULT_SUCCESS:
|
|
var message := "网络请求失败: %s" % _http_result_to_string(result)
|
|
request_failed.emit(endpoint, message)
|
|
callback.call(false, {}, {"message": message})
|
|
return
|
|
|
|
if responseCode == 401 and authenticated and authRetryCount < 1:
|
|
_queue_authenticated_retry(endpoint, payload, callback, method, authRetryCount + 1)
|
|
return
|
|
|
|
var bodyText := body.get_string_from_utf8()
|
|
var json := JSON.new()
|
|
if json.parse(bodyText) != OK:
|
|
var parseMessage := "服务器响应解析失败"
|
|
request_failed.emit(endpoint, parseMessage)
|
|
callback.call(false, {}, {"message": parseMessage, "response_code": responseCode})
|
|
return
|
|
|
|
var payloadVariant: Variant = json.data
|
|
if not (payloadVariant is Dictionary):
|
|
var formatMessage := "服务器响应格式错误"
|
|
request_failed.emit(endpoint, formatMessage)
|
|
callback.call(false, {}, {"message": formatMessage, "response_code": responseCode})
|
|
return
|
|
|
|
var response: Dictionary = payloadVariant
|
|
var ok := responseCode >= 200 and responseCode < 300 and bool(response.get("success", true))
|
|
if ok:
|
|
callback.call(true, response, {})
|
|
return
|
|
|
|
var errorInfo := {
|
|
"message": str(response.get("message", "请求失败")),
|
|
"response_code": responseCode,
|
|
"error_code": str(response.get("error_code", ""))
|
|
}
|
|
request_failed.emit(endpoint, str(errorInfo.get("message", "请求失败")))
|
|
callback.call(false, response, errorInfo)
|
|
|
|
func _queue_authenticated_retry(endpoint: String, payload: Dictionary, callback: Callable, method: int, authRetryCount: int) -> void:
|
|
_pendingAuthRetries.append({
|
|
"endpoint": endpoint,
|
|
"payload": payload.duplicate(true),
|
|
"callback": callback,
|
|
"method": method,
|
|
"auth_retry_count": authRetryCount,
|
|
"account_generation": _current_account_generation(),
|
|
})
|
|
if _awaitingSessionRefresh:
|
|
return
|
|
|
|
var authManager := get_node_or_null("/root/AuthManager")
|
|
if authManager == null or not authManager.has_method("refresh_session"):
|
|
_finish_authenticated_retries(false)
|
|
return
|
|
_awaitingSessionRefresh = true
|
|
if not bool(authManager.call("refresh_session")):
|
|
_finish_authenticated_retries(false)
|
|
|
|
func _on_session_refresh_completed(success: bool) -> void:
|
|
_finish_authenticated_retries(success)
|
|
|
|
func _finish_authenticated_retries(success: bool) -> void:
|
|
var retries := _pendingAuthRetries.duplicate()
|
|
_pendingAuthRetries.clear()
|
|
_awaitingSessionRefresh = false
|
|
var currentGeneration := _current_account_generation()
|
|
for retryVariant in retries:
|
|
if not (retryVariant is Dictionary):
|
|
continue
|
|
var retry: Dictionary = retryVariant
|
|
var callback: Callable = retry.get("callback", Callable())
|
|
if not callback.is_valid():
|
|
continue
|
|
if not success or int(retry.get("account_generation", -1)) != currentGeneration:
|
|
callback.call(false, {}, {
|
|
"message": "登录状态已过期,请重新登录",
|
|
"response_code": 401,
|
|
"error_code": "SESSION_EXPIRED",
|
|
})
|
|
continue
|
|
_request_json(
|
|
str(retry.get("endpoint", "")),
|
|
retry.get("payload", {}) as Dictionary,
|
|
callback,
|
|
int(retry.get("method", HTTPClient.METHOD_GET)),
|
|
true,
|
|
int(retry.get("auth_retry_count", 1))
|
|
)
|
|
|
|
func _current_account_generation() -> int:
|
|
var authManager := get_node_or_null("/root/AuthManager")
|
|
if authManager == null or not authManager.has_method("get_account_generation"):
|
|
return -1
|
|
return int(authManager.call("get_account_generation"))
|
|
|
|
func _http_result_to_string(result: int) -> String:
|
|
match result:
|
|
HTTPRequest.RESULT_CHUNKED_BODY_SIZE_MISMATCH:
|
|
return "响应分块大小不匹配"
|
|
HTTPRequest.RESULT_CANT_CONNECT:
|
|
return "无法连接服务器"
|
|
HTTPRequest.RESULT_CANT_RESOLVE:
|
|
return "无法解析服务器地址"
|
|
HTTPRequest.RESULT_CONNECTION_ERROR:
|
|
return "连接中断"
|
|
HTTPRequest.RESULT_TLS_HANDSHAKE_ERROR:
|
|
return "TLS握手失败"
|
|
HTTPRequest.RESULT_NO_RESPONSE:
|
|
return "服务器无响应"
|
|
HTTPRequest.RESULT_BODY_SIZE_LIMIT_EXCEEDED:
|
|
return "响应内容过大"
|
|
HTTPRequest.RESULT_REQUEST_FAILED:
|
|
return "请求失败"
|
|
HTTPRequest.RESULT_DOWNLOAD_FILE_CANT_OPEN:
|
|
return "下载文件无法打开"
|
|
HTTPRequest.RESULT_DOWNLOAD_FILE_WRITE_ERROR:
|
|
return "下载文件写入失败"
|
|
HTTPRequest.RESULT_REDIRECT_LIMIT_REACHED:
|
|
return "重定向次数过多"
|
|
HTTPRequest.RESULT_TIMEOUT:
|
|
return "请求超时"
|
|
_:
|
|
return "未知错误 %s" % result
|