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