mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
f628ccfd35
newAPIService never supplied SteamLogin, so POST /v1/session/steam always returned 503 auth_unavailable in production. The only implementation was cmd/testkit-api's fake, which derives an identity from the ticket string itself and accepts anything -- so the passing integration path was neither deployable nor secure. On the client side the game started with an empty token and a loopback base URL, and no production code called configure() or login_steam(); the menu entered matchmaking directly, so every request failed ERR_UNAUTHORIZED before reaching the network. Add a real ISteamUserAuth/AuthenticateUserTicket adapter behind an interface, so the production login path is testable with only the Valve call stubbed. It rejects family-shared copies (the account playing does not own the app) and, by default, VAC- or publisher-banned accounts, and refuses malformed tickets locally rather than forwarding them. Crucially it separates our faults from the player's: a Valve outage or a revoked publisher key returns 503, not 401. Answering 401 would tell a legitimate player their login failed and send them to fix an account that is fine while the real fault went unnoticed. A banned identity now returns 403 rather than a misleading 503. Sign-in is configuration-gated on the publisher key and App ID: without them the endpoint keeps returning 503, since silently accepting an unverified ticket would be worse than refusing to authenticate. A returning player keeps the player ID they already had, so ratings, penalties and bans follow the account rather than the session. Client side: acquire a web-API ticket through GodotSteam's async signal -- requesting one returns a handle, not a ticket -- using the existing dynamic-call pattern so stock Godot still parses the project. The endpoint is configurable for release builds, and matchmaking completes sign-in before it will queue. Verified against real PostgreSQL; 232 Godot tests pass.
93 lines
3.5 KiB
GDScript
93 lines
3.5 KiB
GDScript
class_name SteamBootstrap
|
|
extends RefCounted
|
|
|
|
# Spacewar is Valve's development App ID. It is intentionally a development
|
|
# default, never a public-server identity or discovery configuration.
|
|
const SPACEWAR_APP_ID := 480
|
|
const APP_ID_ENV := "COSMIC_CLASH_STEAM_APP_ID"
|
|
|
|
|
|
static func app_id() -> int:
|
|
var configured := OS.get_environment(APP_ID_ENV).strip_edges()
|
|
if configured.is_valid_int() and int(configured) > 0:
|
|
return int(configured)
|
|
return SPACEWAR_APP_ID
|
|
|
|
|
|
static func is_runtime_available() -> bool:
|
|
return OS.has_feature("steam") and ClassDB.class_exists("SteamMultiplayerPeer") and Engine.has_singleton("Steam")
|
|
|
|
|
|
static func unavailable_reason() -> String:
|
|
if not OS.has_feature("steam"):
|
|
return "this export was not built with the steam feature"
|
|
if not ClassDB.class_exists("SteamMultiplayerPeer"):
|
|
return "SteamMultiplayerPeer is missing from this custom Godot build"
|
|
if not Engine.has_singleton("Steam"):
|
|
return "the GodotSteam Steam singleton is missing from this custom Godot build"
|
|
return "Steam is unavailable"
|
|
|
|
|
|
static func initialize() -> Dictionary:
|
|
if not is_runtime_available():
|
|
return {"error": ERR_UNAVAILABLE, "reason": unavailable_reason()}
|
|
var steam := Engine.get_singleton("Steam")
|
|
# `steamInit` is deliberately called dynamically: stock Godot must be able
|
|
# to parse and run this project without GodotSteam symbols installed.
|
|
var result = steam.call("steamInit")
|
|
if result is bool and result:
|
|
return {"error": OK, "app_id": app_id()}
|
|
if result is Dictionary and bool(result.get("status", false)):
|
|
return {"error": OK, "app_id": app_id()}
|
|
return {"error": ERR_CANT_CONNECT, "reason": "Steam initialization failed for App ID %d" % app_id()}
|
|
|
|
|
|
# Web-API auth ticket acquisition (task 7.6). The control plane exchanges this
|
|
# ticket with Valve's publisher API for a verified Steam identity; the client
|
|
# never chooses its own identity, which is what makes this the fix for slot
|
|
# reclaim being keyed on a display name.
|
|
#
|
|
# GodotSteam delivers the ticket asynchronously through the
|
|
# `get_auth_ticket_for_web_api` signal, because the ticket is not usable until
|
|
# Steam has confirmed it with its backend. Requesting one and reading the
|
|
# return value alone yields a handle, not a ticket.
|
|
#
|
|
# Everything here is called dynamically so stock Godot, which has no GodotSteam
|
|
# symbols, can still parse and run the project.
|
|
const WEB_API_IDENTITY := "cosmicclash"
|
|
|
|
|
|
static func supports_web_api_ticket() -> bool:
|
|
if not is_runtime_available():
|
|
return false
|
|
var steam := Engine.get_singleton("Steam")
|
|
return steam.has_signal("get_auth_ticket_for_web_api") and steam.has_method("getAuthTicketForWebApi")
|
|
|
|
|
|
# Returns the request handle, or 0 when unavailable. The caller must await the
|
|
# `get_auth_ticket_for_web_api` signal for the ticket itself.
|
|
static func request_web_api_ticket() -> int:
|
|
if not supports_web_api_ticket():
|
|
return 0
|
|
var steam := Engine.get_singleton("Steam")
|
|
var handle = steam.call("getAuthTicketForWebApi", WEB_API_IDENTITY)
|
|
return int(handle) if handle is int or handle is float else 0
|
|
|
|
|
|
static func cancel_web_api_ticket(handle: int) -> void:
|
|
if handle <= 0 or not is_runtime_available():
|
|
return
|
|
var steam := Engine.get_singleton("Steam")
|
|
if steam.has_method("cancelAuthTicket"):
|
|
steam.call("cancelAuthTicket", handle)
|
|
|
|
|
|
# GodotSteam hands back raw ticket bytes; the Web API expects them hex encoded.
|
|
static func encode_web_api_ticket(buffer: PackedByteArray) -> String:
|
|
if buffer.is_empty():
|
|
return ""
|
|
var encoded := ""
|
|
for byte in buffer:
|
|
encoded += "%02x" % int(byte)
|
|
return encoded
|