update:新增比赛服断线重连机制
This commit is contained in:
+18
@@ -80,6 +80,10 @@
|
|||||||
// audioManager.play("射箭声音")
|
// audioManager.play("射箭声音")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onNetworkStatusChange(status) {
|
||||||
|
matchWebsocket.handleMatchNetworkStatusChange(status);
|
||||||
|
}
|
||||||
|
|
||||||
function connectMatchServerFromMessage(content) {
|
function connectMatchServerFromMessage(content) {
|
||||||
const messages = Array.isArray(content) ? content : [content];
|
const messages = Array.isArray(content) ? content : [content];
|
||||||
messages.forEach((message) => {
|
messages.forEach((message) => {
|
||||||
@@ -108,6 +112,17 @@
|
|||||||
uni.$on("update-online", emitUpdateOnline);
|
uni.$on("update-online", emitUpdateOnline);
|
||||||
uni.$on("session-kicked-out", onSessionKickedOut);
|
uni.$on("session-kicked-out", onSessionKickedOut);
|
||||||
uni.$on("device-bind-invalid", onDeviceBindInvalid);
|
uni.$on("device-bind-invalid", onDeviceBindInvalid);
|
||||||
|
if (typeof uni.offNetworkStatusChange === "function") {
|
||||||
|
uni.offNetworkStatusChange(onNetworkStatusChange);
|
||||||
|
}
|
||||||
|
if (typeof uni.onNetworkStatusChange === "function") {
|
||||||
|
uni.onNetworkStatusChange(onNetworkStatusChange);
|
||||||
|
}
|
||||||
|
if (typeof uni.getNetworkType === "function") {
|
||||||
|
uni.getNetworkType({
|
||||||
|
success: onNetworkStatusChange
|
||||||
|
});
|
||||||
|
}
|
||||||
const token = uni.getStorageSync(
|
const token = uni.getStorageSync(
|
||||||
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
|
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
|
||||||
);
|
);
|
||||||
@@ -122,6 +137,9 @@
|
|||||||
uni.$off("update-online", emitUpdateOnline);
|
uni.$off("update-online", emitUpdateOnline);
|
||||||
uni.$off("session-kicked-out", onSessionKickedOut);
|
uni.$off("session-kicked-out", onSessionKickedOut);
|
||||||
uni.$off("device-bind-invalid", onDeviceBindInvalid);
|
uni.$off("device-bind-invalid", onDeviceBindInvalid);
|
||||||
|
if (typeof uni.offNetworkStatusChange === "function") {
|
||||||
|
uni.offNetworkStatusChange(onNetworkStatusChange);
|
||||||
|
}
|
||||||
matchWebsocket.closeMatchWebSocket({
|
matchWebsocket.closeMatchWebSocket({
|
||||||
reason: "app-hide"
|
reason: "app-hide"
|
||||||
});
|
});
|
||||||
|
|||||||
+232
-12
@@ -24,11 +24,20 @@ let isConnecting = false;
|
|||||||
let manualClose = false;
|
let manualClose = false;
|
||||||
let audioAckListenerReady = false;
|
let audioAckListenerReady = false;
|
||||||
let lastReadyRouteKey = "";
|
let lastReadyRouteKey = "";
|
||||||
|
let reconnectTimer = null;
|
||||||
|
let connectTimeoutTimer = null;
|
||||||
|
let livenessTimer = null;
|
||||||
|
let reconnectAttempt = 0;
|
||||||
|
let networkConnected = true;
|
||||||
|
let lastNetworkType = "";
|
||||||
|
|
||||||
// 后端要求非心跳消息必须等前端语音播报完成后再 ACK。
|
// 后端要求非心跳消息必须等前端语音播报完成后再 ACK。
|
||||||
const pendingAcks = [];
|
const pendingAcks = [];
|
||||||
const ACK_AUDIO_TIMEOUT_MS = 9000;
|
const ACK_AUDIO_TIMEOUT_MS = 9000;
|
||||||
const READY_ROUTE_FALLBACK_DELAY_MS = 300;
|
const READY_ROUTE_FALLBACK_DELAY_MS = 300;
|
||||||
|
const CONNECT_TIMEOUT_MS = 8000;
|
||||||
|
const LIVENESS_TIMEOUT_MS = 30000;
|
||||||
|
const RECONNECT_DELAYS_MS = [1000, 2000, 4000, 8000, 15000];
|
||||||
const ROUND_AUDIO_NAMES = ["一", "二", "三", "四", "五"];
|
const ROUND_AUDIO_NAMES = ["一", "二", "三", "四", "五"];
|
||||||
const MATCH_STATUS_HALF_REST = 3;
|
const MATCH_STATUS_HALF_REST = 3;
|
||||||
|
|
||||||
@@ -47,6 +56,7 @@ const BUSINESS_TYPE_BY_SERVER_TYPE = {
|
|||||||
|
|
||||||
const MATCH_READY_SNAPSHOT_PREFIX = "match-ready-snapshot:";
|
const MATCH_READY_SNAPSHOT_PREFIX = "match-ready-snapshot:";
|
||||||
export const MATCH_WS_AUDIO_ACK_EVENT = "match-ws-audio-ack";
|
export const MATCH_WS_AUDIO_ACK_EVENT = "match-ws-audio-ack";
|
||||||
|
export const MATCH_WS_STATE_EVENT = "match-ws-state";
|
||||||
|
|
||||||
function normalizeShootData(shootData) {
|
function normalizeShootData(shootData) {
|
||||||
if (!shootData || typeof shootData !== "object") return shootData;
|
if (!shootData || typeof shootData !== "object") return shootData;
|
||||||
@@ -382,6 +392,166 @@ function normalizeServerUrl(serverAddr, token) {
|
|||||||
return appendQuery(url, "token", token);
|
return appendQuery(url, "token", token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function emitMatchSocketState(state, extra = {}) {
|
||||||
|
uni.$emit(MATCH_WS_STATE_EVENT, {
|
||||||
|
state,
|
||||||
|
matchId: currentContext?.matchId || "",
|
||||||
|
...extra,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearReconnectTimer() {
|
||||||
|
if (!reconnectTimer) return;
|
||||||
|
clearTimeout(reconnectTimer);
|
||||||
|
reconnectTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearConnectTimeout() {
|
||||||
|
if (!connectTimeoutTimer) return;
|
||||||
|
clearTimeout(connectTimeoutTimer);
|
||||||
|
connectTimeoutTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearLivenessTimer() {
|
||||||
|
if (!livenessTimer) return;
|
||||||
|
clearTimeout(livenessTimer);
|
||||||
|
livenessTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSocketHealthTimers() {
|
||||||
|
clearConnectTimeout();
|
||||||
|
clearLivenessTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSocketTask(socketTask, reason) {
|
||||||
|
if (!socketTask) return;
|
||||||
|
try {
|
||||||
|
socketTask.close({ reason });
|
||||||
|
} catch (err) {
|
||||||
|
console.log("[match-ws] close socket task failed", reason, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function detachCurrentSocket(reason) {
|
||||||
|
const currentSocket = socket;
|
||||||
|
socket = null;
|
||||||
|
isConnecting = false;
|
||||||
|
clearSocketHealthTimers();
|
||||||
|
closeSocketTask(currentSocket, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleReconnect(reason, { immediate = false } = {}) {
|
||||||
|
if (
|
||||||
|
manualClose ||
|
||||||
|
!currentContext ||
|
||||||
|
!networkConnected ||
|
||||||
|
reconnectTimer ||
|
||||||
|
socket ||
|
||||||
|
isConnecting
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const delay = immediate
|
||||||
|
? 0
|
||||||
|
: RECONNECT_DELAYS_MS[
|
||||||
|
Math.min(reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)
|
||||||
|
];
|
||||||
|
const attempt = reconnectAttempt + 1;
|
||||||
|
reconnectAttempt = attempt;
|
||||||
|
emitMatchSocketState("reconnecting", { reason, delay, attempt });
|
||||||
|
|
||||||
|
reconnectTimer = setTimeout(() => {
|
||||||
|
reconnectTimer = null;
|
||||||
|
if (manualClose || !currentContext || !networkConnected) return;
|
||||||
|
connectMatchWebSocket({
|
||||||
|
...currentContext,
|
||||||
|
force: true,
|
||||||
|
reconnecting: true,
|
||||||
|
reconnectReason: reason,
|
||||||
|
});
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUnexpectedDisconnect(
|
||||||
|
socketTask,
|
||||||
|
reason,
|
||||||
|
detail,
|
||||||
|
{ closeTask = true } = {}
|
||||||
|
) {
|
||||||
|
if (socket !== socketTask) return;
|
||||||
|
|
||||||
|
socket = null;
|
||||||
|
isConnecting = false;
|
||||||
|
clearSocketHealthTimers();
|
||||||
|
if (closeTask) closeSocketTask(socketTask, reason);
|
||||||
|
|
||||||
|
console.log("[match-ws] unexpected disconnect", {
|
||||||
|
reason,
|
||||||
|
detail,
|
||||||
|
context: currentContext,
|
||||||
|
});
|
||||||
|
emitMatchSocketState(networkConnected ? "disconnected" : "offline", {
|
||||||
|
reason,
|
||||||
|
});
|
||||||
|
scheduleReconnect(reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startConnectTimeout(socketTask) {
|
||||||
|
clearConnectTimeout();
|
||||||
|
connectTimeoutTimer = setTimeout(() => {
|
||||||
|
handleUnexpectedDisconnect(socketTask, "connect-timeout");
|
||||||
|
}, CONNECT_TIMEOUT_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetLivenessTimer(socketTask) {
|
||||||
|
clearLivenessTimer();
|
||||||
|
livenessTimer = setTimeout(() => {
|
||||||
|
handleUnexpectedDisconnect(socketTask, "heartbeat-timeout");
|
||||||
|
}, LIVENESS_TIMEOUT_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function forceReconnectMatchWebSocket(reason = "force-reconnect") {
|
||||||
|
if (manualClose || !currentContext) return false;
|
||||||
|
|
||||||
|
clearReconnectTimer();
|
||||||
|
detachCurrentSocket(reason);
|
||||||
|
if (!networkConnected) {
|
||||||
|
emitMatchSocketState("offline", { reason });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
reconnectAttempt = 0;
|
||||||
|
scheduleReconnect(reason, { immediate: true });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleMatchNetworkStatusChange(status = {}) {
|
||||||
|
const previousConnected = networkConnected;
|
||||||
|
const networkType = String(status.networkType || "");
|
||||||
|
const nextConnected =
|
||||||
|
status.isConnected !== false && networkType.toLowerCase() !== "none";
|
||||||
|
const networkTypeChanged =
|
||||||
|
!!lastNetworkType && !!networkType && lastNetworkType !== networkType;
|
||||||
|
|
||||||
|
networkConnected = nextConnected;
|
||||||
|
if (networkType) lastNetworkType = networkType;
|
||||||
|
if (manualClose || !currentContext) return;
|
||||||
|
|
||||||
|
if (!nextConnected) {
|
||||||
|
clearReconnectTimer();
|
||||||
|
detachCurrentSocket("network-offline");
|
||||||
|
emitMatchSocketState("offline", { reason: "network-offline" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!previousConnected || networkTypeChanged) {
|
||||||
|
forceReconnectMatchWebSocket(
|
||||||
|
networkTypeChanged ? "network-type-changed" : "network-recovered"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 统一发送二进制 protobuf 消息,并在日志里保留发送类型。
|
// 统一发送二进制 protobuf 消息,并在日志里保留发送类型。
|
||||||
function sendBuffer(buffer, label, clientMessage) {
|
function sendBuffer(buffer, label, clientMessage) {
|
||||||
if (!socket || !buffer) return;
|
if (!socket || !buffer) return;
|
||||||
@@ -395,6 +565,7 @@ function sendBuffer(buffer, label, clientMessage) {
|
|||||||
fail: (err) => {
|
fail: (err) => {
|
||||||
if (socket !== currentSocket) return;
|
if (socket !== currentSocket) return;
|
||||||
console.log("比赛服 WebSocket 消息发送失败", label, clientMessage, err);
|
console.log("比赛服 WebSocket 消息发送失败", label, clientMessage, err);
|
||||||
|
handleUnexpectedDisconnect(currentSocket, "send-failed", err);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -582,7 +753,16 @@ function sendLeave() {
|
|||||||
|
|
||||||
export function connectMatchWebSocket(options = {}) {
|
export function connectMatchWebSocket(options = {}) {
|
||||||
// 入口参数来自原 websocket 的比赛服地址通知,不影响原有 websocket 连接。
|
// 入口参数来自原 websocket 的比赛服地址通知,不影响原有 websocket 连接。
|
||||||
const { serverAddr, matchId, userId, token, mode } = options;
|
const {
|
||||||
|
serverAddr,
|
||||||
|
matchId,
|
||||||
|
userId,
|
||||||
|
token,
|
||||||
|
mode,
|
||||||
|
force = false,
|
||||||
|
reconnecting = false,
|
||||||
|
reconnectReason = "",
|
||||||
|
} = options;
|
||||||
const url = normalizeServerUrl(serverAddr, token);
|
const url = normalizeServerUrl(serverAddr, token);
|
||||||
const normalizedMatchId = normalizeId(matchId);
|
const normalizedMatchId = normalizeId(matchId);
|
||||||
const normalizedUserId = normalizeId(userId);
|
const normalizedUserId = normalizeId(userId);
|
||||||
@@ -593,11 +773,10 @@ export function connectMatchWebSocket(options = {}) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
const isSameContext =
|
||||||
socket &&
|
|
||||||
currentContext?.url === url &&
|
currentContext?.url === url &&
|
||||||
currentContext?.matchId === normalizedMatchId
|
currentContext?.matchId === normalizedMatchId;
|
||||||
) {
|
if (!force && socket && isSameContext) {
|
||||||
// 同一场比赛同一地址重复通知时不重复建连。
|
// 同一场比赛同一地址重复通知时不重复建连。
|
||||||
console.log("[match-ws] already connected or connecting", {
|
console.log("[match-ws] already connected or connecting", {
|
||||||
matchId: normalizedMatchId,
|
matchId: normalizedMatchId,
|
||||||
@@ -606,22 +785,38 @@ export function connectMatchWebSocket(options = {}) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换比赛服地址时先关闭旧连接,不发送 LEAVE,避免误通知旧比赛离场。
|
if (force && isSameContext) {
|
||||||
closeMatchWebSocket({ sendLeave: false, reason: "switch" });
|
// 网络切换或假死重连时只销毁旧链路,保留比赛上下文且不发送 LEAVE。
|
||||||
|
detachCurrentSocket(reconnectReason || "force-reconnect");
|
||||||
|
} else if (!isSameContext) {
|
||||||
|
// 切换比赛服地址时先关闭旧连接,不发送 LEAVE,避免误通知旧比赛离场。
|
||||||
|
closeMatchWebSocket({ sendLeave: false, reason: "switch" });
|
||||||
|
}
|
||||||
|
|
||||||
|
clearReconnectTimer();
|
||||||
manualClose = false;
|
manualClose = false;
|
||||||
isConnecting = true;
|
isConnecting = true;
|
||||||
|
if (!isSameContext) reconnectAttempt = 0;
|
||||||
|
const connectedOnce = isSameContext && currentContext?.connectedOnce === true;
|
||||||
currentContext = {
|
currentContext = {
|
||||||
serverAddr,
|
serverAddr,
|
||||||
matchId: normalizedMatchId,
|
matchId: normalizedMatchId,
|
||||||
userId: normalizedUserId,
|
userId: normalizedUserId,
|
||||||
|
token,
|
||||||
url,
|
url,
|
||||||
mode: Number.isFinite(normalizedMode) ? normalizedMode : undefined,
|
mode: Number.isFinite(normalizedMode) ? normalizedMode : undefined,
|
||||||
isMelee:
|
isMelee:
|
||||||
Number.isFinite(normalizedMode) ? normalizedMode > 3 : undefined,
|
Number.isFinite(normalizedMode) ? normalizedMode > 3 : undefined,
|
||||||
meleeHalfRest: false,
|
meleeHalfRest: isSameContext
|
||||||
|
? currentContext?.meleeHalfRest === true
|
||||||
|
: false,
|
||||||
|
connectedOnce,
|
||||||
};
|
};
|
||||||
ensureAudioAckListener();
|
ensureAudioAckListener();
|
||||||
|
emitMatchSocketState(reconnecting ? "reconnecting" : "connecting", {
|
||||||
|
reason: reconnectReason,
|
||||||
|
attempt: reconnectAttempt,
|
||||||
|
});
|
||||||
|
|
||||||
const socketTask = uni.connectSocket({
|
const socketTask = uni.connectSocket({
|
||||||
url,
|
url,
|
||||||
@@ -636,40 +831,60 @@ export function connectMatchWebSocket(options = {}) {
|
|||||||
socket = null;
|
socket = null;
|
||||||
isConnecting = false;
|
isConnecting = false;
|
||||||
console.log("[match-ws] connect failed", err);
|
console.log("[match-ws] connect failed", err);
|
||||||
|
clearSocketHealthTimers();
|
||||||
|
emitMatchSocketState(networkConnected ? "disconnected" : "offline", {
|
||||||
|
reason: "connect-failed",
|
||||||
|
});
|
||||||
|
scheduleReconnect("connect-failed");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
socket = socketTask;
|
socket = socketTask;
|
||||||
|
startConnectTimeout(socketTask);
|
||||||
|
|
||||||
socketTask.onOpen(() => {
|
socketTask.onOpen(() => {
|
||||||
if (socket !== socketTask) return;
|
if (socket !== socketTask) return;
|
||||||
|
clearConnectTimeout();
|
||||||
isConnecting = false;
|
isConnecting = false;
|
||||||
|
const wasReconnected =
|
||||||
|
currentContext?.connectedOnce === true || reconnecting || reconnectAttempt > 0;
|
||||||
|
if (currentContext) currentContext.connectedOnce = true;
|
||||||
|
reconnectAttempt = 0;
|
||||||
|
resetLivenessTimer(socketTask);
|
||||||
console.log("[match-ws] connected", {
|
console.log("[match-ws] connected", {
|
||||||
matchId: normalizedMatchId,
|
matchId: normalizedMatchId,
|
||||||
url,
|
url,
|
||||||
|
reconnected: wasReconnected,
|
||||||
|
});
|
||||||
|
emitMatchSocketState("open", {
|
||||||
|
reason: reconnectReason,
|
||||||
|
reconnected: wasReconnected,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
socketTask.onMessage((res) => {
|
socketTask.onMessage((res) => {
|
||||||
if (socket !== socketTask) return;
|
if (socket !== socketTask) return;
|
||||||
|
resetLivenessTimer(socketTask);
|
||||||
handleMessage(res.data);
|
handleMessage(res.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
socketTask.onError((err) => {
|
socketTask.onError((err) => {
|
||||||
if (socket !== socketTask) return;
|
if (socket !== socketTask) return;
|
||||||
console.log("[match-ws] socket error", err);
|
console.log("[match-ws] socket error", err);
|
||||||
|
handleUnexpectedDisconnect(socketTask, "socket-error", err);
|
||||||
});
|
});
|
||||||
|
|
||||||
socketTask.onClose((result) => {
|
socketTask.onClose((result) => {
|
||||||
if (socket !== socketTask) return;
|
if (socket !== socketTask) return;
|
||||||
socket = null;
|
|
||||||
isConnecting = false;
|
|
||||||
console.log("[match-ws] closed", {
|
console.log("[match-ws] closed", {
|
||||||
manualClose,
|
manualClose,
|
||||||
result,
|
result,
|
||||||
context: currentContext,
|
context: currentContext,
|
||||||
});
|
});
|
||||||
if (!manualClose) removeAudioAckListener();
|
if (manualClose) return;
|
||||||
|
handleUnexpectedDisconnect(socketTask, "socket-close", result, {
|
||||||
|
closeTask: false,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -690,6 +905,9 @@ export function closeMatchWebSocket(options = {}) {
|
|||||||
const { sendLeave: shouldSendLeave = true, reason = "manual" } = options;
|
const { sendLeave: shouldSendLeave = true, reason = "manual" } = options;
|
||||||
|
|
||||||
manualClose = true;
|
manualClose = true;
|
||||||
|
clearReconnectTimer();
|
||||||
|
clearSocketHealthTimers();
|
||||||
|
reconnectAttempt = 0;
|
||||||
pendingAcks.forEach((task) => {
|
pendingAcks.forEach((task) => {
|
||||||
if (task?.timer) clearTimeout(task.timer);
|
if (task?.timer) clearTimeout(task.timer);
|
||||||
});
|
});
|
||||||
@@ -700,7 +918,7 @@ export function closeMatchWebSocket(options = {}) {
|
|||||||
if (shouldSendLeave) sendLeave();
|
if (shouldSendLeave) sendLeave();
|
||||||
socket = null;
|
socket = null;
|
||||||
try {
|
try {
|
||||||
currentSocket.close();
|
currentSocket.close({ reason });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("[match-ws] close failed", err);
|
console.log("[match-ws] close failed", err);
|
||||||
}
|
}
|
||||||
@@ -718,4 +936,6 @@ export default {
|
|||||||
connectMatchWebSocket,
|
connectMatchWebSocket,
|
||||||
connectMatchWebSocketFromNotice,
|
connectMatchWebSocketFromNotice,
|
||||||
closeMatchWebSocket,
|
closeMatchWebSocket,
|
||||||
|
forceReconnectMatchWebSocket,
|
||||||
|
handleMatchNetworkStatusChange,
|
||||||
};
|
};
|
||||||
|
|||||||
+74
-26
@@ -17,6 +17,7 @@ import {
|
|||||||
closeMatchWebSocket,
|
closeMatchWebSocket,
|
||||||
connectMatchWebSocket,
|
connectMatchWebSocket,
|
||||||
MATCH_WS_AUDIO_ACK_EVENT,
|
MATCH_WS_AUDIO_ACK_EVENT,
|
||||||
|
MATCH_WS_STATE_EVENT,
|
||||||
} from "@/matchWebsocket";
|
} from "@/matchWebsocket";
|
||||||
import { MESSAGETYPESV2 } from "@/constants";
|
import { MESSAGETYPESV2 } from "@/constants";
|
||||||
import { takeMatchReturnSnapshot } from "@/utils/matchReturn";
|
import { takeMatchReturnSnapshot } from "@/utils/matchReturn";
|
||||||
@@ -55,6 +56,7 @@ const showOfflineModal = ref(false);
|
|||||||
const xRingStreaks = ref({});
|
const xRingStreaks = ref({});
|
||||||
let battleEnded = false;
|
let battleEnded = false;
|
||||||
let skipNextRestoreOnShow = false;
|
let skipNextRestoreOnShow = false;
|
||||||
|
let restoreGeneration = 0;
|
||||||
|
|
||||||
function clearHalfRestCountdown() {
|
function clearHalfRestCountdown() {
|
||||||
if (halfRestTimer) {
|
if (halfRestTimer) {
|
||||||
@@ -78,13 +80,24 @@ function getHalfRestSeconds(battleInfo) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const endTime = Number(battleInfo?.halfRestEndTime ?? battleInfo?.restEndTime);
|
const timeoutTime = normalizeTimestamp(battleInfo?.timeoutTime);
|
||||||
if (!Number.isFinite(endTime) || endTime <= 0) return HALF_REST_SECONDS;
|
if (timeoutTime) {
|
||||||
|
const elapsedSeconds = Math.max(0, (Date.now() - timeoutTime) / 1000);
|
||||||
|
return Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(HALF_REST_SECONDS, Math.ceil(HALF_REST_SECONDS - elapsedSeconds))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const timestamp = endTime < 1e12 ? endTime * 1000 : endTime;
|
const endTime = normalizeTimestamp(
|
||||||
const diffSeconds = (timestamp - Date.now()) / 1000;
|
battleInfo?.halfRestEndTime ?? battleInfo?.restEndTime
|
||||||
if (diffSeconds > 0 && diffSeconds <= HALF_REST_SECONDS) {
|
);
|
||||||
return Math.ceil(diffSeconds);
|
if (endTime) {
|
||||||
|
const diffSeconds = (endTime - Date.now()) / 1000;
|
||||||
|
return Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(HALF_REST_SECONDS, Math.ceil(diffSeconds))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return HALF_REST_SECONDS;
|
return HALF_REST_SECONDS;
|
||||||
@@ -92,7 +105,14 @@ function getHalfRestSeconds(battleInfo) {
|
|||||||
|
|
||||||
function startHalfRestCountdown(seconds = HALF_REST_SECONDS) {
|
function startHalfRestCountdown(seconds = HALF_REST_SECONDS) {
|
||||||
clearHalfRestCountdown();
|
clearHalfRestCountdown();
|
||||||
halfRestRemain.value = Math.max(0, Math.ceil(Number(seconds) || HALF_REST_SECONDS));
|
const secondsValue = Number(seconds);
|
||||||
|
const normalizedSeconds = Number.isFinite(secondsValue)
|
||||||
|
? secondsValue
|
||||||
|
: HALF_REST_SECONDS;
|
||||||
|
halfRestRemain.value = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(HALF_REST_SECONDS, Math.ceil(normalizedSeconds))
|
||||||
|
);
|
||||||
|
|
||||||
if (halfRestRemain.value <= 0) return;
|
if (halfRestRemain.value <= 0) return;
|
||||||
|
|
||||||
@@ -305,6 +325,47 @@ function recoverData(battleInfo, { force = false } = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function restoreLatestBattle() {
|
||||||
|
if (!battleId.value) return;
|
||||||
|
|
||||||
|
const currentRestoreId = ++restoreGeneration;
|
||||||
|
let result = null;
|
||||||
|
try {
|
||||||
|
result = normalizeBattleInfo(await getBattleAPI(battleId.value));
|
||||||
|
} catch (err) {
|
||||||
|
console.log("restore latest melee battle failed:", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentRestoreId !== restoreGeneration || !result) return;
|
||||||
|
|
||||||
|
if (result.status === 2) {
|
||||||
|
battleEnded = true;
|
||||||
|
uni.showToast({
|
||||||
|
title: "比赛已结束",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
uni.navigateBack({
|
||||||
|
delta: 2,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reconnectMatchServer(result);
|
||||||
|
recoverData(result, { force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMatchSocketState(event) {
|
||||||
|
if (event?.state !== "open" || event?.reconnected !== true) return;
|
||||||
|
if (
|
||||||
|
event.matchId &&
|
||||||
|
battleId.value &&
|
||||||
|
String(event.matchId) !== String(battleId.value)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
restoreLatestBattle();
|
||||||
|
}
|
||||||
|
|
||||||
onLoad(async (options) => {
|
onLoad(async (options) => {
|
||||||
const returnSnapshot = options.fromReturn ? takeMatchReturnSnapshot() : null;
|
const returnSnapshot = options.fromReturn ? takeMatchReturnSnapshot() : null;
|
||||||
skipNextRestoreOnShow = false;
|
skipNextRestoreOnShow = false;
|
||||||
@@ -392,7 +453,7 @@ async function onReceiveMessage(msg) {
|
|||||||
halfTimeTip.value = true;
|
halfTimeTip.value = true;
|
||||||
halfRest.value = true;
|
halfRest.value = true;
|
||||||
tips.value = "准备下半场";
|
tips.value = "准备下半场";
|
||||||
startHalfRestCountdown();
|
startHalfRestCountdown(getHalfRestSeconds(msg));
|
||||||
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
||||||
battleEnded = true;
|
battleEnded = true;
|
||||||
notifyMatchAudioAck(msg);
|
notifyMatchAudioAck(msg);
|
||||||
@@ -409,14 +470,17 @@ onMounted(async () => {
|
|||||||
keepScreenOn: true,
|
keepScreenOn: true,
|
||||||
});
|
});
|
||||||
uni.$on("socket-inbox", onReceiveMessage);
|
uni.$on("socket-inbox", onReceiveMessage);
|
||||||
|
uni.$on(MATCH_WS_STATE_EVENT, onMatchSocketState);
|
||||||
await laserCloseAPI();
|
await laserCloseAPI();
|
||||||
});
|
});
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
uni.setKeepScreenOn({
|
uni.setKeepScreenOn({
|
||||||
keepScreenOn: false,
|
keepScreenOn: false,
|
||||||
});
|
});
|
||||||
|
restoreGeneration += 1;
|
||||||
clearHalfRestCountdown();
|
clearHalfRestCountdown();
|
||||||
uni.$off("socket-inbox", onReceiveMessage);
|
uni.$off("socket-inbox", onReceiveMessage);
|
||||||
|
uni.$off(MATCH_WS_STATE_EVENT, onMatchSocketState);
|
||||||
closeBattleServer("melee-battle-unmount");
|
closeBattleServer("melee-battle-unmount");
|
||||||
audioManager.stopAll();
|
audioManager.stopAll();
|
||||||
});
|
});
|
||||||
@@ -425,28 +489,12 @@ onHide(() => {
|
|||||||
closeBattleServer("melee-battle-hide");
|
closeBattleServer("melee-battle-hide");
|
||||||
});
|
});
|
||||||
|
|
||||||
onShow(async () => {
|
onShow(() => {
|
||||||
if (skipNextRestoreOnShow) {
|
if (skipNextRestoreOnShow) {
|
||||||
skipNextRestoreOnShow = false;
|
skipNextRestoreOnShow = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (battleId.value) {
|
restoreLatestBattle();
|
||||||
const result = normalizeBattleInfo(await getBattleAPI(battleId.value));
|
|
||||||
if (!result) return;
|
|
||||||
if (result.status === 2) {
|
|
||||||
battleEnded = true;
|
|
||||||
uni.showToast({
|
|
||||||
title: "比赛已结束",
|
|
||||||
icon: "none",
|
|
||||||
});
|
|
||||||
uni.navigateBack({
|
|
||||||
delta: 2,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
reconnectMatchServer(result);
|
|
||||||
recoverData(result, { force: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
closeMatchWebSocket,
|
closeMatchWebSocket,
|
||||||
connectMatchWebSocket,
|
connectMatchWebSocket,
|
||||||
MATCH_WS_AUDIO_ACK_EVENT,
|
MATCH_WS_AUDIO_ACK_EVENT,
|
||||||
|
MATCH_WS_STATE_EVENT,
|
||||||
} from "@/matchWebsocket";
|
} from "@/matchWebsocket";
|
||||||
import { MESSAGETYPESV2 } from "@/constants";
|
import { MESSAGETYPESV2 } from "@/constants";
|
||||||
import { getDirectionText } from "@/util";
|
import { getDirectionText } from "@/util";
|
||||||
@@ -1227,6 +1228,18 @@ function scheduleRestoreLatestBattle() {
|
|||||||
}, RESTORE_EMPTY_ID_RETRY_DELAY);
|
}, RESTORE_EMPTY_ID_RETRY_DELAY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onMatchSocketState(event) {
|
||||||
|
if (event?.state !== "open" || event?.reconnected !== true) return;
|
||||||
|
if (
|
||||||
|
event.matchId &&
|
||||||
|
battleId.value &&
|
||||||
|
String(event.matchId) !== String(battleId.value)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scheduleRestoreLatestBattle();
|
||||||
|
}
|
||||||
|
|
||||||
// 页面统一的 socket 回调,只负责把消息送进战况队列。
|
// 页面统一的 socket 回调,只负责把消息送进战况队列。
|
||||||
function onReceiveMessage(message) {
|
function onReceiveMessage(message) {
|
||||||
enqueueBattleMessage(message);
|
enqueueBattleMessage(message);
|
||||||
@@ -1302,6 +1315,7 @@ onMounted(async () => {
|
|||||||
uni.$on(AUDIO_INTERRUPTION_END_EVENT, handleBattleRecovered);
|
uni.$on(AUDIO_INTERRUPTION_END_EVENT, handleBattleRecovered);
|
||||||
uni.$on(PROGRESS_ZERO_EVENT, onProgressZero);
|
uni.$on(PROGRESS_ZERO_EVENT, onProgressZero);
|
||||||
uni.$on(COUNTDOWN_READY_EVENT, hideRestoreLoading);
|
uni.$on(COUNTDOWN_READY_EVENT, hideRestoreLoading);
|
||||||
|
uni.$on(MATCH_WS_STATE_EVENT, onMatchSocketState);
|
||||||
await laserCloseAPI();
|
await laserCloseAPI();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1314,6 +1328,7 @@ onBeforeUnmount(() => {
|
|||||||
uni.$off("audioEnded", onAudioEnded);
|
uni.$off("audioEnded", onAudioEnded);
|
||||||
uni.$off(PROGRESS_ZERO_EVENT, onProgressZero);
|
uni.$off(PROGRESS_ZERO_EVENT, onProgressZero);
|
||||||
uni.$off(COUNTDOWN_READY_EVENT, hideRestoreLoading);
|
uni.$off(COUNTDOWN_READY_EVENT, hideRestoreLoading);
|
||||||
|
uni.$off(MATCH_WS_STATE_EVENT, onMatchSocketState);
|
||||||
if (pendingRestoreTimer) {
|
if (pendingRestoreTimer) {
|
||||||
clearTimeout(pendingRestoreTimer);
|
clearTimeout(pendingRestoreTimer);
|
||||||
pendingRestoreTimer = null;
|
pendingRestoreTimer = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user