extends Node ## NEO-122: HTTP client for quest-progress GET (NEO-119) and quest accept POST (NEO-120). signal quest_progress_received(snapshot: Dictionary) signal quest_sync_failed(reason: String) signal quest_accept_result_received(quest_id: String, result: Dictionary) signal quest_accept_failed(quest_id: String, reason: String) const SCHEMA_VERSION := 1 @export var base_url: String = "http://127.0.0.1:5253" @export var dev_player_id: String = "dev-local-1" @export var injected_sync_http: Node = null @export var injected_accept_http: Node = null var _sync_http: Node var _accept_http: Node var _sync_busy: bool = false var _accept_busy: bool = false var _current_accept_quest_id: String = "" func _ready() -> void: _sync_http = _resolve_http(injected_sync_http) _accept_http = _resolve_http(injected_accept_http) @warning_ignore("unsafe_method_access") _sync_http.request_completed.connect(_on_sync_request_completed) @warning_ignore("unsafe_method_access") _accept_http.request_completed.connect(_on_accept_request_completed) func is_accept_busy() -> bool: return _accept_busy func request_sync_from_server() -> void: if _sync_busy: return _sync_busy = true var url := "%s/game/players/%s/quest-progress" % [_base_root(), _player_path_segment()] var err: Error = _sync_http.request(url) if err != OK: var reason := "GET failed to start (%s)" % err push_warning("QuestProgressClient: %s" % reason) _sync_busy = false quest_sync_failed.emit(reason) ## Returns [code]true[/code] when the HTTP POST was queued. func request_accept(quest_id: String) -> bool: if _accept_busy: return false var qid := quest_id.strip_edges() if qid.is_empty(): return false _current_accept_quest_id = qid _accept_busy = true var payload: Dictionary = {"schemaVersion": SCHEMA_VERSION} var url := ( "%s/game/players/%s/quests/%s/accept" % [_base_root(), _player_path_segment(), qid] ) var headers := PackedStringArray(["Content-Type: application/json"]) var err: Error = _accept_http.request( url, headers, HTTPClient.METHOD_POST, JSON.stringify(payload) ) if err != OK: _accept_busy = false _current_accept_quest_id = "" push_warning("QuestProgressClient: POST failed to start (%s)" % err) return false return true func quest_row(quest_id: String, snapshot: Dictionary = {}) -> Dictionary: var quests: Variant = snapshot.get("quests", null) if quests == null or not quests is Array: return {} for row_variant in quests as Array: if not row_variant is Dictionary: continue var row: Dictionary = row_variant if str(row.get("questId", "")) == quest_id: return row return {} func _resolve_http(injected: Node) -> Node: if injected != null: return injected var http := HTTPRequest.new() add_child(http) http.timeout = 30.0 return http func _base_root() -> String: return base_url.strip_edges().rstrip("/") func _player_path_segment() -> String: return dev_player_id.strip_edges() static func parse_quest_progress_json(text: String) -> Variant: var parsed: Variant = JSON.parse_string(text) if not parsed is Dictionary: return null var root: Dictionary = parsed if int(root.get("schemaVersion", -1)) != SCHEMA_VERSION: return null var quests: Variant = root.get("quests", null) if quests == null or not quests is Array: return null if str(root.get("playerId", "")).strip_edges().is_empty(): return null return root static func parse_quest_accept_json(text: String) -> Variant: var parsed: Variant = JSON.parse_string(text) if not parsed is Dictionary: return null var root: Dictionary = parsed if int(root.get("schemaVersion", -1)) != SCHEMA_VERSION: return null if not root.has("accepted"): return null return root func _on_sync_request_completed( result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray ) -> void: _sync_busy = false if result != HTTPRequest.RESULT_SUCCESS: var reason := "HTTP failed (result=%s)" % result push_warning("QuestProgressClient: %s" % reason) quest_sync_failed.emit(reason) return if response_code == 404: var reason404 := "HTTP 404 (player unknown)" push_warning("QuestProgressClient: %s" % reason404) quest_sync_failed.emit(reason404) return if response_code < 200 or response_code >= 300: var reason_code := "HTTP %s" % response_code push_warning("QuestProgressClient: %s" % reason_code) quest_sync_failed.emit(reason_code) return var text := body.get_string_from_utf8() var snapshot: Variant = parse_quest_progress_json(text) if snapshot == null: var reason_json := "non-JSON body or schemaVersion mismatch" push_warning("QuestProgressClient: %s" % reason_json) quest_sync_failed.emit(reason_json) return quest_progress_received.emit(snapshot as Dictionary) func _on_accept_request_completed( result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray ) -> void: var quest_echo: String = _current_accept_quest_id _current_accept_quest_id = "" _accept_busy = false if result != HTTPRequest.RESULT_SUCCESS: var reason := "HTTP failed (result=%s)" % result push_warning("QuestProgressClient: %s" % reason) quest_accept_failed.emit(quest_echo, reason) return if response_code == 404: var reason404 := "HTTP 404 (player unknown)" push_warning("QuestProgressClient: %s" % reason404) quest_accept_failed.emit(quest_echo, reason404) return if response_code < 200 or response_code >= 300: var reason_code := "HTTP %s" % response_code push_warning("QuestProgressClient: %s" % reason_code) quest_accept_failed.emit(quest_echo, reason_code) return var text := body.get_string_from_utf8() var parsed: Variant = parse_quest_accept_json(text) if parsed == null: var reason_json := "non-JSON body or schemaVersion mismatch" push_warning("QuestProgressClient: %s" % reason_json) quest_accept_failed.emit(quest_echo, reason_json) return var data: Dictionary = parsed as Dictionary if not bool(data.get("accepted", false)): var reason_variant: Variant = data.get("reasonCode", "") var reason_str: String = reason_variant as String if reason_variant is String else "" push_warning("quest_accept_denied reasonCode=%s" % reason_str) quest_accept_result_received.emit(quest_echo, data.duplicate(true))