|
@@ -0,0 +1,723 @@
|
|
|
+import { Level, Ranks, SeasonInfo, transUserDataform, transUserExtraDataform, UserRank } from "./NetBase";
|
|
|
+import { NetRoom } from "./NetRoom";
|
|
|
+import { NetTeam } from "./NetTeam";
|
|
|
+import { TTWsClient } from "./TTWsClient";
|
|
|
+import { WsClient } from "./WsClient";
|
|
|
+import { WXWsClient } from "./WXWsClient";
|
|
|
+/**自定义同步游戏变量(只能以r_或p_开头,按帧率同步,重新进入会收到最新的数据)
|
|
|
+ * 只有第一个参数有效,可加上第一个参数跟第一个参数同类型,用于接收改变前的数据
|
|
|
+ * t_开头为队伍事件
|
|
|
+ * r_开头 主机游戏数据 主机权限
|
|
|
+ * revt_ 开头房间事件
|
|
|
+ * p_ 开头玩家数据 玩家权限和主机ai权限
|
|
|
+ * online 玩家在线下线状态
|
|
|
+ * 尽量拆分数据类型,不要设置过于复杂的数据类型*/
|
|
|
+type protocol = { extra?: Record<string, any> } & chsdk.OmitIndex<chsdk.EventsMap>
|
|
|
+export interface game_protocol extends protocol {
|
|
|
+ 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_exit(id: string, location: number, nickName: string): void;//房间某个玩家离开
|
|
|
+ r_state(state: number, old_state?: number): void;//房间游戏状态
|
|
|
+ //
|
|
|
+ p_obj(key: string, data: any, old: any): void;
|
|
|
+ p_state(state: number, old_state?: number): void;//玩家游戏状态
|
|
|
+ p_extra(): void;//扩展数据改变
|
|
|
+ exit(): void;//玩家离开房间
|
|
|
+ finish(rank: number): void;//玩家完成游戏获得名次
|
|
|
+ online(online: boolean): void;//玩家断线重连消息
|
|
|
+ extra: Record<string, any>;
|
|
|
+}
|
|
|
+/**网络状态*/
|
|
|
+interface ws_event_protocol {
|
|
|
+ Connecting(): void;//连接中
|
|
|
+ Reconnecting(reconnectCount: number): void;//重新连接中
|
|
|
+ Connected(): void;//进入大厅连接成功
|
|
|
+ Reconnected(): void;//重连成功
|
|
|
+ Error(err: any | string): void;//错误消息
|
|
|
+ Closing(): void;//关闭连接中
|
|
|
+ Closed(event: string): void;//连接已关闭
|
|
|
+ NextSeason(): void;//进入下一个赛季
|
|
|
+}
|
|
|
+interface options {
|
|
|
+ /**(可选)query连接token等数据*/query?: string,
|
|
|
+ /**(可选) CA 证书字符串,用于 SSL 连接的验证*/ca?: string,
|
|
|
+ /**(可选) 指示是否启用重连机制,默认为 `true`*/ reconnection?: boolean,
|
|
|
+ /**(可选) 在连接失败后重连的最大次数,默认值为 `10`*/ reconnectCount?: number,
|
|
|
+ /**(可选) 每次重连之间的间隔时间,以毫秒为单位,默认值为 `2000`*/ reconnectInterval?: number,
|
|
|
+ /**(可选) 心跳断线时间,以毫秒为单位,默认值为 `30000`*/ timeoutInterval?: number,
|
|
|
+ /**(可选) 游戏事件同步间隔,默认值为 `66` (0.06秒 1秒钟15次)*/ gameEvtInterval?: number,
|
|
|
+ /**(可选) 游戏变量同步间隔,默认值为 `200` (0.2秒 1秒钟5次)*/ gameDataInterval?: number,
|
|
|
+ /**(可选) 是否用json序列化否则用messagepck 默认为 `false`*/ json?: boolean
|
|
|
+}
|
|
|
+function isArrayBuffer(it: unknown): it is ArrayBuffer {
|
|
|
+ try {
|
|
|
+ return it instanceof ArrayBuffer
|
|
|
+ } catch (_) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+}
|
|
|
+type JsonPrimitive = string | number | boolean | null;
|
|
|
+type Serializable<T> = T extends JsonPrimitive ? T : T extends Date | Function | Symbol | undefined ? never :
|
|
|
+ T extends Array<infer U> ? SerializableArray<U> : T extends object ? SerializableObject<T> : never;
|
|
|
+type SerializableArray<T> = Array<Serializable<T>>;
|
|
|
+type SerializableObject<T extends object> = { [K in keyof T]: Serializable<T[K]>; };
|
|
|
+//
|
|
|
+const HEARTBEAT_INTERVAL = 1000; //每秒发送一次 Ping
|
|
|
+const HEARTBEAT_TIMEOUT = 80000; //80 秒的断线超时
|
|
|
+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_EXIT_ROOM = 's2c_exit_room';
|
|
|
+const S2C_FINISH = 's2c_finish';
|
|
|
+const S2C_READY = 's2c_ready';
|
|
|
+const S2C_NOT_READY = 's2c_not_ready';
|
|
|
+const S2C_RECEIVE_REWARD = 's2c_receive_reward';
|
|
|
+const S2C_SEASON = 's2c_season';
|
|
|
+const S2C_SET_USER_DATA_EXTRA = 's2c_set_user_data_extra';
|
|
|
+const S2C_RANK_DATA = 's2c_rank_data';
|
|
|
+//
|
|
|
+const C2S_MESSAGE = 'c2s_message';
|
|
|
+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_BACKED_RETURN = 'c2s_backed_return';
|
|
|
+const C2S_BACKED = 'c2s_to_backed';
|
|
|
+const C2S_RECEIVE_REWARD = 'c2s_receive_reward';
|
|
|
+const C2S_SEASON = 'c2s_season';
|
|
|
+const C2S_SET_USER_DATA_EXTRA = 'c2s_set_user_data_extra';
|
|
|
+const C2S_RANK_DATA = 'c2s_rank_data';
|
|
|
+//
|
|
|
+const Ver: string = '0.6.2';
|
|
|
+/**客户端*/
|
|
|
+export class ch_net<GD extends game_protocol = game_protocol> {
|
|
|
+ /**连接状态事件*/
|
|
|
+ public state = chsdk.get_new_event<ws_event_protocol>();
|
|
|
+ private _ws: WsClient | TTWsClient | WXWsClient | 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 season_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<string | ArrayBuffer> = [];//| ArrayBufferLike | Blob | ArrayBufferView
|
|
|
+ private _new_resolve<T>(id: string): Promise<T> { return new Promise(resolve => this._callbacks[id] = resolve); }
|
|
|
+ private _lv: Level = { LowerRank: 0, Rank: 0, Star: 0 };
|
|
|
+ private _userKey: string;
|
|
|
+ private _userDataExtra: GD['extra'] = null;
|
|
|
+ /**玩家扩展数据 */
|
|
|
+ public get userDataExtra(): GD['extra'] { return this._userDataExtra; };
|
|
|
+ /**获取玩家扩展数据某个字段*/
|
|
|
+ public getUserDataExtraField<K extends keyof GD['extra']>(key: K): GD['extra'][K] { return this._userDataExtra?.[key]; }
|
|
|
+ /**自己的段位信息
|
|
|
+ * @returns {Object}段位信息对象
|
|
|
+ */
|
|
|
+ public get ownLevel(): Level { return this._lv };
|
|
|
+ private _seasonInfo: any;
|
|
|
+ /**
|
|
|
+ * 赛季信息 包含当前赛季的基本信息以及排名历史。
|
|
|
+ * @returns {SeasonInfo} 赛季信息对象
|
|
|
+ */
|
|
|
+ public get seasonInfo(): SeasonInfo { return this._seasonInfo };
|
|
|
+ private _do_resolve<T>(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<GD> | null;
|
|
|
+ private _room: NetRoom<GD> | 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<GD> {
|
|
|
+ return this._team;
|
|
|
+ }
|
|
|
+ /**是否在队伍中*/
|
|
|
+ public get inTeam(): boolean {
|
|
|
+ return this._team != null && this._team.own != null;
|
|
|
+ }
|
|
|
+ /**房间信息*/
|
|
|
+ public get room(): NetRoom<GD> {
|
|
|
+ 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 - 可选的配置选项对象,支持以下属性:
|
|
|
+ * @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<gd, message>();
|
|
|
+ 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?: options) {
|
|
|
+ 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);
|
|
|
+ //if (chsdk.get_pf() === chsdk.pf.wx) this._ws = new WXWsClient(this._url, this._ca);
|
|
|
+ //if (chsdk.get_pf() === chsdk.pf.tt) this._ws = new TTWsClient(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 SimulatedDisconnection(): void {
|
|
|
+ this._ws.close();
|
|
|
+ }
|
|
|
+ /**主动断开连接*/
|
|
|
+ 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): string | ArrayBuffer {
|
|
|
+ return this.json ? JSON.stringify(msg) : msgpack.encode(msg).buffer;
|
|
|
+ }
|
|
|
+ private decode(medata: any): any {
|
|
|
+ return this.json ? JSON.parse(medata) : msgpack.decode(new Uint8Array(medata));
|
|
|
+ }
|
|
|
+ private send_data(type: string, data?: any): void {
|
|
|
+ //console.log(type, data);
|
|
|
+ const d = this.encode(data ? { type: type, data: data } : { type: type });
|
|
|
+ //console.log(d);
|
|
|
+ this._send(d);
|
|
|
+ }
|
|
|
+ /**开始连接,返回null为成功,否则为错误提示*/
|
|
|
+ public async connect(): Promise<string | null> {
|
|
|
+ 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 {
|
|
|
+ this._userKey = null;
|
|
|
+ this.state.emit(this.state.key.Reconnecting, this._currentReconnectCount);
|
|
|
+ this._ws.connect();
|
|
|
+ }
|
|
|
+ /**玩家创建队伍,返回null为成功,否则为错误提示*/
|
|
|
+ public async creat_team(player_count: number = 4): Promise<string | null> {
|
|
|
+ if (!this._ws) return 'No network connection';
|
|
|
+ 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<string | null> {
|
|
|
+ if (!this._ws) return 'No network connection';
|
|
|
+ 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<string | null> {
|
|
|
+ try {
|
|
|
+ if (!this._ws) return 'No network connection';
|
|
|
+ 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');
|
|
|
+ } catch (error) {
|
|
|
+ return 'Unexpected error occurred';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ /**主机结束游戏关闭房间,成功后收到房间关闭事件*/
|
|
|
+ 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<string | null> {
|
|
|
+ try {
|
|
|
+ if (!this._ws) return 'No network connection';
|
|
|
+ if (!this.inTeam) return 'Not in team';
|
|
|
+ if (!this.inRoom) return 'Not in room';
|
|
|
+ this.send_data(C2S_ROOM_EXIT);
|
|
|
+ return this._new_resolve('exit_room');
|
|
|
+ } catch (error) {
|
|
|
+ return 'Unexpected error occurred';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ private _cache_rank: Ranks<GD['extra']> | null = null;
|
|
|
+ private _cache_rank_time: number = 0;
|
|
|
+ /**获取排行榜行信息 如果是string,就是错误信息,否则为排行榜信息*/
|
|
|
+ public async getRankData(): Promise<string | Ranks<GD['extra']>> {
|
|
|
+ if (chsdk.date.now() - this._cache_rank_time >= 30000) {
|
|
|
+ this._cache_rank = null;
|
|
|
+ }
|
|
|
+ if (this._cache_rank) return this._cache_rank;
|
|
|
+ if (!this._ws) return 'No network connection';
|
|
|
+ this.send_data(C2S_RANK_DATA);
|
|
|
+ return this._new_resolve('get_rank_data');
|
|
|
+ }
|
|
|
+ /**强制转化成用护排行类型*/
|
|
|
+ public asUserRankType<T extends UserRank<GD['extra']> = UserRank<GD['extra']>>(data: any): T {
|
|
|
+ return data as T;
|
|
|
+ }
|
|
|
+ /**领取赛季奖励 ,返回null为成功,否则为错误提示*/
|
|
|
+ public async receive_reward(season: number): Promise<string | null> {
|
|
|
+ if (!this._ws) return 'No network connection';
|
|
|
+ this.send_data(C2S_RECEIVE_REWARD, season);
|
|
|
+ return this._new_resolve('receive_reward');
|
|
|
+ }
|
|
|
+ /**主动刷新赛季信息,返回null为成功,否则为错误提示*/
|
|
|
+ public async update_season(): Promise<string | null> {
|
|
|
+ if (!this._ws) return 'No network connection';
|
|
|
+ this.send_data(C2S_SEASON);
|
|
|
+ return this._new_resolve('update_season');
|
|
|
+ }
|
|
|
+ /**更新设置玩家整个扩展数据,返回null为成功,否则为错误提示*/
|
|
|
+ public async setUserExtra(data: GD['extra'] & SerializableObject<GD['extra']>): Promise<string | null> {
|
|
|
+ if (!this._ws) return 'No network connection';
|
|
|
+ this._userDataExtra = data;
|
|
|
+ this.send_data(C2S_SET_USER_DATA_EXTRA, JSON.stringify(data));
|
|
|
+ if (this.inTeam) (this.team.own as any).set_userExtra(this._userDataExtra);
|
|
|
+ if (this.inRoom) (this.room.own as any).set_userExtra(this._userDataExtra);
|
|
|
+ return this._new_resolve('set_user_extra');
|
|
|
+ }
|
|
|
+
|
|
|
+ /**更新设置玩家扩展数据某个字段的值 */
|
|
|
+ public async setUserExtraField<K extends keyof GD['extra']>(key: K, value: GD['extra'][K] & SerializableObject<GD['extra'][K]>): Promise<string | null> {
|
|
|
+ this._userDataExtra ??= {};
|
|
|
+ this._userDataExtra[key] = value;
|
|
|
+ return this.setUserExtra(this._userDataExtra);
|
|
|
+ }
|
|
|
+ //
|
|
|
+ private _send(data: string | ArrayBuffer) {
|
|
|
+ if (!this._ws) return;
|
|
|
+ if (this._ws.isActive) {
|
|
|
+ this._ws.send(data);
|
|
|
+ } else {
|
|
|
+ this._waitSends.push(data);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ //
|
|
|
+ private _clear_team(): void {
|
|
|
+ this._team?.dispose();
|
|
|
+ this._team = null;
|
|
|
+ }
|
|
|
+ private _clear_room(): void {
|
|
|
+ this._room?.dispose();
|
|
|
+ this._room = null;
|
|
|
+ }
|
|
|
+ private _temp_team: any | null;
|
|
|
+ private _team_info(data?: any | null) {
|
|
|
+ this._clear_team();
|
|
|
+ if (!this._userKey) {
|
|
|
+ this._temp_team = data;
|
|
|
+ } else if (this._temp_team || data) {
|
|
|
+ data ??= this._temp_team;
|
|
|
+ this._team = new NetTeam<GD>(this._userKey, data, this.send_data.bind(this));
|
|
|
+ if (!this._do_resolve('creat_team', null)) this._do_resolve('join_team', null);
|
|
|
+ this._temp_team = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ private _temp_match_data: any | null;
|
|
|
+ private _match_success(data?: any | null): void {
|
|
|
+ this._clear_room();
|
|
|
+ if (!this._userKey) {
|
|
|
+ this._temp_match_data = data;
|
|
|
+ } else if (this._temp_match_data || data) {
|
|
|
+ data ??= this._temp_match_data;
|
|
|
+ const roomData = data.roomData;
|
|
|
+ const pData = data.playerData;
|
|
|
+ this._room = new NetRoom<GD>(this._userKey, roomData, pData, this.send_data.bind(this));
|
|
|
+ if (this._team) (this._team as any).match_success();
|
|
|
+ this._temp_match_data = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ 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: string | ArrayBuffer): void {
|
|
|
+ this.resetHeartbeat();
|
|
|
+ try {
|
|
|
+ const data = this.decode(msg);
|
|
|
+ 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.level;
|
|
|
+ this._userKey = data.data.userKey;
|
|
|
+ this._seasonInfo = data.data.seasonInfo;
|
|
|
+ this._userDataExtra = transUserExtraDataform(data.data.userDataExtra);
|
|
|
+ this._team_info();
|
|
|
+ this._match_success();
|
|
|
+ //
|
|
|
+ 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:
|
|
|
+ this._team_info(data.data);
|
|
|
+ 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:
|
|
|
+ if (this.inTeam) (this._team as any).changeHost(data.data);
|
|
|
+ break;
|
|
|
+ case S2C_ROOM_CHANGE_HOST:
|
|
|
+ if (this.inRoom) (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._match_success(data.data);
|
|
|
+ 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);
|
|
|
+ const list = this.room.all;
|
|
|
+ for (let i = 0; i < list.length; i++) {
|
|
|
+ const rp = list[i];
|
|
|
+ if (rp.isAI) continue;
|
|
|
+ if (rp.isOwn) {
|
|
|
+ this._lv.LowerRank = rp.level.LowerRank;
|
|
|
+ this._lv.Rank = rp.level.Rank;
|
|
|
+ this._lv.Star = rp.level.Star;
|
|
|
+ }
|
|
|
+ if (this.inTeam) {
|
|
|
+ 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_EXIT_ROOM:
|
|
|
+ (this._room as any).exit(data.data);
|
|
|
+ break;
|
|
|
+ case S2C_RECEIVE_REWARD:
|
|
|
+ this._do_resolve('receive_reward', null);
|
|
|
+ break;
|
|
|
+ case S2C_SEASON:
|
|
|
+ this._seasonInfo = data.data;
|
|
|
+ this._do_resolve('update_season', null);
|
|
|
+ this.state.emit('NextSeason');
|
|
|
+ break;
|
|
|
+ case S2C_SET_USER_DATA_EXTRA:
|
|
|
+ this._do_resolve('set_user_extra', null);
|
|
|
+ break;
|
|
|
+ case S2C_RANK_DATA:
|
|
|
+ this._cache_rank_time = chsdk.date.now();
|
|
|
+ const r = data.data;
|
|
|
+ const rr = r.ownRank;
|
|
|
+ const ownRank: UserRank<GD['extra']> = rr;
|
|
|
+ ownRank.UserData = transUserDataform(rr.UserData);
|
|
|
+ ownRank.UserDataExtra = transUserExtraDataform(rr.UserDataExtra);
|
|
|
+ const elementsRank: UserRank<GD['extra']>[] = [];
|
|
|
+ for (let i = 0; i < r.elementsRank.length; i++) {
|
|
|
+ const rr = r.elementsRank[i];
|
|
|
+ rr.UserData = transUserDataform(rr.UserData);
|
|
|
+ rr.UserDataExtra = transUserExtraDataform(rr.UserDataExtra);
|
|
|
+ elementsRank.push(rr);
|
|
|
+ }
|
|
|
+ this._cache_rank = { list: elementsRank, own: ownRank };
|
|
|
+ this._do_resolve('get_rank_data', this._cache_rank);
|
|
|
+ break;
|
|
|
+ case C2S_MESSAGE:
|
|
|
+ if (Array.isArray(data.data)) {
|
|
|
+ if (this.inRoom) {
|
|
|
+ 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(() => {
|
|
|
+ chsdk.log.warn("Closing connection whit no replay logoin!");
|
|
|
+ this._ws.close();
|
|
|
+ }, 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.Closing);
|
|
|
+ }
|
|
|
+ private _onClosed(code: number, reason: string): void {
|
|
|
+ code ??= 0;
|
|
|
+ reason ??= '';
|
|
|
+ chsdk.log.log("WebSocket connection closed", code, reason);
|
|
|
+ this.stopHeartbeat();
|
|
|
+ if (!this.reconnection || (reason && code > 4000)) {
|
|
|
+ this.state.emit(this.state.key.Closed, reason);
|
|
|
+ this._do_resolve('connect', reason);
|
|
|
+ chsdk.log.log("WebSocket connection closed", reason);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (this._currentReconnectCount < this.reconnectCount) {
|
|
|
+ this._currentReconnectCount += 1;
|
|
|
+ chsdk.log.log(`Reconnecting... Attempt ${this._currentReconnectCount}`);
|
|
|
+ setTimeout(() => { this.do_reconnect() }, this.reconnectInterval)
|
|
|
+ } else {
|
|
|
+ this._clear_team();
|
|
|
+ this._clear_room();
|
|
|
+ chsdk.log.error("Max reconnect attempts reached. Giving up.");
|
|
|
+ this.state.emit(this.state.key.Closed, reason);
|
|
|
+ this._do_resolve('connect', reason);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ private startHeartbeat() {
|
|
|
+ this.heartbeatInterval = setInterval(() => {
|
|
|
+ this.sendHeartbeat();
|
|
|
+ }, HEARTBEAT_INTERVAL);
|
|
|
+ this.dirtyDataCheckInterval = setInterval(() => {
|
|
|
+ this.dirtyDataCheck();
|
|
|
+ }, this.gameDataInterval);
|
|
|
+ this.evtCheckInterval = setInterval(() => {
|
|
|
+ this.evtCheck();
|
|
|
+ }, this.gameEvtInterval);
|
|
|
+ if (this.season_timeout) clearTimeout(this.season_timeout);
|
|
|
+ this.season_timeout = setTimeout(() => {
|
|
|
+ this.update_season();
|
|
|
+ }, this.seasonInfo.SeasonEndTime - chsdk.date.now() + Math.floor(Math.random() * 2000));
|
|
|
+ }
|
|
|
+ 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);
|
|
|
+ if (this.season_timeout) clearTimeout(this.season_timeout);
|
|
|
+ }
|
|
|
+ private evtCheck() {
|
|
|
+ if (this.inRoom) {
|
|
|
+ (this.room as any).doSendChat();
|
|
|
+ } else if (this.inTeam) {
|
|
|
+ (this.team as any).doSendChat();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ private dirtyDataCheck() {
|
|
|
+ if (!this.inRoom) return;
|
|
|
+ this.room.doSendDirty();
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * 分享队伍信息,邀请进队
|
|
|
+ * @param extend (可选)分享数据
|
|
|
+ * @param title 分享显示标题
|
|
|
+ * @param imageUrl 分享显示图片
|
|
|
+ */
|
|
|
+ public shareNetTeam<T extends { [key: string]: any }>(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<T extends { [key: string]: any }>(): { 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 };
|
|
|
+ }
|
|
|
+}
|