class_name NetworkConfig # ============================================================================ # NetworkConfig.gd - 前端网络环境配置 # ============================================================================ # 统一解析本地调试/线上环境的 HTTP 和 WebSocket 地址。 # 优先级: # 1) 环境变量 # 2) Config/game_config.json 中 active_profile 对应的 profiles 配置 # 3) network 下的兼容字段 # 4) 线上默认值 # ============================================================================ const CONFIG_PATHS: Array[String] = [ "res://Config/game_config.json", "res://config/game_config.json" ] const DEFAULT_PROFILE: String = "production" const DEFAULT_API_BASE_URL: String = "https://whaletownend.xinghangee.icu" const DEFAULT_WS_URL: String = "wss://whaletownend.xinghangee.icu/game" const API_BASE_URL_ENV_KEY: String = "WHALETOWN_API_BASE_URL" const CHAT_WS_URL_ENV_KEY: String = "WHALETOWN_CHAT_WS_URL" const LOCATION_WS_URL_ENV_KEY: String = "WHALETOWN_LOCATION_WS_URL" const GAME_WS_URL_ENV_KEY: String = "WHALETOWN_GAME_WS_URL" const PROFILE_ENV_KEY: String = "WHALETOWN_NETWORK_PROFILE" static func get_api_base_url() -> String: var query_url: String = _get_browser_url_override("api_base_url", ["http://", "https://"]) if not query_url.is_empty(): return _trim_trailing_slash(query_url) var env_url: String = OS.get_environment(API_BASE_URL_ENV_KEY).strip_edges() if not env_url.is_empty(): return _trim_trailing_slash(env_url) var network_config := _get_active_network_config() var url: String = str(network_config.get("api_base_url", "")).strip_edges() if not url.is_empty(): return _trim_trailing_slash(url) return DEFAULT_API_BASE_URL static func get_chat_ws_url() -> String: var query_url: String = _get_browser_url_override("chat_ws_url", ["ws://", "wss://"]) if not query_url.is_empty(): return query_url var env_url: String = OS.get_environment(CHAT_WS_URL_ENV_KEY).strip_edges() if not env_url.is_empty(): return env_url var network_config := _get_active_network_config() var url: String = str(network_config.get("chat_ws_url", network_config.get("game_ws_url", ""))).strip_edges() if not url.is_empty(): return url return DEFAULT_WS_URL static func get_location_ws_url() -> String: var query_url: String = _get_browser_url_override("location_ws_url", ["ws://", "wss://"]) if not query_url.is_empty(): return query_url var game_query_url: String = _get_browser_url_override("game_ws_url", ["ws://", "wss://"]) if not game_query_url.is_empty(): return game_query_url var env_url: String = OS.get_environment(LOCATION_WS_URL_ENV_KEY).strip_edges() if not env_url.is_empty(): return env_url var game_env_url: String = OS.get_environment(GAME_WS_URL_ENV_KEY).strip_edges() if not game_env_url.is_empty(): return game_env_url var network_config := _get_active_network_config() var url: String = str(network_config.get("location_ws_url", network_config.get("game_ws_url", ""))).strip_edges() if not url.is_empty(): return url return DEFAULT_WS_URL static func get_active_profile() -> String: var query_profile: String = _get_browser_query_value("network_profile") if not query_profile.is_empty(): return query_profile var short_query_profile: String = _get_browser_query_value("profile") if not short_query_profile.is_empty(): return short_query_profile var env_profile: String = OS.get_environment(PROFILE_ENV_KEY).strip_edges() if not env_profile.is_empty(): return env_profile var root := _load_config() var network_variant: Variant = root.get("network", {}) if network_variant is Dictionary: var network_config: Dictionary = network_variant var active_profile: String = str(network_config.get("active_profile", "")).strip_edges() if not active_profile.is_empty(): return active_profile return DEFAULT_PROFILE static func is_local_debug() -> bool: return get_active_profile() == "local" static func get_summary() -> Dictionary: return { "profile": get_active_profile(), "api_base_url": get_api_base_url(), "chat_ws_url": get_chat_ws_url(), "location_ws_url": get_location_ws_url() } static func _get_active_network_config() -> Dictionary: var root := _load_config() var network_variant: Variant = root.get("network", {}) if not (network_variant is Dictionary): return {} var network_config: Dictionary = network_variant var active_profile := get_active_profile() var profiles_variant: Variant = network_config.get("profiles", {}) if profiles_variant is Dictionary: var profiles: Dictionary = profiles_variant var profile_variant: Variant = profiles.get(active_profile, {}) if profile_variant is Dictionary: return profile_variant return network_config static func _load_config() -> Dictionary: for config_path in CONFIG_PATHS: if not FileAccess.file_exists(config_path): continue var content: String = FileAccess.get_file_as_string(config_path) if content.is_empty(): continue var json := JSON.new() if json.parse(content) != OK: push_warning("NetworkConfig: 读取配置失败 %s - %s" % [config_path, json.get_error_message()]) continue var data_variant: Variant = json.data if data_variant is Dictionary: return data_variant return {} static func _get_browser_query_value(key: String) -> String: if OS.get_name() != "Web" or not OS.is_debug_build(): return "" if not Engine.has_singleton("JavaScriptBridge"): return "" var js_bridge: Object = Engine.get_singleton("JavaScriptBridge") if js_bridge == null: return "" var script := "new URLSearchParams(window.location.search).get('%s') || ''" % key var value: Variant = js_bridge.eval(script, true) return str(value).strip_edges() static func _get_browser_url_override(key: String, allowed_schemes: Array[String]) -> String: var value := _get_browser_query_value(key) if value.is_empty(): return "" var normalized := value.to_lower() for scheme in allowed_schemes: if normalized.begins_with(scheme): return value push_warning("NetworkConfig: 忽略无效的浏览器网络地址覆盖 %s" % key) return "" static func _trim_trailing_slash(url: String) -> String: var trimmed := url.strip_edges() while trimmed.ends_with("/") and trimmed.length() > 1: trimmed = trimmed.substr(0, trimmed.length() - 1) return trimmed