// Flickcade GameReels SDK — Unity facade. v1.1.3 // // One static class. WebGL builds talk to the host through GameReels.jslib; // every other platform (Editor Play mode, Android/iOS native, standalone) // compiles to safe no-ops with OnActive fired once on startup, so the same // game code runs everywhere unchanged. Every send returns the bridge's own // answer — true when the event was accepted, false when it was dropped (over // a limit, no bridge, not a WebGL build) — exactly like the JavaScript surface. // // Usage: // void Start() { // Flickcade.OnActive += () => Time.timeScale = 1f; // Flickcade.OnHidden += () => { Time.timeScale = 0f; AudioListener.pause = true; }; // Flickcade.Ready(); // call when gameplay could start // } // // Your game BOOTS HIDDEN inside Flickcade (the feed warm-loads games off // screen). Never start gameplay/audio on scene load — start on OnActive. using System; using System.Collections.Generic; using UnityEngine; #if UNITY_WEBGL && !UNITY_EDITOR using System.Runtime.InteropServices; using System.Text; #endif namespace FlickcadeSDK { /// Level outcome reported to the host. public enum Outcome { Win, Lose, Quit, } /// Reward kind reported to the host. public enum RewardType { Currency, Item, } /// /// The Flickcade host bridge. All calls are fire-and-forget, never throw, /// never block, and are safe no-ops outside a Flickcade WebGL build. Each /// send reports whether the bridge accepted the event (true) or /// dropped it (false: over a limit, invalid argument, no bridge, or /// not a WebGL build) — the same boolean the JavaScript API returns. /// public static class Flickcade { /// Your game became the visible reel — resume. public static event Action OnActive { add { EnsureBootstrap(); // subscribing IS first use — wire the receiver onActive += value; // Late subscribers still get the initial "you are visible" // signal they would have missed. if (initialActiveDelivered && visible) value?.Invoke(); } remove => onActive -= value; } /// Your game was hibernated — pause everything. public static event Action OnHidden { add { EnsureBootstrap(); onHidden += value; } remove => onHidden -= value; } private static Action onActive; private static Action onHidden; private static bool initialActiveDelivered; /// True while your game is the visible reel. public static bool Visible { get { EnsureBootstrap(); return visible; } } private static bool visible; #if UNITY_WEBGL && !UNITY_EDITOR [DllImport("__Internal")] private static extern int FlickcadeIsHost(); [DllImport("__Internal")] private static extern int FlickcadeVisible(); [DllImport("__Internal")] private static extern void FlickcadeSubscribe(); [DllImport("__Internal")] private static extern string FlickcadeVersion(); // Sends return the bridge's boolean as 1/0 (a .jslib cannot return a C# bool). [DllImport("__Internal")] private static extern int FlickcadeReady(); [DllImport("__Internal")] private static extern int FlickcadeLevelStart(string level); [DllImport("__Internal")] private static extern int FlickcadeLevelEnd(string level, string outcome, int ms); [DllImport("__Internal")] private static extern int FlickcadeScore(double value); // amount = NaN means "not given": the bridge then omits the field from the wire. [DllImport("__Internal")] private static extern int FlickcadeReward(string type, string id, double amount); [DllImport("__Internal")] private static extern int FlickcadeCustom(string name, string dataJson); [DllImport("__Internal")] private static extern int FlickcadeAdBreak(string kind); /// True when running inside the Flickcade app. public static bool IsHost => Bootstrap() && FlickcadeIsHost() == 1; /// /// Version of the bridge this build is talking to (the host's copy wins) — /// GameReels.version. Empty when no bridge is present. /// public static string Version => Bootstrap() ? FlickcadeVersion() ?? string.Empty : string.Empty; #else /// True when running inside the Flickcade app. public static bool IsHost => false; /// /// Version of the bridge this build is talking to. Empty outside a WebGL /// build — there is no bridge to ask. /// public static string Version => string.Empty; #endif /// /// Your game is playable. Call exactly once, as early as gameplay /// could start — off-screen boots keep running until this call /// (bounded ~8 s), then the host freezes them to save battery. /// Note: the loading cover is lifted by the host's own first-frame /// detection; Ready() does not control it. /// /// True when the bridge accepted the call. public static bool Ready() { #if UNITY_WEBGL && !UNITY_EDITOR Bootstrap(); return FlickcadeReady() == 1; #else return false; #endif } /// A level/stage/wave began. /// True when the bridge accepted the event. public static bool LevelStart(string level) { #if UNITY_WEBGL && !UNITY_EDITOR Bootstrap(); return FlickcadeLevelStart(level ?? string.Empty) == 1; #else return false; #endif } /// A level ended. < 0 = not reported. /// True when the bridge accepted the event. public static bool LevelEnd(string level, Outcome outcome, int ms = -1) { #if UNITY_WEBGL && !UNITY_EDITOR Bootstrap(); return FlickcadeLevelEnd(level ?? string.Empty, OutcomeWire(outcome), ms) == 1; #else return false; #endif } /// /// Current or best score of this play session. NaN and infinities are /// refused here rather than sent: the bridge would drop the whole event /// (invalid JSON numbers), and a silent drop is worse than a no-op. /// /// True when the bridge accepted the event. public static bool Score(double value) { #if UNITY_WEBGL && !UNITY_EDITOR if (double.IsNaN(value) || double.IsInfinity(value)) return false; Bootstrap(); return FlickcadeScore(value) == 1; #else return false; #endif } /// /// The player earned currency or an item. Leave /// null to omit it from the event (the JavaScript surface's "amount not /// given"); NaN/infinite amounts are refused for the same reason as /// . /// /// True when the bridge accepted the event. public static bool Reward(RewardType type, string id, double? amount = null) { #if UNITY_WEBGL && !UNITY_EDITOR if (amount is double a && (double.IsNaN(a) || double.IsInfinity(a))) return false; Bootstrap(); // NaN is the "omitted" sentinel across the extern boundary; a caller's // own NaN was refused above, so the jslib sees NaN only for null. return FlickcadeReward( type == RewardType.Currency ? "currency" : "item", id ?? string.Empty, amount ?? double.NaN) == 1; #else return false; #endif } /// /// Bounded custom event. Name: letters/digits/underscore, max 32 /// chars; data: max 8 scalar values — string, bool, int, long, or a /// finite float/double (other types and NaN/Infinity are skipped). /// /// True when the bridge accepted the event. public static bool Custom(string name, IReadOnlyDictionary data = null) { #if UNITY_WEBGL && !UNITY_EDITOR Bootstrap(); return FlickcadeCustom(name ?? string.Empty, data == null ? string.Empty : ToJson(data)) == 1; #else return false; #endif } /// /// Mark a natural pause point (level end, game-over screen) where the /// host may later place an ad. Recorded only for now. /// /// True when the bridge accepted the event. public static bool AdBreak(string kind = null) { #if UNITY_WEBGL && !UNITY_EDITOR Bootstrap(); return FlickcadeAdBreak(kind ?? string.Empty) == 1; #else return false; #endif } #if UNITY_WEBGL && !UNITY_EDITOR private static bool bootstrapped; /// /// Idempotent wiring: creates the hidden receiver GameObject the jslib /// SendMessages into and subscribes to the host lifecycle. Runs on /// first use AND after scene loads (the receiver is DontDestroyOnLoad, /// so it survives them). /// private static bool Bootstrap() { if (bootstrapped) return true; bootstrapped = true; var host = new GameObject("__FlickcadeSDK"); UnityEngine.Object.DontDestroyOnLoad(host); // No hideFlags: HideInHierarchy removes the object from the Scene // internally (Unity HideFlags docs), which breaks the name-based // SendMessage delivery the jslib depends on. host.AddComponent(); FlickcadeSubscribe(); visible = FlickcadeVisible() == 1; initialActiveDelivered = true; if (visible) onActive?.Invoke(); return true; } private static void EnsureBootstrap() => Bootstrap(); private static string OutcomeWire(Outcome outcome) => outcome switch { Outcome.Win => "win", Outcome.Lose => "lose", _ => "quit", }; /// /// Minimal flat-object JSON writer (the SDK's payload limits make a /// full serializer pointless): string/bool/numeric values only — /// anything else is skipped, mirroring the bridge's own validation. /// private static string ToJson(IReadOnlyDictionary data) { var sb = new StringBuilder(128); sb.Append('{'); var first = true; foreach (var pair in data) { string value; switch (pair.Value) { case string s: value = Quote(s); break; case bool b: value = b ? "true" : "false"; break; case int or long: value = Convert.ToString(pair.Value, System.Globalization.CultureInfo.InvariantCulture); break; case float f: if (float.IsNaN(f) || float.IsInfinity(f)) continue; // NaN/Inf is not JSON value = f.ToString("R", System.Globalization.CultureInfo.InvariantCulture); break; case double d: if (double.IsNaN(d) || double.IsInfinity(d)) continue; value = d.ToString("R", System.Globalization.CultureInfo.InvariantCulture); break; default: continue; } if (!first) sb.Append(','); first = false; sb.Append(Quote(pair.Key)).Append(':').Append(value); } sb.Append('}'); return sb.ToString(); } private static string Quote(string s) { var sb = new StringBuilder(s.Length + 2); 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 < 0x20) sb.Append("\\u").Append(((int)c).ToString("x4")); else sb.Append(c); break; } } sb.Append('"'); return sb.ToString(); } /// /// Hidden receiver the jslib SendMessages into. Internal — never add /// this to a scene yourself. /// private sealed class FlickcadeReceiver : MonoBehaviour { // ReSharper disable UnusedMember.Local — invoked via SendMessage. private void OnHostActive(string _) { visible = true; onActive?.Invoke(); } private void OnHostHidden(string _) { visible = false; onHidden?.Invoke(); } // ReSharper restore UnusedMember.Local } #else static Flickcade() { // Outside Flickcade (Editor, native platforms) the game is simply // always visible; deliver the initial OnActive to late subscribers. visible = true; initialActiveDelivered = true; } private static void EnsureBootstrap() { // Non-WebGL platforms have no receiver to wire — the static // constructor above already established the always-visible state. } #endif } }