fix: 修复客户端中继协议 + 全平台房间详情/分享

关键修复:
- RelayClient: 二进制头部改为FUNMC_JOIN:roomId|playerName|password协议
- RelayClient: 等待服务端OK:CONNECTED/ERROR:*握手响应
- rooms:join: 先连接中继再启动本地代理, 传入playerName和password
- 连接失败自动cleanup

Web管理面板:
- 房间详情弹窗: 点击房间卡片打开
- 玩家列表 + 踢出功能 (UserX图标)
- 复制房间号 / 删除房间按钮

Mobile:
- 房间详情底部弹窗 (Modal slide)
- 在线玩家列表
- 分享房间号 (Share API)
- 复制房间号
- apiClient.getRoomDetail 方法
This commit is contained in:
FunMC
2026-02-23 08:26:25 +08:00
parent 80fe5e6e6e
commit eb6e901440
5 changed files with 233 additions and 24 deletions

View File

@@ -147,18 +147,13 @@ ipcMain.handle('rooms:delete', async (_event, roomId: string) => {
});
// Join room - start local proxy and relay connection
ipcMain.handle('rooms:join', async (_event, opts: { serverHost: string; serverPort: number; roomId: string; localPort: number }) => {
ipcMain.handle('rooms:join', async (_event, opts: { serverHost: string; serverPort: number; roomId: string; localPort: number; password?: string }) => {
try {
cleanup();
relayClient = new RelayClient(opts.serverHost, opts.serverPort, opts.roomId);
localProxy = new LocalProxy(opts.localPort, relayClient);
const playerName = (store.get('playerName') as string) || 'Player';
relayClient = new RelayClient(opts.serverHost, opts.serverPort, opts.roomId, playerName, opts.password);
await localProxy.start();
relayClient.on('connected', () => {
mainWindow?.webContents.send('relay:status', { status: 'connected' });
});
relayClient.on('disconnected', () => {
mainWindow?.webContents.send('relay:status', { status: 'disconnected' });
});
@@ -166,8 +161,17 @@ ipcMain.handle('rooms:join', async (_event, opts: { serverHost: string; serverPo
mainWindow?.webContents.send('relay:status', { status: 'error', error: err });
});
// Connect to relay server first (handshake)
await relayClient.connect();
// Then start local proxy for Minecraft to connect to
localProxy = new LocalProxy(opts.localPort, relayClient);
await localProxy.start();
mainWindow?.webContents.send('relay:status', { status: 'connected' });
return { success: true, localPort: opts.localPort };
} catch (err: any) {
cleanup();
return { success: false, error: err.message };
}
});

View File

@@ -9,7 +9,9 @@ export class RelayClient extends EventEmitter {
constructor(
private host: string,
private port: number,
private roomId: string
private roomId: string,
private playerName: string = 'Player',
private password?: string
) {
super();
}
@@ -17,21 +19,35 @@ export class RelayClient extends EventEmitter {
connect(): Promise<void> {
return new Promise((resolve, reject) => {
this.socket = new net.Socket();
let handshakeDone = false;
this.socket.connect(this.port, this.host, () => {
this.connected = true;
// Send room ID as initial handshake
const header = Buffer.alloc(2 + this.roomId.length);
header.writeUInt16BE(this.roomId.length, 0);
header.write(this.roomId, 2);
this.socket!.write(header);
this.emit('connected');
resolve();
// Send FUNMC_JOIN handshake matching server relay protocol
const parts = [this.roomId, this.playerName];
if (this.password) parts.push(this.password);
this.socket!.write(`FUNMC_JOIN:${parts.join('|')}\n`);
});
// Wait for server response before marking as connected
this.socket.once('data', (data) => {
const response = data.toString('utf8').trim();
handshakeDone = true;
if (response.startsWith('OK:')) {
this.connected = true;
this.socket!.setTimeout(0); // clear timeout after handshake
this.emit('connected');
resolve();
} else {
// e.g. ERROR:ROOM_NOT_FOUND, ERROR:WRONG_PASSWORD, ERROR:ROOM_FULL, ERROR:HOST_OFFLINE
const errorMsg = response.replace('ERROR:', '');
this.socket!.destroy();
reject(new Error(errorMsg));
}
});
this.socket.on('error', (err) => {
this.emit('error', err.message);
if (!this.connected) reject(err);
if (!handshakeDone) reject(err);
});
this.socket.on('close', () => {
@@ -40,7 +56,9 @@ export class RelayClient extends EventEmitter {
});
this.socket.setTimeout(10000, () => {
this.socket?.destroy(new Error('Connection timeout'));
if (!handshakeDone) {
this.socket?.destroy(new Error('Connection timeout'));
}
});
});
}