extends Node const EndpointConfig := preload("res://scripts/autoload/api_endpoint_config.gd") const DEFAULT_API_URL := "https://s3q.ranaze.com/api/exam-simulations" signal readiness_changed signal state_changed signal request_failed(message: String) signal outcome_changed var api_url := DEFAULT_API_URL var outcome_api_url := "https://s3q.ranaze.com/api/step3-outcomes" var request: HTTPRequest var readiness := {} var simulation := {} var outcome := {} var is_loading := false var pending_action := "" var request_queue: Array = [] func _ready() -> void: api_url = EndpointConfig.resolve( str(ProjectSettings.get_setting("step3/exam_simulation_api_url", DEFAULT_API_URL)), "/api/exam-simulations") request = HTTPRequest.new() request.request_completed.connect(_on_request_completed) add_child(request) outcome_api_url = EndpointConfig.resolve("https://s3q.ranaze.com/api/step3-outcomes", "/api/step3-outcomes") func load_readiness() -> void: _send("readiness", HTTPClient.METHOD_GET, "/readiness") func load_active() -> void: _send("active", HTTPClient.METHOD_GET, "/active") func create_simulation() -> void: _send("create", HTTPClient.METHOD_POST, "") func start_next_block() -> void: if simulation.is_empty(): return _send("start", HTTPClient.METHOD_POST, "/%s/blocks/start" % str(simulation.get("id", ""))) func save_response(question_id: String, choice_id: String, eliminated: Array, flagged: bool, elapsed_seconds: int) -> void: if simulation.is_empty(): return _send("save", HTTPClient.METHOD_PUT, "/%s/responses" % str(simulation.get("id", "")), { "questionExternalId": question_id, "choiceId": choice_id, "eliminatedChoiceIds": eliminated, "flagged": flagged, "elapsedSeconds": elapsed_seconds }) func submit_block() -> void: if simulation.is_empty(): return _send("submit", HTTPClient.METHOD_POST, "/%s/blocks/submit" % str(simulation.get("id", ""))) func abandon() -> void: if simulation.is_empty(): return _send("abandon", HTTPClient.METHOD_POST, "/%s/abandon" % str(simulation.get("id", ""))) func load_outcome() -> void: _send_absolute("outcome_get", HTTPClient.METHOD_GET, "%s/me" % outcome_api_url.rstrip("/")) func save_outcome(official_score: int, exam_date: String) -> void: _send_absolute("outcome_save", HTTPClient.METHOD_PUT, "%s/me" % outcome_api_url.rstrip("/"), { "officialScore": official_score, "examDate": "%sT00:00:00Z" % exam_date, "consent": true }) func delete_outcome() -> void: _send_absolute("outcome_delete", HTTPClient.METHOD_DELETE, "%s/me" % outcome_api_url.rstrip("/")) func get_block_deadline_unix() -> float: var value := str(simulation.get("blockDeadlineUtc", "")) if value == "": return 0.0 return Time.get_unix_time_from_datetime_string(value) func get_current_questions() -> Array: return simulation.get("questions", []) func _send(action: String, method: int, suffix: String, payload: Dictionary = {}) -> void: _send_absolute(action, method, "%s%s" % [api_url.rstrip("/"), suffix], payload) func _send_absolute(action: String, method: int, url: String, payload: Dictionary = {}) -> void: if AuthState.access_token == "": return if is_loading: request_queue.append({"action": action, "method": method, "url": url, "payload": payload.duplicate(true)}) return is_loading = true pending_action = action var headers := PackedStringArray(["Authorization: Bearer %s" % AuthState.access_token]) var body := "" if not payload.is_empty(): headers.append("Content-Type: application/json") body = JSON.stringify(payload) var err := request.request(url, headers, method, body) if err != OK: is_loading = false pending_action = "" request_failed.emit("The simulation request could not be started.") func _on_request_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void: var action := pending_action pending_action = "" is_loading = false call_deferred("_process_request_queue") if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300: request_failed.emit("Simulation request failed with status %d." % response_code) return if response_code == 204: if action == "active" or action == "abandon": simulation = {} state_changed.emit() elif action == "outcome_delete" or action == "outcome_get": outcome = {} outcome_changed.emit() return var parsed: Variant = JSON.parse_string(body.get_string_from_utf8()) if typeof(parsed) != TYPE_DICTIONARY: request_failed.emit("The simulation API returned an invalid response.") return if action == "readiness": readiness = parsed readiness_changed.emit() return if action.begins_with("outcome_"): outcome = parsed outcome_changed.emit() return simulation = _normalize_simulation(parsed) state_changed.emit() func _process_request_queue() -> void: if is_loading or request_queue.is_empty(): return var queued: Dictionary = request_queue.pop_front() _send_absolute(str(queued.get("action", "")), int(queued.get("method", HTTPClient.METHOD_GET)), str(queued.get("url", "")), queued.get("payload", {})) func _normalize_simulation(raw: Dictionary) -> Dictionary: var normalized := raw.duplicate(true) var normalized_questions: Array = [] for question in raw.get("questions", []): if typeof(question) != TYPE_DICTIONARY: continue normalized_questions.append({ "id": str(question.get("externalId", "")), "stem": str(question.get("stem", "")), "lead_in": str(question.get("leadIn", "")), "choices": question.get("choices", []), "discipline": str(question.get("discipline", "")), "system": str(question.get("system", "")), "performance_category": str(question.get("performanceCategory", "")), "labs": question.get("labs", []), "media": question.get("media", []), "correct_choice_id": "", "explanation": "" }) normalized["questions"] = normalized_questions return normalized