forked from xiangwang25/whale-town-end-v2
feat: integrate invitation access, world NPCs, and deployment
This commit is contained in:
@@ -33,9 +33,11 @@
|
||||
import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||
import * as WebSocket from 'ws';
|
||||
import { ChatService } from '../../business/chat/chat.service';
|
||||
import { WorldNpcService } from '../../business/world_npc/world_npc.service';
|
||||
|
||||
/** WebSocket 服务器默认端口 */
|
||||
const DEFAULT_WEBSOCKET_PORT = 3001;
|
||||
const WEBSOCKET_HEARTBEAT_INTERVAL_MS = 30_000;
|
||||
|
||||
/** 默认地图 ID */
|
||||
const DEFAULT_MAP_ID = 'whale_port';
|
||||
@@ -54,12 +56,18 @@ interface ExtendedWebSocket extends WebSocket {
|
||||
worldReady?: boolean;
|
||||
welcomed?: boolean;
|
||||
messageQueue?: Promise<void>;
|
||||
movementSequence?: number;
|
||||
guest?: boolean;
|
||||
lastNpcInteractionAt?: number;
|
||||
}
|
||||
|
||||
interface MapPositionMessage {
|
||||
mapId: string;
|
||||
x: number;
|
||||
y: number;
|
||||
direction: 'down' | 'up' | 'right' | 'left';
|
||||
movementState: 'idle' | 'walk';
|
||||
sequence?: number;
|
||||
}
|
||||
|
||||
const WELCOME_RECONNECT_GRACE_MS = 15_000;
|
||||
@@ -101,8 +109,14 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
private clients = new Map<string, ExtendedWebSocket>();
|
||||
private mapRooms = new Map<string, Set<string>>();
|
||||
private lastWelcomeAtByUserId = new Map<string, number>();
|
||||
private heartbeatTimer?: NodeJS.Timeout;
|
||||
private npcActionTimer?: NodeJS.Timeout;
|
||||
private npcTickRunning = false;
|
||||
|
||||
constructor(private readonly chatService: ChatService) {}
|
||||
constructor(
|
||||
private readonly chatService: ChatService,
|
||||
private readonly worldNpcService: WorldNpcService,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
const port = process.env.WEBSOCKET_PORT ? parseInt(process.env.WEBSOCKET_PORT) : DEFAULT_WEBSOCKET_PORT;
|
||||
@@ -128,6 +142,9 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
.then(() => this.handleRawMessage(ws, data))
|
||||
.catch((error) => this.logger.error(`消息处理失败: ${ws.id}`, error));
|
||||
});
|
||||
ws.on('pong', () => {
|
||||
ws.isAlive = true;
|
||||
});
|
||||
ws.on('close', (code, reason) => this.handleClose(ws, code, reason));
|
||||
ws.on('error', (error) => this.handleError(ws, error));
|
||||
|
||||
@@ -140,10 +157,22 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
|
||||
// 设置网关引用到业务层
|
||||
this.chatService.setWebSocketGateway(this);
|
||||
this.heartbeatTimer = setInterval(() => this.checkClientHeartbeats(), WEBSOCKET_HEARTBEAT_INTERVAL_MS);
|
||||
this.npcActionTimer = setInterval(() => void this.tickNpcActions(), 1_000);
|
||||
this.heartbeatTimer.unref();
|
||||
this.npcActionTimer.unref();
|
||||
this.logger.log(`WebSocket服务器启动成功,端口: ${port},路径: /game`);
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
if (this.heartbeatTimer) {
|
||||
clearInterval(this.heartbeatTimer);
|
||||
this.heartbeatTimer = undefined;
|
||||
}
|
||||
if (this.npcActionTimer) {
|
||||
clearInterval(this.npcActionTimer);
|
||||
this.npcActionTimer = undefined;
|
||||
}
|
||||
if (this.server) {
|
||||
this.server.close();
|
||||
this.logger.log('WebSocket服务器已关闭');
|
||||
@@ -174,12 +203,26 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
*/
|
||||
private async routeMessage(ws: ExtendedWebSocket, message: any) {
|
||||
const messageType = message.type || message.t;
|
||||
this.logger.log(`收到消息: ${ws.id}, 类型: ${messageType}`);
|
||||
if (messageType !== 'position' && messageType !== 'ping') {
|
||||
this.logger.log(`收到消息: ${ws.id}, 类型: ${messageType}`);
|
||||
}
|
||||
|
||||
if (ws.guest && !['ping', 'world_ready', 'logout', 'npc_session_end'].includes(messageType)) {
|
||||
this.sendMessage(ws, { t: 'error', code: 'GUEST_READ_ONLY', message: '游客模式只能参观' });
|
||||
return;
|
||||
}
|
||||
|
||||
switch (messageType) {
|
||||
case 'ping':
|
||||
ws.isAlive = true;
|
||||
this.sendMessage(ws, { t: 'pong', timestamp: Date.now() });
|
||||
break;
|
||||
case 'login':
|
||||
await this.handleLogin(ws, message);
|
||||
break;
|
||||
case 'guest_login':
|
||||
await this.handleGuestLogin(ws);
|
||||
break;
|
||||
case 'logout':
|
||||
await this.handleLogout(ws);
|
||||
break;
|
||||
@@ -195,6 +238,12 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
case 'world_ready':
|
||||
await this.handleWorldReady(ws, message);
|
||||
break;
|
||||
case 'npc_interact':
|
||||
await this.handleNpcInteract(ws, message);
|
||||
break;
|
||||
case 'npc_session_end':
|
||||
await this.handleNpcSessionEnd(ws, message);
|
||||
break;
|
||||
case 'leave_world':
|
||||
await this.handleLeaveWorld(ws, message);
|
||||
break;
|
||||
@@ -225,6 +274,72 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
}
|
||||
}
|
||||
|
||||
private async handleGuestLogin(ws: ExtendedWebSocket): Promise<void> {
|
||||
ws.authenticated = true;
|
||||
ws.guest = true;
|
||||
ws.username = '游客';
|
||||
ws.currentMap = DEFAULT_MAP_ID;
|
||||
ws.worldReady = false;
|
||||
this.sendMessage(ws, { t: 'guest_login_success', currentMap: DEFAULT_MAP_ID, readOnly: true });
|
||||
}
|
||||
|
||||
private async handleNpcInteract(ws: ExtendedWebSocket, message: any): Promise<void> {
|
||||
if (!ws.authenticated || ws.guest || !ws.userId || !ws.worldReady) {
|
||||
this.sendMessage(ws, { t: 'npc_interaction_error', code: 'AUTH_REQUIRED', message: '请登录后再与NPC交流' });
|
||||
return;
|
||||
}
|
||||
const now = Date.now();
|
||||
if (ws.lastNpcInteractionAt && now - ws.lastNpcInteractionAt < 1_000) {
|
||||
this.sendMessage(ws, { t: 'npc_interaction_error', code: 'RATE_LIMITED', message: '请稍后再交流' });
|
||||
return;
|
||||
}
|
||||
const npcId = String(message.npcId || message.npc_id || '').trim();
|
||||
if (!npcId) {
|
||||
this.sendMessage(ws, { t: 'npc_interaction_error', code: 'NPC_REQUIRED', message: 'NPC不能为空' });
|
||||
return;
|
||||
}
|
||||
const session = await this.chatService.getSession(ws.id);
|
||||
if (!session) {
|
||||
this.sendMessage(ws, { t: 'npc_interaction_error', code: 'SESSION_EXPIRED', message: '会话已失效,请重新登录' });
|
||||
return;
|
||||
}
|
||||
ws.lastNpcInteractionAt = now;
|
||||
try {
|
||||
const result = await this.worldNpcService.interact({
|
||||
npcId,
|
||||
userId: String(ws.userId),
|
||||
username: String(ws.username || session.username || '居民'),
|
||||
mapId: String(session.currentMap || ws.currentMap || DEFAULT_MAP_ID),
|
||||
x: Number(session.position?.x),
|
||||
y: Number(session.position?.y),
|
||||
message: String(message.message || '').trim(),
|
||||
sessionId: String(message.sessionId || message.session_id || ''),
|
||||
});
|
||||
this.sendMessage(ws, { t: 'npc_interaction_success', ...result });
|
||||
this.broadcastToMap(session.currentMap, {
|
||||
t: 'npc_spoke',
|
||||
...result,
|
||||
targetUserId: ws.userId,
|
||||
targetUsername: ws.username,
|
||||
});
|
||||
} catch (error) {
|
||||
this.sendMessage(ws, {
|
||||
t: 'npc_interaction_error',
|
||||
code: 'INTERACTION_REJECTED',
|
||||
message: error instanceof Error ? error.message : 'NPC暂时无法回应',
|
||||
npcId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async handleNpcSessionEnd(ws: ExtendedWebSocket, message: any): Promise<void> {
|
||||
if (!ws.authenticated || ws.guest || !ws.userId) return;
|
||||
await this.worldNpcService.endResidentSession(
|
||||
String(message.npcId || message.npc_id || ''), String(ws.userId), String(ws.username || ''),
|
||||
String(message.sessionId || message.session_id || ''),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理登录 - 协议转换后调用业务层
|
||||
*
|
||||
@@ -284,7 +399,7 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
}
|
||||
|
||||
try {
|
||||
await this.chatService.handlePlayerLogout(ws.id, 'manual');
|
||||
if (!ws.guest) await this.chatService.handlePlayerLogout(ws.id, 'manual');
|
||||
this.cleanupClient(ws);
|
||||
|
||||
this.sendMessage(ws, {
|
||||
@@ -326,18 +441,23 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
targetUsername: message.targetUsername || message.target_username,
|
||||
privateContext: message.privateContext || message.private_context,
|
||||
bubble: Boolean(message.bubble ?? message.showBubble ?? message.show_bubble),
|
||||
worldBulletin: message.worldBulletin === true || message.world_bulletin === true,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
this.sendMessage(ws, {
|
||||
t: 'chat_sent',
|
||||
messageId: result.messageId,
|
||||
charged: result.charged,
|
||||
balance: result.balance,
|
||||
worldBulletin: message.worldBulletin === true || message.world_bulletin === true,
|
||||
message: '消息发送成功'
|
||||
});
|
||||
} else {
|
||||
this.sendMessage(ws, {
|
||||
t: 'chat_error',
|
||||
code: this.toClientErrorCode(result.error),
|
||||
worldBulletin: message.worldBulletin === true || message.world_bulletin === true,
|
||||
message: result.error || '消息发送失败'
|
||||
});
|
||||
}
|
||||
@@ -563,6 +683,10 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
this.sendError(ws, '位置消息无效');
|
||||
return;
|
||||
}
|
||||
const nextSequence = positionMessage.sequence ?? Number(ws.movementSequence ?? 0) + 1;
|
||||
if (ws.movementSequence !== undefined && nextSequence <= ws.movementSequence) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const oldMapId = ws.currentMap || DEFAULT_MAP_ID;
|
||||
@@ -575,16 +699,20 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
ws.currentMap = positionMessage.mapId;
|
||||
}
|
||||
|
||||
await this.chatService.updatePlayerPosition({
|
||||
const updatedPresence = await this.chatService.updatePlayerPositionAndGetPresence({
|
||||
socketId: ws.id,
|
||||
x: positionMessage.x,
|
||||
y: positionMessage.y,
|
||||
mapId: positionMessage.mapId,
|
||||
direction: positionMessage.direction,
|
||||
movementState: positionMessage.movementState,
|
||||
sequence: nextSequence,
|
||||
});
|
||||
const updatedSession = await this.chatService.getSession(ws.id);
|
||||
const broadcastX = Number(updatedSession?.position?.x ?? positionMessage.x);
|
||||
const broadcastY = Number(updatedSession?.position?.y ?? positionMessage.y);
|
||||
const broadcastAppearance = updatedSession?.appearance;
|
||||
if (!updatedPresence) {
|
||||
this.sendMessage(ws, { type: 'error', code: 'SESSION_EXPIRED', message: '会话不存在,请重新登录' });
|
||||
return;
|
||||
}
|
||||
ws.movementSequence = nextSequence;
|
||||
|
||||
if (mapChanged) {
|
||||
this.broadcastToMap(oldMapId, {
|
||||
@@ -595,20 +723,24 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
}, ws.id);
|
||||
|
||||
await this.sendMapPlayersSnapshot(ws, positionMessage.mapId);
|
||||
this.sendMapNpcSnapshot(ws, positionMessage.mapId);
|
||||
}
|
||||
|
||||
const presencePayload = {
|
||||
t: 'position_update',
|
||||
userId: ws.userId,
|
||||
username: ws.username,
|
||||
x: broadcastX,
|
||||
y: broadcastY,
|
||||
x: updatedPresence.x,
|
||||
y: updatedPresence.y,
|
||||
mapId: positionMessage.mapId,
|
||||
skinId: broadcastAppearance?.skinId,
|
||||
avatarId: broadcastAppearance?.avatarId,
|
||||
skinAsset: broadcastAppearance?.skinAsset,
|
||||
cafeCompanion: updatedSession?.cafeCompanion ?? null,
|
||||
movementLocked: Boolean(updatedSession?.movementLocked),
|
||||
skinId: updatedPresence.skinId,
|
||||
avatarId: updatedPresence.avatarId,
|
||||
skinAsset: updatedPresence.skinAsset,
|
||||
cafeCompanion: updatedPresence.cafeCompanion ?? null,
|
||||
movementLocked: Boolean(updatedPresence.movementLocked),
|
||||
direction: updatedPresence.direction || positionMessage.direction,
|
||||
movementState: updatedPresence.movementState || positionMessage.movementState,
|
||||
sequence: Number(updatedPresence.sequence ?? nextSequence),
|
||||
};
|
||||
|
||||
this.broadcastToMap(positionMessage.mapId, mapChanged ? {
|
||||
@@ -635,6 +767,20 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
this.sendError(ws, '世界就绪消息无效');
|
||||
return;
|
||||
}
|
||||
if (ws.guest) {
|
||||
const guestMapId = DEFAULT_MAP_ID;
|
||||
if (ws.currentMap) this.leaveMapRoom(ws.id, ws.currentMap);
|
||||
ws.currentMap = guestMapId;
|
||||
ws.worldReady = true;
|
||||
this.joinMapRoom(ws.id, guestMapId);
|
||||
this.sendMessage(ws, { t: 'world_ready_success', mapId: guestMapId, readOnly: true });
|
||||
await this.sendMapPlayersSnapshot(ws, guestMapId);
|
||||
this.sendMapNpcSnapshot(ws, guestMapId);
|
||||
return;
|
||||
}
|
||||
const direction = this.normalizeDirection(message.direction);
|
||||
const movementState = this.normalizeMovementState(message.movementState ?? message.movement_state, 'idle');
|
||||
const sequence = this.normalizeSequence(message.sequence) ?? 0;
|
||||
|
||||
const wasWorldReady = Boolean(ws.worldReady);
|
||||
const oldMapId = ws.currentMap || DEFAULT_MAP_ID;
|
||||
@@ -650,7 +796,15 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
this.leaveMapRoom(ws.id, oldMapId);
|
||||
}
|
||||
|
||||
await this.chatService.updatePlayerPosition({ socketId: ws.id, mapId, x, y });
|
||||
await this.chatService.updatePlayerPosition({
|
||||
socketId: ws.id,
|
||||
mapId,
|
||||
x,
|
||||
y,
|
||||
direction,
|
||||
movementState,
|
||||
sequence,
|
||||
});
|
||||
const refreshedPresence = await this.chatService.refreshPlayerAppearance(ws.id);
|
||||
if (!refreshedPresence) {
|
||||
this.sendError(ws, '外观刷新失败');
|
||||
@@ -659,10 +813,12 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
|
||||
ws.currentMap = mapId;
|
||||
ws.worldReady = true;
|
||||
ws.movementSequence = sequence;
|
||||
this.joinMapRoom(ws.id, mapId);
|
||||
|
||||
this.sendMessage(ws, { t: 'world_ready_success', mapId });
|
||||
await this.sendMapPlayersSnapshot(ws, mapId);
|
||||
this.sendMapNpcSnapshot(ws, mapId);
|
||||
|
||||
const appearance = refreshedPresence.appearance;
|
||||
this.broadcastToMap(mapId, {
|
||||
@@ -677,6 +833,9 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
skinAsset: appearance?.skinAsset,
|
||||
cafeCompanion: refreshedPresence.cafeCompanion ?? null,
|
||||
movementLocked: Boolean(refreshedPresence.movementLocked),
|
||||
direction: refreshedPresence.direction || direction,
|
||||
movementState: refreshedPresence.movementState || movementState,
|
||||
sequence: Number(refreshedPresence.sequence ?? sequence),
|
||||
}, ws.id);
|
||||
|
||||
if (!wasWorldReady && !ws.welcomed) {
|
||||
@@ -746,6 +905,11 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
t: 'appearance_changed',
|
||||
...presence,
|
||||
}, ws.id);
|
||||
this.sendMessage(ws, {
|
||||
t: 'appearance_changed_success',
|
||||
mapId: presence.mapId,
|
||||
skinId: presence.skinId ?? presence.appearance?.skinId ?? '',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -825,6 +989,7 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
}, ws.id);
|
||||
|
||||
await this.sendMapPlayersSnapshot(ws, newMapId);
|
||||
this.sendMapNpcSnapshot(ws, newMapId);
|
||||
this.logger.log(`用户切换地图: ${ws.username} (${oldMapId} -> ${newMapId})`);
|
||||
|
||||
} catch (error) {
|
||||
@@ -933,6 +1098,47 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
this.sendMessage(ws, { type: 'error', code: this.toClientErrorCode(message), message });
|
||||
}
|
||||
|
||||
private checkClientHeartbeats(): void {
|
||||
this.clients.forEach((client) => {
|
||||
if (client.isAlive === false) {
|
||||
this.logger.warn(`WebSocket心跳超时: ${client.id}`);
|
||||
client.terminate();
|
||||
return;
|
||||
}
|
||||
client.isAlive = false;
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.ping();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async tickNpcActions(): Promise<void> {
|
||||
if (this.npcTickRunning) return;
|
||||
this.npcTickRunning = true;
|
||||
try {
|
||||
const result = await this.worldNpcService.tick();
|
||||
result.completed.forEach((completed) => this.broadcastToMap(completed.mapId, {
|
||||
t: 'npc_action_completed',
|
||||
...completed,
|
||||
x: completed.action.toX,
|
||||
y: completed.action.toY,
|
||||
}));
|
||||
result.started.forEach((started) => this.broadcastToMap(started.mapId, {
|
||||
t: 'npc_action_started',
|
||||
...started,
|
||||
}));
|
||||
result.conversations.forEach((conversation) => this.broadcastToMap(conversation.mapId, {
|
||||
t: 'npc_conversation',
|
||||
...conversation,
|
||||
}));
|
||||
result.changedMaps.forEach((mapId) => this.broadcastMapNpcSnapshot(mapId));
|
||||
} catch (error) {
|
||||
this.logger.error(`NPC 运行时 tick 失败: ${error instanceof Error ? error.message : error}`);
|
||||
} finally {
|
||||
this.npcTickRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
private toClientErrorCode(message?: string): string {
|
||||
const normalizedMessage = String(message || '');
|
||||
if (normalizedMessage.includes('会话不存在') || normalizedMessage.includes('重新登录')) {
|
||||
@@ -941,6 +1147,12 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
if (normalizedMessage.includes('请先登录') || normalizedMessage.includes('Token')) {
|
||||
return 'AUTH_FAILED';
|
||||
}
|
||||
if (normalizedMessage.includes('余额不足')) {
|
||||
return 'INSUFFICIENT_BALANCE';
|
||||
}
|
||||
if (normalizedMessage.includes('钱包服务')) {
|
||||
return 'WALLET_UNAVAILABLE';
|
||||
}
|
||||
return 'CHAT_ERROR';
|
||||
}
|
||||
|
||||
@@ -972,9 +1184,28 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
mapId,
|
||||
x,
|
||||
y,
|
||||
direction: this.normalizeDirection(message.direction),
|
||||
movementState: this.normalizeMovementState(message.movementState ?? message.movement_state, 'walk'),
|
||||
sequence: this.normalizeSequence(message.sequence),
|
||||
};
|
||||
}
|
||||
|
||||
private normalizeDirection(value: unknown): 'down' | 'up' | 'right' | 'left' {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized === 'up' || normalized === 'right' || normalized === 'left' ? normalized : 'down';
|
||||
}
|
||||
|
||||
private normalizeMovementState(value: unknown, fallback: 'idle' | 'walk'): 'idle' | 'walk' {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized === 'idle' || normalized === 'walk' ? normalized : fallback;
|
||||
}
|
||||
|
||||
private normalizeSequence(value: unknown): number | undefined {
|
||||
if (value === undefined || value === null || value === '') return undefined;
|
||||
const sequence = Number(value);
|
||||
return Number.isSafeInteger(sequence) && sequence >= 0 ? sequence : undefined;
|
||||
}
|
||||
|
||||
private async sendMapPlayersSnapshot(ws: ExtendedWebSocket, mapId?: string): Promise<void> {
|
||||
const normalizedMapId = String(mapId || DEFAULT_MAP_ID).trim();
|
||||
if (!normalizedMapId) return;
|
||||
@@ -993,9 +1224,25 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
});
|
||||
}
|
||||
|
||||
private sendMapNpcSnapshot(ws: ExtendedWebSocket, mapId?: string): void {
|
||||
const normalizedMapId = String(mapId || DEFAULT_MAP_ID).trim();
|
||||
if (!normalizedMapId) return;
|
||||
|
||||
const snapshot = this.worldNpcService.getMapSnapshot(normalizedMapId);
|
||||
this.sendMessage(ws, {
|
||||
t: 'npc_snapshot',
|
||||
...snapshot,
|
||||
});
|
||||
}
|
||||
|
||||
private broadcastMapNpcSnapshot(mapId: string): void {
|
||||
const snapshot = this.worldNpcService.getMapSnapshot(mapId);
|
||||
this.broadcastToMap(mapId, { t: 'npc_snapshot', ...snapshot });
|
||||
}
|
||||
|
||||
private async cleanupClient(ws: ExtendedWebSocket, reason: 'manual' | 'timeout' | 'disconnect' = 'disconnect') {
|
||||
try {
|
||||
if (ws.authenticated && ws.worldReady && ws.currentMap) {
|
||||
if (ws.authenticated && !ws.guest && ws.worldReady && ws.currentMap) {
|
||||
this.broadcastToMap(ws.currentMap, {
|
||||
t: 'player_left',
|
||||
userId: ws.userId,
|
||||
@@ -1003,7 +1250,7 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
mapId: ws.currentMap,
|
||||
}, ws.id);
|
||||
}
|
||||
if (ws.authenticated && ws.id) {
|
||||
if (ws.authenticated && !ws.guest && ws.id) {
|
||||
await this.chatService.handlePlayerLogout(ws.id, reason);
|
||||
}
|
||||
if (ws.currentMap) {
|
||||
|
||||
Reference in New Issue
Block a user