Files
shoot-miniprograms/src/utils/matchProtocol.js
T

375 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
});
}