// 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
}
}