217 lines
7.4 KiB
GDScript
217 lines
7.4 KiB
GDScript
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 rating_changed
|
|
signal attempt_abandoned
|
|
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 rating := {"elo": 1000, "ratedCaseCount": 0}
|
|
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 load_rating() -> void:
|
|
_send("rating", HTTPClient.METHOD_GET, "/ratings/me")
|
|
|
|
|
|
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 changed := false
|
|
for index in range(ranges.size() - 1, -1, -1):
|
|
var item: Dictionary = ranges[index]
|
|
var highlight_start := int(item.get("start", 0))
|
|
var highlight_end := int(item.get("end", 0))
|
|
if start < highlight_end and end > highlight_start:
|
|
ranges.remove_at(index)
|
|
if highlight_start < start:
|
|
ranges.insert(index, {"start": highlight_start, "end": start})
|
|
index += 1
|
|
if end < highlight_end:
|
|
ranges.insert(index, {"start": end, "end": highlight_end})
|
|
changed = true
|
|
if not changed:
|
|
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 == "<null>" else float(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 = {}
|
|
if action == "abandon": attempt_abandoned.emit()
|
|
else: 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()
|
|
"rating": rating = parsed; rating_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
|