import protobuf from "protobufjs/minimal.js"; const { Reader, Writer } = protobuf; // 比赛服 protobuf 协议适配层: // 小程序环境不支持 protobufjs 反射模式里的动态 Function codegen, // 所以这里使用 minimal Reader/Writer 做静态字段解码和客户端消息编码。 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, }; export const ClientMessageType = { CLIENT_MSG_UNKNOWN: 0, CLIENT_MSG_HEARTBEAT_ACK: 1, CLIENT_MSG_SHOOT_DATA: 2, CLIENT_MSG_ACK: 3, CLIENT_MSG_LEAVE: 4, }; // protobufjs 的 enum 默认是 name -> value,这里反转成 value -> name 用于日志打印。 const ServerMessageTypeNameByValue = Object.keys(ServerMessageType).reduce( (result, name) => { result[ServerMessageType[name]] = name; return result; }, {} ); // 当前只需要解码比赛服下发的字段并打印,字段表按后端 proto 定义维护。 const SCHEMAS = { MatchShoot: { 1: { name: "player_id", kind: "int64" }, 2: { name: "status", kind: "int32" }, 3: { name: "x", kind: "float" }, 4: { name: "y", kind: "float" }, 5: { name: "ring", kind: "int32" }, 6: { name: "ring_x", kind: "bool" }, 7: { name: "angle", kind: "float" }, 8: { name: "distance", kind: "float" }, 9: { name: "three_consecutive_10_rings", kind: "bool" }, }, 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", }, 10: { name: "ack_time", kind: "int64" }, }, 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" }, 9: { name: "three_consecutive_10_rings", kind: "bool" }, }, 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: "string" }, 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" }, 26: { name: "countdown_start_time", kind: "int64" }, }, ServerMessage: { 1: { name: "type", kind: "int32" }, 2: { name: "match_id", kind: "string" }, 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。 function toUint8Array(data) { if (data instanceof Uint8Array) return data; if (data instanceof ArrayBuffer) return new Uint8Array(data); if (ArrayBuffer.isView(data)) { return new Uint8Array(data.buffer, data.byteOffset, data.byteLength); } if (typeof data === "string") { const bytes = new Uint8Array(data.length); for (let i = 0; i < data.length; i += 1) { bytes[i] = data.charCodeAt(i) & 0xff; } return bytes; } throw new TypeError("Unsupported match websocket message payload"); } // uni.sendSocketMessage 发送二进制时使用 ArrayBuffer。 function toArrayBuffer(bytes) { const output = new Uint8Array(bytes.length); output.set(bytes); 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) { // 后端已确认比赛服 websocket 下发的是原始 protobuf frame,直接解 ServerMessage。 return decodeMessage("ServerMessage", toUint8Array(data)); } export function encodeClientMessage(payload) { // ClientMessage 字段名按 proto 定义写入,例如 match_id、user_id。 const writer = Writer.create(); 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(18).string(String(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) { return ServerMessageTypeNameByValue[type] || String(type); } export function createHeartbeatAckMessage() { // 心跳 ACK 按文档立即回,不等待语音播报。 return encodeClientMessage({ type: ClientMessageType.CLIENT_MSG_HEARTBEAT_ACK, }); } export function createAckMessage({ matchId, sequence }) { // 普通消息 ACK 仍携带 sequence,但前端不做 sequence 补发或排序。 return encodeClientMessage({ type: ClientMessageType.CLIENT_MSG_ACK, match_id: matchId, sequence, }); } export function createLeaveMessage({ matchId, userId }) { // 比赛结束或页面离开时通知比赛服释放当前玩家连接。 return encodeClientMessage({ type: ClientMessageType.CLIENT_MSG_LEAVE, match_id: matchId, user_id: userId, }); }