鲸鱼小镇
主程序正在后台加载 0%
- - -diff --git a/_Core/managers/SceneManager.gd b/_Core/managers/SceneManager.gd index 845be54..9d54b6c 100644 --- a/_Core/managers/SceneManager.gd +++ b/_Core/managers/SceneManager.gd @@ -47,6 +47,7 @@ var _active_pack_scene_name: String = "" var _pack_overlay: CanvasLayer var _pack_status_label: Label var _pack_progress_bar: ProgressBar +var _web_pack_progress_last_emit_msec: int = 0 const PACK_MANIFEST_PATH: String = "/packs/manifest.json" const PACK_CACHE_DIR: String = "user://scene-packs" @@ -160,8 +161,21 @@ func _process(_delta: float) -> void: _pack_progress_bar.value = downloaded if is_instance_valid(_pack_status_label): _pack_status_label.text = "正在加载场景 %s" % _format_download_progress(downloaded, total) + _notify_web_auth_pack_progress(downloaded, total) scene_pack_progress.emit(_active_pack_scene_name, downloaded, total) +func _notify_web_auth_pack_progress(downloaded: int, total: int) -> void: + if OS.get_name() != "Web" or _active_pack_scene_name != "auth": + return + var now := Time.get_ticks_msec() + if now - _web_pack_progress_last_emit_msec < 250: + return + _web_pack_progress_last_emit_msec = now + if not Engine.has_singleton("JavaScriptBridge"): + return + var bridge: Object = Engine.get_singleton("JavaScriptBridge") + bridge.eval("window.whaletownPackProgress && window.whaletownPackProgress(%d, %d)" % [downloaded, total], true) + func _ensure_scene_pack(scene_name: String) -> bool: var packKey := str(SCENE_PACK_KEYS.get(scene_name, "")) if packKey.is_empty() or bool(_loaded_scene_packs.get(packKey, false)): @@ -181,10 +195,11 @@ func _ensure_scene_pack(scene_name: String) -> bool: if fileName.is_empty() or not fileName.ends_with(".pck"): _show_pack_error(scene_name, "场景资源清单格式错误") return false + var expectedSize := int(entry.get("size", 0)) var cacheDirAbsolute := ProjectSettings.globalize_path(PACK_CACHE_DIR) DirAccess.make_dir_recursive_absolute(cacheDirAbsolute) var localPath := "%s/%s" % [PACK_CACHE_DIR, fileName] - if FileAccess.file_exists(localPath) and ProjectSettings.load_resource_pack(localPath, true): + if _is_scene_pack_file_valid(localPath, fileName, expectedSize) and ProjectSettings.load_resource_pack(localPath, true): _loaded_scene_packs[packKey] = true _hide_pack_overlay() return true @@ -194,6 +209,10 @@ func _ensure_scene_pack(scene_name: String) -> bool: if packUrl.is_empty() or not await _download_pack(scene_name, packUrl, localPath): _show_pack_error(scene_name, "场景下载失败,请检查网络后重试") return false + if not _is_scene_pack_file_valid(localPath, fileName, expectedSize): + DirAccess.remove_absolute(ProjectSettings.globalize_path(localPath)) + _show_pack_error(scene_name, "场景资源下载不完整,请重新进入") + return false if not ProjectSettings.load_resource_pack(localPath, true): DirAccess.remove_absolute(ProjectSettings.globalize_path(localPath)) _show_pack_error(scene_name, "场景资源损坏,请重新进入") @@ -202,6 +221,26 @@ func _ensure_scene_pack(scene_name: String) -> bool: _hide_pack_overlay() return true +func _is_scene_pack_file_valid(local_path: String, file_name: String, expected_size: int) -> bool: + if not FileAccess.file_exists(local_path): + return false + var file := FileAccess.open(local_path, FileAccess.READ) + if file == null: + return false + var actualSize := file.get_length() + file.close() + if expected_size > 0 and actualSize != expected_size: + return false + var stem := file_name.trim_suffix(".pck") + var separator := stem.rfind("-") + if separator < 0: + return true + var expectedHashPrefix := stem.substr(separator + 1).to_lower() + if expectedHashPrefix.length() != 12: + return true + var actualHash := FileAccess.get_sha256(local_path).to_lower() + return not actualHash.is_empty() and actualHash.begins_with(expectedHashPrefix) + func _download_pack_manifest() -> Dictionary: var request := HTTPRequest.new() request.timeout = 12.0 diff --git a/scenes/ui/AuthScene.gd b/scenes/ui/AuthScene.gd index 852aa7f..91522df 100644 --- a/scenes/ui/AuthScene.gd +++ b/scenes/ui/AuthScene.gd @@ -696,6 +696,7 @@ func _ready() -> void: _connect_signals() _refresh_appearance_ui() _show_login() + _notify_web_shell_ready() if _auth_manager != null and bool(_auth_manager.call("is_authenticated")) and _should_auto_resume_cached_session(): _resume_cached_session() @@ -741,6 +742,12 @@ func _connect_signals() -> void: if _auth_manager.has_signal("browser_bootstrap_received"): _auth_manager.connect("browser_bootstrap_received", _complete_browser_bootstrap) +func _notify_web_shell_ready() -> void: + if OS.get_name() != "Web" or not Engine.has_singleton("JavaScriptBridge"): + return + var bridge: Object = Engine.get_singleton("JavaScriptBridge") + bridge.eval("window.whaletownGodotReady && window.whaletownGodotReady()", true) + func _complete_browser_bootstrap(kind: String) -> void: if _is_submitting: return diff --git a/scenes/ui/WebBootstrap.gd b/scenes/ui/WebBootstrap.gd index 825b934..a4dcbfc 100644 --- a/scenes/ui/WebBootstrap.gd +++ b/scenes/ui/WebBootstrap.gd @@ -1,40 +1,9 @@ extends Control -var _auth_manager: Node var _scene_manager: Node -var _handled: bool = false func _ready() -> void: - _auth_manager = get_node_or_null("/root/AuthManager") _scene_manager = get_node_or_null("/root/SceneManager") - if OS.get_name() != "Web": - _scene_manager.call_deferred("change_scene", "auth", false) + if _scene_manager == null: return - if _auth_manager == null: - return - if _auth_manager.has_signal("browser_bootstrap_received"): - _auth_manager.connect("browser_bootstrap_received", _on_browser_bootstrap_received) - var kind := str(_auth_manager.call("get_browser_bootstrap_kind")) if _auth_manager.has_method("get_browser_bootstrap_kind") else "" - if not kind.is_empty(): - call_deferred("_on_browser_bootstrap_received", kind) - -func _on_browser_bootstrap_received(kind: String) -> void: - if _handled or _scene_manager == null: - return - _handled = true - if kind == "register": - _scene_manager.call("change_scene", "auth", false) - return - if _auth_manager.has_method("consume_browser_bootstrap_kind"): - _auth_manager.call("consume_browser_bootstrap_kind") - var appearanceManager := get_node_or_null("/root/AppearanceManager") - if appearanceManager != null and appearanceManager.has_method("apply_account_profile"): - appearanceManager.call("apply_account_profile", _auth_manager.call("get_current_profile")) - var chatManager := get_node_or_null("/root/ChatManager") - var token := str(_auth_manager.call("get_access_token")) - if chatManager != null and not token.is_empty(): - chatManager.call("set_game_token", token) - chatManager.call("connect_to_chat_server") - if _auth_manager.has_method("fetch_profile"): - _auth_manager.call("fetch_profile") - _scene_manager.call("change_scene", "square", false) + _scene_manager.call_deferred("change_scene", "auth", false) diff --git a/scripts/build_progressive_web.sh b/scripts/build_progressive_web.sh index 5f3550e..6d7aa2e 100755 --- a/scripts/build_progressive_web.sh +++ b/scripts/build_progressive_web.sh @@ -13,9 +13,17 @@ fi mkdir -p "$WEB_DIR" "$PACK_DIR" find "$PACK_DIR" -maxdepth 1 -type f \( -name '*.pck' -o -name 'manifest.json' \) -delete +find "$WEB_DIR" -maxdepth 1 -type f -name 'index-*.pck' -delete "$GODOT_BIN" --headless --path "$PROJECT_DIR" --export-release "Web" "$WEB_DIR/index.html" +core_pack_path="$WEB_DIR/index.pck" +core_hash=$(shasum -a 256 "$core_pack_path" | awk '{print substr($1, 1, 12)}') +core_pack_name="index-$core_hash.pck" +core_pack_size=$(stat -f '%z' "$core_pack_path") +mv "$core_pack_path" "$WEB_DIR/$core_pack_name" +perl -0pi -e "s/__WHALETOWN_CORE_PACK__/$core_pack_name/g; s/__WHALETOWN_CORE_PACK_SIZE__/$core_pack_size/g" "$WEB_DIR/index.html" + build_pack() { pack_key=$1 preset=$2 @@ -46,4 +54,4 @@ jq -n \ cp "$PROJECT_DIR/web/auth-background.jpg" "$WEB_DIR/auth-background.jpg" echo "Progressive web build created in $WEB_DIR" -du -h "$WEB_DIR/index.pck" "$WEB_DIR/index.wasm" "$WEB_DIR/auth-background.jpg" "$PACK_DIR"/*.pck +du -h "$WEB_DIR/$core_pack_name" "$WEB_DIR/index.wasm" "$WEB_DIR/auth-background.jpg" "$PACK_DIR"/*.pck diff --git a/web/progressive_shell.html b/web/progressive_shell.html index 18a8d88..fc32929 100644 --- a/web/progressive_shell.html +++ b/web/progressive_shell.html @@ -9,197 +9,58 @@ :root { color-scheme: light; font-family: "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; } * { box-sizing: border-box; } html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: #b9d9df; } - button, input { font: inherit; letter-spacing: 0; } #canvas { position: fixed; inset: 0; display: block; width: 100%; height: 100%; border: 0; background: #0a2437; } - #auth-shell { position: fixed; inset: 0; display: grid; place-items: center; padding: 24px; background: #b9d9df url("auth-background.jpg") center / cover no-repeat; } - #auth-shell::before { content: ""; position: absolute; inset: 0; background: rgba(222, 242, 244, .38); } - .auth-panel { position: relative; width: min(440px, 100%); max-height: calc(100vh - 48px); overflow: auto; padding: 30px 32px 24px; border: 2px solid rgba(38, 112, 151, .68); border-radius: 8px; background: rgba(249, 253, 252, .96); box-shadow: 0 14px 38px rgba(15, 65, 89, .22); } - .brand { display: flex; align-items: center; justify-content: center; gap: 12px; margin-bottom: 22px; color: #174f72; } - .brand-mark { display: grid; place-items: center; width: 48px; height: 48px; border-radius: 50%; background: #4c9ac5; color: white; font-size: 27px; font-weight: 800; } - .brand h1 { margin: 0; font-size: 26px; line-height: 1.1; letter-spacing: 0; } - .tabs { display: grid; grid-template-columns: 1fr 1fr; margin-bottom: 22px; border-bottom: 1px solid #c8dbe2; } - .tab { min-height: 42px; border: 0; border-bottom: 3px solid transparent; background: transparent; color: #647b86; cursor: pointer; } - .tab[aria-selected="true"] { border-bottom-color: #257aa8; color: #15577b; font-weight: 700; } - .form { display: grid; gap: 15px; } - .form[hidden] { display: none; } - .field { display: grid; gap: 7px; } - .field label { color: #294b5b; font-size: 14px; font-weight: 650; } - .field input { width: 100%; min-height: 44px; padding: 9px 12px; border: 1px solid #a9c4cf; border-radius: 6px; outline: none; background: #fff; color: #102e3d; } - .field input:focus { border-color: #257aa8; box-shadow: 0 0 0 3px rgba(37, 122, 168, .14); } - .code-row { display: grid; grid-template-columns: 1fr 126px; gap: 8px; } - .primary, .secondary { min-height: 44px; border-radius: 6px; cursor: pointer; font-weight: 700; } - .primary { border: 1px solid #17638e; background: #257aa8; color: #fff; } - .secondary { border: 1px solid #84adbe; background: #e7f3f5; color: #205f7c; } - button:disabled { cursor: wait; opacity: .62; } - .status { min-height: 22px; margin: 13px 0 0; color: #4b6875; font-size: 13px; line-height: 1.5; text-align: center; } - .status.error { color: #9b3340; } - .load-track { height: 5px; margin-top: 12px; overflow: hidden; border-radius: 3px; background: #d5e5e9; } - .load-bar { width: 0; height: 100%; background: #3292b9; transition: width .2s ease; } - #failure { display: none; margin-top: 12px; color: #9b3340; font-size: 13px; text-align: center; } - @media (max-width: 560px) { - #auth-shell { align-items: stretch; padding: 0; } - .auth-panel { width: 100%; max-height: 100%; margin-top: 18vh; padding: 24px 22px; border-width: 1px 0 0; border-radius: 8px 8px 0 0; } - .brand { margin-bottom: 16px; } - .brand h1 { font-size: 23px; } - } + #loading-shell { position: fixed; inset: 0; z-index: 10; display: grid; place-items: end center; padding: 0 24px max(9vh, 34px); background: #b9d9df url("auth-background.jpg") center / cover no-repeat; transition: opacity .18s ease; } + #loading-shell::before { content: ""; position: absolute; inset: 0; background: rgba(222, 242, 244, .22); } + .loading { position: relative; width: min(420px, 88vw); color: #174f72; text-align: center; text-shadow: 0 1px 0 rgba(255, 255, 255, .9); } + .status { min-height: 24px; margin: 0 0 10px; font-size: 15px; font-weight: 700; line-height: 1.5; letter-spacing: 0; } + .track { height: 7px; overflow: hidden; border: 1px solid rgba(23, 79, 114, .28); border-radius: 4px; background: rgba(255, 255, 255, .78); box-shadow: 0 2px 8px rgba(15, 65, 89, .16); } + .bar { width: 0; height: 100%; background: #2b86b6; transition: width .2s ease; } + .failure { display: none; margin-top: 10px; color: #8f2634; font-size: 13px; line-height: 1.5; } + #loading-shell.done { opacity: 0; pointer-events: none; }
-主程序正在后台加载 0%
- - -正在加载小镇 0%
+ + +