feat: expand multiplayer, chat, and release support
- 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
This commit is contained in:
@@ -16,6 +16,9 @@ signal email_verification_sent(email: String)
|
||||
signal email_verification_failed(message: String)
|
||||
signal profile_update_succeeded(profile: Dictionary)
|
||||
signal profile_update_failed(message: String)
|
||||
signal appearance_update_succeeded(skin_id: String, profile: Dictionary)
|
||||
signal appearance_update_failed(skin_id: String, message: String)
|
||||
signal session_refresh_completed(success: bool)
|
||||
signal logout_completed()
|
||||
signal browser_bootstrap_received(kind: String)
|
||||
|
||||
@@ -25,6 +28,8 @@ const DEFAULT_AUTH_CONFIG_PATH: String = "user://auth.cfg"
|
||||
const REQUEST_TIMEOUT: float = 12.0
|
||||
const BROWSER_BOOTSTRAP_STORAGE_KEY: String = "whaletown.auth.bootstrap"
|
||||
const BROWSER_BOOTSTRAP_POLL_INTERVAL: float = 0.25
|
||||
const SESSION_REFRESH_CHECK_INTERVAL: float = 15.0
|
||||
const SESSION_REFRESH_LEAD_TIME: float = 120.0
|
||||
|
||||
var _access_token: String = ""
|
||||
var _refresh_token: String = ""
|
||||
@@ -41,19 +46,27 @@ var _registration_recovery_in_flight: bool = false
|
||||
var _registration_recovery_error: String = ""
|
||||
var _browser_bootstrap_kind: String = ""
|
||||
var _browser_bootstrap_poll_elapsed: float = 0.0
|
||||
var _session_refresh_check_elapsed: float = 0.0
|
||||
var _access_token_expires_at: float = 0.0
|
||||
|
||||
func _ready() -> void:
|
||||
_load_cached_session()
|
||||
_try_import_browser_bootstrap(false)
|
||||
if is_authenticated() and _token_needs_refresh():
|
||||
call_deferred("refresh_session")
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if OS.get_name() != "Web" or not _browser_bootstrap_kind.is_empty():
|
||||
return
|
||||
_browser_bootstrap_poll_elapsed += delta
|
||||
if _browser_bootstrap_poll_elapsed < BROWSER_BOOTSTRAP_POLL_INTERVAL:
|
||||
return
|
||||
_browser_bootstrap_poll_elapsed = 0.0
|
||||
_try_import_browser_bootstrap(true)
|
||||
_session_refresh_check_elapsed += delta
|
||||
if _session_refresh_check_elapsed >= SESSION_REFRESH_CHECK_INTERVAL:
|
||||
_session_refresh_check_elapsed = 0.0
|
||||
if is_authenticated() and _token_needs_refresh():
|
||||
refresh_session()
|
||||
|
||||
if OS.get_name() == "Web" and _browser_bootstrap_kind.is_empty():
|
||||
_browser_bootstrap_poll_elapsed += delta
|
||||
if _browser_bootstrap_poll_elapsed >= BROWSER_BOOTSTRAP_POLL_INTERVAL:
|
||||
_browser_bootstrap_poll_elapsed = 0.0
|
||||
_try_import_browser_bootstrap(true)
|
||||
|
||||
func _exit_tree() -> void:
|
||||
for request in _active_requests:
|
||||
@@ -112,8 +125,12 @@ func login(identifier: String, password: String) -> void:
|
||||
"password": password
|
||||
}, _on_login_response, HTTPClient.METHOD_POST, false, true)
|
||||
|
||||
func send_email_verification(email: String) -> void:
|
||||
func send_email_verification(email: String, invitation_code: String = "") -> void:
|
||||
var normalized_email := email.strip_edges()
|
||||
var normalized_invitation_code := invitation_code.strip_edges().to_upper()
|
||||
if normalized_invitation_code.is_empty():
|
||||
email_verification_failed.emit("请输入邀请码")
|
||||
return
|
||||
if normalized_email.is_empty():
|
||||
email_verification_failed.emit("请输入邮箱")
|
||||
return
|
||||
@@ -122,7 +139,8 @@ func send_email_verification(email: String) -> void:
|
||||
return
|
||||
|
||||
_request_json("/auth/send-email-verification", {
|
||||
"email": normalized_email
|
||||
"email": normalized_email,
|
||||
"invitation_code": normalized_invitation_code
|
||||
}, func(success: bool, _data: Dictionary, error_info: Dictionary) -> void:
|
||||
if success:
|
||||
email_verification_sent.emit(normalized_email)
|
||||
@@ -130,11 +148,12 @@ func send_email_verification(email: String) -> void:
|
||||
email_verification_failed.emit(str(error_info.get("message", "验证码发送失败")))
|
||||
)
|
||||
|
||||
func register(username: String, password: String, nickname: String = "", email: String = "", email_verification_code: String = "", skin_id: String = "") -> void:
|
||||
func register(username: String, password: String, nickname: String = "", email: String = "", email_verification_code: String = "", skin_id: String = "", invitation_code: String = "") -> void:
|
||||
var normalized_username := username.strip_edges()
|
||||
var normalized_nickname := nickname.strip_edges()
|
||||
var normalized_email := email.strip_edges()
|
||||
var normalized_code := email_verification_code.strip_edges()
|
||||
var normalized_invitation_code := invitation_code.strip_edges().to_upper()
|
||||
|
||||
if normalized_username.is_empty():
|
||||
register_failed.emit("请输入用户名")
|
||||
@@ -142,6 +161,9 @@ func register(username: String, password: String, nickname: String = "", email:
|
||||
if not _is_valid_username(normalized_username):
|
||||
register_failed.emit("用户名只能包含字母、数字和下划线,长度 1-50")
|
||||
return
|
||||
if normalized_invitation_code.is_empty():
|
||||
register_failed.emit("请输入邀请码")
|
||||
return
|
||||
if password.length() < 8:
|
||||
register_failed.emit("密码至少 8 位,并需要包含字母和数字")
|
||||
return
|
||||
@@ -165,7 +187,8 @@ func register(username: String, password: String, nickname: String = "", email:
|
||||
"password": password,
|
||||
"nickname": normalized_nickname,
|
||||
"email": normalized_email,
|
||||
"email_verification_code": normalized_code
|
||||
"email_verification_code": normalized_code,
|
||||
"invitation_code": normalized_invitation_code
|
||||
}
|
||||
var normalized_skin_id := skin_id.strip_edges()
|
||||
if not normalized_skin_id.is_empty():
|
||||
@@ -197,10 +220,11 @@ func update_profile(profile_data: Dictionary) -> void:
|
||||
assetPayload.erase("skin_id")
|
||||
assetPayload.erase("settings")
|
||||
if profile_data.has("skin_id") and playerStateManager.has_method("update_appearance"):
|
||||
var requestedSkinId := str(profile_data.get("skin_id", "")).strip_edges()
|
||||
playerStateManager.call(
|
||||
"update_appearance",
|
||||
str(profile_data.get("skin_id", "")),
|
||||
Callable(self, "_on_profile_response")
|
||||
requestedSkinId,
|
||||
Callable(self, "_on_appearance_profile_response").bind(requestedSkinId)
|
||||
)
|
||||
if profile_data.has("settings") and playerStateManager.has_method("update_settings"):
|
||||
var settingsVariant: Variant = profile_data.get("settings", {})
|
||||
@@ -227,14 +251,17 @@ func update_profile(profile_data: Dictionary) -> void:
|
||||
if not profile_data.has("skin_id") and not profile_data.has("settings"):
|
||||
profile_update_failed.emit("没有可保存的玩家资料")
|
||||
|
||||
func refresh_session() -> void:
|
||||
if _refresh_token.strip_edges().is_empty() or _refresh_in_flight:
|
||||
return
|
||||
func refresh_session() -> bool:
|
||||
if _refresh_token.strip_edges().is_empty():
|
||||
return false
|
||||
if _refresh_in_flight:
|
||||
return true
|
||||
|
||||
_refresh_in_flight = true
|
||||
_request_json("/auth/refresh-token", {
|
||||
"refresh_token": _refresh_token
|
||||
}, _on_refresh_response, HTTPClient.METHOD_POST, false, true)
|
||||
return true
|
||||
|
||||
func logout() -> void:
|
||||
_clear_session_memory()
|
||||
@@ -388,10 +415,12 @@ func _clear_pending_registration() -> void:
|
||||
func _on_refresh_response(success: bool, data: Dictionary, _error_info: Dictionary) -> void:
|
||||
_refresh_in_flight = false
|
||||
if not success:
|
||||
session_refresh_completed.emit(false)
|
||||
logout()
|
||||
return
|
||||
|
||||
_apply_auth_payload(data)
|
||||
session_refresh_completed.emit(true)
|
||||
auth_state_changed.emit(true, get_current_user())
|
||||
_refresh_player_snapshot()
|
||||
|
||||
@@ -403,6 +432,11 @@ func _apply_auth_payload(payload: Dictionary) -> void:
|
||||
var auth_data: Dictionary = data_variant
|
||||
_access_token = str(auth_data.get("access_token", "")).strip_edges()
|
||||
_refresh_token = str(auth_data.get("refresh_token", _refresh_token)).strip_edges()
|
||||
_access_token_expires_at = _jwt_expiration_time(_access_token)
|
||||
if _access_token_expires_at <= 0.0:
|
||||
var expiresIn := float(auth_data.get("expires_in", 0.0))
|
||||
if expiresIn > 0.0:
|
||||
_access_token_expires_at = Time.get_unix_time_from_system() + expiresIn
|
||||
|
||||
var user_variant: Variant = auth_data.get("user", {})
|
||||
if user_variant is Dictionary:
|
||||
@@ -466,6 +500,36 @@ func _on_profile_response(success: bool, data: Dictionary, _error_info: Dictiona
|
||||
profile_update_succeeded.emit(get_current_profile())
|
||||
auth_state_changed.emit(is_authenticated(), get_current_user())
|
||||
|
||||
func _on_appearance_profile_response(
|
||||
success: bool,
|
||||
data: Dictionary,
|
||||
errorInfo: Dictionary,
|
||||
requestedSkinId: String
|
||||
) -> void:
|
||||
if not success:
|
||||
var message := str(errorInfo.get("message", "玩家资料保存失败"))
|
||||
appearance_update_failed.emit(requestedSkinId, message)
|
||||
profile_update_failed.emit(message)
|
||||
auth_state_changed.emit(is_authenticated(), get_current_user())
|
||||
return
|
||||
|
||||
var dataVariant: Variant = data.get("data", {})
|
||||
if not (dataVariant is Dictionary):
|
||||
var message := "玩家状态响应格式错误"
|
||||
appearance_update_failed.emit(requestedSkinId, message)
|
||||
profile_update_failed.emit(message)
|
||||
auth_state_changed.emit(is_authenticated(), get_current_user())
|
||||
return
|
||||
var snapshot := dataVariant as Dictionary
|
||||
var playerStateManager := get_node_or_null("/root/PlayerStateManager")
|
||||
if playerStateManager != null and playerStateManager.has_method("apply_snapshot"):
|
||||
playerStateManager.call("apply_snapshot", snapshot)
|
||||
_apply_snapshot_profile_payload(snapshot)
|
||||
var profile := get_current_profile()
|
||||
appearance_update_succeeded.emit(requestedSkinId, profile)
|
||||
profile_update_succeeded.emit(profile)
|
||||
auth_state_changed.emit(is_authenticated(), get_current_user())
|
||||
|
||||
func _on_profile_assets_response(success: bool, data: Dictionary, _error_info: Dictionary, requestGeneration: int) -> void:
|
||||
if requestGeneration != _account_generation:
|
||||
return
|
||||
@@ -539,6 +603,7 @@ func _load_cached_session(emit_cached_state: bool = true) -> void:
|
||||
|
||||
_refresh_token = str(config.get_value("auth", "refresh_token", "")).strip_edges()
|
||||
_access_token = str(config.get_value("auth", "access_token", "")).strip_edges()
|
||||
_access_token_expires_at = _jwt_expiration_time(_access_token)
|
||||
_current_user.clear()
|
||||
_current_profile.clear()
|
||||
|
||||
@@ -554,6 +619,7 @@ func _emit_cached_auth_state() -> void:
|
||||
func _clear_session_memory() -> void:
|
||||
_access_token = ""
|
||||
_refresh_token = ""
|
||||
_access_token_expires_at = 0.0
|
||||
_current_user.clear()
|
||||
_current_profile.clear()
|
||||
_advance_account_generation()
|
||||
@@ -610,6 +676,26 @@ func _password_has_letter_and_number(password: String) -> bool:
|
||||
has_number = true
|
||||
return has_letter and has_number
|
||||
|
||||
func _token_needs_refresh() -> bool:
|
||||
if _refresh_in_flight or _refresh_token.is_empty():
|
||||
return false
|
||||
if _access_token_expires_at <= 0.0:
|
||||
return false
|
||||
return Time.get_unix_time_from_system() + SESSION_REFRESH_LEAD_TIME >= _access_token_expires_at
|
||||
|
||||
func _jwt_expiration_time(token: String) -> float:
|
||||
var parts := token.split(".")
|
||||
if parts.size() != 3:
|
||||
return 0.0
|
||||
var payload := str(parts[1]).replace("-", "+").replace("_", "/")
|
||||
while payload.length() % 4 != 0:
|
||||
payload += "="
|
||||
var jsonText := Marshalls.base64_to_raw(payload).get_string_from_utf8()
|
||||
var json := JSON.new()
|
||||
if json.parse(jsonText) != OK or not (json.data is Dictionary):
|
||||
return 0.0
|
||||
return float((json.data as Dictionary).get("exp", 0.0))
|
||||
|
||||
func _http_result_to_string(result: int) -> String:
|
||||
match result:
|
||||
HTTPRequest.RESULT_SUCCESS:
|
||||
|
||||
Reference in New Issue
Block a user