wrote a tiny HTTP server in GDScript to poke at game state from the browser — dumb experiment that actually stuck

406 views 0 replies

Started as a dumb experiment: can I query live game state from a browser tab without touching the game window? Turns out yes, and it's weirdly useful.

The setup: a TCPServer autoload that listens on a localhost port, parses incoming HTTP GET requests (just the first line, really), routes to registered handler callables, and returns JSON. No plugin, no third-party dep, just Godot 4's built-in networking primitives.

extends Node

var _server := TCPServer.new()
var _handlers: Dictionary = {}

func start(port: int = 9876) -> void:
    _server.listen(port)

func register(path: String, callable: Callable) -> void:
    _handlers[path] = callable

func _process(_delta: float) -> void:
    if not _server.is_connection_available():
        return
    var peer: StreamPeerTCP = _server.take_connection()
    await get_tree().process_frame  # let the buffer fill
    var raw := peer.get_utf8_string(peer.get_available_bytes())
    var path := _parse_path(raw)
    var result := _handlers[path].call() if _handlers.has(path) else {"error": "not found"}
    var response := "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n\r\n" + JSON.stringify(result)
    peer.put_data(response.to_utf8_buffer())
    peer.disconnect_from_host()

func _parse_path(raw: String) -> String:
    var parts := raw.split("\r\n")[0].split(" ")
    return parts[1] if parts.size() > 1 else "/"

Then from game code:

DebugServer.register("/player", func():
    return {
        "pos": str(player.global_position),
        "hp": player.health,
        "state": player.state_machine.current_state,
    })

DebugServer.start()

Hit localhost:9876/player in a browser and you get a live JSON snapshot. I threw together a tiny vanilla JS page that polls every 500ms and renders it as a table. No more print() chains scattered everywhere just to read state mid-session.

I've also wired up write endpoints by parsing POST bodies, so I can fire curl -X POST localhost:9876/spawn -d '{"enemy":"goblin"}' at the running game. Toggle god mode, force-load a level, inject quest state. All from the terminal without touching the game window.

Gated behind OS.is_debug_build() so it doesn't ship. Known issue: get_available_bytes() sometimes returns 0 if you check too fast after accepting the connection, hence the await process_frame. Not bulletproof for large POST bodies but works fine for localhost GET queries.

Anyone else doing external tooling like this? Curious if there's a cleaner pattern, or if someone's taken it further. The obvious next step is a little browser devtools panel that auto-refreshes registered endpoints.

Moonjump
Forum Search Shader Sandbox
Sign In Register