# Hyperstone Unity SDK

Hyperstone Unity SDK integrates your Unity application with [Hyperstone](https://console.hyperstone.ai/) — a real-time user experience optimization platform. Instead of running manual A/B tests, Hyperstone continuously tunes your app's configuration using stochastic optimization algorithms, adapting to user behavior and maximizing your target metric (e.g., revenue, engagement).

The SDK handles the full lifecycle: fetching optimized configurations from the Hyperstone service, applying them in your app, and reporting user events back so the optimization algorithm can learn and improve.

**Requirements:** Unity 2022.3 or later.

## Table of Contents

- [Installation](#installation)
  - [Option 1: UPM Scoped Registry (Recommended)](#option-1-upm-scoped-registry-recommended)
  - [Option 2: Local Tarball Package](#option-2-local-tarball-package)
  - [Option 3: .unitypackage](#option-3-unitypackage)
- [Quick Start](#quick-start)
- [Initialization](#initialization)
- [Configuration](#configuration)
  - [Subscribing to Config Changes](#subscribing-to-config-changes)
  - [Reading Config Values](#reading-config-values)
  - [Deserializing Config as an Object](#deserializing-config-as-an-object)
- [Event Tracking](#event-tracking)
  - [Ad Events](#ad-events)
  - [In-App Purchase Events](#in-app-purchase-events)
  - [Custom Events and Metrics](#custom-events-and-metrics)
- [Ad Impression Revenue (ILRD) Examples](#ad-impression-revenue-ilrd-examples)
  - [AdMob](#admob)
  - [IronSource (LevelPlay)](#ironsource-levelplay)
- [Clearing User Data](#clearing-user-data)
- [Logging](#logging)
- [How It Works](#how-it-works)
- [Thread Safety](#thread-safety)
- [Advanced: Multi-Tenancy](#advanced-multi-tenancy)
- [API Reference](#api-reference)

## Installation

### Option 1: UPM Scoped Registry (Recommended)

Add the Hyperstone scoped registry to your project's `Packages/manifest.json`:

```json
{
  "scopedRegistries": [
    {
      "name": "Hyperstone",
      "url": "https://fra1.digitaloceanspaces.com/hyperstone/upm",
      "scopes": ["com.theoreticalstudio"]
    }
  ],
  "dependencies": {
    "com.theoreticalstudio.hyperstone": "1.0.0"
  }
}
```

Alternatively, in the Unity Editor:

1. Open **Edit → Project Settings → Package Manager**
2. Under **Scoped Registries**, click **+** and enter:
   - **Name:** `Hyperstone`
   - **URL:** `https://fra1.digitaloceanspaces.com/hyperstone/upm`
   - **Scope(s):** `com.theoreticalstudio`
3. Open **Window → Package Manager**, switch to **My Registries**, and install **Hyperstone SDK**

<!-- TODO: Screenshot of Package Manager scoped registry setup -->

### Option 2: Local Tarball Package

1. Download the latest `.tgz` package from [here](https://hyperstone.ai/docs/unity-sdk/versions/)
2. In Unity, open **Window → Package Manager**
3. Click **+** → **Add package from tarball…**
4. Select the downloaded `.tgz` file

<!-- TODO: Screenshot of "Add package from tarball" dialog -->

### Option 3: .unitypackage

1. Download the latest `.unitypackage` from [here](https://hyperstone.ai/docs/unity-sdk/versions/)
2. In Unity, go to **Assets → Import Package → Custom Package…**
3. Select the downloaded file and click **Import**

> **Note:** The UPM approaches (Options 1 & 2) are preferred as they provide proper dependency management and easy updates.

## Quick Start

```csharp
using UnityEngine;
using HyperstoneSdk;

public class MyGame : MonoBehaviour
{
    void Start()
    {
        // Subscribe to config changes — can be called before Initialize
        Hyperstone.WatchConfig(OnConfigChanged);

        // Initialize with your credentials from the Hyperstone Console
        Hyperstone.Initialize("YOUR_APP_ID", "YOUR_APP_SECRET");
    }

    void OnConfigChanged(HyperstoneConfig config)
    {
        // Read optimized values sent by Hyperstone
        int lives = config.GetIntOrDefault("n_lives", 3);
        bool showAdsAfterDeath = config.GetBoolOrDefault("show_ads_after_death", true);

        // Apply to your game logic
        GameManager.Instance.SetLives(lives);
        GameManager.Instance.SetAdsAfterDeath(showAdsAfterDeath);
    }

    void OnDestroy()
    {
        Hyperstone.UnwatchConfig(OnConfigChanged);
    }
}
```

That's it. Hyperstone will automatically:
1. Fetch an optimized configuration tailored for this user
2. Deliver it to your callback
3. Track session metrics (sessions count, engagement time)
4. Send accumulated metrics to the Hyperstone backend

## Initialization

Call `Hyperstone.Initialize()` once during your app startup (typically in a `MonoBehaviour.Start()` or a bootstrap script):

```csharp
Hyperstone.Initialize("YOUR_APP_ID", "YOUR_APP_SECRET");
```

| Parameter   | Type     | Description                                                                 |
|-------------|----------|-----------------------------------------------------------------------------|
| `appId`     | `string` | Your application ID from the Hyperstone Console.                              |
| `appSecret` | `string` | Your application secret from the Hyperstone Console.                          |
| `logLevel`  | `HyperstoneClient.LogLevel` | *(Optional)* Logging verbosity. Default: `LogLevel.Info`. |

You can find your App ID and App Secret in the [Hyperstone Console](https://console.hyperstone.ai/).

> **Important:** `Initialize` must only be called once. Subsequent calls are ignored with a warning.

### Custom API Endpoint

For self-hosted or staging environments, you can specify a custom API URL:

```csharp
Hyperstone.Initialize("YOUR_APP_ID", "YOUR_APP_SECRET", apiUrl: "https://your-custom-endpoint.example.com");

// Both overloads accept an optional logLevel parameter:
Hyperstone.Initialize("YOUR_APP_ID", "YOUR_APP_SECRET", "https://your-custom-endpoint.example.com", HyperstoneClient.LogLevel.Debug);
```

## Configuration

### Subscribing to Config Changes

Use `Hyperstone.WatchConfig()` to receive configuration updates. **You can subscribe at any point in your project — even before calling `Initialize`.** This is particularly useful for decoupled architectures where different systems register their interest in config independently.

```csharp
// Subscribe early — even before Initialize
Hyperstone.WatchConfig(OnConfigChanged);

void OnConfigChanged(HyperstoneConfig config)
{
    // This fires:
    // 1. Immediately with the cached config (if one exists)
    // 2. Again when a fresh config arrives from the server
}
```

When you subscribe:
- If a config is already available (cached from a previous session or fetched from the server), your callback fires **immediately** with the current config.
- When a fresh config is fetched from the server, all registered callbacks fire again with the new config.

To unsubscribe:

```csharp
Hyperstone.UnwatchConfig(OnConfigChanged);
```

> **Tip:** Always unsubscribe in `OnDestroy()` to prevent callbacks to destroyed objects.

### Reading Config Values

`HyperstoneConfig` provides typed accessors with default value fallbacks. The config keys and their value types are defined by you in the Hyperstone Console when configuring your optimization parameters.

```csharp
void OnConfigChanged(HyperstoneConfig config)
{
    // Primitives with safe defaults
    int lives       = config.GetIntOrDefault("n_lives", 3);
    float speed     = config.GetFloatOrDefault("player_speed", 5.0f);
    double ratio    = config.GetDoubleOrDefault("reward_ratio", 1.5);
    long bigNum     = config.GetLongOrDefault("score_cap", 1000000L);
    bool showAds    = config.GetBoolOrDefault("show_ads_after_death", true);
    string theme    = config.GetStringOrDefault("ui_theme", "dark");

    // TryGet pattern (returns false if key is missing or type mismatch)
    if (config.TryGetInt("bonus_level", out int bonus))
    {
        // Use bonus
    }
}
```

**Available accessors:**

| Method                    | Return Type | Description                                   |
|---------------------------|-------------|-----------------------------------------------|
| `GetIntOrDefault`         | `int`       | Integer value or default                      |
| `GetLongOrDefault`        | `long`      | Long integer value or default                 |
| `GetFloatOrDefault`       | `float`     | Single-precision float value or default       |
| `GetDoubleOrDefault`      | `double`    | Double-precision float value or default       |
| `GetBoolOrDefault`        | `bool`      | Boolean value or default                      |
| `GetStringOrDefault`      | `string?`   | String value or default                       |
| `TryGetInt`               | `bool`      | Tries to get an `int`, returns success        |
| `TryGetLong`              | `bool`      | Tries to get a `long`, returns success        |
| `TryGetFloat`             | `bool`      | Tries to get a `float`, returns success       |
| `TryGetDouble`            | `bool`      | Tries to get a `double`, returns success      |
| `TryGetString`            | `bool`      | Tries to get a `string`, returns success      |
| `TryGetBool`              | `bool`      | Tries to get a `bool`, returns success        |
| `TryGetValueAsJson`       | `bool`      | Returns the raw JSON string for a nested key  |
| `TryGetValueAsObject<T>`  | `bool`      | Deserializes a nested JSON value via `JsonUtility` |
| `AsObject<T>`             | `T`         | Deserializes the **entire** config via `JsonUtility` |
| `OverwriteObject`         | `void`      | Overwrites fields on an existing object from config |

You can also iterate over all available keys:

```csharp
foreach (string key in config.Keys)
{
    Debug.Log($"Config key: {key}");
}
```

### Deserializing Config as an Object

If your config maps cleanly to a C# class, you can deserialize it in one call using Unity's `JsonUtility`:

```csharp
[Serializable]
public class GameConfig
{
    public int n_lives;
    public bool show_ads_after_death;
    public float player_speed;
}

void OnConfigChanged(HyperstoneConfig config)
{
    GameConfig gameConfig = config.AsObject<GameConfig>();
    // or overwrite an existing instance:
    // config.OverwriteObject(existingGameConfig);
}
```

> **Note:** `AsObject<T>()` and `OverwriteObject()` use Unity's `JsonUtility` under the hood, so the usual `JsonUtility` rules apply (public fields or `[SerializeField]`, no properties, etc.).

## Event Tracking

Hyperstone needs to know what happens in your app to optimize the configuration. The SDK provides methods for tracking **standard events** (ads, in-app purchases) and **custom events/metrics**.

Some events are tracked automatically by the SDK and don't require any code:
- **Session count** — incremented each time the app starts or resumes from a long pause
- **Engagement time** — measured in minutes while the app is in the foreground

### Ad Events

```csharp
// User saw an ad — include the revenue for ILRD
Hyperstone.LogAdImpression(
    adPlatform: "admob",       // Mediation platform (e.g., "admob", "ironsource", "max")
    adNetwork:  "meta",        // The actual ad network that served the ad
    adFormat:   "rewarded",    // Ad format ("banner", "interstitial", "rewarded", "native")
    revenueUsd: 0.0032         // Estimated revenue in USD
);

// Ad was requested
Hyperstone.LogAdRequest(
    adPlatform: "admob",
    adFormat:   "interstitial"
);

// Ad failed to load/show
Hyperstone.LogAdError(
    adPlatform: "admob",
    adNetwork:  "meta",
    adFormat:   "interstitial",
    failReason: "no_fill"       // Optional, defaults to "unknown"
);
```

### In-App Purchase Events

```csharp
// Successful purchase
Hyperstone.LogIapTransactionSuccess(
    productId: "gems_100",
    currency:  "USD",
    price:     4.99
);

// Failed purchase
Hyperstone.LogIapTransactionError(
    productId: "gems_100",
    failReason: "cancelled"
);
```

### Custom Events and Metrics

For app-specific tracking, use custom events and metrics:

```csharp
// Simple event (counted with value = 1)
Hyperstone.LogCustomEvent("level_completed");

// Event with labels for segmentation
Hyperstone.LogCustomEvent("level_completed",
    new Label("difficulty", "hard"),
    new Label("world", "forest")
);

// Numeric metric (accumulates the value)
Hyperstone.IncrementCustomMetric("coins_spent", 50);

// Numeric metric with labels
Hyperstone.IncrementCustomMetric("damage_dealt", 127.5f,
    new Label("weapon", "sword"),
    new Label("enemy_type", "boss")
);
```

**Naming rules for custom metrics:**
- Alphanumeric characters, hyphens, and underscores only (`[a-zA-Z0-9-_]`)
- Maximum 100 characters
- Must **not** start with `_` (underscore-prefixed names are reserved for system metrics)

**Labels** attach low-cardinality metadata to measurements. The Hyperstone service uses labels to segment and aggregate data. Good label candidates are things like ad networks, difficulty levels, or item categories — not unique user IDs or timestamps.

## Ad Impression Revenue (ILRD) Examples

Impression-Level Revenue Data (ILRD) is critical for optimizing ad revenue. Here's how to integrate with popular ad mediation platforms.

### AdMob

```csharp
using GoogleMobileAds.Api;

// When setting up your rewarded ad:
rewardedAd.OnAdPaid += (AdValue adValue) =>
{
    // AdMob reports value in micros (1,000,000 micros = 1 unit of currency)
    double revenueUsd = adValue.Value / 1_000_000.0;

    Hyperstone.LogAdImpression(
        adPlatform: "admob",
        adNetwork:  "admob",           // AdMob as network when not using mediation
        adFormat:   "rewarded",
        revenueUsd: revenueUsd
    );
};

// For mediated ads, extract the actual ad network from the adapter class name or response info:
rewardedAd.OnAdPaid += (AdValue adValue) =>
{
    double revenueUsd = adValue.Value / 1_000_000.0;

    var responseInfo = rewardedAd.GetResponseInfo();
    var loadedAdapterName = responseInfo?.GetLoadedAdapterResponseInfo()?.AdSourceName ?? "unknown";

    Hyperstone.LogAdImpression(
        adPlatform: "admob",
        adNetwork:  loadedAdapterName,
        adFormat:   "rewarded",
        revenueUsd: revenueUsd
    );
};
```

### IronSource (LevelPlay)

```csharp
using Unity.Services.LevelPlay;

// Subscribe to the impression data event
IronSourceEvents.onImpressionDataReadyEvent += OnImpressionDataReady;

void OnImpressionDataReady(IronSourceImpressionData impressionData)
{
    if (impressionData.revenue.HasValue)
    {
        Hyperstone.LogAdImpression(
            adPlatform: "ironsource",
            adNetwork:  impressionData.adNetwork ?? "unknown",
            adFormat:   impressionData.adUnit ?? "unknown",
            revenueUsd: impressionData.revenue.Value
        );
    }
}
```

## Clearing User Data

To reset all locally stored Hyperstone data (e.g., when a user logs out or you want a clean slate):

```csharp
Hyperstone.ClearData();
```

This clears the stored user ID, cached config, accumulated metrics, session history, and purchaser status. The next `Initialize` call will treat this as a brand-new user.

## Logging

Control the SDK's log verbosity:

```csharp
Hyperstone.LogLevel = HyperstoneClient.LogLevel.Debug;   // Verbose — useful during development
Hyperstone.LogLevel = HyperstoneClient.LogLevel.Info;     // Default — initialization and key events
Hyperstone.LogLevel = HyperstoneClient.LogLevel.Warning;  // Only warnings and errors
Hyperstone.LogLevel = HyperstoneClient.LogLevel.Error;    // Only errors
```

You can also pass the log level directly at initialization:

```csharp
Hyperstone.Initialize("YOUR_APP_ID", "YOUR_APP_SECRET", HyperstoneClient.LogLevel.Debug);
```

All SDK logs are prefixed with `[Hyperstone]` for easy filtering in the Unity Console.

## How It Works

Understanding the SDK internals helps you use it effectively and debug integration issues.

### Architecture

The `Hyperstone` static class is a **facade** over a singleton `HyperstoneClient` MonoBehaviour. On first access, the SDK creates a `[Hyperstone Client]` GameObject marked with `DontDestroyOnLoad` so it persists across scene loads.

```mermaid
sequenceDiagram
    participant App as Your App
    participant SDK as Hyperstone SDK
    participant Storage as PlayerPrefs
    participant Server as Hyperstone Server

    Note over App, Server: Initialization & Config Fetch
    App->>SDK: Hyperstone.WatchConfig(callback)
    App->>SDK: Hyperstone.Initialize(appId, appSecret)
    SDK->>Storage: Load cached UID, config, metrics
    Storage-->>SDK: Cached data (if any)
    SDK-->>App: callback(cached config)

    SDK->>Server: POST /v1/c {user properties}
    Note right of SDK: Headers: xp-app-id, xp-app-secret, xp-uid
    Server-->>SDK: Config JSON + UID
    SDK->>Storage: Persist UID + config
    SDK-->>App: callback(fresh config)

    Note over App, Server: Event Tracking
    App->>SDK: LogAdImpression / LogIap / IncrementCustomMetric
    SDK->>SDK: Accumulate in metrics buffer
    SDK->>Storage: Persist buffer (crash-safe)

    Note over SDK, Server: Metric Sending (debounced, with cooldown)
    SDK->>Server: POST /v1/i {batched metrics}
    Note right of SDK: Headers: xp-app-id, xp-app-secret, xp-uid
    alt Success
        Server-->>SDK: 200 OK
        SDK->>Storage: Clear sent metrics
    else Failure
        Server-->>SDK: Error
        SDK->>SDK: Restore metrics to buffer for retry
    end

    Note over SDK, Server: Automatic (no code required)
    loop Every 15s while app is active
        SDK->>SDK: Increment engagement time
    end
    SDK->>SDK: Track sessions
```

### Lifecycle

1. **Initialization** — `Hyperstone.Initialize()` creates/finds the singleton, stores your credentials, loads any cached config and metrics from `PlayerPrefs`, and begins a config fetch request.

2. **Config Fetch** — The SDK sends a `POST` request to the Hyperstone API with device/user properties. The server responds with an optimized configuration and a unique user identifier (**UID**). Both are persisted locally.

3. **Config Delivery** — Once the config arrives, all registered `WatchConfig` callbacks are invoked. If a cached config existed from a previous session, callbacks fire immediately on subscribe with the cached version, and again when the fresh config arrives.

4. **Metric Collection** — Events (ad impressions, purchases, custom metrics, sessions, engagement time) are accumulated in an in-memory buffer and periodically persisted to `PlayerPrefs`. This ensures no data is lost on crashes.

5. **Metric Sending** — The SDK batches metrics and sends them to the backend with a debounce window (0.5s) and a cooldown period (20s) to avoid overwhelming the server. If a send fails, metrics are restored to the buffer and retried on the next opportunity.

6. **Session Management** — The SDK automatically tracks sessions, detecting new sessions on app launch and when the app resumes from a background pause longer than 30 seconds. Engagement time is measured in 15-second increments.

### Local Persistence

All data is stored in Unity's `PlayerPrefs` under keys namespaced by your App ID (e.g., `xp-uid-{appId}`, `xp-config-{appId}`). This means:
- Config and metrics survive app restarts
- Metrics that fail to send are buffered and retried
- Each app ID has isolated storage (relevant for multi-tenancy)

## Thread Safety

**All calls to the `Hyperstone` static API are thread-safe.** You can safely call `Hyperstone.Initialize()`, `Hyperstone.WatchConfig()`, `Hyperstone.LogAdImpression()`, and all other methods from any thread — including background threads, async tasks, or job system callbacks.

Under the hood, the `Hyperstone` facade captures Unity's main-thread `SynchronizationContext` at startup (via `[RuntimeInitializeOnLoadMethod]`). If a call is made from a non-main thread, it is automatically marshalled to the main thread using `SynchronizationContext.Post()`. This ensures that the underlying `HyperstoneClient` MonoBehaviour (which uses coroutines and Unity APIs) always runs on the main thread, while your calling code doesn't need to worry about thread affinity.

## Advanced: Multi-Tenancy

The `Hyperstone` static API manages a **single shared client** instance (singleton pattern). In most apps, this is all you need.

However, if your application needs to communicate with multiple Hyperstone apps simultaneously (e.g., a publishing platform hosting multiple titles, or a single app with separate optimization scopes), you can create and manage `HyperstoneClient` instances manually:

```csharp
using UnityEngine;
using HyperstoneSdk;

public class MultiTenantExample : MonoBehaviour
{
    private HyperstoneClient _gameplayClient;
    private HyperstoneClient _monetizationClient;

    void Start()
    {
        // Create separate client instances for different app scopes
        _gameplayClient = CreateClient("GameplayOptimizer");
        _gameplayClient.Initialize("GAMEPLAY_APP_ID", "GAMEPLAY_APP_SECRET");
        _gameplayClient.WatchConfig(OnGameplayConfigChanged);

        _monetizationClient = CreateClient("MonetizationOptimizer");
        _monetizationClient.Initialize("MONETIZATION_APP_ID", "MONETIZATION_APP_SECRET");
        _monetizationClient.WatchConfig(OnMonetizationConfigChanged);
    }

    private HyperstoneClient CreateClient(string name)
    {
        var go = new GameObject($"[Hyperstone - {name}]");
        DontDestroyOnLoad(go);
        return go.AddComponent<HyperstoneClient>();
    }

    void OnGameplayConfigChanged(HyperstoneConfig config)
    {
        int lives = config.GetIntOrDefault("n_lives", 3);
        // ...
    }

    void OnMonetizationConfigChanged(HyperstoneConfig config)
    {
        int adCooldown = config.GetIntOrDefault("ad_cooldown_seconds", 60);
        // ...
    }
}
```

> **Important:** When creating `HyperstoneClient` instances manually, you bypass the `Hyperstone` static facade entirely. Multi-tenant clients are **not** thread-safe by default — you must ensure they are only accessed from the main thread, or implement your own synchronization.

> **Note:** Each `HyperstoneClient` instance uses its own `PlayerPrefs` namespace based on the App ID, so data between clients does not collide.

## API Reference

### `Hyperstone` (Static Facade)

| Method | Description |
|--------|-------------|
| `Initialize(appId, appSecret, logLevel?)` | Initialize the SDK with your credentials. |
| `Initialize(appId, appSecret, apiUrl, logLevel?)` | Initialize with a custom API endpoint. |
| `WatchConfig(callback)` | Subscribe to config updates. Safe to call before `Initialize`. |
| `UnwatchConfig(callback)` | Unsubscribe from config updates. |
| `LogAdImpression(platform, network, format, revenueUsd)` | Track an ad impression with revenue. |
| `LogAdRequest(platform, format)` | Track an ad request. |
| `LogAdError(platform, network, format, failReason?)` | Track an ad error. |
| `LogIapTransactionSuccess(productId, currency, price)` | Track a successful in-app purchase. |
| `LogIapTransactionError(productId, failReason)` | Track a failed in-app purchase. |
| `IncrementCustomMetric(name, value)` | Increment a custom numeric metric. |
| `IncrementCustomMetric(name, value, labels...)` | Increment a custom metric with labels. |
| `LogCustomEvent(name, labels...)` | Log a custom event (equivalent to incrementing by 1). |
| `ClearData()` | Clear all locally stored SDK data. |
| `LogLevel` | Get or set the current log verbosity. |

### `HyperstoneConfig`

| Method | Description |
|--------|-------------|
| `GetIntOrDefault(key, default)` | Get an integer config value. |
| `GetLongOrDefault(key, default)` | Get a long config value. |
| `GetFloatOrDefault(key, default)` | Get a float config value. |
| `GetDoubleOrDefault(key, default)` | Get a double config value. |
| `GetBoolOrDefault(key, default)` | Get a boolean config value. |
| `GetStringOrDefault(key, default)` | Get a string config value. |
| `TryGetInt(key, out value)` | Try to get an int. Returns `false` if missing. |
| `TryGetLong(key, out value)` | Try to get a long. Returns `false` if missing. |
| `TryGetFloat(key, out value)` | Try to get a float. Returns `false` if missing. |
| `TryGetDouble(key, out value)` | Try to get a double. Returns `false` if missing. |
| `TryGetBool(key, out value)` | Try to get a bool. Returns `false` if missing. |
| `TryGetString(key, out value)` | Try to get a string. Returns `false` if missing. |
| `TryGetValueAsJson(key, out json)` | Get the raw JSON string for a key. |
| `TryGetValueAsObject<T>(key, out obj)` | Deserialize a nested value to `T`. |
| `AsObject<T>()` | Deserialize the entire config to `T`. |
| `OverwriteObject(obj)` | Overwrite fields on an existing object. |
| `Keys` | Enumerate all config keys. |
| `ToString()` | Returns the raw JSON string. |

### `Label`

```csharp
// Constructor
new Label(string name, string value)
```

A readonly struct used to attach metadata to metrics. Labels help Hyperstone segment and analyze measurements.
