# IGP.UnitySDK.GameKit

IGP Unity SDK 的 GameKit 可选模块。

GameKit 是一组**基于功能（feature-based）的游戏实用工具集**，通过主包 `IGP.UnitySDK` 的 desktop session 承载。SDK 不直接访问后端 API，也不持有用户 JWT；SDK 会使用 `IGPConfig.appId` 填充必要的应用标识，桌面端负责鉴权、校验、API 转发或桌面自主控制的功能流程。

目前包含的功能：

| 功能 | 入口类 | 说明 |
| --- | --- | --- |
| Profile 当前用户资料 | `IGPProfile` | 通过 desktop session 获取当前登录用户最新的完整 profile |
| Players 玩家简单资料 | `IGPPlayers` | 按用户 ID 批量查询昵称、头像和静态头像框 URL |
| Leaderboard 排行榜 | `IGPLeaderboard` | 查询游戏排行榜元数据、读取排行榜区间、提交当前玩家成绩、批量查询指定用户分数 |
| Share 分享 | `IGPShare` | 提交文本、多张图片字节和游戏自定义扩展数据，由 desktop 自主控制分享流程并返回结果 |

> 后续更多游戏实用功能会持续加入本模块，复用同一套 desktop session 与能力（capability）机制。

## 安装

先安装主包：

```text
adapters/unity/Runtime/IGP.UnitySDK/package.json
```

再安装本模块：

```text
adapters/unity/Runtime/IGP.UnitySDK.GameKit/package.json
```

正式发布包对应：

```text
cn.indiegp.sdk.unity.game-kit-<version>.unitypackage
```

所有功能共用命名空间 `IGP.UnitySDK.GameKit`。

---

## Profile 当前用户资料

Profile 功能依赖 desktop session attach、当前用户已登录，并要求 `capabilities.userContext == true`。每次调用都会由 desktop 刷新认证 session，并返回完整 `SessionResponse.profile`；游戏不接触平台 JWT。

```csharp
using IGP.UnitySDK;
using IGP.UnitySDK.GameKit;

public sealed class ProfileDriver
{
    private IGPRuntimeManager runtimeManager;

    public async System.Threading.Tasks.Task LoadProfileAsync()
    {
        IGPUserProfile profile =
            await IGPProfile.GetProfileAsync(runtimeManager);

        // profile.avatar / profile.avatarUrl
        // profile.avatarFrame / profile.background
        // profile.bio / profile.lastActiveAt
    }
}
```

返回值包含头像选择和 URL、头像框及渲染配置、主页背景、简介、改名状态、最近活跃时间和待处理昵称审核。需要头像 PNG 字节时，继续调用主包的 `runtimeManager.GetDesktopUserAvatarAsync()`。

失败时抛出 `IGPProfileException`：

- `DESKTOP_SESSION_REQUIRED`：desktop session 尚未 attach。
- `DESKTOP_USER_CONTEXT_REQUIRED`：desktop 未登录或没有可用用户上下文。
- `PROFILE_UNAVAILABLE`：当前认证 session 未返回 profile。
- `PROFILE_INVALID_RESPONSE`：desktop 返回的 profile JSON 无法解析。

---

## Players 玩家简单资料

Players 功能依赖 desktop session attach、当前用户已登录，并要求 `capabilities.userContext == true`。只提供批量接口；查询单个玩家时传入只包含一个 ID 的数组。

```csharp
IGPPlayerProfile[] profiles = await IGPPlayers.GetProfilesAsync(
    runtimeManager,
    new[] { "user-2", "user-3" });
```

每次接受 1 到 50 个非空用户 ID。重复 ID 会按第一次出现的位置去重，不存在的用户从结果中省略。每个结果只包含 `id`、`nickname`、`avatarUrl` 和 `avatarFrameUrl`；头像框 URL 是静态图片，不包含动画或渲染配置。

---

## Leaderboard 排行榜

### 前置条件

排行榜功能依赖 desktop session attach，并要求 `capabilities.leaderboard == true`。如果能力不可用，调用会抛出 `IGPLeaderboardException`，`Code` 为 `DESKTOP_SESSION_CAPABILITY_MISSING`。

### 最小接入

每个接口都有类型化的 request / response DTO：

```csharp
using IGP.UnitySDK;
using IGP.UnitySDK.GameKit;

public sealed class LeaderboardDriver
{
    private IGPRuntimeManager runtimeManager;

    public async void ListLeaderboards()
    {
        IGPListLeaderboardsByGameResult result =
            await IGPLeaderboard.ListLeaderboardsByGameAsync(
                runtimeManager);

        foreach (IGPLeaderboardMetadataItem item in result.leaderboards)
        {
            // item.leaderboardId, item.status, item.displayName
        }
    }

    public async void ShowLeaderboard(string leaderboardId)
    {
        IGPGetLeaderboardResult result =
            await IGPLeaderboard.GetLeaderboardAsync(
                runtimeManager,
                leaderboardId,
                new IGPGetLeaderboardQuery
                {
                    start = 0,
                    count = 20,
                    includeSelf = true,
                });

        foreach (IGPLeaderboardEntry entry in result.entries)
        {
            // entry.rank, entry.user.nickname, entry.value
        }
    }

    public async void SubmitScore(string leaderboardId, long score)
    {
        IGPSubmitLeaderboardScoreResult submit =
            await IGPLeaderboard.SubmitLeaderboardScoreAsync(
                runtimeManager,
                leaderboardId,
                score,
                new IGPSubmitLeaderboardScoreOptions
                {
                    // 同一次结算的重试必须复用同一个 eventId。
                    eventId = "match-20260626-0001",
                });
    }

    public async void QueryFriends(string leaderboardId, string[] userIds)
    {
        IGPQueryLeaderboardScoresResult scores =
            await IGPLeaderboard.QueryLeaderboardScoresAsync(
                runtimeManager,
                leaderboardId,
                userIds);

        // scores.scores 顺序与请求的 userIds 一致；未提交过成绩的用户 value 为 null。
    }
}
```

### 当前范围

- 依赖主包 `cn.indiegp.sdk.unity`。
- 使用 desktop session 调用 Runtime Leaderboard API（command `LeaderboardForward`）。
- 只支持四个 Runtime Leaderboard 接口：查询游戏排行榜元数据、读取排行榜区间、批量查询用户分数、提交当前玩家成绩。
- 不支持创建 / 审核 / 启用排行榜，不支持 cursor、includeMe。
- `period` 当前只支持 `PERMANENT`。
- 不保证同分顺序，也不保证返回条目数量一定等于请求的 `count`。

### 本地校验

调用前会做基础校验，失败时不会发送 desktop command：

- `leaderboardId`: 非空，trim 后最大 96 字符。
- `IGPConfig.appId`: 必填，SDK 会用它填充排行榜转发 payload 的 `appId/gameId`。
- `start`: 大于等于 0 的整数，默认 `0`。
- `count`: 1 到 100 的整数，默认 `20`。
- `value`: 整数（C# `long`）。
- `eventId` / `sessionId`: 如果传入，trim 后非空。
- `userIds`: 数组长度 1 到 50，每项非空。
- `period`: 只能是 `PERMANENT`。

### 错误处理

失败统一抛出 `IGPLeaderboardException`：

- `Code`: 桌面端 / API 返回的稳定错误码（例如 `LEADERBOARD_HTTP_403`、`APP_ID_MISMATCH`）。
- `UpstreamJson`: 原始 API 错误 body（如果有）。
- `HttpStatus()`: 从 `LEADERBOARD_HTTP_<status>` 解析出的 HTTP 状态码。

---

## Share 分享

### 前置条件

分享功能依赖 desktop session attach，并要求 `capabilities.share == true`。如果能力不可用，调用会抛出 `IGPShareException`，`Code` 为 `DESKTOP_SESSION_CAPABILITY_MISSING`。登录、确认面板、发布、取消和失败流程由 desktop 自主控制。

### 最小接入

```csharp
using IGP.UnitySDK;
using IGP.UnitySDK.GameKit;

public sealed class ShareDriver
{
    private IGPRuntimeManager runtimeManager;

    public async void ShareScore(byte[] posterPngBytes)
    {
        IGPShareResult result = await IGPShare.ShareAsync(
            runtimeManager,
            new IGPShareRequest
            {
                contentType = "score-card",
                title = "New record",
                text = "I scored 12345",
                templateId = "score",
                images = new[]
                {
                    new IGPShareImage
                    {
                        mimeType = "image/png",
                        bytes = posterPngBytes,
                        width = 1280,
                        height = 720,
                        fileName = "score.png",
                    },
                },
                data = new
                {
                    score = 12345,
                },
                metadata = new
                {
                    matchId = "match-20260709-0001",
                },
            });

        // result.success / result.code / result.shareId / result.status / result.message
    }
}
```

### 图片字节

- 图片内容是二进制字节，不是路径、URL，也不是 JSON/base64 字符串。
- `images` 支持多张图片；SDK 会把所有图片字节拼成 desktop command 的 `contentBytes`。
- 转发 JSON 中只包含 `mimeType`、`byteOffset`、`byteLength`、`width`、`height`、`fileName`、`altText` 等描述符。
- 支持 `image/png` 和 `image/jpeg`。
- 所有图片原始字节合计最大 `10 MiB`，超过时本地抛出 `SHARE_IMAGES_TOO_LARGE`，不会发送 desktop command。

### 扩展数据

`contentType`、`templateId`、`data`、`metadata` 都由游戏和 desktop 约定。SDK 不解释这些字段，只保证它们随 `schemaVersion = 1` 的 payload 一起提交给 desktop。用户取消会作为正常结果返回，例如 `success = false`、`status = "cancelled"`、`code = "SHARE_CANCELLED"`，不会抛异常。
