connectbase-client 3.14.0 → 3.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/dist/connect-base.umd.js +4 -4
- package/dist/index.d.mts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +135 -33
- package/dist/index.mjs +135 -33
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -6115,7 +6115,11 @@ declare class GameAPI {
|
|
|
6115
6115
|
readonly config: GameConfigAPI;
|
|
6116
6116
|
constructor(http: HttpClient, gameServerUrl?: string, appId?: string);
|
|
6117
6117
|
/**
|
|
6118
|
-
* 게임 룸 클라이언트
|
|
6118
|
+
* 게임 룸 클라이언트 생성.
|
|
6119
|
+
*
|
|
6120
|
+
* `appId` 는 `createClient({ appId })` 로 호출별 지정하거나, 생략 시 `new ConnectBase({ appId })`
|
|
6121
|
+
* 의 전역 값을 사용한다. 둘 다 없으면 `connect()` 가 명확한 에러로 reject 한다 — 빈 appId 로
|
|
6122
|
+
* `/v1/game//ws` 에 붙어 server 측이 SCRIPT_NOT_FOUND 로 깨지던 silent 회귀 차단 (NJB 019e2210).
|
|
6119
6123
|
*/
|
|
6120
6124
|
createClient(config: Omit<GameClientConfig, 'gameServerUrl'>): GameRoom;
|
|
6121
6125
|
/**
|
|
@@ -6149,6 +6153,7 @@ declare class GameAPI {
|
|
|
6149
6153
|
downloadReplay(replayId: string): Promise<ArrayBuffer>;
|
|
6150
6154
|
getReplayHighlights(replayId: string): Promise<ReplayHighlight[]>;
|
|
6151
6155
|
private getHeaders;
|
|
6156
|
+
private gameFetch;
|
|
6152
6157
|
/**
|
|
6153
6158
|
* matchqueue 에 ticket 등록.
|
|
6154
6159
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -6115,7 +6115,11 @@ declare class GameAPI {
|
|
|
6115
6115
|
readonly config: GameConfigAPI;
|
|
6116
6116
|
constructor(http: HttpClient, gameServerUrl?: string, appId?: string);
|
|
6117
6117
|
/**
|
|
6118
|
-
* 게임 룸 클라이언트
|
|
6118
|
+
* 게임 룸 클라이언트 생성.
|
|
6119
|
+
*
|
|
6120
|
+
* `appId` 는 `createClient({ appId })` 로 호출별 지정하거나, 생략 시 `new ConnectBase({ appId })`
|
|
6121
|
+
* 의 전역 값을 사용한다. 둘 다 없으면 `connect()` 가 명확한 에러로 reject 한다 — 빈 appId 로
|
|
6122
|
+
* `/v1/game//ws` 에 붙어 server 측이 SCRIPT_NOT_FOUND 로 깨지던 silent 회귀 차단 (NJB 019e2210).
|
|
6119
6123
|
*/
|
|
6120
6124
|
createClient(config: Omit<GameClientConfig, 'gameServerUrl'>): GameRoom;
|
|
6121
6125
|
/**
|
|
@@ -6149,6 +6153,7 @@ declare class GameAPI {
|
|
|
6149
6153
|
downloadReplay(replayId: string): Promise<ArrayBuffer>;
|
|
6150
6154
|
getReplayHighlights(replayId: string): Promise<ReplayHighlight[]>;
|
|
6151
6155
|
private getHeaders;
|
|
6156
|
+
private gameFetch;
|
|
6152
6157
|
/**
|
|
6153
6158
|
* matchqueue 에 ticket 등록.
|
|
6154
6159
|
*
|
package/dist/index.js
CHANGED
|
@@ -6382,6 +6382,12 @@ var GameRoom = class {
|
|
|
6382
6382
|
resolve();
|
|
6383
6383
|
return;
|
|
6384
6384
|
}
|
|
6385
|
+
if (!this.config.appId) {
|
|
6386
|
+
reject(new Error(
|
|
6387
|
+
"cb.game: appId is required to connect. Pass it to `new ConnectBase({ appId })` or per-client via `cb.game.createClient({ appId, clientId })`."
|
|
6388
|
+
));
|
|
6389
|
+
return;
|
|
6390
|
+
}
|
|
6385
6391
|
const url = this.buildConnectionUrl(roomId);
|
|
6386
6392
|
this.ws = new WebSocket(url);
|
|
6387
6393
|
const onOpen = () => {
|
|
@@ -6636,8 +6642,10 @@ var GameRoom = class {
|
|
|
6636
6642
|
if (this.config.accessToken) {
|
|
6637
6643
|
params.set("token", this.config.accessToken);
|
|
6638
6644
|
}
|
|
6639
|
-
|
|
6640
|
-
|
|
6645
|
+
if (!this.config.appId) {
|
|
6646
|
+
throw new Error("cb.game: appId is required to build a game connection URL");
|
|
6647
|
+
}
|
|
6648
|
+
return `${wsUrl}/v1/game/${this.config.appId}/ws?${params.toString()}`;
|
|
6641
6649
|
}
|
|
6642
6650
|
send(type, data, msgId) {
|
|
6643
6651
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
@@ -6844,13 +6852,19 @@ var GameAPI = class {
|
|
|
6844
6852
|
this.config = new GameConfigAPI(http, appId);
|
|
6845
6853
|
}
|
|
6846
6854
|
/**
|
|
6847
|
-
* 게임 룸 클라이언트
|
|
6855
|
+
* 게임 룸 클라이언트 생성.
|
|
6856
|
+
*
|
|
6857
|
+
* `appId` 는 `createClient({ appId })` 로 호출별 지정하거나, 생략 시 `new ConnectBase({ appId })`
|
|
6858
|
+
* 의 전역 값을 사용한다. 둘 다 없으면 `connect()` 가 명확한 에러로 reject 한다 — 빈 appId 로
|
|
6859
|
+
* `/v1/game//ws` 에 붙어 server 측이 SCRIPT_NOT_FOUND 로 깨지던 silent 회귀 차단 (NJB 019e2210).
|
|
6848
6860
|
*/
|
|
6849
6861
|
createClient(config) {
|
|
6850
6862
|
return new GameRoom({
|
|
6851
6863
|
...config,
|
|
6852
6864
|
gameServerUrl: this.gameServerUrl.replace(/^http/, "ws"),
|
|
6853
|
-
appId
|
|
6865
|
+
// per-call appId 가 있으면 그것을 우선 (없을 때만 전역 fallback). 이전엔 this.appId 가
|
|
6866
|
+
// config.appId 를 무조건 덮어써서 호출별 지정이 불가능했다.
|
|
6867
|
+
appId: config.appId ?? this.appId,
|
|
6854
6868
|
publicKey: this.http.getPublicKey(),
|
|
6855
6869
|
accessToken: this.http.getAccessToken()
|
|
6856
6870
|
});
|
|
@@ -7025,6 +7039,38 @@ var GameAPI = class {
|
|
|
7025
7039
|
}
|
|
7026
7040
|
return headers;
|
|
7027
7041
|
}
|
|
7042
|
+
// game-server (`game.connectbase.world`) 전용 fetch.
|
|
7043
|
+
//
|
|
7044
|
+
// 회귀 가드 (v3.14.1, NJB report 2026-05-14): matchqueue / leaderboard / scripts 16개
|
|
7045
|
+
// 메서드가 v3.0.0 BREAKING 이후 `this.http.*` 를 통해 기본 baseUrl (=core-server,
|
|
7046
|
+
// `api.connectbase.world`) 로 라우팅되면서 404 회귀. 호스트 분리된 game-server 경로는
|
|
7047
|
+
// 반드시 `this.gameServerUrl` 을 사용해야 한다. 회귀 재발 방지 테스트:
|
|
7048
|
+
// test/game-host-routing.test.ts.
|
|
7049
|
+
async gameFetch(method, path, body, errCode = "GAME_REQUEST_FAILED") {
|
|
7050
|
+
const init = { method, headers: { ...this.getHeaders() } };
|
|
7051
|
+
if (body !== void 0) {
|
|
7052
|
+
init.headers["Content-Type"] = "application/json";
|
|
7053
|
+
init.body = JSON.stringify(body);
|
|
7054
|
+
}
|
|
7055
|
+
const response = await fetch(`${this.gameServerUrl}${path}`, init);
|
|
7056
|
+
if (!response.ok) {
|
|
7057
|
+
let serverCode = errCode;
|
|
7058
|
+
let message = `${method} ${path} failed: ${response.statusText}`;
|
|
7059
|
+
try {
|
|
7060
|
+
const data = await response.json();
|
|
7061
|
+
if (data && typeof data.error === "string") {
|
|
7062
|
+
message = data.error;
|
|
7063
|
+
serverCode = data.error;
|
|
7064
|
+
}
|
|
7065
|
+
} catch {
|
|
7066
|
+
}
|
|
7067
|
+
throw new ApiError(response.status, message, serverCode);
|
|
7068
|
+
}
|
|
7069
|
+
if (response.status === 204) return void 0;
|
|
7070
|
+
const contentType = response.headers.get("content-type") || "";
|
|
7071
|
+
if (!contentType.includes("application/json")) return void 0;
|
|
7072
|
+
return response.json();
|
|
7073
|
+
}
|
|
7028
7074
|
// ─────────────────────────────────────────────────────────────────────
|
|
7029
7075
|
// Phase 3 — Matchqueue primitive (mechanism only)
|
|
7030
7076
|
//
|
|
@@ -7045,9 +7091,11 @@ var GameAPI = class {
|
|
|
7045
7091
|
* @constraints appId/queueKey/ticketId 는 [a-zA-Z0-9_-] 만 허용. ttlSec=0 이면 만료 없음.
|
|
7046
7092
|
*/
|
|
7047
7093
|
async enqueueMatch(appId, queueKey, ticketId, attributes, ttlSec) {
|
|
7048
|
-
return this.
|
|
7094
|
+
return this.gameFetch(
|
|
7095
|
+
"POST",
|
|
7049
7096
|
`/v1/game/${appId}/matchqueue/${queueKey}/tickets`,
|
|
7050
|
-
{ ticket_id: ticketId, attributes, ttl_sec: ttlSec ?? 0 }
|
|
7097
|
+
{ ticket_id: ticketId, attributes, ttl_sec: ttlSec ?? 0 },
|
|
7098
|
+
"GAME_ENQUEUE_MATCH_FAILED"
|
|
7051
7099
|
);
|
|
7052
7100
|
}
|
|
7053
7101
|
/**
|
|
@@ -7056,16 +7104,22 @@ var GameAPI = class {
|
|
|
7056
7104
|
* @example const tickets = await cb.game.listMatchqueue(appId, "ranked")
|
|
7057
7105
|
*/
|
|
7058
7106
|
async listMatchqueue(appId, queueKey) {
|
|
7059
|
-
return this.
|
|
7060
|
-
|
|
7107
|
+
return this.gameFetch(
|
|
7108
|
+
"GET",
|
|
7109
|
+
`/v1/game/${appId}/matchqueue/${queueKey}`,
|
|
7110
|
+
void 0,
|
|
7111
|
+
"GAME_LIST_MATCHQUEUE_FAILED"
|
|
7061
7112
|
);
|
|
7062
7113
|
}
|
|
7063
7114
|
/**
|
|
7064
7115
|
* 매칭 큐에서 ticket 제거 (예: 사용자가 매칭 취소).
|
|
7065
7116
|
*/
|
|
7066
7117
|
async cancelMatch(appId, queueKey, ticketId) {
|
|
7067
|
-
await this.
|
|
7068
|
-
|
|
7118
|
+
await this.gameFetch(
|
|
7119
|
+
"DELETE",
|
|
7120
|
+
`/v1/game/${appId}/matchqueue/${queueKey}/tickets/${ticketId}`,
|
|
7121
|
+
void 0,
|
|
7122
|
+
"GAME_CANCEL_MATCH_FAILED"
|
|
7069
7123
|
);
|
|
7070
7124
|
}
|
|
7071
7125
|
// ─────────────────────────────────────────────────────────────────────
|
|
@@ -7081,47 +7135,66 @@ var GameAPI = class {
|
|
|
7081
7135
|
* await cb.game.submitScore(appId, "global_elo", userId, 32, "incr")
|
|
7082
7136
|
*/
|
|
7083
7137
|
async submitScore(appId, leaderboardKey, member, score, mode = "set") {
|
|
7084
|
-
return this.
|
|
7138
|
+
return this.gameFetch(
|
|
7139
|
+
"POST",
|
|
7085
7140
|
`/v1/game/${appId}/leaderboards/${leaderboardKey}/scores`,
|
|
7086
|
-
{ member, score, mode }
|
|
7141
|
+
{ member, score, mode },
|
|
7142
|
+
"GAME_SUBMIT_SCORE_FAILED"
|
|
7087
7143
|
);
|
|
7088
7144
|
}
|
|
7089
7145
|
/**
|
|
7090
7146
|
* 상위 n 명 조회 (기본 10).
|
|
7091
7147
|
*/
|
|
7092
7148
|
async getTopScores(appId, leaderboardKey, n = 10) {
|
|
7093
|
-
return this.
|
|
7094
|
-
|
|
7149
|
+
return this.gameFetch(
|
|
7150
|
+
"GET",
|
|
7151
|
+
`/v1/game/${appId}/leaderboards/${leaderboardKey}/top?n=${n}`,
|
|
7152
|
+
void 0,
|
|
7153
|
+
"GAME_GET_TOP_SCORES_FAILED"
|
|
7095
7154
|
);
|
|
7096
7155
|
}
|
|
7097
7156
|
/**
|
|
7098
7157
|
* 단일 member 의 rank + score.
|
|
7099
7158
|
*/
|
|
7100
7159
|
async getMemberRank(appId, leaderboardKey, member) {
|
|
7101
|
-
return this.
|
|
7102
|
-
|
|
7160
|
+
return this.gameFetch(
|
|
7161
|
+
"GET",
|
|
7162
|
+
`/v1/game/${appId}/leaderboards/${leaderboardKey}/members/${member}`,
|
|
7163
|
+
void 0,
|
|
7164
|
+
"GAME_GET_MEMBER_RANK_FAILED"
|
|
7103
7165
|
);
|
|
7104
7166
|
}
|
|
7105
7167
|
/**
|
|
7106
7168
|
* member 주변 (위 above 명 + 본인 + 아래 below 명) 조회.
|
|
7107
7169
|
*/
|
|
7108
7170
|
async getRankAround(appId, leaderboardKey, member, above = 5, below = 5) {
|
|
7109
|
-
return this.
|
|
7110
|
-
|
|
7171
|
+
return this.gameFetch(
|
|
7172
|
+
"GET",
|
|
7173
|
+
`/v1/game/${appId}/leaderboards/${leaderboardKey}/around/${member}?above=${above}&below=${below}`,
|
|
7174
|
+
void 0,
|
|
7175
|
+
"GAME_GET_RANK_AROUND_FAILED"
|
|
7111
7176
|
);
|
|
7112
7177
|
}
|
|
7113
7178
|
/**
|
|
7114
7179
|
* leaderboard 전체 reset (시즌 종료 시).
|
|
7115
7180
|
*/
|
|
7116
7181
|
async resetLeaderboard(appId, leaderboardKey) {
|
|
7117
|
-
await this.
|
|
7182
|
+
await this.gameFetch(
|
|
7183
|
+
"DELETE",
|
|
7184
|
+
`/v1/game/${appId}/leaderboards/${leaderboardKey}`,
|
|
7185
|
+
void 0,
|
|
7186
|
+
"GAME_RESET_LEADERBOARD_FAILED"
|
|
7187
|
+
);
|
|
7118
7188
|
}
|
|
7119
7189
|
/**
|
|
7120
7190
|
* 특정 member 만 제거 (계정 삭제 등).
|
|
7121
7191
|
*/
|
|
7122
7192
|
async removeFromLeaderboard(appId, leaderboardKey, member) {
|
|
7123
|
-
await this.
|
|
7124
|
-
|
|
7193
|
+
await this.gameFetch(
|
|
7194
|
+
"DELETE",
|
|
7195
|
+
`/v1/game/${appId}/leaderboards/${leaderboardKey}/members/${member}`,
|
|
7196
|
+
void 0,
|
|
7197
|
+
"GAME_REMOVE_FROM_LEADERBOARD_FAILED"
|
|
7125
7198
|
);
|
|
7126
7199
|
}
|
|
7127
7200
|
// ─────────────────────────────────────────────────────────────────────
|
|
@@ -7136,47 +7209,66 @@ var GameAPI = class {
|
|
|
7136
7209
|
* await cb.game.uploadScript(appId, "ranked_br", fs.readFileSync("./ranked.lua", "utf-8"))
|
|
7137
7210
|
*/
|
|
7138
7211
|
async uploadScript(appId, name, code) {
|
|
7139
|
-
return this.
|
|
7212
|
+
return this.gameFetch(
|
|
7213
|
+
"POST",
|
|
7140
7214
|
`/v1/game/${appId}/scripts`,
|
|
7141
|
-
{ name, code }
|
|
7215
|
+
{ name, code },
|
|
7216
|
+
"GAME_UPLOAD_SCRIPT_FAILED"
|
|
7142
7217
|
);
|
|
7143
7218
|
}
|
|
7144
7219
|
/**
|
|
7145
7220
|
* app 의 모든 스크립트 메타데이터 목록.
|
|
7146
7221
|
*/
|
|
7147
7222
|
async listScripts(appId) {
|
|
7148
|
-
return this.
|
|
7223
|
+
return this.gameFetch(
|
|
7224
|
+
"GET",
|
|
7225
|
+
`/v1/game/${appId}/scripts`,
|
|
7226
|
+
void 0,
|
|
7227
|
+
"GAME_LIST_SCRIPTS_FAILED"
|
|
7228
|
+
);
|
|
7149
7229
|
}
|
|
7150
7230
|
/**
|
|
7151
7231
|
* 단일 스크립트 메타 + active version code.
|
|
7152
7232
|
*/
|
|
7153
7233
|
async getScript(appId, name) {
|
|
7154
|
-
return this.
|
|
7234
|
+
return this.gameFetch(
|
|
7235
|
+
"GET",
|
|
7236
|
+
`/v1/game/${appId}/scripts/${name}`,
|
|
7237
|
+
void 0,
|
|
7238
|
+
"GAME_GET_SCRIPT_FAILED"
|
|
7239
|
+
);
|
|
7155
7240
|
}
|
|
7156
7241
|
/**
|
|
7157
7242
|
* 단일 스크립트의 모든 버전 이력.
|
|
7158
7243
|
*/
|
|
7159
7244
|
async listScriptVersions(appId, name) {
|
|
7160
|
-
return this.
|
|
7161
|
-
|
|
7245
|
+
return this.gameFetch(
|
|
7246
|
+
"GET",
|
|
7247
|
+
`/v1/game/${appId}/scripts/${name}/versions`,
|
|
7248
|
+
void 0,
|
|
7249
|
+
"GAME_LIST_SCRIPT_VERSIONS_FAILED"
|
|
7162
7250
|
);
|
|
7163
7251
|
}
|
|
7164
7252
|
/**
|
|
7165
7253
|
* 특정 버전 활성화 (hot reload 자동). version=0 또는 미지정 → latest 활성화.
|
|
7166
7254
|
*/
|
|
7167
7255
|
async activateScript(appId, name, version) {
|
|
7168
|
-
return this.
|
|
7256
|
+
return this.gameFetch(
|
|
7257
|
+
"POST",
|
|
7169
7258
|
`/v1/game/${appId}/scripts/${name}/activate`,
|
|
7170
|
-
{ version: version ?? 0 }
|
|
7259
|
+
{ version: version ?? 0 },
|
|
7260
|
+
"GAME_ACTIVATE_SCRIPT_FAILED"
|
|
7171
7261
|
);
|
|
7172
7262
|
}
|
|
7173
7263
|
/**
|
|
7174
7264
|
* 직전 active 버전으로 롤백 (hot reload 자동).
|
|
7175
7265
|
*/
|
|
7176
7266
|
async rollbackScript(appId, name) {
|
|
7177
|
-
return this.
|
|
7267
|
+
return this.gameFetch(
|
|
7268
|
+
"POST",
|
|
7178
7269
|
`/v1/game/${appId}/scripts/${name}/rollback`,
|
|
7179
|
-
{}
|
|
7270
|
+
{},
|
|
7271
|
+
"GAME_ROLLBACK_SCRIPT_FAILED"
|
|
7180
7272
|
);
|
|
7181
7273
|
}
|
|
7182
7274
|
/**
|
|
@@ -7188,7 +7280,12 @@ var GameAPI = class {
|
|
|
7188
7280
|
* `POST /scripts/:name/deactivate` 로 endpoint 이동. 메서드명도 의도 명확화 위해 rename.
|
|
7189
7281
|
*/
|
|
7190
7282
|
async deactivateScript(appId, name) {
|
|
7191
|
-
await this.
|
|
7283
|
+
await this.gameFetch(
|
|
7284
|
+
"POST",
|
|
7285
|
+
`/v1/game/${appId}/scripts/${name}/deactivate`,
|
|
7286
|
+
{},
|
|
7287
|
+
"GAME_DEACTIVATE_SCRIPT_FAILED"
|
|
7288
|
+
);
|
|
7192
7289
|
}
|
|
7193
7290
|
/**
|
|
7194
7291
|
* 스크립트 영구 삭제 (hard-delete) — 메타 + 모든 버전 영구 제거. 복구 불가.
|
|
@@ -7200,7 +7297,12 @@ var GameAPI = class {
|
|
|
7200
7297
|
* 변경된 것에 대응.
|
|
7201
7298
|
*/
|
|
7202
7299
|
async deleteScript(appId, name) {
|
|
7203
|
-
await this.
|
|
7300
|
+
await this.gameFetch(
|
|
7301
|
+
"DELETE",
|
|
7302
|
+
`/v1/game/${appId}/scripts/${name}`,
|
|
7303
|
+
void 0,
|
|
7304
|
+
"GAME_DELETE_SCRIPT_FAILED"
|
|
7305
|
+
);
|
|
7204
7306
|
}
|
|
7205
7307
|
};
|
|
7206
7308
|
|
package/dist/index.mjs
CHANGED
|
@@ -6339,6 +6339,12 @@ var GameRoom = class {
|
|
|
6339
6339
|
resolve();
|
|
6340
6340
|
return;
|
|
6341
6341
|
}
|
|
6342
|
+
if (!this.config.appId) {
|
|
6343
|
+
reject(new Error(
|
|
6344
|
+
"cb.game: appId is required to connect. Pass it to `new ConnectBase({ appId })` or per-client via `cb.game.createClient({ appId, clientId })`."
|
|
6345
|
+
));
|
|
6346
|
+
return;
|
|
6347
|
+
}
|
|
6342
6348
|
const url = this.buildConnectionUrl(roomId);
|
|
6343
6349
|
this.ws = new WebSocket(url);
|
|
6344
6350
|
const onOpen = () => {
|
|
@@ -6593,8 +6599,10 @@ var GameRoom = class {
|
|
|
6593
6599
|
if (this.config.accessToken) {
|
|
6594
6600
|
params.set("token", this.config.accessToken);
|
|
6595
6601
|
}
|
|
6596
|
-
|
|
6597
|
-
|
|
6602
|
+
if (!this.config.appId) {
|
|
6603
|
+
throw new Error("cb.game: appId is required to build a game connection URL");
|
|
6604
|
+
}
|
|
6605
|
+
return `${wsUrl}/v1/game/${this.config.appId}/ws?${params.toString()}`;
|
|
6598
6606
|
}
|
|
6599
6607
|
send(type, data, msgId) {
|
|
6600
6608
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
@@ -6801,13 +6809,19 @@ var GameAPI = class {
|
|
|
6801
6809
|
this.config = new GameConfigAPI(http, appId);
|
|
6802
6810
|
}
|
|
6803
6811
|
/**
|
|
6804
|
-
* 게임 룸 클라이언트
|
|
6812
|
+
* 게임 룸 클라이언트 생성.
|
|
6813
|
+
*
|
|
6814
|
+
* `appId` 는 `createClient({ appId })` 로 호출별 지정하거나, 생략 시 `new ConnectBase({ appId })`
|
|
6815
|
+
* 의 전역 값을 사용한다. 둘 다 없으면 `connect()` 가 명확한 에러로 reject 한다 — 빈 appId 로
|
|
6816
|
+
* `/v1/game//ws` 에 붙어 server 측이 SCRIPT_NOT_FOUND 로 깨지던 silent 회귀 차단 (NJB 019e2210).
|
|
6805
6817
|
*/
|
|
6806
6818
|
createClient(config) {
|
|
6807
6819
|
return new GameRoom({
|
|
6808
6820
|
...config,
|
|
6809
6821
|
gameServerUrl: this.gameServerUrl.replace(/^http/, "ws"),
|
|
6810
|
-
appId
|
|
6822
|
+
// per-call appId 가 있으면 그것을 우선 (없을 때만 전역 fallback). 이전엔 this.appId 가
|
|
6823
|
+
// config.appId 를 무조건 덮어써서 호출별 지정이 불가능했다.
|
|
6824
|
+
appId: config.appId ?? this.appId,
|
|
6811
6825
|
publicKey: this.http.getPublicKey(),
|
|
6812
6826
|
accessToken: this.http.getAccessToken()
|
|
6813
6827
|
});
|
|
@@ -6982,6 +6996,38 @@ var GameAPI = class {
|
|
|
6982
6996
|
}
|
|
6983
6997
|
return headers;
|
|
6984
6998
|
}
|
|
6999
|
+
// game-server (`game.connectbase.world`) 전용 fetch.
|
|
7000
|
+
//
|
|
7001
|
+
// 회귀 가드 (v3.14.1, NJB report 2026-05-14): matchqueue / leaderboard / scripts 16개
|
|
7002
|
+
// 메서드가 v3.0.0 BREAKING 이후 `this.http.*` 를 통해 기본 baseUrl (=core-server,
|
|
7003
|
+
// `api.connectbase.world`) 로 라우팅되면서 404 회귀. 호스트 분리된 game-server 경로는
|
|
7004
|
+
// 반드시 `this.gameServerUrl` 을 사용해야 한다. 회귀 재발 방지 테스트:
|
|
7005
|
+
// test/game-host-routing.test.ts.
|
|
7006
|
+
async gameFetch(method, path, body, errCode = "GAME_REQUEST_FAILED") {
|
|
7007
|
+
const init = { method, headers: { ...this.getHeaders() } };
|
|
7008
|
+
if (body !== void 0) {
|
|
7009
|
+
init.headers["Content-Type"] = "application/json";
|
|
7010
|
+
init.body = JSON.stringify(body);
|
|
7011
|
+
}
|
|
7012
|
+
const response = await fetch(`${this.gameServerUrl}${path}`, init);
|
|
7013
|
+
if (!response.ok) {
|
|
7014
|
+
let serverCode = errCode;
|
|
7015
|
+
let message = `${method} ${path} failed: ${response.statusText}`;
|
|
7016
|
+
try {
|
|
7017
|
+
const data = await response.json();
|
|
7018
|
+
if (data && typeof data.error === "string") {
|
|
7019
|
+
message = data.error;
|
|
7020
|
+
serverCode = data.error;
|
|
7021
|
+
}
|
|
7022
|
+
} catch {
|
|
7023
|
+
}
|
|
7024
|
+
throw new ApiError(response.status, message, serverCode);
|
|
7025
|
+
}
|
|
7026
|
+
if (response.status === 204) return void 0;
|
|
7027
|
+
const contentType = response.headers.get("content-type") || "";
|
|
7028
|
+
if (!contentType.includes("application/json")) return void 0;
|
|
7029
|
+
return response.json();
|
|
7030
|
+
}
|
|
6985
7031
|
// ─────────────────────────────────────────────────────────────────────
|
|
6986
7032
|
// Phase 3 — Matchqueue primitive (mechanism only)
|
|
6987
7033
|
//
|
|
@@ -7002,9 +7048,11 @@ var GameAPI = class {
|
|
|
7002
7048
|
* @constraints appId/queueKey/ticketId 는 [a-zA-Z0-9_-] 만 허용. ttlSec=0 이면 만료 없음.
|
|
7003
7049
|
*/
|
|
7004
7050
|
async enqueueMatch(appId, queueKey, ticketId, attributes, ttlSec) {
|
|
7005
|
-
return this.
|
|
7051
|
+
return this.gameFetch(
|
|
7052
|
+
"POST",
|
|
7006
7053
|
`/v1/game/${appId}/matchqueue/${queueKey}/tickets`,
|
|
7007
|
-
{ ticket_id: ticketId, attributes, ttl_sec: ttlSec ?? 0 }
|
|
7054
|
+
{ ticket_id: ticketId, attributes, ttl_sec: ttlSec ?? 0 },
|
|
7055
|
+
"GAME_ENQUEUE_MATCH_FAILED"
|
|
7008
7056
|
);
|
|
7009
7057
|
}
|
|
7010
7058
|
/**
|
|
@@ -7013,16 +7061,22 @@ var GameAPI = class {
|
|
|
7013
7061
|
* @example const tickets = await cb.game.listMatchqueue(appId, "ranked")
|
|
7014
7062
|
*/
|
|
7015
7063
|
async listMatchqueue(appId, queueKey) {
|
|
7016
|
-
return this.
|
|
7017
|
-
|
|
7064
|
+
return this.gameFetch(
|
|
7065
|
+
"GET",
|
|
7066
|
+
`/v1/game/${appId}/matchqueue/${queueKey}`,
|
|
7067
|
+
void 0,
|
|
7068
|
+
"GAME_LIST_MATCHQUEUE_FAILED"
|
|
7018
7069
|
);
|
|
7019
7070
|
}
|
|
7020
7071
|
/**
|
|
7021
7072
|
* 매칭 큐에서 ticket 제거 (예: 사용자가 매칭 취소).
|
|
7022
7073
|
*/
|
|
7023
7074
|
async cancelMatch(appId, queueKey, ticketId) {
|
|
7024
|
-
await this.
|
|
7025
|
-
|
|
7075
|
+
await this.gameFetch(
|
|
7076
|
+
"DELETE",
|
|
7077
|
+
`/v1/game/${appId}/matchqueue/${queueKey}/tickets/${ticketId}`,
|
|
7078
|
+
void 0,
|
|
7079
|
+
"GAME_CANCEL_MATCH_FAILED"
|
|
7026
7080
|
);
|
|
7027
7081
|
}
|
|
7028
7082
|
// ─────────────────────────────────────────────────────────────────────
|
|
@@ -7038,47 +7092,66 @@ var GameAPI = class {
|
|
|
7038
7092
|
* await cb.game.submitScore(appId, "global_elo", userId, 32, "incr")
|
|
7039
7093
|
*/
|
|
7040
7094
|
async submitScore(appId, leaderboardKey, member, score, mode = "set") {
|
|
7041
|
-
return this.
|
|
7095
|
+
return this.gameFetch(
|
|
7096
|
+
"POST",
|
|
7042
7097
|
`/v1/game/${appId}/leaderboards/${leaderboardKey}/scores`,
|
|
7043
|
-
{ member, score, mode }
|
|
7098
|
+
{ member, score, mode },
|
|
7099
|
+
"GAME_SUBMIT_SCORE_FAILED"
|
|
7044
7100
|
);
|
|
7045
7101
|
}
|
|
7046
7102
|
/**
|
|
7047
7103
|
* 상위 n 명 조회 (기본 10).
|
|
7048
7104
|
*/
|
|
7049
7105
|
async getTopScores(appId, leaderboardKey, n = 10) {
|
|
7050
|
-
return this.
|
|
7051
|
-
|
|
7106
|
+
return this.gameFetch(
|
|
7107
|
+
"GET",
|
|
7108
|
+
`/v1/game/${appId}/leaderboards/${leaderboardKey}/top?n=${n}`,
|
|
7109
|
+
void 0,
|
|
7110
|
+
"GAME_GET_TOP_SCORES_FAILED"
|
|
7052
7111
|
);
|
|
7053
7112
|
}
|
|
7054
7113
|
/**
|
|
7055
7114
|
* 단일 member 의 rank + score.
|
|
7056
7115
|
*/
|
|
7057
7116
|
async getMemberRank(appId, leaderboardKey, member) {
|
|
7058
|
-
return this.
|
|
7059
|
-
|
|
7117
|
+
return this.gameFetch(
|
|
7118
|
+
"GET",
|
|
7119
|
+
`/v1/game/${appId}/leaderboards/${leaderboardKey}/members/${member}`,
|
|
7120
|
+
void 0,
|
|
7121
|
+
"GAME_GET_MEMBER_RANK_FAILED"
|
|
7060
7122
|
);
|
|
7061
7123
|
}
|
|
7062
7124
|
/**
|
|
7063
7125
|
* member 주변 (위 above 명 + 본인 + 아래 below 명) 조회.
|
|
7064
7126
|
*/
|
|
7065
7127
|
async getRankAround(appId, leaderboardKey, member, above = 5, below = 5) {
|
|
7066
|
-
return this.
|
|
7067
|
-
|
|
7128
|
+
return this.gameFetch(
|
|
7129
|
+
"GET",
|
|
7130
|
+
`/v1/game/${appId}/leaderboards/${leaderboardKey}/around/${member}?above=${above}&below=${below}`,
|
|
7131
|
+
void 0,
|
|
7132
|
+
"GAME_GET_RANK_AROUND_FAILED"
|
|
7068
7133
|
);
|
|
7069
7134
|
}
|
|
7070
7135
|
/**
|
|
7071
7136
|
* leaderboard 전체 reset (시즌 종료 시).
|
|
7072
7137
|
*/
|
|
7073
7138
|
async resetLeaderboard(appId, leaderboardKey) {
|
|
7074
|
-
await this.
|
|
7139
|
+
await this.gameFetch(
|
|
7140
|
+
"DELETE",
|
|
7141
|
+
`/v1/game/${appId}/leaderboards/${leaderboardKey}`,
|
|
7142
|
+
void 0,
|
|
7143
|
+
"GAME_RESET_LEADERBOARD_FAILED"
|
|
7144
|
+
);
|
|
7075
7145
|
}
|
|
7076
7146
|
/**
|
|
7077
7147
|
* 특정 member 만 제거 (계정 삭제 등).
|
|
7078
7148
|
*/
|
|
7079
7149
|
async removeFromLeaderboard(appId, leaderboardKey, member) {
|
|
7080
|
-
await this.
|
|
7081
|
-
|
|
7150
|
+
await this.gameFetch(
|
|
7151
|
+
"DELETE",
|
|
7152
|
+
`/v1/game/${appId}/leaderboards/${leaderboardKey}/members/${member}`,
|
|
7153
|
+
void 0,
|
|
7154
|
+
"GAME_REMOVE_FROM_LEADERBOARD_FAILED"
|
|
7082
7155
|
);
|
|
7083
7156
|
}
|
|
7084
7157
|
// ─────────────────────────────────────────────────────────────────────
|
|
@@ -7093,47 +7166,66 @@ var GameAPI = class {
|
|
|
7093
7166
|
* await cb.game.uploadScript(appId, "ranked_br", fs.readFileSync("./ranked.lua", "utf-8"))
|
|
7094
7167
|
*/
|
|
7095
7168
|
async uploadScript(appId, name, code) {
|
|
7096
|
-
return this.
|
|
7169
|
+
return this.gameFetch(
|
|
7170
|
+
"POST",
|
|
7097
7171
|
`/v1/game/${appId}/scripts`,
|
|
7098
|
-
{ name, code }
|
|
7172
|
+
{ name, code },
|
|
7173
|
+
"GAME_UPLOAD_SCRIPT_FAILED"
|
|
7099
7174
|
);
|
|
7100
7175
|
}
|
|
7101
7176
|
/**
|
|
7102
7177
|
* app 의 모든 스크립트 메타데이터 목록.
|
|
7103
7178
|
*/
|
|
7104
7179
|
async listScripts(appId) {
|
|
7105
|
-
return this.
|
|
7180
|
+
return this.gameFetch(
|
|
7181
|
+
"GET",
|
|
7182
|
+
`/v1/game/${appId}/scripts`,
|
|
7183
|
+
void 0,
|
|
7184
|
+
"GAME_LIST_SCRIPTS_FAILED"
|
|
7185
|
+
);
|
|
7106
7186
|
}
|
|
7107
7187
|
/**
|
|
7108
7188
|
* 단일 스크립트 메타 + active version code.
|
|
7109
7189
|
*/
|
|
7110
7190
|
async getScript(appId, name) {
|
|
7111
|
-
return this.
|
|
7191
|
+
return this.gameFetch(
|
|
7192
|
+
"GET",
|
|
7193
|
+
`/v1/game/${appId}/scripts/${name}`,
|
|
7194
|
+
void 0,
|
|
7195
|
+
"GAME_GET_SCRIPT_FAILED"
|
|
7196
|
+
);
|
|
7112
7197
|
}
|
|
7113
7198
|
/**
|
|
7114
7199
|
* 단일 스크립트의 모든 버전 이력.
|
|
7115
7200
|
*/
|
|
7116
7201
|
async listScriptVersions(appId, name) {
|
|
7117
|
-
return this.
|
|
7118
|
-
|
|
7202
|
+
return this.gameFetch(
|
|
7203
|
+
"GET",
|
|
7204
|
+
`/v1/game/${appId}/scripts/${name}/versions`,
|
|
7205
|
+
void 0,
|
|
7206
|
+
"GAME_LIST_SCRIPT_VERSIONS_FAILED"
|
|
7119
7207
|
);
|
|
7120
7208
|
}
|
|
7121
7209
|
/**
|
|
7122
7210
|
* 특정 버전 활성화 (hot reload 자동). version=0 또는 미지정 → latest 활성화.
|
|
7123
7211
|
*/
|
|
7124
7212
|
async activateScript(appId, name, version) {
|
|
7125
|
-
return this.
|
|
7213
|
+
return this.gameFetch(
|
|
7214
|
+
"POST",
|
|
7126
7215
|
`/v1/game/${appId}/scripts/${name}/activate`,
|
|
7127
|
-
{ version: version ?? 0 }
|
|
7216
|
+
{ version: version ?? 0 },
|
|
7217
|
+
"GAME_ACTIVATE_SCRIPT_FAILED"
|
|
7128
7218
|
);
|
|
7129
7219
|
}
|
|
7130
7220
|
/**
|
|
7131
7221
|
* 직전 active 버전으로 롤백 (hot reload 자동).
|
|
7132
7222
|
*/
|
|
7133
7223
|
async rollbackScript(appId, name) {
|
|
7134
|
-
return this.
|
|
7224
|
+
return this.gameFetch(
|
|
7225
|
+
"POST",
|
|
7135
7226
|
`/v1/game/${appId}/scripts/${name}/rollback`,
|
|
7136
|
-
{}
|
|
7227
|
+
{},
|
|
7228
|
+
"GAME_ROLLBACK_SCRIPT_FAILED"
|
|
7137
7229
|
);
|
|
7138
7230
|
}
|
|
7139
7231
|
/**
|
|
@@ -7145,7 +7237,12 @@ var GameAPI = class {
|
|
|
7145
7237
|
* `POST /scripts/:name/deactivate` 로 endpoint 이동. 메서드명도 의도 명확화 위해 rename.
|
|
7146
7238
|
*/
|
|
7147
7239
|
async deactivateScript(appId, name) {
|
|
7148
|
-
await this.
|
|
7240
|
+
await this.gameFetch(
|
|
7241
|
+
"POST",
|
|
7242
|
+
`/v1/game/${appId}/scripts/${name}/deactivate`,
|
|
7243
|
+
{},
|
|
7244
|
+
"GAME_DEACTIVATE_SCRIPT_FAILED"
|
|
7245
|
+
);
|
|
7149
7246
|
}
|
|
7150
7247
|
/**
|
|
7151
7248
|
* 스크립트 영구 삭제 (hard-delete) — 메타 + 모든 버전 영구 제거. 복구 불가.
|
|
@@ -7157,7 +7254,12 @@ var GameAPI = class {
|
|
|
7157
7254
|
* 변경된 것에 대응.
|
|
7158
7255
|
*/
|
|
7159
7256
|
async deleteScript(appId, name) {
|
|
7160
|
-
await this.
|
|
7257
|
+
await this.gameFetch(
|
|
7258
|
+
"DELETE",
|
|
7259
|
+
`/v1/game/${appId}/scripts/${name}`,
|
|
7260
|
+
void 0,
|
|
7261
|
+
"GAME_DELETE_SCRIPT_FAILED"
|
|
7262
|
+
);
|
|
7161
7263
|
}
|
|
7162
7264
|
};
|
|
7163
7265
|
|