extends Node const EndpointConfig := preload("res://scripts/autoload/api_endpoint_config.gd") const DEFAULT_API_URL := "https://s3q.ranaze.com/api/ccs" signal cases_changed signal attempt_changed signal orders_changed signal history_changed signal prediction_changed signal report_submitted(report_id: String) signal report_failed(message: String) signal request_failed(message: String) const TEXT_SCALE_MIN := 0.85 const TEXT_SCALE_MAX := 1.35 const TEXT_SCALE_STEP := 0.1 var api_url := DEFAULT_API_URL var request: HTTPRequest var cases: Array = [] var attempt := {} var orders: Array = [] var history: Array = [] var prediction := {} var is_loading := false var pending_action := "" var queue: Array = [] var text_scale := 1.0 var highlights := {} func _ready() -> void: api_url = EndpointConfig.resolve(str(ProjectSettings.get_setting("step3/ccs_api_url", DEFAULT_API_URL)), "/api/ccs") request = HTTPRequest.new() request.request_completed.connect(_on_request_completed) add_child(request) AuthState.auth_changed.connect(_on_auth_changed) func load_cases() -> void: _send("cases", HTTPClient.METHOD_GET, "/cases") func create_attempt(case_external_id: String, mode: String) -> void: _send("attempt", HTTPClient.METHOD_POST, "/attempts", {"caseExternalId": case_external_id, "mode": mode}) func load_attempt(attempt_id: String) -> void: _send("attempt", HTTPClient.METHOD_GET, "/attempts/%s" % attempt_id.uri_encode()) func send_command(type: String, code: String = "", minutes: int = 0) -> void: if attempt.is_empty(): return _send("attempt", HTTPClient.METHOD_POST, "/attempts/%s/commands" % str(attempt.get("id", "")).uri_encode(), { "commandId": "%d-%d" % [Time.get_ticks_usec(), randi()], "expectedRevision": int(attempt.get("revision", 0)), "type": type, "code": code, "minutes": minutes }) func search_orders(query: String) -> void: if query.strip_edges().length() < 2: orders = [] orders_changed.emit() return _send("orders", HTTPClient.METHOD_GET, "/orders/search?q=%s&limit=20" % query.strip_edges().uri_encode()) func load_history() -> void: _send("history", HTTPClient.METHOD_GET, "/history?limit=50") func load_prediction() -> void: _send("prediction", HTTPClient.METHOD_GET, "/prediction") func submit_report(attempt_id: String, category: String, details: String) -> void: _send("report", HTTPClient.METHOD_POST, "/reports", {"attemptId": attempt_id, "category": category, "details": details}) func adjust_text_scale(delta: float) -> void: text_scale = clampf(text_scale + delta, TEXT_SCALE_MIN, TEXT_SCALE_MAX) func toggle_highlight(attempt_id: String, region_id: String, start: int, end: int) -> void: var key := "%s:%s" % [attempt_id, region_id] var ranges: Array = highlights.get(key, []) var removed := false for index in range(ranges.size() - 1, -1, -1): var item: Dictionary = ranges[index] if start < int(item.get("end", 0)) and end > int(item.get("start", 0)): ranges.remove_at(index); removed = true if not removed: ranges.append({"start": start, "end": end}) highlights[key] = ranges func clear_highlights(attempt_id: String) -> void: for key in highlights.keys(): if str(key).begins_with("%s:" % attempt_id): highlights.erase(key) func highlighted_text(attempt_id: String, region_id: String, plain_text: String) -> String: var ranges: Array = highlights.get("%s:%s" % [attempt_id, region_id], []).duplicate() ranges.sort_custom(func(a: Dictionary, b: Dictionary): return int(a.get("start", 0)) < int(b.get("start", 0))) var result := ""; var cursor := 0 for item in ranges: var start := clampi(int(item.get("start", 0)), cursor, plain_text.length()) var end := clampi(int(item.get("end", 0)), start, plain_text.length()) result += _escape_bbcode(plain_text.substr(cursor, start - cursor)) result += "[bgcolor=yellow][color=black]%s[/color][/bgcolor]" % _escape_bbcode(plain_text.substr(start, end - start)) cursor = end return result + _escape_bbcode(plain_text.substr(cursor)) func abandon() -> void: if attempt.is_empty(): return _send("abandon", HTTPClient.METHOD_POST, "/attempts/%s/abandon" % str(attempt.get("id", "")).uri_encode()) func deadline_unix() -> float: var value := str(attempt.get("finalOrdersDeadlineUtc", attempt.get("deadlineUtc", ""))) return 0.0 if value.is_empty() or value == "" else Time.get_unix_time_from_datetime_string(value) func _send(action: String, method: int, suffix: String, payload: Dictionary = {}) -> void: var url := "%s%s" % [api_url.rstrip("/"), suffix] if is_loading: queue.append({"action": action, "method": method, "url": url, "payload": payload.duplicate(true)}) return if AuthState.access_token.is_empty(): 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 error := request.request(url, headers, method, body) if error != OK: is_loading = false request_failed.emit("The CCS 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_queue") if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300: var detail := body.get_string_from_utf8().strip_edges().left(240) var message := "CCS request failed (%d). %s" % [response_code, detail] if action == "report": report_failed.emit(message) else: request_failed.emit(message) return if response_code == 204: attempt = {} attempt_changed.emit() return var parsed: Variant = JSON.parse_string(body.get_string_from_utf8()) if action in ["cases", "orders", "history"] and typeof(parsed) != TYPE_ARRAY: request_failed.emit("The CCS API returned an invalid list.") return if action not in ["cases", "orders", "history"] and typeof(parsed) != TYPE_DICTIONARY: request_failed.emit("The CCS API returned an invalid response.") return match action: "cases": cases = parsed; cases_changed.emit() "orders": orders = parsed; orders_changed.emit() "history": history = parsed; history_changed.emit() "prediction": prediction = parsed; prediction_changed.emit() "report": report_submitted.emit(str(parsed.get("id", ""))) _: attempt = parsed attempt_changed.emit() func _process_queue() -> void: if is_loading or queue.is_empty(): return var queued: Dictionary = queue.pop_front() var suffix := str(queued.get("url", "")).trim_prefix(api_url.rstrip("/")) _send(str(queued.get("action", "")), int(queued.get("method", HTTPClient.METHOD_GET)), suffix, queued.get("payload", {})) func _escape_bbcode(value: String) -> String: return value.replace("[", "[lb]").replace("]", "[rb]") func _on_auth_changed() -> void: if not AuthState.is_logged_in: highlights.clear() text_scale = 1.0