Real-time messaging for Godot 4 in pure GDScript — channels, presence, history and game features on every export target
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.
One addon folder, zero native dependencies. Works identically in the editor, on desktop, mobile and web exports.
Every request/response call is awaitable: await channel.publish_async(...) returns the acknowledgement or an error Dictionary. Pushes arrive as typed signals.
Exponential backoff after unexpected drops, with automatic channel resubscription — and no reconnect storm after an intentional disconnect.
Join/leave pushes, occupancy queries and server-retained message history, all scoped per channel.
v1.0.1 of the GitHub repository:
addons/oddsockets/ into your project's addons/ folder.class_name. Enabling the plugin under Project Settings → Plugins is optional.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().
Main client. Add it to the scene tree — it polls its connection every frame.
initialize(config: OddSocketsConfig) — configure before connectingconnect_async() -> Dictionary — connect; {} on success, {"error": ...} otherwisedisconnect_client() — intentional close (no auto-reconnect follows)get_channel(name) -> OddSocketsChannel — channel accessoremit_async(event, payload) — send a custom eventprovide_token(fresh) — answer a token_requested firing{"error": ...}):subscribe_async(options) — options: enable_presence, retain_historyunsubscribe_async()publish_async(message) — Dictionaries/Arrays JSON-encoded; strings sent as-is; 32KB cap checked client-sideget_presence_async() / get_history_async(count)update_state_async(state) — publish this client's presence stateMessages 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 }.
connected / disconnected(code, reason) / connection_error(message)request_error(event, message) — the platform rejected a requestenhanced_event(event_name, data) — everything not routed to a channeltoken_requested — answer with provide_token(...)message_received(envelope) — a published message arrivedpresence_changed(data) — someone joined or leftpresence_received(data) / history_received(data) / published(data)subscribed(data) / unsubscribed(data)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.
send_challenge_invite_async(to_user_id, type, payload, ttl, channel) — directed invite. Resolves with { inviteId, ... }.reply_challenge_invite_async(invite_id, accept, payload, reason) — accept/decline; your payload reaches the inviter as replyPayload.cancel_challenge_invite_async(invite_id) — withdraw a pending invite.get_challenge_invites_async() — pending invites (e.g. on reconnect).create_challenge_async(challenge_id, metric, ranked, options) — create a challenge/leaderboard.report_progress_async(challenge_id, value, metric) — fire-and-forget metric progress.complete_challenge_async(challenge_id, outcome, options) — finalize; resolves with the final rank. Outcomes: completed, failed, tied, conceded, expired.get_standings_async(challenge_id, limit, offset) — top-N + caller rank.unlock_achievement_async(achievement_id, name, percent_complete) — percent_complete < 100 reports progress; >= 100 unlocks. The platform is authoritative and picks the broadcast.get_achievements_async(achievement_id) — state arrives on achievement_state.start_typing / stop_typing / add_reaction / remove_reaction — social surface, fire-and-forget.challenge_invited, challenge_reply_received, challenge_invite_cancelled, challenge_inviteschallenge_progress, challenge_complete, leaderboard_rank_change, achievement_state, standings_data, user_typing, reaction_added, reaction_removedPlayer 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:
Prefer signals? Connect token_requested and answer each firing with provide_token({"token": ..., "expires_at": ...}).
subscribe_async returns {"error": ...} on failure) and the message size (32KB limit).get_history_async().token_provider (or the token_requested signal) so refresh happens before expiry; a static config.token dies with its expires_at.