# Tài liệu Hướng dẫn & Mô tả Tích hợp GameOps (GameOps Integration)

## 1. Tổng quan (Overview)

**GameOps Integration** (Tích hợp Hệ thống Quản trị Game Operations) là phân hệ kết nối giữa các công cụ **Level Editor / Level Studio** với hệ thống máy chủ Quản trị Game (GameOps Backend Server).

Tài liệu này cung cấp hướng dẫn tổng quát và chuẩn hóa để hỗ trợ mọi tựa game (như Puzzle, Match-3, Sorting, Arcade, Strategy...) có thể dễ dàng tích hợp, quản lý, kiểm thử, phân phiên bản và xuất bản các màn chơi (levels) hoàn toàn trực tuyến thông qua các chuẩn giao tiếp RESTful API.

---

## 2. Các Tính năng Cốt lõi (Core Features)

### 2.1. Tải màn chơi theo Alias (Fetch Level by Alias)
- Truy xuất dữ liệu của một màn chơi cụ thể từ máy chủ thông qua mã định danh duy nhất (**Level Alias** / Config Alias, ví dụ: `{GameID}_{LevelNumber}_v{Version}`).
- Hỗ trợ xem lại các phiên bản lịch sử (versioning `v1`, `v2`, ...) cũng như tự động lấy phiên bản mới nhất khi sử dụng từ khóa `vlatest`.

### 2.2. Tải lên & Cập nhật màn chơi (Upload / Upsert Level)
- Lưu trữ và cập nhật cấu hình màn chơi trực tiếp từ Editor lên máy chủ GameOps.
- Cơ chế **Upsert** tự động: Nếu level chưa tồn tại hệ thống sẽ thêm mới bản ghi; nếu level đã có hệ thống sẽ tự động cập nhật và sinh ra phiên bản dữ liệu mới (`latest_data_version`).
- Hỗ trợ tải lên từng màn chơi lẻ hoặc đẩy hàng loạt (Bulk Import với giới hạn tối đa 1,000 màn chơi mỗi đợt request).

### 2.3. Quản lý Danh sách & Lọc Trạng thái (List View, Query Filtering & Export View ZIP)
- Tải danh sách màn chơi theo từng góc nhìn quản lý (View ID, ví dụ `all` hoặc View theo bộ phận) kèm phân trang (`skip` & `limit`).
- Hỗ trợ cả truy vấn cơ bản qua GET lẫn truy vấn nâng cao qua POST Query Body.
- Lọc màn chơi theo trạng thái xuất bản trong vòng đời sản phẩm (Workflow Status):
  - **Draft**: Bản nháp đang được thiết kế.
  - **QA**: Đang trong quá trình kiểm thử chất lượng và tính cân bằng.
  - **Softlaunch**: Đã phát hành ở các thị trường chạy thử nghiệm.
  - **Live**: Đã phát hành chính thức cho người chơi toàn cầu.
  - **Rejected**: Màn chơi bị từ chối hoặc chưa đạt tiêu chuẩn.
  - **Archived**: Màn chơi đã lưu trữ / không còn sử dụng.
- **Xuất ZIP danh sách View**: Hỗ trợ nén toàn bộ danh sách màn chơi thu thập từ View thành file ZIP (`gameops_{gameId}_view_{viewId}_levels.zip`) để lưu trữ offline.

### 2.4. Quản lý Danh sách Màn chơi (Level List Items & Export ZIP)
- Truy xuất danh sách các màn chơi đã được ghim (pinned levels) trong một **Level List** cụ thể.
- Hỗ trợ xuất và tải file ZIP toàn bộ dữ liệu bàn chơi của Level List thông qua `export?format=zip`.
- Giải nén file ZIP trực tiếp trong bộ nhớ trình duyệt bằng `JSZip` giúp nạp và render toàn bộ Level List với vị trí (`Level`/`position`) và số lượng chuẩn xác 100% chỉ trong **1 HTTP Request**.
- **Tải xuống trực tiếp ZIP Bundle**: Tích hợp nút xuất trực tiếp file ZIP gói danh sách (`gameops_{gameId}_list_{listId}_levels.zip`) từ máy chủ.

---

## 3. Chi tiết Các API Đã Tích Hợp (API Endpoint Reference)

### 3.1. API Lấy thông tin màn chơi theo Alias (Fetch Level by Alias)
- **HTTP Method**: `GET`
- **Endpoint**: `/api/v1/games/{gameId}/levels/alias/{alias}`
- **Cú pháp Alias**: `{gameId}_{levelNumber}_v{version}` 
  - Ví dụ bản ghi cụ thể: `4_1_v1`
  - Ví dụ lấy bản mới nhất: `4_1_vlatest`
- **Headers**:
  ```http
  X-API-Key: <GAMEOPS_API_KEY>
  Accept: application/json
  ```
- **Phản hồi mẫu (Response Format)**:
  ```json
  {
    "id": "6f1c7d2e...",
    "game_id": 4,
    "level_number": 1,
    "version": 1,
    "latest_data_version": 3,
    "status": "live",
    "level_data": {
      "grid_size": { "cols": 10, "rows": 10 },
      "elements": [...]
    }
  }
  ```

### 3.2. API Nhập / Cập nhật màn chơi (Import & Upsert Level Payload)
- **HTTP Method**: `POST`
- **Endpoint**: `/api/v1/games/{gameId}/levels/imports`
- **Giới hạn (Constraint)**: Tối đa **1,000 items** cho mỗi đợt request.
- **Headers**:
  ```http
  X-API-Key: <GAMEOPS_API_KEY>
  Content-Type: application/json
  Accept: application/json
  ```
- **Request Body mẫu**:
  ```json
  {
    "items": [
      {
        "level_number": 1,
        "level_data": {
          "grid_size": { "cols": 10, "rows": 10 },
          "elements": [...]
        }
      }
    ]
  }
  ```
- **Phản hồi mẫu (Response Format)**:
  ```json
  {
    "game_id": 4,
    "inserted": 0,
    "updated": 1,
    "deleted": 0,
    "total_levels": 150
  }
  ```

### 3.3. API Duyệt danh sách màn chơi theo View (List Levels by View)
- **HTTP Method**: `GET`
- **Endpoint**: `/api/v1/games/{gameId}/levels/views/{viewId}?skip={skip}&limit={limit}`
- **Query Parameters**:
  - `skip` (integer): Số lượng bản ghi bỏ qua (mặc định: `0`).
  - `limit` (integer): Số lượng bản ghi lấy về (mặc định: `100`).
- **Headers**:
  ```http
  X-API-Key: <GAMEOPS_API_KEY>
  Accept: application/json
  ```
- **Phản hồi mẫu**: Mảng danh sách các đối tượng màn chơi `[...]`.

### 3.4. API Truy vấn & Lọc màn chơi Nâng cao (Query View Filter)
- **HTTP Method**: `POST`
- **Endpoint**: `/api/v1/games/{gameId}/levels/views/{viewId}/query`
- **Headers**:
  ```http
  X-API-Key: <GAMEOPS_API_KEY>
  Content-Type: application/json
  Accept: application/json
  ```
- **Request Body mẫu**:
  ```json
  {
    "filters": {
      "status": "live",
      "level_number_gte": 1,
      "level_number_lte": 50
    },
    "page": 1,
    "page_size": 100
  }
  ```
- **Phản hồi mẫu**: Đối tượng chứa danh sách kết quả lọc `items` và các thông tin phân trang `total`, `page`, `page_size`.

### 3.5. API Tải danh sách màn chơi đính kèm trong Level List (List Level List Items)
- **HTTP Method**: `GET`
- **Endpoint**: `/api/v1/games/{game_id}/levels/level-lists/{list_id}/items`
- **Query Parameters**:
  - `page` (integer): Số trang truy vấn (mặc định: `1`).
  - `page_size` (integer): Kích thước mỗi trang, giới hạn tối đa 500 (mặc định: `50`).
- **Headers**:
  ```http
  X-API-Key: <GAMEOPS_API_KEY>
  Accept: application/json
  ```
- **Phản hồi mẫu (Response Format)**:
  ```json
  {
    "items": [
      {
        "id": "6f1c7d2e...",
        "level_id": "lvl_4_1",
        "level_number": "1",
        "data_version": "1",
        "config_id": "4_1_v1",
        "status": "live",
        "is_latest": true,
        "latest_data_version": "1",
        "metrics": "...",
        "position": "1",
        "metrics_updated_at": "2026-08-11T00:00:00Z",
        "created_at": "2026-08-11T00:00:00Z",
        "updated_at": "2026-08-11T00:00:00Z"
      }
    ],
    "total": 1,
    "page": 1,
    "page_size": 50
  }
  ```

### 3.6. API Xuất file ZIP toàn bộ dữ liệu màn chơi trong Level List (Export Level List Stream)
- **HTTP Method**: `GET`
- **Endpoint**: `/api/v1/games/{game_id}/levels/level-lists/{list_id}/export`
- **Query Parameters**:
  - `format` (string): `zip` (mặc định: tải file ZIP chứa dữ liệu cấu hình level của tất cả các màn đã đính kèm, mỗi file tên `{level_config_id}.json`) hoặc `json` (chỉ tải mảng báo cáo metrics).
- **Headers**:
  ```http
  X-API-Key: <GAMEOPS_API_KEY>
  ```
- **Mô tả cấu trúc dữ liệu bên trong ZIP**:
  - File ZIP giải nén thu được mảng các file JSON theo định dạng `{level_config_id}.json` (ví dụ: `4_481_v3.json`).
  - Mỗi file chứa đầy đủ thông số cấu hình bàn chơi (`board`, `snakes`, `blockers`, `level_config_id`, `Level`, `version`, ...) hỗ trợ render trực tiếp lên Canvas mà không cần gọi API phụ.
  - Trường `Level` trong mỗi tệp đại diện cho vị trí thứ tự (1-based position string) trong Level List, giúp ứng dụng sắp xếp màn chơi chính xác từ 1 đến N.

---

## 4. Hướng dẫn Triển khai Chi tiết cho Mọi Tựa Game (Universal Implementation Guide)

### Bước 1: Cấu hình Biến Môi trường (Environment Variables)
Khai báo API Host URL và API Key trong tệp cấu hình dự án (`.env`):

```env
VITE_GAMEOPS_API_HOST=https://gameops.suncs.app
VITE_GAMEOPS_API_KEY=gok_your_api_key_here
VITE_USE_PROXY=true
```

### Bước 2: Thiết lập Dev Proxy Tránh Lỗi CORS (Proxy Setup)
Cấu hình Reverse Proxy cho môi trường phát triển (như Vite Dev Server):

```javascript
// vite.config.js
export default defineConfig({
  server: {
    proxy: {
      '/api-proxy': {
        target: process.env.VITE_GAMEOPS_API_HOST || 'https://gameops.suncs.app',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api-proxy/, '')
      }
    }
  }
});
```

### Bước 3: Đóng gói Module Service GameOps (GameOps Service Module)
Đóng gói bộ hàm bọc API để sử dụng thống nhất trên toàn dự án:

```javascript
import JSZip from 'jszip';

const API_HOST = import.meta.env.VITE_GAMEOPS_API_HOST || 'https://gameops.suncs.app';
const API_KEY = import.meta.env.VITE_GAMEOPS_API_KEY || '';
const USE_PROXY = import.meta.env.VITE_USE_PROXY !== 'false';

function getEndpointUrl(path) {
  const cleanHost = API_HOST.endsWith('/') ? API_HOST.slice(0, -1) : API_HOST;
  if (USE_PROXY && !cleanHost.includes('localhost')) {
    return `/api-proxy${path}`;
  }
  return `${cleanHost}${path}`;
}

/**
 * Build Alias chuẩn hóa cho level
 */
export function buildLevelAlias(gameId, levelNum, versionStr = 'latest') {
  const trimmed = (versionStr || '').toString().trim().toLowerCase();
  if (!trimmed || trimmed === 'latest' || trimmed === 'vlatest') {
    return `${gameId}_${levelNum}_vlatest`;
  }
  const cleanVer = trimmed.replace(/^v/i, '');
  return `${gameId}_${levelNum}_v${cleanVer}`;
}

/**
 * 1. Tải dữ liệu level theo Alias
 */
export async function fetchLevel(gameId, levelNum, version = 'latest') {
  const alias = buildLevelAlias(gameId, levelNum, version);
  const url = getEndpointUrl(`/api/v1/games/${gameId}/levels/alias/${alias}`);

  const response = await fetch(url, {
    method: 'GET',
    headers: {
      'X-API-Key': API_KEY,
      'Accept': 'application/json'
    }
  });

  if (!response.ok) throw new Error(`HTTP Error: ${response.status}`);
  return await response.json();
}

/**
 * 2. Tải lên / Cập nhật màn chơi (Tối đa 1,000 items / request)
 */
export async function uploadLevels(gameId, levelItems) {
  const url = getEndpointUrl(`/api/v1/games/${gameId}/levels/imports`);
  
  const payload = {
    items: levelItems.map(item => ({
      level_number: parseInt(item.level_number),
      level_data: item.level_data
    }))
  };

  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'X-API-Key': API_KEY,
      'Content-Type': 'application/json',
      'Accept': 'application/json'
    },
    body: JSON.stringify(payload)
  });

  if (!response.ok) throw new Error(`Upload Failed: ${response.status}`);
  return await response.json();
}

/**
 * 3. Truy vấn danh sách màn chơi theo View & Pagination
 */
export async function fetchLevelView(gameId, viewId = 'all', skip = 0, limit = 100) {
  const url = getEndpointUrl(`/api/v1/games/${gameId}/levels/views/${viewId}?skip=${skip}&limit=${limit}`);

  const response = await fetch(url, {
    method: 'GET',
    headers: {
      'X-API-Key': API_KEY,
      'Accept': 'application/json'
    }
  });

  if (!response.ok) throw new Error(`Fetch View Failed: ${response.status}`);
  return await response.json();
}

/**
 * 4. Tải danh sách màn chơi trong Level List (Level List Items)
 */
export async function fetchLevelListItems(gameId, listId, page = 1, pageSize = 500) {
  const url = getEndpointUrl(`/api/v1/games/${gameId}/levels/level-lists/${listId}/items?page=${page}&page_size=${pageSize}`);

  const response = await fetch(url, {
    method: 'GET',
    headers: {
      'X-API-Key': API_KEY,
      'Accept': 'application/json'
    }
  });

  if (!response.ok) throw new Error(`Fetch Level List Items Failed: ${response.status}`);
  return await response.json();
}

/**
 * 5. Tải & Giải nén trực tiếp file ZIP Level List (Export ZIP Stream)
 */
export async function fetchAndUnzipLevelList(gameId, listId) {
  const url = getEndpointUrl(`/api/v1/games/${gameId}/levels/level-lists/${listId}/export?format=zip`);

  const response = await fetch(url, {
    method: 'GET',
    headers: {
      'X-API-Key': API_KEY
    }
  });

  if (!response.ok) throw new Error(`Export ZIP Failed: ${response.status}`);

  const buffer = await response.arrayBuffer();
  const zip = await JSZip.loadAsync(buffer);
  const levels = [];

  const jsonPromises = [];
  zip.forEach((relativePath, zipEntry) => {
    if (!zipEntry.dir && zipEntry.name.endsWith('.json')) {
      jsonPromises.push(zipEntry.async('text'));
    }
  });

  const jsonTexts = await Promise.all(jsonPromises);
  const parsedLevels = jsonTexts.map(text => JSON.parse(text));

  // Sắp xếp thuần số học theo thứ tự vị trí position (Level key)
  parsedLevels.sort((a, b) => {
    const posA = parseInt(a.Level || a.position || 0);
    const posB = parseInt(b.Level || b.position || 0);
    return posA - posB;
  });

  return parsedLevels;
}
```

---

## 5. Quy chuẩn Định danh & Phân quyền (Naming & Authorization)

### 5.1. Cú pháp Mã Màn chơi (Level Alias & `level_config_id`)
Hệ thống sử dụng cú pháp chuẩn hóa chung:
`{GameID}_{LevelNumber}_v{DataVersion}`

- **Game ID**: Mã tựa game trên GameOps (Ví dụ: `4`, `101`, `102`...).
- **Level Number**: Số thứ tự của màn chơi trong tuyến nội dung (Ví dụ: `1`, `2`, `100`...).
- **Data Version**: Phiên bản cấu hình màn chơi (Ví dụ: `1`, `2`, `latest`).

**Tự động bổ sung `level_config_id` khi tải/xuất dữ liệu:**
Khi xuất file `.json` đơn lẻ hoặc nén thành tập tin `.zip` từ Editor/GameOps, hệ thống tự động bổ sung trường `level_config_id` vào đối tượng cấu hình bàn chơi:
```json
{
  "id": "level_00001",
  "level_config_id": "4_1_v1",
  "version": 1,
  "board": { "cols": 10, "rows": 12 },
  "snakes": [...],
  "blockers": [...]
}
```

### 5.2. Phân quyền API Key (Security Scopes)
Mọi kết nối sử dụng Header: `X-API-Key: gok_...`
- `read`: Quyền truy vấn và đọc dữ liệu level.
- `write`: Quyền tạo mới và đẩy bản cập nhật level.
- `delete`: Quyền lưu trữ hoặc xóa màn chơi.

---

## 6. Quy trình Vòng đời Màn chơi (Level Lifecycle Management)

```
[Khởi tạo trên Editor] ➔ [Upload Level (Status: Draft)] ➔ [Kiểm thử (Status: QA)] ➔ [Softlaunch] ➔ [Release Live]
```

1. **Khởi tạo (Draft)**: Level Designer tạo bố cục và thiết lập các thông số cơ bản, đẩy lên GameOps dưới dạng nháp.
2. **Kiểm thử (QA)**: Đội ngũ Tester / QA tải các màn chơi có trạng thái `QA` để kiểm tra độ khó và trải nghiệm người dùng.
3. **Phát hành (Live)**: Màn chơi vượt qua bài kiểm thử sẽ được chuyển sang trạng thái `Live` để client trong game tải về cho người chơi chính thức.

---

## 7. Các Luồng Xử lý & Kỹ thuật Triển khai Thực tế (Client-side Implementation Practices)

Trong thực tế triển khai trên **Level Studio / Level Player**, hệ thống áp dụng các quy trình xử lý nâng cao nhằm tối ưu hiệu năng và đảm bảo tính toàn vẹn dữ liệu:

### 7.1. Phân trang Lặp (Pagination Loop) cho Level List Items
Khi truy xuất danh sách items của một Level List (`/level-lists/{list_id}/items`), client không chỉ gọi đơn lẻ một trang mà sử dụng **vòng lặp phân trang tự động** (`page_size=500`, tăng `page` từ `1` đến khi `allItems.length >= total` hoặc lặp tối đa 50 trang) để đảm bảo thu thập đầy đủ 100% metadata danh sách màn chơi mà không bị sót dữ liệu do giới hạn server.

### 7.2. Chiến lược Tải Hybrid Kết hợp (Metadata Items + Export ZIP Bundle)
Để vừa hiển thị thông tin danh sách nhanh vừa tối ưu tốc độ nạp dữ liệu level:
1. **Bước 1**: Gọi API `items` lặp phân trang để lấy danh sách thứ tự `position`, `metrics`, `status` và `config_id`.
2. **Bước 2**: Gọi API `export?format=zip` tải file ZIP chứa dữ liệu cấu hình chi tiết của tất cả màn chơi và dùng `JSZip` giải nén trực tiếp trong bộ nhớ (In-memory Map).
3. **Bước 3 (Fallback)**: Khớp nối dữ liệu ZIP vào từng vị trí `position`. Nếu API `items` gặp lỗi, client tự động chuyển sang luồng dự phòng (Fallback) đọc trực tiếp từ file ZIP export.

### 7.3. Xuất file nén ZIP offline cho View Levels & Level List
- **Export View Levels as ZIP**: Client truy vấn danh sách màn chơi của View theo bộ lọc, tự động chuẩn hóa cấu hình bàn chơi (`cropLevelData`), gán `level_config_id` và đóng gói thành file ZIP (`gameops_{gameId}_view_{viewId}_levels.zip`).
- **Export Level List ZIP**: Tải trực tiếp stream tập tin nén ZIP (`gameops_{gameId}_list_{listId}_levels.zip`) từ máy chủ backend qua endpoint `/level-lists/{list_id}/export?format=zip`.

### 7.4. Xác thực ID Tự động (Silent Fetch Config ID) trước khi Upload
Trước khi thực hiện lệnh Upload màn chơi (`POST /imports`), Editor gửi một request `GET` ẩn tới đường dẫn `/alias/{gameId}_{levelNum}_v{version}` để kiểm tra xem level alias đã tồn tại trên server hay chưa. 
- Nếu đã tồn tại: Client tự động gán `id` cũ vào `level_data.id` trước khi đẩy lên, giúp máy chủ GameOps thực hiện lệnh **Upsert** (cập nhật đúng bản ghi cũ thay vì sinh ra bản ghi mới bị trùng lặp).

### 7.5. Đồng bộ Phiên bản Dữ liệu (Auto Sync `latest_data_version`)
Sau khi Upload thành công, Editor tự động gửi request truy vấn lại alias để lấy giá trị `latest_data_version` mới nhất được máy chủ cấp phát, sau đó tự động cập nhật số phiên bản trên ô nhập `goLevelVer` và đồng bộ vào bộ nhớ trạng thái local (`state.levelData._goLevelVer`).


