🎮 OddSockets Godot SDK

Real-time messaging for Godot 4 in pure GDScript — channels, presence, history and game features on every export target

📋 Overview

The OddSockets Godot SDK is a pure GDScript addon for Godot 4.3+. No native builds, no platform-specific setup — it runs on every Godot export target. It provides pub/sub channels, presence, retained history, and a game-feature surface (challenges, leaderboards, achievements, 1:1 invites) whose method names mirror the Unity SDK one-to-one, so a project ported from Unity maps line-for-line.

📜 Pure GDScript

One addon folder, zero native dependencies. Works identically in the editor, on desktop, mobile and web exports.

⏳ Awaitable API

Every request/response call is awaitable: await channel.publish_async(...) returns the acknowledgement or an error Dictionary. Pushes arrive as typed signals.

🔄 Auto-Reconnection

Exponential backoff after unexpected drops, with automatic channel resubscription — and no reconnect storm after an intentional disconnect.

👥 Presence & History

Join/leave pushes, occupancy queries and server-retained message history, all scoped per channel.

🚀 Quick Start

1. Installation

  1. Clone release tag v1.0.1 of the GitHub repository:
    git clone --depth 1 --branch v1.0.1 https://github.com/jyswee/oddsockets-godot-sdk
  2. Copy addons/oddsockets/ into your project's addons/ folder.
  3. Done — the classes register globally via class_name. Enabling the plugin under Project Settings → Plugins is optional.

2. Connect and publish

extends Node var client: OddSocketsClient func _ready() -> void: client = OddSocketsClient.new() add_child(client) # the client polls its connection every frame var config := OddSocketsConfig.new() config.api_key = "YOUR_API_KEY" config.user_id = "player-123" client.initialize(config) await client.connect_async() var channel := client.get_channel("game-lobby") channel.message_received.connect(_on_message) channel.presence_changed.connect(_on_roster_changed) # joins/leaves await channel.subscribe_async({"enable_presence": true, "retain_history": true}) await channel.publish_async({"move": "e4"}) # dictionaries are JSON-encoded func _on_message(envelope: Dictionary) -> void: # envelope: { id, channel, message, publisher, timestamp } print("%s says: %s" % [envelope.publisher, envelope.message]) func _on_roster_changed(data: Dictionary) -> void: # data: { channel, action: "join"|"leave", user: { userId, joinedAt }, occupancy } print("%s %sed (%d here)" % [data.user.userId, data.action, data.occupancy])

3. Presence and history are queries

var presence := await channel.get_presence_async() # { channel, occupancy, occupants } var history := await channel.get_history_async(50) # { channel, messages, count }

Retention is not replay. retain_history only tells the server to store messages; subscribing never replays that store into your handler, so a late joiner sees nothing published before it joined. To catch up, subscribe first, then fetch explicitly with get_history_async().

💻 API Reference

OddSocketsClient (extends Node)

Main client. Add it to the scene tree — it polls its connection every frame.

Key methods:

OddSocketsChannel

Key methods (all awaitable, all return the acknowledgement or {"error": ...}):

OddSocketsConfig

var config := OddSocketsConfig.new() config.api_key = "..." # or leave empty for token auth config.user_id = "player-123" # required config.token = {} # { token, expires_at } — usually via a provider config.token_refresh_lead_seconds = 60.0 config.auto_reconnect = true config.max_reconnect_attempts = 10 # 0 = unlimited config.request_timeout_seconds = 10.0

Message size

Messages are capped at 32KB (UTF-8 bytes). publish_async rejects oversized messages client-side; check earlier with OddSocketsMessageSizeValidator.get_message_size_info(text) → { size_bytes, max_bytes, valid, percent }.

🚦 Signals

OddSocketsClient:

  • connected / disconnected(code, reason) / connection_error(message)
  • request_error(event, message) — the platform rejected a request
  • enhanced_event(event_name, data) — everything not routed to a channel
  • token_requested — answer with provide_token(...)

OddSocketsChannel:

  • message_received(envelope) — a published message arrived
  • presence_changed(data) — someone joined or left
  • presence_received(data) / history_received(data) / published(data)
  • subscribed(data) / unsubscribed(data)

🏆 Challenges & Leaderboards

Game features live on OddSocketsEnhancedFeatures, initialized with your client. Method names mirror the Unity SDK one-to-one, snake_cased (SendChallengeInviteAsync → send_challenge_invite_async, same arguments in the same order). Request/query methods are awaitable; progress and achievement calls are fire-and-forget; inbound broadcasts arrive as typed signals.

var game := OddSocketsEnhancedFeatures.new() game.initialize(client) # Receive invites and replies game.challenge_invited.connect(func(data): print("%s invited you: %s" % [data.from.userId, data.payload])) game.challenge_reply_received.connect(func(data): # data.replyPayload = the replier's own object # data.payload = your original invite payload, echoed back # data.reason = the replier's free-text note print("accept=%s loadout=%s" % [data.accept, data.replyPayload.get("loadout")])) # Invite another player — the payload travels as a JSON object (8KB max) var invite := await game.send_challenge_invite_async( "player-456", "match", {"arena": "ruins", "seed": 42}, 120) # Answer an invite with your own payload and a note await game.reply_challenge_invite_async(invite.inviteId, true, {"loadout": "bow"}, "gl hf") # Challenges and leaderboards await game.create_challenge_async("weekly-sprint", "score", true) game.report_progress_async("weekly-sprint", 1250.0) # fire-and-forget var standings := await game.get_standings_async("weekly-sprint", 10) # Achievements game.unlock_achievement_async("first-win", "First Win!") # fire-and-forget game.get_achievements_async() # results arrive on the achievement_state signal

Methods

Signals

🔐 Token Auth

Player devices should not carry your API key. Exchange a short-lived token server-side and hand it to the client via a token provider — it is called before every (re)connect and again token_refresh_lead_seconds (default 60) before the token expires:

var config := OddSocketsConfig.new() config.user_id = "player-123" # no api_key: token auth client.initialize(config) client.token_provider = func() -> Dictionary: var fresh = await MyBackend.mint_realtime_token() # your server does the exchange return {"token": fresh.token, "expires_at": fresh.expires_at} # ISO 8601 await client.connect_async()

Prefer signals? Connect token_requested and answer each firing with provide_token({"token": ..., "expires_at": ...}).

🔧 Troubleshooting

🤝 Support & Resources