update:更新比赛服逻辑
This commit is contained in:
+70
-15
@@ -19,6 +19,10 @@ let audioAckListenerReady = false;
|
|||||||
// 后端要求非心跳消息必须等前端语音播报完成后再 ACK。
|
// 后端要求非心跳消息必须等前端语音播报完成后再 ACK。
|
||||||
const pendingAcks = [];
|
const pendingAcks = [];
|
||||||
|
|
||||||
|
// 当前仍是“只连接、只解码、只打印”阶段,还没有真正接管比赛语音播报。
|
||||||
|
// 为了先验证协议闭环,这里临时把“打印完成”当作“播报完成”并立即 ACK。
|
||||||
|
const AUTO_ACK_WITHOUT_AUDIO = true;
|
||||||
|
|
||||||
// 这些比赛消息需要在 audioEnded 后补 ACK;心跳 ACK 单独即时处理。
|
// 这些比赛消息需要在 audioEnded 后补 ACK;心跳 ACK 单独即时处理。
|
||||||
const ACK_REQUIRED_TYPES = new Set([
|
const ACK_REQUIRED_TYPES = new Set([
|
||||||
ServerMessageType.SERVER_MSG_MATCH_READY,
|
ServerMessageType.SERVER_MSG_MATCH_READY,
|
||||||
@@ -36,6 +40,11 @@ function pickField(source, camelKey, snakeKey) {
|
|||||||
return source?.[camelKey] ?? source?.[snakeKey];
|
return source?.[camelKey] ?? source?.[snakeKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeId(value) {
|
||||||
|
if (value === undefined || value === null || value === "") return "";
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
// 追加 token 查询参数;如果后端地址已经带 token,则保持原样。
|
// 追加 token 查询参数;如果后端地址已经带 token,则保持原样。
|
||||||
function appendQuery(url, key, value) {
|
function appendQuery(url, key, value) {
|
||||||
if (!value || new RegExp(`[?&]${key}=`).test(url)) return url;
|
if (!value || new RegExp(`[?&]${key}=`).test(url)) return url;
|
||||||
@@ -87,14 +96,11 @@ function sendHeartbeatAck() {
|
|||||||
|
|
||||||
function sendAck({ matchId, sequence }) {
|
function sendAck({ matchId, sequence }) {
|
||||||
// sequence 由后端处理,前端只原样带回,不做重排和断线补发。
|
// sequence 由后端处理,前端只原样带回,不做重排和断线补发。
|
||||||
if (!sequence) return;
|
if (sequence === undefined || sequence === null || sequence === "") return;
|
||||||
sendBuffer(createAckMessage({ matchId, sequence }), `ack ${sequence}`);
|
sendBuffer(createAckMessage({ matchId, sequence }), `ack ${sequence}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function flushPendingAcks() {
|
function completeAckTask(task) {
|
||||||
// 每次语音播报完成,只确认一条已播放完成的服务端消息。
|
|
||||||
if (!pendingAcks.length) return;
|
|
||||||
const task = pendingAcks.shift();
|
|
||||||
sendAck(task);
|
sendAck(task);
|
||||||
if (task.leaveAfterAck) {
|
if (task.leaveAfterAck) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -103,6 +109,12 @@ function flushPendingAcks() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function flushPendingAcks() {
|
||||||
|
// 每次语音播报完成,只确认一条已播放完成的服务端消息。
|
||||||
|
if (!pendingAcks.length) return;
|
||||||
|
completeAckTask(pendingAcks.shift());
|
||||||
|
}
|
||||||
|
|
||||||
function ensureAudioAckListener() {
|
function ensureAudioAckListener() {
|
||||||
// 复用现有全局 audioEnded 事件,确保 ACK 时机落在播报结束之后。
|
// 复用现有全局 audioEnded 事件,确保 ACK 时机落在播报结束之后。
|
||||||
if (audioAckListenerReady) return;
|
if (audioAckListenerReady) return;
|
||||||
@@ -118,12 +130,33 @@ function removeAudioAckListener() {
|
|||||||
|
|
||||||
function queueAckAfterAudio(message) {
|
function queueAckAfterAudio(message) {
|
||||||
// 非 ACK_REQUIRED_TYPES 的消息只打印,不自动确认,避免扩大发送面。
|
// 非 ACK_REQUIRED_TYPES 的消息只打印,不自动确认,避免扩大发送面。
|
||||||
if (!ACK_REQUIRED_TYPES.has(message.type) || !message.sequence) return;
|
if (
|
||||||
pendingAcks.push({
|
!ACK_REQUIRED_TYPES.has(message.type) ||
|
||||||
matchId: pickField(message, "matchId", "match_id") || currentContext?.matchId,
|
message.sequence === undefined ||
|
||||||
|
message.sequence === null ||
|
||||||
|
message.sequence === ""
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const task = {
|
||||||
|
matchId: normalizeId(
|
||||||
|
pickField(message, "matchId", "match_id") || currentContext?.matchId
|
||||||
|
),
|
||||||
sequence: message.sequence,
|
sequence: message.sequence,
|
||||||
leaveAfterAck: message.type === ServerMessageType.SERVER_MSG_MATCH_END,
|
leaveAfterAck: message.type === ServerMessageType.SERVER_MSG_MATCH_END,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (AUTO_ACK_WITHOUT_AUDIO) {
|
||||||
|
console.log(
|
||||||
|
"[match-ws] ack immediately in print-only mode",
|
||||||
|
getServerMessageTypeName(message.type),
|
||||||
|
message.sequence
|
||||||
|
);
|
||||||
|
completeAckTask(task);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingAcks.push(task);
|
||||||
console.log(
|
console.log(
|
||||||
"[match-ws] ack queued until audioEnded",
|
"[match-ws] ack queued until audioEnded",
|
||||||
getServerMessageTypeName(message.type),
|
getServerMessageTypeName(message.type),
|
||||||
@@ -144,6 +177,11 @@ function handleMessage(data) {
|
|||||||
const typeName = getServerMessageTypeName(message.type);
|
const typeName = getServerMessageTypeName(message.type);
|
||||||
console.log("[match-ws] message decoded", typeName, message);
|
console.log("[match-ws] message decoded", typeName, message);
|
||||||
|
|
||||||
|
const decodedMatchId = normalizeId(pickField(message, "matchId", "match_id"));
|
||||||
|
if (decodedMatchId && currentContext) {
|
||||||
|
currentContext.matchId = decodedMatchId;
|
||||||
|
}
|
||||||
|
|
||||||
if (message.type === ServerMessageType.SERVER_MSG_HEARTBEAT) {
|
if (message.type === ServerMessageType.SERVER_MSG_HEARTBEAT) {
|
||||||
sendHeartbeatAck();
|
sendHeartbeatAck();
|
||||||
return;
|
return;
|
||||||
@@ -168,16 +206,22 @@ export function connectMatchWebSocket(options = {}) {
|
|||||||
// 入口参数来自原 websocket 的比赛服地址通知,不影响原有 websocket 连接。
|
// 入口参数来自原 websocket 的比赛服地址通知,不影响原有 websocket 连接。
|
||||||
const { serverAddr, matchId, userId, token } = options;
|
const { serverAddr, matchId, userId, token } = options;
|
||||||
const url = normalizeServerUrl(serverAddr, token);
|
const url = normalizeServerUrl(serverAddr, token);
|
||||||
|
const normalizedMatchId = normalizeId(matchId);
|
||||||
|
const normalizedUserId = normalizeId(userId);
|
||||||
|
|
||||||
if (!url || !matchId) {
|
if (!url || !normalizedMatchId) {
|
||||||
console.log("[match-ws] missing serverAddr or matchId", options);
|
console.log("[match-ws] missing serverAddr or matchId", options);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (socket && currentContext?.url === url && currentContext?.matchId === matchId) {
|
if (
|
||||||
|
socket &&
|
||||||
|
currentContext?.url === url &&
|
||||||
|
currentContext?.matchId === normalizedMatchId
|
||||||
|
) {
|
||||||
// 同一场比赛同一地址重复通知时不重复建连。
|
// 同一场比赛同一地址重复通知时不重复建连。
|
||||||
console.log("[match-ws] already connected or connecting", {
|
console.log("[match-ws] already connected or connecting", {
|
||||||
matchId,
|
matchId: normalizedMatchId,
|
||||||
url,
|
url,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -188,13 +232,21 @@ export function connectMatchWebSocket(options = {}) {
|
|||||||
|
|
||||||
manualClose = false;
|
manualClose = false;
|
||||||
isConnecting = true;
|
isConnecting = true;
|
||||||
currentContext = { serverAddr, matchId, userId, url };
|
currentContext = {
|
||||||
|
serverAddr,
|
||||||
|
matchId: normalizedMatchId,
|
||||||
|
userId: normalizedUserId,
|
||||||
|
url,
|
||||||
|
};
|
||||||
ensureAudioAckListener();
|
ensureAudioAckListener();
|
||||||
|
|
||||||
const socketTask = uni.connectSocket({
|
const socketTask = uni.connectSocket({
|
||||||
url,
|
url,
|
||||||
success: () => {
|
success: () => {
|
||||||
console.log("[match-ws] connect requested", { matchId, url });
|
console.log("[match-ws] connect requested", {
|
||||||
|
matchId: normalizedMatchId,
|
||||||
|
url,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
fail: (err) => {
|
fail: (err) => {
|
||||||
if (socket !== socketTask) return;
|
if (socket !== socketTask) return;
|
||||||
@@ -209,7 +261,10 @@ export function connectMatchWebSocket(options = {}) {
|
|||||||
socketTask.onOpen(() => {
|
socketTask.onOpen(() => {
|
||||||
if (socket !== socketTask) return;
|
if (socket !== socketTask) return;
|
||||||
isConnecting = false;
|
isConnecting = false;
|
||||||
console.log("[match-ws] connected", { matchId, url });
|
console.log("[match-ws] connected", {
|
||||||
|
matchId: normalizedMatchId,
|
||||||
|
url,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
socketTask.onMessage((res) => {
|
socketTask.onMessage((res) => {
|
||||||
|
|||||||
+301
-30
@@ -1,34 +1,199 @@
|
|||||||
import matchRoot from "./match.min.js";
|
import protobuf from "protobufjs/minimal.js";
|
||||||
|
|
||||||
|
const { Reader, Writer } = protobuf;
|
||||||
|
|
||||||
// 比赛服 protobuf 协议适配层:
|
// 比赛服 protobuf 协议适配层:
|
||||||
// 这里只负责 raw protobuf frame 的解码和客户端消息编码,不处理 UI 状态。
|
// 小程序环境不支持 protobufjs 反射模式里的动态 Function codegen,
|
||||||
const ServerMessage = matchRoot.lookupType("rpc.ServerMessage");
|
// 所以这里使用 minimal Reader/Writer 做静态字段解码和客户端消息编码。
|
||||||
const ClientMessage = matchRoot.lookupType("rpc.ClientMessage");
|
export const ServerMessageType = {
|
||||||
|
SERVER_MSG_UNKNOWN: 0,
|
||||||
|
SERVER_MSG_MATCH_READY: 1,
|
||||||
|
SERVER_MSG_MATCH_START: 2,
|
||||||
|
SERVER_MSG_NOW_YOU: 3,
|
||||||
|
SERVER_MSG_SHOT: 4,
|
||||||
|
SERVER_MSG_NEW_ROUND: 5,
|
||||||
|
SERVER_MSG_MATCH_END: 6,
|
||||||
|
SERVER_MSG_TIMEOUT: 7,
|
||||||
|
SERVER_MSG_CHECK: 8,
|
||||||
|
SERVER_MSG_RAND: 9,
|
||||||
|
SERVER_MSG_NOT_ENOUGH_DISTANCE: 10,
|
||||||
|
SERVER_MSG_PLAYER_LEFT: 11,
|
||||||
|
SERVER_MSG_HEARTBEAT: 12,
|
||||||
|
SERVER_MSG_PRACTICE_END: 13,
|
||||||
|
};
|
||||||
|
|
||||||
// 从反射 Root 中读取枚举,避免直接依赖生成文件内部结构。
|
export const ClientMessageType = {
|
||||||
function getEnumValues(name) {
|
CLIENT_MSG_UNKNOWN: 0,
|
||||||
return matchRoot.lookupEnum(name).values || {};
|
CLIENT_MSG_HEARTBEAT_ACK: 1,
|
||||||
}
|
CLIENT_MSG_SHOOT_DATA: 2,
|
||||||
|
CLIENT_MSG_ACK: 3,
|
||||||
|
CLIENT_MSG_LEAVE: 4,
|
||||||
|
};
|
||||||
|
|
||||||
// protobufjs 的 enum 默认是 name -> value,这里反转成 value -> name 用于日志打印。
|
// protobufjs 的 enum 默认是 name -> value,这里反转成 value -> name 用于日志打印。
|
||||||
function invertEnum(values) {
|
const ServerMessageTypeNameByValue = Object.keys(ServerMessageType).reduce(
|
||||||
return Object.keys(values).reduce((result, name) => {
|
(result, name) => {
|
||||||
result[values[name]] = name;
|
result[ServerMessageType[name]] = name;
|
||||||
return result;
|
return result;
|
||||||
}, {});
|
},
|
||||||
}
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
export const ServerMessageType = getEnumValues("rpc.ServerMessageType");
|
// 当前只需要解码比赛服下发的字段并打印,字段表按后端 proto 定义维护。
|
||||||
export const ClientMessageType = getEnumValues("rpc.ClientMessageType");
|
const SCHEMAS = {
|
||||||
|
MatchShoot: {
|
||||||
const ServerMessageTypeNameByValue = invertEnum(ServerMessageType);
|
1: { name: "player_id", kind: "int64" },
|
||||||
|
2: { name: "status", kind: "int32" },
|
||||||
// 统一把 int64 转成字符串,避免小程序环境下大整数精度丢失。
|
3: { name: "x", kind: "float" },
|
||||||
const TO_OBJECT_OPTIONS = {
|
4: { name: "y", kind: "float" },
|
||||||
longs: String,
|
5: { name: "ring", kind: "int32" },
|
||||||
enums: Number,
|
6: { name: "ring_x", kind: "bool" },
|
||||||
bytes: Array,
|
7: { name: "angle", kind: "float" },
|
||||||
oneofs: true,
|
8: { name: "distance", kind: "float" },
|
||||||
|
},
|
||||||
|
MatchShootList: {
|
||||||
|
1: { name: "items", kind: "message", type: "MatchShoot", repeated: true },
|
||||||
|
},
|
||||||
|
RoundScore: {
|
||||||
|
1: { name: "total_ring", kind: "int32" },
|
||||||
|
2: { name: "score", kind: "int32" },
|
||||||
|
3: { name: "if_win", kind: "bool" },
|
||||||
|
},
|
||||||
|
MatchRound: {
|
||||||
|
1: {
|
||||||
|
name: "shoots",
|
||||||
|
kind: "map",
|
||||||
|
keyKind: "int64",
|
||||||
|
valueKind: "message",
|
||||||
|
valueType: "MatchShootList",
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
name: "scores",
|
||||||
|
kind: "map",
|
||||||
|
keyKind: "int32",
|
||||||
|
valueKind: "message",
|
||||||
|
valueType: "RoundScore",
|
||||||
|
},
|
||||||
|
3: { name: "round", kind: "int32" },
|
||||||
|
4: { name: "if_gold", kind: "bool" },
|
||||||
|
5: { name: "status", kind: "int32" },
|
||||||
|
6: { name: "gold_round", kind: "int32" },
|
||||||
|
},
|
||||||
|
PlayerMatchResult: {
|
||||||
|
1: { name: "total_ring", kind: "int32" },
|
||||||
|
2: { name: "user_id", kind: "int64" },
|
||||||
|
3: { name: "ten_ring_count", kind: "int32" },
|
||||||
|
4: { name: "average_ring", kind: "float" },
|
||||||
|
},
|
||||||
|
PlayerFull: {
|
||||||
|
1: { name: "id", kind: "int64" },
|
||||||
|
2: { name: "name", kind: "string" },
|
||||||
|
3: { name: "avatar", kind: "string" },
|
||||||
|
4: { name: "exp", kind: "int32" },
|
||||||
|
5: { name: "score", kind: "int32" },
|
||||||
|
6: { name: "level", kind: "int32" },
|
||||||
|
7: { name: "before_level", kind: "int32" },
|
||||||
|
8: { name: "before_exp", kind: "int32" },
|
||||||
|
9: { name: "current_exp", kind: "int32" },
|
||||||
|
10: { name: "upgrade_exp", kind: "int32" },
|
||||||
|
11: { name: "active", kind: "int32" },
|
||||||
|
12: { name: "device_id", kind: "string" },
|
||||||
|
13: { name: "s_vip", kind: "bool" },
|
||||||
|
14: { name: "vip", kind: "bool" },
|
||||||
|
15: { name: "player_match_result", kind: "message", type: "PlayerMatchResult" },
|
||||||
|
},
|
||||||
|
TeamInfo: {
|
||||||
|
1: { name: "players", kind: "message", type: "PlayerFull", repeated: true },
|
||||||
|
2: { name: "id", kind: "int32" },
|
||||||
|
3: { name: "name", kind: "string" },
|
||||||
|
4: { name: "score", kind: "int32" },
|
||||||
|
},
|
||||||
|
CurrentShoot: {
|
||||||
|
1: { name: "round", kind: "int32" },
|
||||||
|
2: { name: "round_id", kind: "int64" },
|
||||||
|
3: { name: "index", kind: "int32" },
|
||||||
|
4: { name: "start_time", kind: "int64" },
|
||||||
|
5: { name: "player_id", kind: "int64" },
|
||||||
|
6: { name: "gold_round", kind: "bool" },
|
||||||
|
7: { name: "start_time_text", kind: "string" },
|
||||||
|
8: { name: "my_index", kind: "int32" },
|
||||||
|
9: {
|
||||||
|
name: "index_map",
|
||||||
|
kind: "map",
|
||||||
|
keyKind: "int64",
|
||||||
|
valueKind: "int32",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ShootData: {
|
||||||
|
1: { name: "x", kind: "float" },
|
||||||
|
2: { name: "y", kind: "float" },
|
||||||
|
3: { name: "r", kind: "float" },
|
||||||
|
4: { name: "dst", kind: "float" },
|
||||||
|
5: { name: "m", kind: "string" },
|
||||||
|
6: { name: "adc", kind: "float" },
|
||||||
|
7: { name: "device_id", kind: "string" },
|
||||||
|
8: { name: "shoot_id", kind: "string" },
|
||||||
|
},
|
||||||
|
PracticeInfo: {
|
||||||
|
1: { name: "id", kind: "int64" },
|
||||||
|
2: { name: "user_id", kind: "int64" },
|
||||||
|
3: { name: "status", kind: "int32" },
|
||||||
|
4: { name: "status_text", kind: "string" },
|
||||||
|
5: { name: "start_time", kind: "int64" },
|
||||||
|
6: { name: "target_type", kind: "int32" },
|
||||||
|
7: { name: "vip", kind: "bool" },
|
||||||
|
8: { name: "s_vip", kind: "bool" },
|
||||||
|
9: { name: "device_id", kind: "string" },
|
||||||
|
10: { name: "shoot_data", kind: "message", type: "MatchShoot" },
|
||||||
|
11: { name: "details", kind: "message", type: "MatchShoot", repeated: true },
|
||||||
|
},
|
||||||
|
MatchInfo: {
|
||||||
|
1: { name: "match_id", kind: "int64" },
|
||||||
|
2: { name: "create_time", kind: "int64" },
|
||||||
|
3: { name: "start_time", kind: "int64" },
|
||||||
|
4: { name: "server_time", kind: "int64" },
|
||||||
|
5: { name: "shoot_time", kind: "int32" },
|
||||||
|
6: { name: "shoot_number", kind: "int32" },
|
||||||
|
7: { name: "ready_time", kind: "int32" },
|
||||||
|
8: { name: "way", kind: "int32" },
|
||||||
|
9: { name: "mode", kind: "int32" },
|
||||||
|
10: { name: "status", kind: "int32" },
|
||||||
|
11: { name: "status_text", kind: "string" },
|
||||||
|
12: { name: "rounds", kind: "message", type: "MatchRound", repeated: true },
|
||||||
|
13: {
|
||||||
|
name: "teams",
|
||||||
|
kind: "map",
|
||||||
|
keyKind: "int32",
|
||||||
|
valueKind: "message",
|
||||||
|
valueType: "TeamInfo",
|
||||||
|
},
|
||||||
|
14: { name: "current", kind: "message", type: "CurrentShoot" },
|
||||||
|
15: { name: "next", kind: "message", type: "CurrentShoot" },
|
||||||
|
16: { name: "shoot_data", kind: "message", type: "MatchShoot" },
|
||||||
|
17: { name: "win_team", kind: "int32" },
|
||||||
|
18: { name: "mvp", kind: "message", type: "PlayerFull" },
|
||||||
|
19: { name: "room_id", kind: "string" },
|
||||||
|
20: {
|
||||||
|
name: "result_list",
|
||||||
|
kind: "message",
|
||||||
|
type: "PlayerMatchResult",
|
||||||
|
repeated: true,
|
||||||
|
},
|
||||||
|
21: { name: "timeout_time", kind: "int64" },
|
||||||
|
22: { name: "target_type", kind: "int32" },
|
||||||
|
23: { name: "event_type", kind: "int32" },
|
||||||
|
24: { name: "timeout", kind: "int32" },
|
||||||
|
25: { name: "server_addr", kind: "string" },
|
||||||
|
},
|
||||||
|
ServerMessage: {
|
||||||
|
1: { name: "type", kind: "int32" },
|
||||||
|
2: { name: "match_id", kind: "int64" },
|
||||||
|
3: { name: "timestamp", kind: "int64" },
|
||||||
|
4: { name: "match_info", kind: "message", type: "MatchInfo", oneof: "payload" },
|
||||||
|
5: { name: "shoot_data", kind: "message", type: "ShootData", oneof: "payload" },
|
||||||
|
6: { name: "practice_info", kind: "message", type: "PracticeInfo", oneof: "payload" },
|
||||||
|
7: { name: "sequence", kind: "int64" },
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 小程序 websocket 收到的 data 可能是 ArrayBuffer、TypedArray 或字符串,这里统一成 Uint8Array。
|
// 小程序 websocket 收到的 data 可能是 ArrayBuffer、TypedArray 或字符串,这里统一成 Uint8Array。
|
||||||
@@ -55,16 +220,122 @@ function toArrayBuffer(bytes) {
|
|||||||
return output.buffer;
|
return output.buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readScalar(reader, kind) {
|
||||||
|
switch (kind) {
|
||||||
|
case "int32":
|
||||||
|
return reader.int32();
|
||||||
|
case "int64":
|
||||||
|
// 统一把 int64 转成字符串,避免小程序环境下大整数精度丢失。
|
||||||
|
return reader.int64().toString();
|
||||||
|
case "float":
|
||||||
|
return reader.float();
|
||||||
|
case "bool":
|
||||||
|
return reader.bool();
|
||||||
|
case "string":
|
||||||
|
return reader.string();
|
||||||
|
case "bytes":
|
||||||
|
return Array.from(reader.bytes());
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported protobuf scalar kind: ${kind}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readMapEntry(reader, field) {
|
||||||
|
const end = reader.uint32() + reader.pos;
|
||||||
|
let key = "";
|
||||||
|
let value;
|
||||||
|
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1:
|
||||||
|
key = readScalar(reader, field.keyKind);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
value =
|
||||||
|
field.valueKind === "message"
|
||||||
|
? decodeMessage(field.valueType, reader, reader.uint32())
|
||||||
|
: readScalar(reader, field.valueKind);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
reader.skipType(tag & 7);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { key: String(key), value };
|
||||||
|
}
|
||||||
|
|
||||||
|
function readValue(reader, field) {
|
||||||
|
if (field.kind === "message") {
|
||||||
|
return decodeMessage(field.type, reader, reader.uint32());
|
||||||
|
}
|
||||||
|
return readScalar(reader, field.kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeMessage(schemaName, readerOrData, length) {
|
||||||
|
const schema = SCHEMAS[schemaName];
|
||||||
|
const reader =
|
||||||
|
readerOrData instanceof Reader ? readerOrData : Reader.create(readerOrData);
|
||||||
|
const end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = {};
|
||||||
|
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
const field = schema[tag >>> 3];
|
||||||
|
|
||||||
|
if (!field) {
|
||||||
|
reader.skipType(tag & 7);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.kind === "map") {
|
||||||
|
const entry = readMapEntry(reader, field);
|
||||||
|
const map = message[field.name] || {};
|
||||||
|
map[entry.key] = entry.value;
|
||||||
|
message[field.name] = map;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = readValue(reader, field);
|
||||||
|
|
||||||
|
if (field.repeated) {
|
||||||
|
if (!message[field.name]) message[field.name] = [];
|
||||||
|
message[field.name].push(value);
|
||||||
|
} else {
|
||||||
|
message[field.name] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.oneof) {
|
||||||
|
message[field.oneof] = field.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
export function decodeServerMessage(data) {
|
export function decodeServerMessage(data) {
|
||||||
// 后端已确认比赛服 websocket 下发的是原始 protobuf frame,直接 ServerMessage.decode。
|
// 后端已确认比赛服 websocket 下发的是原始 protobuf frame,直接解 ServerMessage。
|
||||||
const message = ServerMessage.decode(toUint8Array(data));
|
return decodeMessage("ServerMessage", toUint8Array(data));
|
||||||
return ServerMessage.toObject(message, TO_OBJECT_OPTIONS);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function encodeClientMessage(payload) {
|
export function encodeClientMessage(payload) {
|
||||||
// ClientMessage 字段名按 proto 定义传入,例如 match_id、user_id。
|
// ClientMessage 字段名按 proto 定义写入,例如 match_id、user_id。
|
||||||
const message = ClientMessage.create(payload);
|
const writer = Writer.create();
|
||||||
return toArrayBuffer(ClientMessage.encode(message).finish());
|
const matchId = payload.match_id ?? payload.matchId;
|
||||||
|
const userId = payload.user_id ?? payload.userId;
|
||||||
|
|
||||||
|
if (payload.type !== undefined) writer.uint32(8).int32(payload.type);
|
||||||
|
if (matchId !== undefined && matchId !== null) writer.uint32(16).int64(matchId);
|
||||||
|
if (userId !== undefined && userId !== null) writer.uint32(24).int64(userId);
|
||||||
|
if (payload.sequence !== undefined && payload.sequence !== null) {
|
||||||
|
writer.uint32(32).int64(payload.sequence);
|
||||||
|
}
|
||||||
|
if (payload.data !== undefined && payload.data !== null) {
|
||||||
|
writer.uint32(42).bytes(toUint8Array(payload.data));
|
||||||
|
}
|
||||||
|
|
||||||
|
return toArrayBuffer(writer.finish());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getServerMessageTypeName(type) {
|
export function getServerMessageTypeName(type) {
|
||||||
|
|||||||
Reference in New Issue
Block a user