import { NetRoom } from "./NetRoom"; import { NetTeam } from "./NetTeam"; import { WsClient } from "./WsClient"; /**自定义同步游戏变量(只能以r_或p_开头,按帧率同步,重新进入会收到最新的数据) * 只有第一个参数有效,可加上第一个参数跟第一个参数同类型,用于接收改变前的数据 * t_开头为队伍事件 * r_开头 主机游戏数据 主机权限 * revt_ 开头房间事件 * p_ 开头玩家数据 玩家权限和主机ai权限 * online 玩家在线下线状态 * 尽量拆分数据类型,不要设置过于复杂的数据类型*/ export interface game_protocol extends chsdk.OmitIndex { t_entry(id: string, location: number, nickName: string): void;//队伍有人进入 t_exit(id: string, location: number, nickName: string): void;//队伍有人退出 t_host(id: string, location: number, nickName: string): void;//队长切换 t_chat(id: string, msg: string, location: number, nickName: string): void;//队伍聊天 t_online(id: string, online: boolean, location: number, nickName: string): void;//队伍玩家在线情况变化 t_ready(id: string, online: boolean, location: number, nickName: string): void;//队伍玩家准备情况变化 t_matchStart(): void;//匹配开始 t_matchCancel(): void;//匹配取消 t_matchSuccess(): void;//匹配成功 t_no_ready(): void;//有玩家未准备 t_closed(): void;//队伍解散(一般用于被踢出事件) // r_obj(key: string, data: any, old: any): void; r_closed(): void;//房间关闭 关闭后 获取 results 结算信息 r_host(id: string, location: number, nickName: string): void;//房主切换 r_chat(id: string, msg: string, location: number, nickName: string): void;//房间聊天 r_online(id: string, online: boolean, location: number, nickName: string): void;//房间玩家在线情况变化 r_finish(id: string, rank: number): void//房间某个玩家完成游戏获得名次 r_state(state: number, old_state?: number): void;//房间游戏状态 p_state(state: number, old_state?: number): void;//玩家游戏状态 finish(rank: number): void;//玩家完成游戏获得名次 online(online: boolean): void//玩家断线重连消息 } /**网络状态*/ interface ws_event_protocol { Connecting(): void;//连接中 Reconnecting(reconnectCount: number): void;//重新连接中 Connected(): void;//进入大厅连接成功 Reconnected(): void;//重连成功 Error(err: any | string): void;//错误消息 Closing(): void;//关闭连接中 Closed(event: CloseEvent): void;//连接已关闭 } function isArrayBuffer(it: unknown): it is ArrayBuffer { try { return it instanceof ArrayBuffer } catch (_) { return false } } const HEARTBEAT_INTERVAL = 1000; //每秒发送一次 Ping const HEARTBEAT_TIMEOUT = 30000; //30 秒的断线超时 const DIRTYDATACHECK_INTERVAL = 200;//每200毫秒同步一次自定义游戏变量 const EVTCHECK_INTERVAL = 66;//每66毫秒同步一次自定义游戏事件 const RECONNECT_COUNT = 10;//默认重连次数 const RECONNECT_INTERVAL = 2000;//默认重连间隔 const PING = 'ping'; const PONG = 'pong'; // const S2C_LOGIN_SUCCESS = 's2c_login_success'; const S2C_TEAM_SUCCESS = 's2c_team_success'; const S2C_JOIN_TEAM_FAIL = 's2c_join_team_fail'; const S2C_TEAM_USER_JOIN = 's2c_team_user_join'; const S2C_TEAM_USER_EXIT = 's2c_team_user_exit'; const S2C_TEAM_CHANGE_HOST = 's2c_team_change_host'; const S2C_USER_RECONNECT = 's2c_user_reconnect'; const S2C_USER_DISCONNECT = 's2c_user_disconnect'; const S2C_MATCH = 's2c_match'; const S2C_MATCH_SUCCESS = 's2c_match_success'; const S2C_MATCH_CANCEL = 's2c_match_cancel'; const S2C_ROOM_GAMEDATA_CHANGE = 's2c_room_gameData_change'; const S2C_USER_GAMEDATA_CHANGE = 's2c_user_gameData_change'; const S2C_ROOM_CHANGE_HOST = 's2c_room_change_host'; const S2C_ROOM_CLOSED = 's2c_room_closed'; const S2C_TALK = 's2c_talk'; const S2C_FINISH = 's2c_finish'; const S2C_SETTLEMENT = 's2c_settlement'; const S2C_READY = 's2c_ready'; const S2C_NOT_READY = 's2c_not_ready'; // const C2S_UPDATE_USER_GAMEDATA = 'c2s_update_user_gameData'; const C2S_UPDATE_ROOM_GAMEDATA = 'c2s_update_room_gameData'; const C2S_SET_TEAM = 'c2s_set_team'; const C2S_JOIN_TEAM = 'c2s_join_team'; const C2S_EXIT_TEAM = 'c2s_exit_team'; const C2S_CLOSE_ROOM = 'c2s_close_room'; const C2S_ROOM_EXIT = 'c2s_room_exit'; const C2S_MESSAGE = 'c2s_message'; const C2S_MESSAGE_TO_HOST = 'c2s_message_to_host'; const C2S_MESSAGE_TO_OTHER = 'c2s_message_without_self'; const C2S_TALK = 'c2s_talk'; const C2S_BACKED_RETURN = 'c2s_backed_return'; const C2S_BACKED = 'c2s_to_backed'; const C2S_FINISH = 'c2s_finish'; // const Ver: string = '0.1.8'; /**客户端*/ export class ch_net { /**连接状态事件*/ public state = chsdk.get_new_event(); private _ws: WsClient | null = null; private _url?: string | null; private _ca?: string; private reconnection: boolean; private reconnectCount: number; private reconnectInterval: number; private timeoutInterval: number; private gameDataInterval: number; private gameEvtInterval: number; private timeout: any | null = null; private json: boolean = false; private heartbeatInterval: any | null = null; // 心跳定时器 private dirtyDataCheckInterval: any | null = null; //变量同步定时器 private evtCheckInterval: any | null = null; //事件同步定时器 private _currentReconnectCount: number = 0; private _pingTime: number = 0; private _ping: number = 0;//ping值 private _callbacks: { [id: string]: (result: any) => void } = {}; private _waitSends: Array = []; private _new_resolve(id: string): Promise { return new Promise(resolve => this._callbacks[id] = resolve); } private _lv: { LowerRank: number, Rank: number, Star: number } = { LowerRank: 0, Rank: 0, Star: 0 }; public get ownLevel(): { LowerRank: number, Rank: number, Star: number } { return this._lv }; private _do_resolve(id: string, data: T): boolean { const resolveCallback = this._callbacks[id]; if (!resolveCallback) return false; resolveCallback(data); delete this._callbacks[id]; return true; } private _team: NetTeam | null; private _room: NetRoom | null; public get dataInterval(): number { return this.gameDataInterval * 0.001 }; public get evtInterval(): number { return this.gameEvtInterval * 0.001 }; public get isActive(): boolean { return this._ws && this._ws.isActive }; /**队伍信息*/ public get team(): NetTeam { return this._team; } /**是否在队伍中*/ public get inTeam(): boolean { return this._team != null && this._team.own != null; } /**房间信息*/ public get room(): NetRoom { return this._room; } /**是否在房间中*/ public get inRoom(): boolean { return this._room != null && !this._room.cloosed; } /** * 创建一个客户端 */ constructor() { this._waitSends = []; this._callbacks = {}; } /**初始化 * @param url - 连接的地址 'ws://localhost:3000' * @param options - 可选的配置选项对象,支持以下属性: * - **ca**: (可选) CA 证书字符串,用于 SSL 连接的验证。 * - **reconnection**: (可选) 指示是否启用重连机制,默认为 `true`。 * - **reconnectCount**: (可选) 在连接失败后重连的最大次数,默认值为 `10`。 * - **reconnectInterval**: (可选) 每次重连之间的间隔时间,以毫秒为单位,默认值为 `2000` (2 秒)。 * - **timeoutInterval**: (可选) 心跳断线时间,以毫秒为单位,默认值为 `30000` (30 秒)。 * - **gameEvtInterval**:(可选) 游戏事件同步间隔,默认值为 `66` (0.06秒 1秒钟15次)。 * - **gameDataInterval**:(可选) 游戏变量同步间隔,默认值为 `200` (0.2秒 1秒钟5次)。 * - **json**: (可选) 是否用json序列化否则用messagepck 默认为 `false`。 * @example * 自定义消息 interface message extends message_protocol { useItem(userId: string, otherId: string, itemId: number): void//使用道具 } 自定义数据 export interface gd extends game_data { r_lv(lv: number);//关卡信息 r_time(time: number);//关卡时间 } export const net = ch.get_new_net(); const url = chsdk.getUrl('/handle').replace(chsdk.getUrl('/handle').split(':')[0], 'ws'); const token = chsdk.getToken(); net.init(url,{query: `token=${token}`}); */ public init(url: string, options?: { query?: string, ca?: string, reconnection?: boolean, reconnectCount?: number, reconnectInterval?: number, timeoutInterval?: number, gameEvtInterval?: number, gameDataInterval?: number, json?: boolean }) { this._url = url; this._url = `${this._url}${options?.query?.length ? '?' + options.query : ''}`; this._ca = options?.ca; this.reconnection = options?.reconnection ?? true; this.reconnectCount = options?.reconnectCount ?? RECONNECT_COUNT; this.reconnectInterval = options?.reconnectInterval ?? RECONNECT_INTERVAL; this.timeoutInterval = options?.timeoutInterval ?? HEARTBEAT_TIMEOUT; this.gameDataInterval = options?.gameDataInterval ?? DIRTYDATACHECK_INTERVAL; this.gameEvtInterval = options?.gameEvtInterval ?? EVTCHECK_INTERVAL; this.json = options?.json ?? false; this._ws = new WsClient(this._url, this._ca); this._ws.onConnected = this._onConnected.bind(this); this._ws.onError = this._onError.bind(this); this._ws.onClosing = this._onClosing.bind(this); this._ws.onClosed = this._onClosed.bind(this); this._ws.onMessage = this._onMessage.bind(this); const isbuffer = typeof ArrayBuffer !== 'undefined'; console.log('ch_net 初始化 ver:', Ver, isbuffer, this.json, this._url); // chsdk.sdk_event.on('hide', this.on_hide, this); chsdk.sdk_event.on('show', this.on_show, this); } /**主动断开连接*/ public dispose(): void { this.state.clearAll(); this._clear_team(); this._clear_room(); this._callbacks = {}; this._waitSends.length = 0; this._ws.close(5000, 'client disconnect'); chsdk.sdk_event.off('hide', this.on_hide, this); chsdk.sdk_event.off('show', this.on_show, this); } private on_hide(): void { if (!this.inRoom) return; this.send_data(C2S_BACKED); } private on_show(): void { if (!this.inRoom) return; this.send_data(C2S_BACKED_RETURN); } private encode(msg: any): any { return this.json ? JSON.stringify(msg) : msgpack.encode(msg); } private decode(medata: any): any { return isArrayBuffer(medata) ? msgpack.decode(new Uint8Array(medata)) : JSON.parse(medata); } private send_data(type: string, data?: any): void { this._send(this.encode(data ? { type: type, data: data } : { type: type })); } /**开始连接,返回null为成功,否则为错误提示*/ public async connect(): Promise { if (!this._ws) { chsdk.log.error('not init'); return 'not init'; } if (!this._ws.connect()) return 'no netconnect'; this.state.emit(this.state.key.Connecting); return this._new_resolve('connect'); } /**主动重新连接*/ public reconnect(): string | null { if (!this._ws) { chsdk.log.error('not init'); return 'not init'; } this._currentReconnectCount = 1; this.do_reconnect(); } private do_reconnect(): void { if (!this._ws.connect()) return; this.state.emit(this.state.key.Reconnecting, this._currentReconnectCount); } /**玩家创建队伍,返回null为成功,否则为错误提示*/ public async creat_team(player_count: number = 4): Promise { if (!this._ws) return 'no netconnect'; if (this.inTeam) return null; this.send_data(C2S_SET_TEAM, player_count); return this._new_resolve('creat_team'); } /**玩家进入队伍,返回null为成功,否则为错误提示*/ public async join_team(password: string): Promise { if (!this._ws) return 'no netconnect'; if (this.inTeam) return null; this.send_data(C2S_JOIN_TEAM, { password: password }); return this._new_resolve('join_team'); } /**玩家退出队伍,返回null为成功,否则为错误提示*/ public async exit_team(): Promise { if (!this._ws) return 'no netconnect'; if (!this.inTeam) return 'not in team'; if (this.inRoom) return 'in game'; this.send_data(C2S_EXIT_TEAM); return this._new_resolve('exit_team'); } /**主机结束游戏关闭房间,成功后收到房间关闭事件*/ public close_room(): void { if (!this._ws) return; if (!this.inRoom) return; if (!this.room.isHost) return; this.send_data(C2S_CLOSE_ROOM); } /**玩家退出房间 ,返回null为成功,否则为错误提示,退出成功后收到房间关闭事件*/ public async exit_room(): Promise { if (!this._ws) return 'no netconnect'; if (!this.inRoom) return 'not in room'; this.send_data(C2S_ROOM_EXIT); return this._new_resolve('exit_room'); } private _clear_team(): void { this._team?.dispose(); this._team = null; } private _clear_room(): void { if (!this._room) return; this._room?.dispose(); this._room = null; } private _send(data: Parameters[0]) { if (!this._ws) return; if (this._ws.isActive) { this._ws.send(data); } else { this._waitSends.push(data); } } private _updatePlayerStatus(id: string, online: boolean) { if (this.inTeam) (this._team as any).updatePlayerStatus(id, online); if (this.inRoom) (this._room as any).updatePlayerStatus(id, online); } // private _onMessage(msg: MessageEvent): void { this.resetHeartbeat(); try { const { data: medata } = msg; const data = this.decode(medata); if (data.type !== PONG) chsdk.log.log('receive', JSON.stringify(data)); switch (data.type) { case PONG: this._ping = chsdk.date.now() - this._pingTime; chsdk.date.updateServerTime(data.data); //chsdk.log.log("pong:", this._ping, data.data, chsdk.date.now()); break; case S2C_LOGIN_SUCCESS: this._lv = data.data; if (this._do_resolve('connect', null)) { this.state.emit(this.state.key.Connected); } else { this.state.emit(this.state.key.Reconnected); } this._callbacks = {}; this.startHeartbeat(); while (this._waitSends.length) { if (!this._ws) break; const data = this._waitSends.shift(); if (data) this._ws.send(data); } break; case S2C_TEAM_SUCCESS: if (this._team) this._clear_team(); this._team = new NetTeam(data.data, this.send_data.bind(this)); if (!this._do_resolve('creat_team', null)) this._do_resolve('join_team', null); break; case S2C_JOIN_TEAM_FAIL: this._do_resolve('join_team', data.data); break; case S2C_TEAM_USER_JOIN: const join_user = data.data; join_user.status = true; (this._team as any).addPlayer({ key: join_user.userKey, data: join_user }, true); break; case S2C_TEAM_USER_EXIT: const esit_id = data.data; if (esit_id === this._team.own.Id) { (this._team as any).closed(); this._clear_team(); this._do_resolve('exit_team', null); } else { (this._team as any).removePlayer(esit_id); } break; case S2C_TEAM_CHANGE_HOST: (this._team as any).changeHost(data.data); break; case S2C_ROOM_CHANGE_HOST: (this._room as any).changeHost(data.data); break; case S2C_USER_RECONNECT: this._updatePlayerStatus(data.data, true); break; case S2C_USER_DISCONNECT: this._updatePlayerStatus(data.data, false); break; case S2C_MATCH: (this._team as any).match_start(); break; case S2C_MATCH_CANCEL: (this._team as any).match_cancel(); break; case S2C_NOT_READY: (this._team as any).not_ready(); break; case S2C_READY: (this._team as any).updatePlayerReady(data.data, true); break; case S2C_MATCH_SUCCESS: this._clear_room(); const roomData = data.data.roomData; const pData = data.data.playerData; this._room = new NetRoom(roomData, pData); if (this._team) (this._team as any).match_success(); break; case S2C_ROOM_GAMEDATA_CHANGE: (this._room as any).server_change(data.data); break; case S2C_USER_GAMEDATA_CHANGE: const p = this._room.getPlayer(data.data.userKey); if (!p) break; (p as any).server_change(data.data.data); break; case S2C_ROOM_CLOSED: (this._room as any).closed(data.data); if (this.inTeam) { const list = this.room.all; for (let i = 0; i < list.length; i++) { const rp = list[i]; if (rp.isAI) continue; const p = this.team.getPlayer(rp.Id); if (!p) continue; (p as any).set_level(rp.level, rp.rank, rp.score, rp.totalRank); } } this._do_resolve('exit_room', null); break; case S2C_TALK: if (this.inRoom) { (this._room as any).onChat(data.data.userKey, data.data.msg); } else if (this.inTeam) { (this._team as any).onChat(data.data.userKey, data.data.msg); } break; case S2C_FINISH: (this._room as any).finish(data.data.userKey, data.data.rank); break; case S2C_SETTLEMENT: (this._room as any).settlement(data.data); break; case C2S_MESSAGE: if (Array.isArray(data.data)) { for (let i = 0; i < data.data.length; i++) { const d = data.data[i]; (this._room as any)._onEvt(d.type, d.data); } } else { chsdk.log.log('no def type:', data.data); //(this.receive as any)._emit(data.type, data.data); } break; default: break; } } catch (error) { chsdk.log.warn(error) this.state.emit(this.state.key.Error, error); } } private _onConnected(evt: any): void { if (this.timeout) clearTimeout(this.timeout); this._currentReconnectCount = 0; this.timeout = setTimeout(() => { this._ws.close(); chsdk.log.error("Closing connection."); }, 3000); } private _onError(err: any): void { chsdk.log.log('err', err); this.state.emit(this.state.key.Error, err); } private _onClosing(): void { chsdk.log.log('closing'); this.state.emit(this.state.key.Connecting); } private _onClosed(event: CloseEvent): void { chsdk.log.log("WebSocket connection closed", event); this.stopHeartbeat(); if (!this.reconnection || (event.reason && event.code > 4000)) { this.state.emit(this.state.key.Closed, event); this._do_resolve('connect', event); chsdk.log.log("WebSocket connection closed", event); return; } if (this._currentReconnectCount < this.reconnectCount) { this._currentReconnectCount += 1; chsdk.log.log(`Reconnecting... Attempt ${this._currentReconnectCount}`); setTimeout(() => { this.do_reconnect() }, this.reconnectInterval) } else { chsdk.log.error("Max reconnect attempts reached. Giving up."); this.state.emit(this.state.key.Closed, event); this._do_resolve('connect', event); } } private startHeartbeat() { this.heartbeatInterval = setInterval(() => { this.sendHeartbeat(); }, HEARTBEAT_INTERVAL); this.dirtyDataCheckInterval = setInterval(() => { this.dirtyDataCheck(); }, this.gameDataInterval); this.evtCheckInterval = setInterval(() => { this.evtCheck(); }, this.gameEvtInterval); } private resetHeartbeat() { if (this.timeout) clearTimeout(this.timeout); this.timeout = setTimeout(() => { chsdk.log.error("Heartbeat timeout. Closing connection."); this._ws.close(); }, this.timeoutInterval); } private sendHeartbeat() { this._pingTime = chsdk.date.now(); this.send_data(PING, this._ping); } private stopHeartbeat() { if (this.heartbeatInterval) clearInterval(this.heartbeatInterval); if (this.timeout) clearTimeout(this.timeout); if (this.dirtyDataCheckInterval) clearInterval(this.dirtyDataCheckInterval); if (this.evtCheckInterval) clearInterval(this.evtCheckInterval); } private evtCheck() { if (this.inRoom) { (this.room as any).doSendChat((msg: string) => { if (!msg) return; this.send_data(C2S_TALK, msg); }); (this.room as any).doSendMode((mode0, mode1, mode2) => { if (mode0.length > 0) this.send_data(C2S_MESSAGE, mode0); if (mode1.length > 0) this.send_data(C2S_MESSAGE_TO_HOST, mode1); if (mode2.length > 0) this.send_data(C2S_MESSAGE_TO_OTHER, mode2); }); const ps = this.room.all; for (let i = 0; i < ps.length; i++) { const p = ps[i]; (p as any).doFinishGame(() => { this.send_data(C2S_FINISH, p.Id); }); } } else if (this.inTeam) { (this.team as any).doSendChat((msg: string) => { if (!msg) return; this.send_data(C2S_TALK, msg); }); } } private dirtyDataCheck() { if (!this.inRoom) return; (this.room.own as any).doDirtyData((dirty) => { this.send_data(C2S_UPDATE_USER_GAMEDATA, { userKey: this.room.own.Id, data: dirty }); }) if (!this.room.isHost) return; (this._room as any).doDirtyData((dirty) => { this.send_data(C2S_UPDATE_ROOM_GAMEDATA, dirty); }) const ais = this.room.all; for (let i = 0; i < ais.length; i++) { const ai = ais[i]; (ai as any).doDirtyData((dirty) => { this.send_data(C2S_UPDATE_USER_GAMEDATA, { userKey: ai.Id, data: dirty }); }) } } /** * 分享队伍信息,邀请进队 * @param extend (可选)分享数据 * @param title 分享显示标题 * @param imageUrl 分享显示图片 */ public shareNetTeam(title?: string, imageUrl?: string, extend?: T) { if (!this.inTeam) return; chsdk.shareAppMessage(title, '', imageUrl, JSON.stringify({ type: 'NetTeam', pw: this.team.Password, extends: extend })); } /** *获取从好友分享的net数据进入游戏的数据 */ public getShareNetTeam(): { password: string, extend?: T } { const query = chsdk.getQuery(); if (!query) return null; const message = query.message; if (!message) return null; const data = JSON.parse(message); if (!data) return null; if (data.type != 'NetTeam') return null; query.message = null; return { password: data.pw, extend: data.extends as T }; } }