// The Spawn SDK in Unity WebGL builds: https://spawn.co/about/publishing/sdk // Put this file and Spawn.jslib in Assets/Plugins/Spawn/. Every call is window.Spawn's call by its path, awaited: // var answer = await SpawnSDK.Request("saves.set", "progress", new Dictionary { { "best", 120 } }); // if (answer.Ok) { ... answer.DataJson ... } else { Debug.Log(answer.Code + ": " + answer.Message); } // Arguments are strings, numbers, bools, null, byte[], lists and dictionaries. An answer's data is JSON text: read it // with JsonUtility or Newtonsoft. Outside a WebGL build every call answers Code "unavailable". using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using UnityEngine; public static class SpawnSDK { public struct Answer { public bool Ok; /// Why it failed: signed_out, not_found, conflict, cancelled, unavailable, ... public string Code; public string Message; /// The call's data as JSON text ("null" when it has none). public string DataJson; } /// Every Spawn event: its name ("purchase", "player", "lobby.changed", ...) and its data as JSON text. public static event Action OnEvent; /// The person playing as JSON text: {"id","name","handle","avatarUrl","guest"}; "null" outside a WebGL build. public static string PlayerJson { get { #if UNITY_WEBGL && !UNITY_EDITOR return SpawnBridgePlayer() ?? "null"; #else return "null"; #endif } } public static Task Request(string path, params object[] args) { var done = new TaskCompletionSource(); #if UNITY_WEBGL && !UNITY_EDITOR Receiver.Ensure(); var id = (++_next).ToString(CultureInfo.InvariantCulture); _pending[id] = done; SpawnBridgeAsk(path, Json(args ?? new object[0]), id); #else done.SetResult(new Answer { Ok = false, Code = "unavailable", Message = "Spawn calls work in a WebGL build.", DataJson = "null" }); #endif return done.Task; } // ———— the calls (window.Spawn's, by path) ———— public static Task SignIn() => Request("auth.signIn"); public static Task SavesGet(string name) => Request("saves.get", name); public static Task SavesSet(string name, object data) => Request("saves.set", name, data); public static Task SavesList() => Request("saves.list"); public static Task SavesDelete(string name) => Request("saves.delete", name); public static Task StateSet(IDictionary rows) => Request("state.set", rows); public static Task AchievementsUnlock(string key) => Request("achievements.unlock", key); /// Several achievements in one call. public static Task AchievementsUnlock(IList keys) => Request("achievements.unlock", (object)keys); public static Task LeaderboardsSubmit(string board, double value) => Request("leaderboards.submit", board, value); /// options: keepBest (false posts this value even when it is worse), metadata (a few short values shown with the row). public static Task LeaderboardsSubmit(string board, double value, IDictionary options) => Request("leaderboards.submit", board, value, options); public static Task LeaderboardsTop(string board) => Request("leaderboards.top", board); public static Task StoreItems() => Request("store.items"); public static Task StoreOwned() => Request("store.owned"); public static Task StoreBuy(string key) => Request("store.buy", key); public static Task StoreFulfill(string purchaseId) => Request("store.fulfill", purchaseId); public static Task FriendsList() => Request("friends.list"); public static Task PresenceSet(string text) => Request("presence.set", text); public static Task Loaded() => Request("loaded"); #if UNITY_WEBGL && !UNITY_EDITOR [DllImport("__Internal")] private static extern void SpawnBridgeAsk(string path, string argsJson, string id); [DllImport("__Internal")] private static extern void SpawnBridgeListen(); [DllImport("__Internal")] private static extern string SpawnBridgePlayer(); #endif private static int _next; private static readonly Dictionary> _pending = new Dictionary>(); [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] private static void Start() { #if UNITY_WEBGL && !UNITY_EDITOR Receiver.Ensure(); SpawnBridgeListen(); #endif } /// The object Spawn.jslib answers through Unity's SendMessage. private class Receiver : MonoBehaviour { private static Receiver _one; public static void Ensure() { if (_one != null) return; var go = new GameObject("SpawnBridge"); DontDestroyOnLoad(go); _one = go.AddComponent(); } // "id\nok\ncode\ndata-or-message" public void OnAnswer(string word) { var parts = word.Split(new[] { '\n' }, 4); if (parts.Length < 4 || !_pending.TryGetValue(parts[0], out var done)) return; _pending.Remove(parts[0]); var ok = parts[1] == "1"; done.SetResult(new Answer { Ok = ok, Code = ok ? null : parts[2], Message = ok ? null : parts[3], DataJson = ok ? parts[3] : "null", }); } // "event\ndata" public void OnEvent(string word) { var cut = word.IndexOf('\n'); if (cut < 0) return; SpawnSDK.OnEvent?.Invoke(word.Substring(0, cut), word.Substring(cut + 1)); } } // ———— arguments as JSON (bytes as {"$bytes": base64}) ———— private static string Json(object value) { var sb = new StringBuilder(); Write(sb, value); return sb.ToString(); } private static void Write(StringBuilder sb, object value) { switch (value) { case null: sb.Append("null"); return; case string s: Quote(sb, s); return; case bool b: sb.Append(b ? "true" : "false"); return; case byte[] bytes: sb.Append("{\"$bytes\":"); Quote(sb, Convert.ToBase64String(bytes)); sb.Append('}'); return; case float f: sb.Append(f.ToString("R", CultureInfo.InvariantCulture)); return; case double d: sb.Append(d.ToString("R", CultureInfo.InvariantCulture)); return; case IDictionary map: sb.Append('{'); var first = true; foreach (DictionaryEntry entry in map) { if (!first) sb.Append(','); first = false; Quote(sb, Convert.ToString(entry.Key, CultureInfo.InvariantCulture)); sb.Append(':'); Write(sb, entry.Value); } sb.Append('}'); return; case IEnumerable list: sb.Append('['); var head = true; foreach (var item in list) { if (!head) sb.Append(','); head = false; Write(sb, item); } sb.Append(']'); return; case IFormattable number: sb.Append(number.ToString(null, CultureInfo.InvariantCulture)); return; default: Quote(sb, value.ToString()); return; } } private static void Quote(StringBuilder sb, string s) { sb.Append('"'); foreach (var c in s) { switch (c) { case '"': sb.Append("\\\""); break; case '\\': sb.Append("\\\\"); break; case '\n': sb.Append("\\n"); break; case '\r': sb.Append("\\r"); break; case '\t': sb.Append("\\t"); break; default: if (c < ' ') sb.Append("\\u").Append(((int)c).ToString("x4")); else sb.Append(c); break; } } sb.Append('"'); } }