375 lines
12 KiB
JavaScript
375 lines
12 KiB
JavaScript
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,
|
||
});
|
||
}
|