extends Node ## Unified interface for backend services ## All platforms use Nakama for achievements, leaderboards, and shop ## Steam is only used for authentication (auth session ticket for Nakama login) enum Platform { DESKTOP_STEAM, DESKTOP_NAKAMA, MOBILE_NAKAMA } enum ErrorCode { NONE, NETWORK_ERROR, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, INTERNAL_ERROR, UNKNOWN_ERROR, INSUFFICIENT_FUNDS } var current_platform: Platform = Platform.DESKTOP_STEAM var steamworks_manager: Node # Only for auth ticket retrieval var nakama_backend: Node const MAX_RETRIES = 3 const RETRY_BACKOFF_BASE = 0.5 # Error mapping based on HTTP status codes returned by Nakama var error_map := { 401: ErrorCode.UNAUTHORIZED, 403: ErrorCode.FORBIDDEN, 404: ErrorCode.NOT_FOUND, 500: ErrorCode.INTERNAL_ERROR, } func _ready() -> void: _detect_platform() _initialize_backend() func _detect_platform() -> void: if OS.has_feature("android") or OS.has_feature("ios"): current_platform = Platform.MOBILE_NAKAMA else: if ClassDB.class_exists("Steam"): current_platform = Platform.DESKTOP_STEAM else: current_platform = Platform.DESKTOP_NAKAMA func _initialize_backend() -> void: if current_platform == Platform.DESKTOP_STEAM: _initialize_steamworks_for_auth() _initialize_nakama() func _initialize_steamworks_for_auth() -> void: var steamworks_script = load("res://scripts/services/steamworks_manager.gd") if steamworks_script: steamworks_manager = steamworks_script.new() add_child(steamworks_manager) print("BackendService: Initialized Steamworks for auth only") else: push_error("BackendService: Failed to load Steamworks manager") func _initialize_nakama() -> void: nakama_backend = NakamaManager if nakama_backend: _connect_nakama_signals() print("BackendService: Initialized Nakama backend") else: push_error("BackendService: NakamaManager not found") func _connect_nakama_signals() -> void: pass func is_initialized() -> bool: if nakama_backend != null: return true return false func get_platform_name() -> String: return Platform.keys()[current_platform] func get_steamworks_manager() -> Node: return steamworks_manager ## Unified RPC with Retry and Error Mapping func api_rpc_async(rpc_id: String, payload: String = "{}") -> Dictionary: if not nakama_backend or not nakama_backend.client or not nakama_backend.session: return { "success": false, "error": ErrorCode.UNAUTHORIZED, "message": "Not authenticated" } var retries := 0 while retries <= MAX_RETRIES: var result = await nakama_backend.client.rpc_async(nakama_backend.session, rpc_id, payload) # NakamaAPI.ApiRpc has is_exception() if result.is_exception(): var ex = result.get_exception() # Transient network error or internal server logic error (500) # Typically Nakama exception status returns HTTP matching codes or grpc codes if retries < MAX_RETRIES and _is_transient_error(ex): retries += 1 await get_tree().create_timer(RETRY_BACKOFF_BASE * pow(2.0, retries - 1)).timeout continue var parsed_msg := _parse_error_msg(ex.message) var err_code = ErrorCode.UNKNOWN_ERROR if error_map.has(ex.status_code): err_code = error_map[ex.status_code] else: if "insufficient funds" in ex.message.to_lower(): err_code = ErrorCode.INSUFFICIENT_FUNDS return { "success": false, "error": err_code, "message": parsed_msg, "raw_exception": ex } # Success return { "success": true, "payload": result.payload } return { "success": false, "error": ErrorCode.NETWORK_ERROR, "message": "Max retries exceeded" } func _is_transient_error(ex) -> bool: # Retry on network failures (usually status 0 or 5xx) if ex.status_code == 0 or ex.status_code >= 500: return true return false func _parse_error_msg(msg: String) -> String: # Parse JSON error from Nakama if any if msg.begins_with("{"): var test_json_conv = JSON.new() if test_json_conv.parse(msg) == OK: var parsed = test_json_conv.get_data() if parsed.has("message"): return parsed["message"] return msg ## Typed Method Signatures func admin_clear_global_chat(payload: String) -> Dictionary: return await api_rpc_async("admin_clear_global_chat", payload) func admin_get_chat_config() -> Dictionary: return await api_rpc_async("admin_get_chat_config", "{}") func admin_set_chat_config(config: Dictionary) -> Dictionary: return await api_rpc_async("admin_set_chat_config", JSON.stringify(config)) func admin_purge_old_messages(channel_id: String, max_age_days: int) -> Dictionary: return await api_rpc_async("admin_purge_old_messages", JSON.stringify({"channel_id": channel_id, "max_age_days": max_age_days})) func admin_list_channel_messages(channel_id: String, limit: int = 50, cursor: String = "", forward: bool = true) -> Dictionary: return await api_rpc_async("admin_list_channel_messages", JSON.stringify({"channel_id": channel_id, "limit": limit, "cursor": cursor, "forward": forward})) func admin_delete_channel_message(channel_id: String, message_id: String) -> Dictionary: return await api_rpc_async("admin_delete_channel_message", JSON.stringify({"channel_id": channel_id, "message_id": message_id})) func send_friend_request(target_id: String) -> Dictionary: var payload = JSON.stringify({"target_user_id": target_id}) return await api_rpc_async("send_friend_request", payload) func respond_friend_request(target_id: String, accept: bool) -> Dictionary: var payload = JSON.stringify({"target_user_id": target_id, "accept": accept}) return await api_rpc_async("respond_friend_request", payload) func perform_gacha_pull(gacha_id: String, count: int) -> Dictionary: var payload = JSON.stringify({"gacha_id": gacha_id, "count": count}) return await api_rpc_async("perform_gacha_pull", payload) func get_mail(payload: String = "{}") -> Dictionary: return await api_rpc_async("get_mail", payload) func claim_mail_reward(mail_id: String) -> Dictionary: return await api_rpc_async("claim_mail_reward", JSON.stringify({"mail_id": mail_id})) func delete_mail(mail_id: String) -> Dictionary: return await api_rpc_async("delete_mail", JSON.stringify({"mail_id": mail_id})) func send_mail(payload: String) -> Dictionary: return await api_rpc_async("send_mail", payload) func change_avatar(avatar_url: String) -> Dictionary: return await api_rpc_async("change_avatar", JSON.stringify({"avatar_url": avatar_url})) func change_username(new_username: String) -> Dictionary: return await api_rpc_async("change_username", JSON.stringify({"new_username": new_username})) func change_status(new_status: String) -> Dictionary: return await api_rpc_async("change_status", JSON.stringify({"new_status": new_status})) func change_bio(new_bio: String) -> Dictionary: return await api_rpc_async("change_bio", JSON.stringify({"new_bio": new_bio})) func query_users(payload: String) -> Dictionary: return await api_rpc_async("query_users", payload) func admin_give_currency(payload: String) -> Dictionary: return await api_rpc_async("admin_give_currency", payload) func get_daily_reward_config_admin() -> Dictionary: return await api_rpc_async("get_daily_reward_config_admin", "{}") func set_daily_reward_config(req: Dictionary) -> Dictionary: return await api_rpc_async("set_daily_reward_config", JSON.stringify(req)) func get_daily_reward_state() -> Dictionary: return await api_rpc_async("get_daily_reward_state", "{}") func claim_daily_reward() -> Dictionary: return await api_rpc_async("claim_daily_reward", "{}") func sync_leaderboard() -> Dictionary: return await api_rpc_async("sync_leaderboard", "{}") func get_leaderboard_stats() -> Dictionary: return await api_rpc_async("get_leaderboard_stats", "{}") func debug_add_exp(exp_amount: int) -> Dictionary: return await api_rpc_async("debug_add_exp", JSON.stringify({"exp": exp_amount})) func reset_stats() -> Dictionary: return await api_rpc_async("reset_stats", "{}") func search_users(payload: String) -> Dictionary: return await api_rpc_async("search_users", payload) func send_lobby_invite(to_user_id: String, match_id: String) -> Dictionary: var payload = JSON.stringify({"to_user_id": to_user_id, "match_id": match_id}) return await api_rpc_async("send_lobby_invite", payload)