feat: add Godot Agones REST bridge

This commit is contained in:
Josh Creek
2026-09-01 09:16:41 +01:00
parent 6caf719158
commit e7c835af52
7 changed files with 167 additions and 6 deletions
+101
View File
@@ -0,0 +1,101 @@
class_name AgonesSDK
extends Node
# Dependency-free REST bridge for the Agones sidecar. The Go supervisor owns
# the process-ready probe and /ready transition; this node owns the game
# process's periodic Health pings and terminal Shutdown/annotation calls.
const HEALTH_INTERVAL_SECONDS := 2.0
const REQUEST_TIMEOUT_SECONDS := 2.0
const MAX_ANNOTATION_VALUE_LENGTH := 4096
var _base_url := ""
var _health_timer: Timer = null
var _health_in_flight := false
func configure_from_environment() -> bool:
var port := OS.get_environment("AGONES_SDK_HTTP_PORT")
if port.is_empty() or not port.is_valid_int() or int(port) < 1 or int(port) > 65535:
return false
_base_url = "http://127.0.0.1:%d" % int(port)
return true
func configure_for_testing(base_url: String) -> bool:
if not base_url.begins_with("http://127.0.0.1:") and not base_url.begins_with("http://localhost:"):
return false
_base_url = base_url.trim_suffix("/")
return true
func is_available() -> bool:
return not _base_url.is_empty()
func start_health() -> void:
if not is_available() or _health_timer != null:
return
_health_timer = Timer.new()
_health_timer.name = "AgonesHealth"
_health_timer.wait_time = HEALTH_INTERVAL_SECONDS
_health_timer.one_shot = false
_health_timer.timeout.connect(_send_health)
add_child(_health_timer)
_health_timer.start()
_send_health()
func stop_health() -> void:
if _health_timer != null:
_health_timer.stop()
_health_timer.queue_free()
_health_timer = null
func health() -> int:
return await _request(HTTPClient.METHOD_POST, "/health", {})
func mark_ready() -> int:
return await _request(HTTPClient.METHOD_POST, "/ready", {})
func shutdown() -> int:
return await _request(HTTPClient.METHOD_POST, "/shutdown", {})
func set_annotation(key: String, value: String) -> int:
if not annotation_is_valid(key, value):
return 400
return await _request(HTTPClient.METHOD_PUT, "/metadata/annotation", {"key": key, "value": value})
static func annotation_is_valid(key: String, value: String) -> bool:
return not (key.is_empty() or value.is_empty() or value.length() > MAX_ANNOTATION_VALUE_LENGTH or "\n" in key or "\r" in key or "\n" in value or "\r" in value)
func _send_health() -> void:
if _health_in_flight or not is_available():
return
_health_in_flight = true
var status := await health()
_health_in_flight = false
if status < 200 or status >= 300:
push_warning("Agones health ping failed (%d)" % status)
func _request(method: int, path: String, payload: Dictionary) -> int:
if not is_available() or not path.begins_with("/"):
return 408
var request := HTTPRequest.new()
request.timeout = REQUEST_TIMEOUT_SECONDS
add_child(request)
var body := JSON.stringify(payload)
var err := request.request(_base_url + path, PackedStringArray(["Content-Type: application/json"]), method, body)
if err != OK:
request.queue_free()
return 599
var result = await request.request_completed
request.queue_free()
return int(result[1])
+7
View File
@@ -2,6 +2,7 @@ extends Node
const NetCodec = preload("res://scripts/net_codec.gd")
const ServerControlScript = preload("res://scripts/server_control.gd")
const AgonesSDKScript = preload("res://scripts/agones_sdk.gd")
# Headless dedicated server entry point (task 1.6). Parses CLI args, hosts
# via NetworkManager, logs structured lines, and watches for physics-tick
@@ -28,6 +29,7 @@ var _last_physics_frame := 0
var config: ServerConfig = null
var _watchdog_armed := false # skip the first _process(): engine startup scheduling can batch several physics frames before the first idle frame runs, which isn't a real overrun
var _control: ServerControl = null
var _agones = null
var _drain_requested := false
@@ -89,6 +91,11 @@ func _ready() -> void:
printerr("cosmic-clash-server: refusing to start with invalid readiness control port")
get_tree().quit(1)
return
_agones = AgonesSDKScript.new()
_agones.name = "AgonesSDK"
get_tree().root.add_child.call_deferred(_agones)
if _agones.configure_from_environment():
_agones.start_health()
NetworkManager.client_connected.connect(_on_client_connected)
NetworkManager.client_disconnected.connect(_on_client_disconnected)
+2 -2
View File
@@ -67,10 +67,10 @@ func _respond(peer: StreamPeerTCP, request: String) -> void:
var status := 404
var reason := "Not Found"
var body := ""
if method == "GET" and path == "/ready":
if method in ["GET", "POST"] and path == "/ready":
status = 200 if _ready_for_connections else 503
reason = "OK" if status == 200 else "Service Unavailable"
elif method == "GET" and path == "/health":
elif method in ["GET", "POST"] and path == "/health":
status = 200
reason = "OK"
elif method == "POST" and path == "/drain":