gate net messages by stage so we don't accidentally use old data

This commit is contained in:
Brandon Johnson
2026-02-02 17:12:23 -05:00
parent c33b252b2d
commit b630e9df33
2 changed files with 50 additions and 14 deletions

View File

@@ -544,8 +544,10 @@ const game = new Game({
if (config) {
netplayState.currentCourse = config;
netplayState.currentGameSource = activeGameSource;
netplayState.stageSeq += 1;
hostRelay.broadcast({
type: 'start',
stageSeq: netplayState.stageSeq,
gameSource: activeGameSource,
course: config,
stageBasePath: getStageBasePath(activeGameSource),
@@ -627,7 +629,13 @@ let lobbySignal: { send: (msg: any) => void; close: () => void } | null = null;
let hostRelay: HostRelay | null = null;
let clientPeer: ClientPeer | null = null;
let netplayEnabled = false;
let pendingSnapshot: { frame: number; state: any; stageId?: number; gameSource?: GameSource } | null = null;
let pendingSnapshot: {
frame: number;
state: any;
stageId?: number;
gameSource?: GameSource;
stageSeq?: number;
} | null = null;
let lobbyHeartbeatTimer: number | null = null;
let netplayAccumulator = 0;
const NETPLAY_MAX_FRAME_DELTA = 5;
@@ -762,8 +770,7 @@ type NetplayState = {
readyPlayers: Set<number>;
awaitingStageReady: boolean;
awaitingStageSync: boolean;
introEndFrame: number | null;
introSyncPending: boolean;
stageSeq: number;
currentCourse: any | null;
currentGameSource: GameSource | null;
awaitingSnapshot: boolean;
@@ -816,6 +823,7 @@ function ensureNetplayState(role: NetplayRole) {
readyPlayers: new Set(),
awaitingStageReady: false,
awaitingStageSync: false,
stageSeq: 0,
currentCourse: null,
currentGameSource: null,
awaitingSnapshot: false,
@@ -850,6 +858,7 @@ function resetNetplayForStage() {
netplayState.lastAckedLocalFrame = -1;
netplayState.lastReceivedHostFrame = game.simTick;
netplayState.hostFrameBuffer.clear();
pendingSnapshot = null;
netplayState.readyPlayers.clear();
netplayState.awaitingStageReady = false;
netplayState.awaitingStageSync = false;
@@ -883,6 +892,7 @@ function maybeSendStageSync() {
netplayAccumulator = 0;
hostRelay.broadcast({
type: 'stage_sync',
stageSeq: state.stageSeq,
stageId: state.currentStageId ?? game.stage?.stageId ?? 0,
frame: state.session.getFrame(),
});
@@ -919,7 +929,7 @@ function markStageReady(stageId: number) {
return;
}
if (clientPeer) {
clientPeer.send({ type: 'stage_ready', stageId });
clientPeer.send({ type: 'stage_ready', stageSeq: state.stageSeq, stageId });
}
}
@@ -1141,6 +1151,7 @@ function rollbackAndResim(startFrame: number) {
}
state.hostFrameBuffer.set(frame, {
type: 'frame',
stageSeq: state.stageSeq,
frame,
inputs: bundleInputs,
});
@@ -1187,6 +1198,10 @@ function tryApplyPendingSnapshot(stageId: number) {
if (!pendingSnapshot) {
return;
}
if (netplayState && pendingSnapshot.stageSeq !== undefined && pendingSnapshot.stageSeq !== netplayState.stageSeq) {
pendingSnapshot = null;
return;
}
if (pendingSnapshot.stageId !== undefined && pendingSnapshot.stageId !== stageId) {
return;
}
@@ -1538,6 +1553,7 @@ function requestSnapshot(reason: 'mismatch' | 'lag', frame?: number, force = fal
const targetFrame = frame ?? netplayState.session.getFrame();
clientPeer.send({
type: 'snapshot_request',
stageSeq: netplayState.stageSeq,
frame: targetFrame,
reason,
});
@@ -1592,6 +1608,10 @@ function handleHostMessage(msg: HostToClientMessage) {
}
return;
}
const msgStageSeq = (msg as { stageSeq?: number }).stageSeq;
if (msgStageSeq !== undefined && msg.type !== 'start' && msgStageSeq !== state.stageSeq) {
return;
}
if (msg.type === 'stage_sync') {
if (state.currentStageId === null) {
state.currentStageId = msg.stageId;
@@ -1684,14 +1704,19 @@ function handleHostMessage(msg: HostToClientMessage) {
return;
}
if (msg.type === 'start') {
if (netplayState) {
netplayState.stageSeq = msg.stageSeq;
netplayState.currentCourse = msg.course;
netplayState.currentGameSource = msg.gameSource;
netplayState.awaitingSnapshot = false;
netplayState.expectedHashes.clear();
netplayState.hashHistory.clear();
}
pendingSnapshot = null;
activeGameSource = msg.gameSource;
game.setGameSource(activeGameSource);
game.stageBasePath = msg.stageBasePath ?? getStageBasePath(activeGameSource);
currentSmb2LikeMode = activeGameSource !== GAME_SOURCES.SMB1 && msg.course?.mode ? msg.course.mode : null;
if (netplayState) {
netplayState.currentCourse = msg.course;
netplayState.currentGameSource = msg.gameSource;
}
void startStage(msg.course);
}
}
@@ -1701,6 +1726,10 @@ function handleClientMessage(playerId: number, msg: ClientToHostMessage) {
if (!state) {
return;
}
const msgStageSeq = (msg as { stageSeq?: number }).stageSeq;
if (msgStageSeq !== undefined && msgStageSeq !== state.stageSeq) {
return;
}
let clientState = state.clientStates.get(playerId);
if (!clientState) {
clientState = { lastAckedHostFrame: -1, lastAckedClientInput: -1 };
@@ -1774,6 +1803,7 @@ function sendSnapshotToClient(playerId: number, frame?: number) {
}
hostRelay.sendTo(playerId, {
type: 'snapshot',
stageSeq: netplayState.stageSeq,
frame: snapshotFrame,
state: snapshotState,
stageId: game.stage?.stageId,
@@ -1832,6 +1862,7 @@ function clientSendInputBuffer(currentFrame: number) {
}
clientPeer.send({
type: 'input',
stageSeq: netplayState.stageSeq,
frame,
playerId: game.localPlayerId,
input,
@@ -1841,6 +1872,7 @@ function clientSendInputBuffer(currentFrame: number) {
if (start > end) {
clientPeer.send({
type: 'ack',
stageSeq: netplayState.stageSeq,
playerId: game.localPlayerId,
frame: netplayState.lastReceivedHostFrame,
});
@@ -1902,6 +1934,7 @@ function netplayStep() {
}
const bundle: FrameBundleMessage = {
type: 'frame',
stageSeq: state.stageSeq,
frame,
inputs: bundleInputs,
};
@@ -2198,6 +2231,7 @@ function startHost(room: LobbyRoom) {
if (state.currentCourse && state.currentGameSource) {
hostRelay?.sendTo(playerId, {
type: 'start',
stageSeq: state.stageSeq,
gameSource: state.currentGameSource,
course: state.currentCourse,
stageBasePath: getStageBasePath(state.currentGameSource),
@@ -2940,12 +2974,6 @@ startButton.addEventListener('click', () => {
if (netplayEnabled && netplayState?.role === 'host') {
netplayState.currentCourse = difficulty;
netplayState.currentGameSource = activeGameSource;
hostRelay?.broadcast({
type: 'start',
gameSource: activeGameSource,
course: difficulty,
stageBasePath: getStageBasePath(activeGameSource),
});
}
startStage(difficulty).catch((error) => {
if (hudStatus) {

View File

@@ -19,6 +19,7 @@ export type RoomInfo = {
export type InputFrameMessage = {
type: 'input';
stageSeq: number;
frame: number;
playerId: PlayerId;
input: QuantizedInput;
@@ -27,6 +28,7 @@ export type InputFrameMessage = {
export type InputAckMessage = {
type: 'ack';
stageSeq: number;
playerId: PlayerId;
frame: number;
};
@@ -43,17 +45,20 @@ export type PongMessage = {
export type StageReadyMessage = {
type: 'stage_ready';
stageSeq: number;
stageId: number;
};
export type StageSyncMessage = {
type: 'stage_sync';
stageSeq: number;
stageId: number;
frame: number;
};
export type FrameBundleMessage = {
type: 'frame';
stageSeq: number;
frame: number;
inputs: Record<number, QuantizedInput>;
lastAck?: number;
@@ -63,6 +68,7 @@ export type FrameBundleMessage = {
export type SnapshotMessage = {
type: 'snapshot';
stageSeq: number;
frame: number;
state: any;
stageId?: number;
@@ -71,6 +77,7 @@ export type SnapshotMessage = {
export type SnapshotRequestMessage = {
type: 'snapshot_request';
stageSeq: number;
frame: number;
reason: 'mismatch' | 'lag';
};
@@ -94,6 +101,7 @@ export type RoomUpdateMessage = {
export type StartMatchMessage = {
type: 'start';
stageSeq: number;
gameSource: GameSource;
course: any;
stageBasePath?: string;