class_name ApiClient extends Node const DEFAULT_TIMEOUT_SECONDS := 12.0 var base_url: String = "" var timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS func _ready() -> void: base_url = str(ProjectSettings.get_setting("farm_game/api/base_url", "")) func post_json(path: String, payload: Dictionary) -> Dictionary: var base_error := _ensure_base_url() if not base_error.is_empty(): return base_error var request := HTTPRequest.new() request.timeout = timeout_seconds add_child(request) var url := _join_url(base_url, path) var body := _to_egret_msg_body(payload) var headers := PackedStringArray(["Content-Type: application/x-www-form-urlencoded"]) var error := request.request(url, headers, HTTPClient.METHOD_POST, body) if error != OK: request.queue_free() return { "ok": false, "offline": false, "status": 0, "body": {}, "text": "HTTPRequest failed to start: %s" % error } var response: Array = await request.request_completed request.queue_free() return _to_result(response) func get_query(path: String, params: Dictionary = {}) -> Dictionary: var base_error := _ensure_base_url() if not base_error.is_empty(): return base_error var request := HTTPRequest.new() request.timeout = timeout_seconds add_child(request) var url := _join_url(base_url, path) var query := _to_form_body(params) if not query.is_empty(): url = "%s%s%s" % [url, "&" if url.contains("?") else "?", query] var error := request.request(url, PackedStringArray(), HTTPClient.METHOD_GET) if error != OK: request.queue_free() return { "ok": false, "offline": false, "status": 0, "body": {}, "text": "HTTPRequest failed to start: %s" % error } var response: Array = await request.request_completed request.queue_free() return _to_result(response) func post_form(path: String, payload: Dictionary) -> Dictionary: var base_error := _ensure_base_url() if not base_error.is_empty(): return base_error var request := HTTPRequest.new() request.timeout = timeout_seconds add_child(request) var url := _join_url(base_url, path) var headers := PackedStringArray(["Content-Type: application/x-www-form-urlencoded"]) var error := request.request(url, headers, HTTPClient.METHOD_POST, _to_form_body(payload)) if error != OK: request.queue_free() return { "ok": false, "offline": false, "status": 0, "body": {}, "text": "HTTPRequest failed to start: %s" % error } var response: Array = await request.request_completed request.queue_free() return _to_result(response) func _ensure_base_url() -> Dictionary: if base_url.strip_edges().is_empty(): base_url = str(ProjectSettings.get_setting("farm_game/api/base_url", "")) if not base_url.strip_edges().is_empty(): return {} return { "ok": false, "offline": true, "status": 0, "body": {}, "text": "API base URL is not configured." } func _join_url(root: String, path: String) -> String: var normalized_root := root.strip_edges().trim_suffix("/") var normalized_path := path.strip_edges() if normalized_root.ends_with("=") or normalized_root.ends_with("?") or normalized_root.ends_with("&"): return normalized_root + normalized_path.trim_prefix("/") if not normalized_path.begins_with("/"): normalized_path = "/" + normalized_path return normalized_root + normalized_path func _to_egret_msg_body(payload: Dictionary) -> String: var json := JSON.stringify(payload) var encoded := Marshalls.raw_to_base64(json.to_utf8_buffer()).uri_encode() return "msg=%s" % encoded func _to_form_body(payload: Dictionary) -> String: var parts: Array[String] = [] for key in payload.keys(): var encoded_key := str(key).uri_encode() var encoded_value := str(payload.get(key, "")).uri_encode() parts.append("%s=%s" % [encoded_key, encoded_value]) return "&".join(parts) func _to_result(response: Array) -> Dictionary: var result: int = response[0] var status: int = response[1] var headers: PackedStringArray = response[2] var body: PackedByteArray = response[3] var text := body.get_string_from_utf8() var parsed = JSON.parse_string(text) if parsed == null: parsed = {} _decode_embedded_data(parsed) var http_ok := result == HTTPRequest.RESULT_SUCCESS and status >= 200 and status < 300 var game_ok := true var game_status = null if parsed is Dictionary and parsed.has("status"): game_status = parsed.get("status") game_ok = int(game_status) == 0 elif parsed is Dictionary and parsed.has("code"): game_status = parsed.get("code") game_ok = int(game_status) == 0 return { "ok": http_ok and game_ok, "offline": false, "status": status, "game_status": game_status, "headers": headers, "body": parsed, "text": text } func _decode_embedded_data(parsed: Variant) -> void: if not parsed is Dictionary: return if not parsed.has("data") or not parsed["data"] is String: return var decoded_text := Marshalls.base64_to_utf8(str(parsed["data"]).strip_edges()) var decoded = JSON.parse_string(decoded_text) if decoded != null: parsed["data"] = decoded