1.优化MVVM框架代码

2.MVVM组件支持脚本绑定数据
3.VMLabel组件支持时间格式化
This commit is contained in:
dgflash
2025-08-14 14:41:49 +08:00
parent 92fc16c3ec
commit 9214411916
31 changed files with 4818 additions and 78 deletions

View File

@@ -12,14 +12,27 @@ Date.prototype.format = function (format: string): string {
const hours: number = this.getHours();
const minutes: number = this.getMinutes();
const seconds: number = this.getSeconds();
const milliseconds: number = this.getMilliseconds();
return format
let r = format
.replace('yy', year.toString())
.replace('mm', (month < 10 ? '0' : '') + month)
.replace('dd', (day < 10 ? '0' : '') + day)
.replace('hh', (hours < 10 ? '0' : '') + hours)
.replace('mm', (minutes < 10 ? '0' : '') + minutes)
.replace('ss', (seconds < 10 ? '0' : '') + seconds);
if (milliseconds < 10) {
r = r.replace('ms', '00' + milliseconds);
}
else if (milliseconds < 100) {
r = r.replace('ms', '0' + milliseconds);
}
else {
r = r.replace('ms', milliseconds.toString());
}
return r;
};
export { };

View File

@@ -1,8 +1,6 @@
/**
* 数值格式化函数, 通过语义解析自动设置值的范围
* //整数
* 1:def(0)//显示一个默认值
*/
import { oops } from "../../core/Oops";
/** 数值格式化函数, 通过语义解析自动设置值的范围 */
class StringFormat {
deal(value: number | string, format: string): string {
if (format === '') return value as string;
@@ -21,58 +19,58 @@ class StringFormat {
switch (func) {
case 'int': res = this.int(value); break;
case 'fix': res = this.fix(value, num); break;
case 'kmbt': res = this.KMBT(value); break;
case 'kmbt': res = this.kmbt(value); break;
case 'per': res = this.per(value, num); break;
case 'sep': res = this.sep(value); break;
default:
break;
}
case 'tstamp': res = this.time_stamp(value); break;
case 'tm': res = this.time_m(value); break;
case 'ts': res = this.time_s(value); break;
case 'tms': res = this.time_ms(value); break;
default: break;
}
}
else {
switch (func) {
case 'limit': res = this.limit(value, num); break;
default:
break;
default: res = value; break;
}
res = value;
}
return res as string;
}
// 将数字按分号显示
/** 将数字按分号显示 */
private sep(value: number) {
let num = Math.round(value).toString();
return num.replace(new RegExp('(\\d)(?=(\\d{3})+$)', 'ig'), "$1,");
}
// 将数字按分显示 00:00 显示 ms制
/** 将数字按分显示 00:00 显示 时:分) */
private time_m(value: number) {
//todo
return new Date(value).format('hh:ss');
}
// 将数字按秒显示 00:00:00 显示 ms制
/** 将数字按秒显示 00:00:00 显示 时:分:秒) */
private time_s(value: number) {
//todo
return new Date(value).format('hh:mm:ss');
}
// 将数字按 0:00:00:000 显示 ms制
/** 将数字按 0:00:00:000 显示 时:分:秒:毫秒) */
private time_ms(value: number) {
//todo
return new Date(value).format('hh:mm:ss:ms');
}
// 将时间戳显示为详细的内容
private timeStamp(value: number) {
//todo
return new Date(value).toString()
/** 将时间戳显示为详细的内容 */
private time_stamp(value: number) {
return new Date(value).format('yy-mm-dd hh:mm:ss');
}
/** [value:int] 将取值0~1 变成 1~100,可以指定修饰的小数位数 */
private per(value: number, fd: number) {
return Math.round(value * 100).toFixed(fd);
let r = value * 100;
return r.toFixed(fd);
}
/** [value:int] 将取值变成整数 */
@@ -80,7 +78,7 @@ class StringFormat {
return Math.round(value);
}
/** [value:fix2]数值转换为小数*/
/** [value:fix2]数值转换为小数 */
private fix(value: number, fd: number) {
return value.toFixed(fd)
}
@@ -91,18 +89,21 @@ class StringFormat {
}
/** 将数字缩短显示为KMBT单位 大写,目前只支持英文 */
private KMBT(value: number, lang: string = 'en') {
private kmbt(value: number) {
//10^4=万, 10^8=亿,10^12=兆,10^16=京,
let counts = [1000, 1000000, 1000000000, 1000000000000];
let units = ['', 'K', 'M', 'B', 'T'];
let counts: number[] = null!;
let units: string[] = null!;
switch (lang) {
switch (oops.language.current) {
case 'zh':
//10^4=万, 10^8=亿,10^12=兆,10^16=京,
let counts = [10000, 100000000, 1000000000000, 10000000000000000];
let units = ['', '万', '亿', '兆', '京'];
counts = [10000, 100000000, 1000000000000, 10000000000000000];
units = ['', '万', '亿', '兆', '京'];
break;
case 'en':
counts = [1000, 1000000, 1000000000, 1000000000000];
units = ['', 'K', 'M', 'B', 'T'];
break;
default:
break;
}
@@ -110,12 +111,12 @@ class StringFormat {
return this.compressUnit(value, counts, units, 2);
}
//压缩任意单位的数字,后缀加上单位文字
/** 压缩任意单位的数字,后缀加上单位文字 */
private compressUnit(value: any, valueArr: number[], unitArr: string[], fixNum: number = 2): string {
let counts = valueArr;
let units = unitArr;
let res: string = "";
let index;
let index: number;
for (index = 0; index < counts.length; index++) {
const e = counts[index];
if (value < e) {
@@ -127,11 +128,10 @@ class StringFormat {
}
break;
}
}
return res + units[index];
}
}
/**格式化处理函数 */
/** 格式化处理函数 */
export let StringFormatFunction = new StringFormat();

View File

@@ -1,4 +1,4 @@
import { CCString, Component, Enum, log, Node, _decorator } from "cc";
import { _decorator, CCString, Component, Enum, log, Node } from "cc";
import { VMEnv } from "./VMEnv";
const { ccclass, property, executeInEditMode, menu, help } = _decorator;
@@ -36,10 +36,10 @@ export default class MVCompsEdit extends Component {
return this.actionType === ACTION_MODE.SEARCH_COMPONENT;
}
})
public get findTrigger() {
get findTrigger() {
return false;
}
public set findTrigger(v: boolean) {
set findTrigger(v: boolean) {
this.setComponents(0);
}
@@ -50,10 +50,10 @@ export default class MVCompsEdit extends Component {
return this.actionType === ACTION_MODE.ENABLE_COMPONENT;
}
})
public get enableTrigger() {
get enableTrigger() {
return false;
}
public set enableTrigger(v: boolean) {
set enableTrigger(v: boolean) {
this.setComponents(1);
}
@@ -64,10 +64,10 @@ export default class MVCompsEdit extends Component {
return this.actionType === ACTION_MODE.ENABLE_COMPONENT;
}
})
public get disableTrigger() {
get disableTrigger() {
return false;
}
public set disableTrigger(v: boolean) {
set disableTrigger(v: boolean) {
this.setComponents(2);
}
@@ -88,10 +88,10 @@ export default class MVCompsEdit extends Component {
return this.allowDelete && this.actionType === ACTION_MODE.DELETE_COMPONENT;
}
})
public get deleteTrigger() {
get deleteTrigger() {
return false;
}
public set deleteTrigger(v: boolean) {
set deleteTrigger(v: boolean) {
this.setComponents(3);
}
@@ -102,10 +102,10 @@ export default class MVCompsEdit extends Component {
return this.actionType === ACTION_MODE.REPLACE_WATCH_PATH;
}
})
public get replaceTrigger() {
get replaceTrigger() {
return false;
}
public set replaceTrigger(v: boolean) {
set replaceTrigger(v: boolean) {
this.setComponents(4);
}
@@ -269,7 +269,7 @@ export default class MVCompsEdit extends Component {
getNodePath(node: Node) {
let parent = node;
let array = [];
let array: string[] = [];
while (parent) {
let p = parent.getParent();
if (p) {
@@ -282,4 +282,4 @@ export default class MVCompsEdit extends Component {
}
return array.reverse().join('/');
}
}
}

View File

@@ -18,7 +18,6 @@ const COMP_ARRAY_CHECK = [
['cc.Toggle', 'isChecked', true]
];
/**
* [VM-Custom]
* 自定义数值监听, 可以快速对该节点上任意一个组件上的属性进行双向绑定
@@ -170,4 +169,4 @@ export class VMCustom extends VMBase {
this._oldValue = this.getComponentValue();
this.onValueController(newValue, oldValue);
}
}
}

View File

@@ -4,7 +4,6 @@ import { EDITOR } from "cc/env";
export class VMEnv {
/** 编辑状态 */
static get editor() {
// @ts-ignore
return EDITOR && !cc.GAME_VIEW;
return EDITOR;
}
}

View File

@@ -35,7 +35,7 @@ export default class VMEvent extends VMBase {
@property({
tooltip: '使用模板模式,可以使用多路径监听'
})
public templateMode: boolean = false;
templateMode: boolean = false;
@property({
tooltip: '监听获取值的路径',
@@ -65,7 +65,7 @@ export default class VMEvent extends VMBase {
tooltip: '过滤模式,会根据条件过滤掉时间的触发',
type: Enum(FILTER_MODE)
})
public filterMode: FILTER_MODE = FILTER_MODE.none;
filterMode: FILTER_MODE = FILTER_MODE.none;
@property({
visible: function () {
@@ -73,7 +73,7 @@ export default class VMEvent extends VMBase {
return this.filterMode !== FILTER_MODE.none
}
})
public compareValue: string = '';
compareValue: string = '';
@property([EventHandler])
changeEvents: EventHandler[] = [];

View File

@@ -186,4 +186,4 @@ export default class VMLabel extends VMBase {
return false;
}
}
}

View File

@@ -146,4 +146,4 @@ export default class VMModify extends VMBase {
if (int) { a = Math.round(a) }
this.VM.setValue(this.watchPath, this.clampValue(a));
}
}
}

View File

@@ -36,6 +36,8 @@ export default class VMParent extends GameComponent {
*/
onLoad() {
if (this.data == null) return;
this.onBind();
this.tag = '_temp' + '<' + this.node.uuid.replace('.', '') + '>';
VM.add(this.data, this.tag);
// log(VM['_mvs'],this.tag)
@@ -47,8 +49,6 @@ export default class VMParent extends GameComponent {
this.replaceVMPath(comp, this.tag)
}
// console.groupEnd()
this.onBind();
}
/**在 onLoad 完成 和 start() 之前调用,你可以在这里进行初始化数据等操作 */
@@ -119,4 +119,4 @@ export default class VMParent extends GameComponent {
super.onDestroy();
}
}
}

View File

@@ -95,4 +95,4 @@ export default class VMProgress extends VMCustom {
this.setComponentValue(value);
}
}
}

View File

@@ -295,4 +295,4 @@ export default class VMState extends VMBase {
return false;
}
}
}

View File

@@ -34,7 +34,7 @@ function getValueFromPath(obj: any, path: string, def?: any, tag: string | null
/**
* ModelViewer 类
*/
class ViewModel<T>{
class ViewModel<T> {
constructor(data: T, tag: string) {
new JsonOb(data, this._callback.bind(this));
this.$data = data;
@@ -149,6 +149,9 @@ class VMManager {
*/
getValue(path: string, def?: any): any {
path = path.trim(); // 防止空格,自动剔除
if (path === '') return '';
let rs = path.split('.');
if (rs.length < 2) { console.error('Get Value Cant find path:' + path); return; };
let vm = this.get(rs[0]);
@@ -215,10 +218,8 @@ class VMManager {
}
}
// 整数、小数、时间、缩写
/**
* VM管理对象,使用文档:
* https://github.com/wsssheep/cocos_creator_mvvm_tools/blob/master/docs/ViewModelScript.md
* https://gitee.com/dgflash/oops-framework/wikis/pages?sort_id=12037849&doc_id=2873565
*/
export let VM = new VMManager();

View File

@@ -45,10 +45,10 @@ export class BhvRollNumber extends Component {
@property({
tooltip: '滚动的目标值'
})
public get targetValue(): number {
get targetValue(): number {
return this._targetValue;
}
public set targetValue(v: number) {
set targetValue(v: number) {
this._targetValue = v;
this.scroll();//数据变动了就开始滚动
}

View File

@@ -13,13 +13,13 @@ export class BhvSwitchPage extends Component {
@property
private _index: number = 0;
public get index(): number {
get index(): number {
return this._index;
}
@property({
type: CCInteger
})
public set index(v: number) {
set index(v: number) {
if (this.isChanging) return;
v = Math.round(v);
let count = this.node.children.length - 1;
@@ -49,7 +49,7 @@ export class BhvSwitchPage extends Component {
private _isChanging: boolean = false;
/**只读是否在changing 的状态 */
public get isChanging(): boolean {
get isChanging(): boolean {
return this._isChanging;
}
@@ -85,7 +85,7 @@ export class BhvSwitchPage extends Component {
showNode.active = true;
}
public next(): boolean {
next(): boolean {
if (this.isChanging) {
return false;
}
@@ -95,7 +95,7 @@ export class BhvSwitchPage extends Component {
}
}
public previous(): boolean {
previous(): boolean {
if (this.isChanging) {
return false;
}
@@ -105,7 +105,7 @@ export class BhvSwitchPage extends Component {
}
}
public setEventIndex(e: any, index: any): boolean {
setEventIndex(e: any, index: any): boolean {
if (this.index >= 0 && this.index != null && this.isChanging === false) {
this.index = index;
return true;
@@ -114,4 +114,4 @@ export class BhvSwitchPage extends Component {
return false;
}
}
}
}

9
assets/libs/network.meta Normal file
View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "c378c808-8d92-4f96-9eb6-a122b5f1716f",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,3 @@
游戏网络库
1. Http
2. WebSocket

View File

@@ -0,0 +1,317 @@
/*
* @Author: dgflash
* @Date: 2022-09-01 18:00:28
* @LastEditors: dgflash
* @LastEditTime: 2022-09-09 18:10:50
*/
import { error, warn } from "cc";
/**
* 使用流程文档可参考、简化与服务器对接、使用新版API体验可进入下面地址获取新版本替换network目录中的内容
* https://store.cocos.com/app/detail/5877
*/
/** 当前请求地址集合 */
var urls: any = {};
/** 请求参数 */
var reqparams: any = {};
type HttpCallback = (ret: HttpReturn) => void;
/** 请求事件 */
export enum HttpEvent {
/** 断网 */
NO_NETWORK = "http_request_no_network",
/** 未知错误 */
UNKNOWN_ERROR = "http_request_unknown_error",
/** 请求超时 */
TIMEOUT = "http_request_timout"
}
/**
* HTTP请求返回值
*/
export class HttpReturn {
/** 是否请求成功 */
isSucc: boolean = false;
/** 请求返回数据 */
res?: any;
/** 请求错误数据 */
err?: any;
}
/** HTTP请求 */
export class HttpRequest {
/** 服务器地址 */
server: string = "http://127.0.0.1/";
/** 请求超时时间 */
timeout: number = 10000;
/** 自定义请求头信息 */
private header: Map<string, string> = new Map<string, string>();
/**
* 添加自定义请求头信息
* @param name 信息名
* @param value 信息值
*/
addHeader(name: string, value: string) {
this.header.set(name, value);
}
/**
* HTTP GET请求
* @param name 协议名
* @param onComplete 请求完整回调方法
* @param params 查询参数
* @example
var param = '{"uid":12345}'
var complete = (ret: HttpReturn) => {
console.log(ret.res);
}
oops.http.getWithParams(name, complete, param);
*/
get(name: string, onComplete: HttpCallback, params: any = null) {
this.sendRequest(name, params, false, onComplete)
}
/**
* HTTP GET请求
* @param name 协议名
* @param params 查询参数
* @example
var txt = await oops.http.getAsync(name);
if (txt.isSucc) {
console.log(txt.res);
}
*/
getAsync(name: string, params: any = null): Promise<HttpReturn> {
return new Promise((resolve, reject) => {
this.sendRequest(name, params, false, (ret: HttpReturn) => {
resolve(ret);
})
});
}
/**
* HTTP GET请求非文本格式数据
* @param name 协议名
* @param onComplete 请求完整回调方法
* @param params 查询参数
*/
getByArraybuffer(name: string, onComplete: HttpCallback, params: any = null) {
this.sendRequest(name, params, false, onComplete, 'arraybuffer', false);
}
/**
* HTTP GET请求非文本格式数据
* @param name 协议名
* @param params 查询参数
* @returns Promise<any>
*/
getAsyncByArraybuffer(name: string, params: any = null): Promise<HttpReturn> {
return new Promise((resolve, reject) => {
this.sendRequest(name, params, false, (ret: HttpReturn) => {
resolve(ret);
}, 'arraybuffer', false);
});
}
/**
* HTTP POST请求
* @param name 协议名
* @param params 查询参数
* @param onComplete 请求完整回调方法
* @example
var param = '{"LoginCode":"donggang_dev","Password":"e10adc3949ba59abbe56e057f20f883e"}'
var complete = (ret: HttpReturn) => {
console.log(ret.res);
}
oops.http.post(name, complete, param);
*/
post(name: string, onComplete: HttpCallback, params: any = null) {
this.sendRequest(name, params, true, onComplete);
}
/**
* HTTP POST请求
* @param name 协议名
* @param params 查询参数
*/
postAsync(name: string, params: any = null): Promise<HttpReturn> {
return new Promise((resolve, reject) => {
this.sendRequest(name, params, true, (ret: HttpReturn) => {
resolve(ret);
});
});
}
/**
* 取消请求中的请求
* @param name 协议名
*/
abort(name: string) {
var xhr = urls[this.server + name];
if (xhr) {
xhr.abort();
}
}
/**
* 获得字符串形式的参数
* @param params 参数对象
* @returns 参数字符串
*/
private getParamString(params: any) {
var result = "";
for (var name in params) {
let data = params[name];
if (data instanceof Object) {
for (var key in data)
result += `${key}=${data[key]}&`;
}
else {
result += `${name}=${data}&`;
}
}
return result.substring(0, result.length - 1);
}
/**
* Http请求
* @param name(string) 请求地址
* @param params(JSON) 请求参数
* @param isPost(boolen) 是否为POST方式
* @param callback(function) 请求成功回调
* @param responseType(string) 响应类型
* @param isOpenTimeout(boolean) 是否触发请求超时错误
*/
private sendRequest(name: string,
params: any,
isPost: boolean,
onComplete: HttpCallback,
responseType?: string,
isOpenTimeout: boolean = true) {
if (name == null || name == '') {
error("请求地址不能为空");
return;
}
var url: string, newUrl: string, paramsStr: string = "";
if (name.toLocaleLowerCase().indexOf("http") == 0) {
url = name;
}
else {
url = this.server + name;
}
if (params) {
paramsStr = this.getParamString(params);
if (url.indexOf("?") > -1)
newUrl = url + "&" + paramsStr;
else
newUrl = url + "?" + paramsStr;
}
else {
newUrl = url;
}
if (urls[newUrl] != null && reqparams[newUrl] == paramsStr) {
warn(`地址【${url}】已正在请求中,不能重复请求`);
return;
}
var xhr = new XMLHttpRequest();
// 防重复请求功能
urls[newUrl] = xhr;
reqparams[newUrl] = paramsStr;
if (isPost) {
xhr.open("POST", url);
}
else {
xhr.open("GET", newUrl);
}
// 添加自定义请求头信息
for (const [key, value] of this.header) {
xhr.setRequestHeader(key, value);
}
// xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8");
// xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8");
var data: any = {};
data.url = url;
data.params = params;
// 请求超时
if (isOpenTimeout) {
xhr.timeout = this.timeout;
xhr.ontimeout = () => {
this.deleteCache(newUrl);
ret.isSucc = false;
ret.err = HttpEvent.TIMEOUT; // 超时
onComplete(data);
}
}
// 响应结果
var ret: HttpReturn = new HttpReturn();
xhr.onloadend = () => {
if (xhr.status == 500) {
this.deleteCache(newUrl);
ret.isSucc = false;
ret.err = HttpEvent.NO_NETWORK; // 断网
onComplete(ret);
}
}
xhr.onerror = () => {
this.deleteCache(newUrl);
ret.isSucc = false;
if (xhr.readyState == 0 || xhr.readyState == 1 || xhr.status == 0) {
ret.err = HttpEvent.NO_NETWORK; // 断网
}
else {
ret.err = HttpEvent.UNKNOWN_ERROR; // 未知错误
}
onComplete(ret);
};
xhr.onreadystatechange = () => {
if (xhr.readyState != 4) return;
this.deleteCache(newUrl);
if (xhr.status == 200 && onComplete) {
ret.isSucc = true;
if (responseType == 'arraybuffer') {
xhr.responseType = responseType; // 加载非文本格式
ret.res = xhr.response;
}
else {
ret.res = JSON.parse(xhr.response);
}
onComplete(ret);
}
};
// 发送请求
if (params == null || params == "") {
xhr.send();
}
else {
xhr.send(paramsStr);
}
}
private deleteCache(url: string) {
delete urls[url];
delete reqparams[url];
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "806e5b7c-51eb-45cb-8b29-9fcf5d9ee6a7",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,92 @@
/*
* @Author: dgflash
* @Date: 2022-09-01 18:00:28
* @LastEditors: dgflash
* @LastEditTime: 2022-09-09 18:31:18
*/
/*
* 网络相关接口定义
*/
export type NetData = (string | ArrayBufferLike | Blob | ArrayBufferView);
export type NetCallFunc = (data: any) => void;
/** 请求协议 */
export interface IRequestProtocol {
/** 协议命令编号 */
cmd: string,
/** 回调方法名 */
callback?: string,
/** 是否压缩 */
isCompress: boolean,
/** 渠道编号 */
channelid: number,
/** 消息内容 */
data?: any;
}
/** 响应协议 */
export interface IResponseProtocol {
/** 响应协议状态码 */
code: number,
/** 数据是否压缩 */
isCompress: boolean,
/** 协议数据 */
data?: any,
/** 协议回调方法名 */
callback?: string
}
/** 回调对象 */
export interface CallbackObject {
target: any, // 回调对象不为null时调用target.callback(xxx)
callback: NetCallFunc, // 回调函数
}
/** 请求对象 */
export interface RequestObject {
buffer: NetData, // 请求的Buffer
rspCmd: string, // 等待响应指令
rspObject: CallbackObject | null, // 等待响应的回调对象
}
/** 协议辅助接口 */
export interface IProtocolHelper {
/** 返回包头长度 */
getHeadlen(): number;
/** 返回一个心跳包 */
getHearbeat(): NetData;
/** 返回整个包的长度 */
getPackageLen(msg: NetData): number;
/** 检查包数据是否合法(避免客户端报错崩溃) */
checkResponsePackage(msg: IResponseProtocol): boolean;
/** 处理请求包数据 */
handlerRequestPackage(reqProtocol: IRequestProtocol): string;
/** 处理响应包数据 */
handlerResponsePackage(respProtocol: IResponseProtocol): boolean;
/** 返回包的id或协议类型 */
getPackageId(msg: IResponseProtocol): string;
}
export type SocketFunc = (event: any) => void;
export type MessageFunc = (msg: NetData) => void;
/** Socket接口 */
export interface ISocket {
onConnected: SocketFunc | null; // 连接回调
onMessage: MessageFunc | null; // 消息回调
onError: SocketFunc | null; // 错误回调
onClosed: SocketFunc | null; // 关闭回调
connect(options: any): any; // 连接接口
send(buffer: NetData): number; // 数据发送接口
close(code?: number, reason?: string): void; // 关闭接口
}
/** 网络提示接口 */
export interface INetworkTips {
connectTips(isShow: boolean): void;
reconnectTips(isShow: boolean): void;
requestTips(isShow: boolean): void;
responseErrorCode(code: number): void;
}

View File

@@ -0,0 +1,11 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "d9f8bf82-57af-45c8-ac27-51d0a33ad69d",
"files": [],
"subMetas": {},
"userData": {
"simulateGlobals": []
}
}

View File

@@ -0,0 +1,148 @@
/*
* @Author: dgflash
* @Date: 2022-09-01 18:00:28
* @LastEditors: dgflash
* @LastEditTime: 2022-09-09 18:10:50
*/
import { CallbackObject, IRequestProtocol, NetData } from "./NetInterface";
import { NetConnectOptions, NetNode } from "./NetNode";
/**
* 使用流程文档可参考、简化与服务器对接、使用新版API体验可进入下面地址获取新版本替换network目录中的内容
* https://store.cocos.com/app/detail/5877
*/
/*
* 网络节点管理类
*/
export class NetManager {
private static _instance: NetManager;
protected _channels: { [key: number]: NetNode } = {};
/** 网络管理单例对象 */
static getInstance(): NetManager {
if (!this._instance) {
this._instance = new NetManager();
}
return this._instance;
}
/**
* 添加网络节点
* @param node 网络节点
* @param channelId 通道编号
* @example
// 游戏服务器心跳协议
class GameProtocol extends NetProtocolPako {
// 自定义心跳协议
getHearbeat(): NetData {
return '{"action":"LoginAction","method":"heart","data":"null","callback":"LoginAction_heart"}';
}
}
var net = new NetNodeGame();
var ws = new WebSock(); // WebSocket 网络连接对象
var gp = new GameProtocol(); // 网络通讯协议对象
var gt = new NetGameTips() // 网络提示对象
net.init(ws, gp, gt);
NetManager.getInstance().setNetNode(net, NetChannelType.Game);
*/
setNetNode(node: NetNode, channelId: number = 0) {
this._channels[channelId] = node;
}
/** 移除Node */
removeNetNode(channelId: number) {
delete this._channels[channelId];
}
/**
* 网络节点连接服务器
* @param options 连接参数
* @param channelId 通道编号
* @example
var options = {
url: 'ws://127.0.0.1:3000',
autoReconnect: 0 // -1 永久重连0不自动重连其他正整数为自动重试次数
}
NetManager.getInstance().connect(options, NetChannelType.Game);
*/
connect(options: NetConnectOptions, channelId: number = 0): boolean {
if (this._channels[channelId]) {
return this._channels[channelId].connect(options);
}
return false;
}
/** 节点连接发送数据*/
send(buf: NetData, force: boolean = false, channelId: number = 0): number {
let node = this._channels[channelId];
if (node) {
return node!.send(buf, force);
}
return -1;
}
/**
* 发起请求,并在在结果返回时调用指定好的回调函数
* @param reqProtocol 请求协议
* @param rspObject 回调对象
* @param showTips 是否触发请求提示
* @param force 是否强制发送
* @param channelId 通道编号
* @example
let protocol: IRequestProtocol = {
action: action,
method: method,
data: JSON.stringify(data),
isCompress: this.isCompress,
channelid: netConfig.channelid
}
return this.request(protocol, rspObject, showTips, force);
*/
request(reqProtocol: IRequestProtocol, rspObject: CallbackObject, showTips: boolean = true, force: boolean = false, channelId: number = 0) {
let node = this._channels[channelId];
if (node) {
node.request(reqProtocol, rspObject, showTips, force);
}
}
/**
* 同request功能一致但在request之前会先判断队列中是否已有rspCmd如有重复的则直接返回
* @param reqProtocol 请求协议
* @param rspObject 回调对象
* @param showTips 是否触发请求提示
* @param force 是否强制发送
* @param channelId 通道编号
* @example
let protocol: IRequestProtocol = {
action: action,
method: method,
data: JSON.stringify(data),
isCompress: this.isCompress,
channelid: netConfig.channelid
}
return this.request(protocol, rspObject, showTips, force);
*/
requestUnique(reqProtocol: IRequestProtocol, rspObject: CallbackObject, showTips: boolean = true, force: boolean = false, channelId: number = 0): boolean {
let node = this._channels[channelId];
if (node) {
return node.requestUnique(reqProtocol, rspObject, showTips, force);
}
return false;
}
/**
* 节点网络断开
* @param code 关闭码
* @param reason 关闭原因
* @param channelId 通道编号
* @example
* NetManager.getInstance().close(undefined, undefined, NetChannelType.Game);
*/
close(code?: number, reason?: string, channelId: number = 0) {
if (this._channels[channelId]) {
return this._channels[channelId].closeSocket(code, reason);
}
}
}

View File

@@ -0,0 +1,11 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "d8cd57a5-e860-464d-84d6-f8df1bf04b89",
"files": [],
"subMetas": {},
"userData": {
"simulateGlobals": []
}
}

View File

@@ -0,0 +1,491 @@
import { error, warn } from "cc";
import { Logger } from "../../core/common/log/Logger";
import { CallbackObject, INetworkTips, IProtocolHelper, IRequestProtocol, ISocket, NetCallFunc, NetData, RequestObject } from "./NetInterface";
/*
* CocosCreator网络节点基类以及网络相关接口定义
* 1. 网络连接、断开、请求发送、数据接收等基础功能
* 2. 心跳机制
* 3. 断线重连 + 请求重发
* 4. 调用网络屏蔽层
*/
type ExecuterFunc = (callback: CallbackObject, buffer: NetData) => void;
type CheckFunc = (checkedFunc: VoidFunc) => void;
type VoidFunc = () => void;
type BoolFunc = () => boolean;
var NetNodeStateStrs = ["已关闭", "连接中", "验证中", "可传输数据"];
/** 网络提示类型枚举 */
export enum NetTipsType {
Connecting,
ReConnecting,
Requesting,
}
/** 网络状态枚举 */
export enum NetNodeState {
Closed, // 已关闭
Connecting, // 连接中
Checking, // 验证中
Working, // 可传输数据
}
/** 网络连接参数 */
export interface NetConnectOptions {
host?: string, // 地址
port?: number, // 端口
url?: string, // url与地址+端口二选一
autoReconnect?: number, // -1 永久重连0不自动重连其他正整数为自动重试次数
}
/** 网络节点 */
export class NetNode {
protected _connectOptions: NetConnectOptions | null = null;
protected _autoReconnect: number = 0;
protected _isSocketInit: boolean = false; // Socket是否初始化过
protected _isSocketOpen: boolean = false; // Socket是否连接成功过
protected _state: NetNodeState = NetNodeState.Closed; // 节点当前状态
protected _socket: ISocket | null = null; // Socket对象可能是原生socket、websocket、wx.socket...)
protected _networkTips: INetworkTips | null = null; // 网络提示ui对象请求提示、断线重连提示等
protected _protocolHelper: IProtocolHelper | null = null; // 包解析对象
protected _connectedCallback: CheckFunc | null = null; // 连接完成回调
protected _disconnectCallback: BoolFunc | null = null; // 断线回调
protected _callbackExecuter: ExecuterFunc | null = null; // 回调执行
protected _keepAliveTimer: any = null; // 心跳定时器
protected _receiveMsgTimer: any = null; // 接收数据定时器
protected _reconnectTimer: any = null; // 重连定时器
protected _heartTime: number = 10000; // 心跳间隔
protected _receiveTime: number = 6000000; // 多久没收到数据断开
protected _reconnetTimeOut: number = 8000000; // 重连间隔
protected _requests: RequestObject[] = Array<RequestObject>(); // 请求列表
protected _listener: { [key: string]: CallbackObject[] | null } = {} // 监听者列表
/********************** 网络相关处理 *********************/
init(socket: ISocket, protocol: IProtocolHelper, networkTips: INetworkTips | null = null, execFunc: ExecuterFunc | null = null) {
Logger.instance.logNet(`网络初始化`);
this._socket = socket;
this._protocolHelper = protocol;
this._networkTips = networkTips;
this._callbackExecuter = execFunc ? execFunc : (callback: CallbackObject, buffer: NetData) => {
callback.callback.call(callback.target, buffer);
}
}
/**
* 请求连接服务器
* @param options 连接参数
*/
connect(options: NetConnectOptions): boolean {
if (this._socket && this._state == NetNodeState.Closed) {
if (!this._isSocketInit) {
this.initSocket();
}
this._state = NetNodeState.Connecting;
if (!this._socket.connect(options)) {
this.updateNetTips(NetTipsType.Connecting, false);
return false;
}
if (this._connectOptions == null && typeof options.autoReconnect == "number") {
this._autoReconnect = options.autoReconnect;
}
this._connectOptions = options;
this.updateNetTips(NetTipsType.Connecting, true);
return true;
}
return false;
}
protected initSocket() {
if (this._socket) {
this._socket.onConnected = (event) => { this.onConnected(event) };
this._socket.onMessage = (msg) => { this.onMessage(msg) };
this._socket.onError = (event) => { this.onError(event) };
this._socket.onClosed = (event) => { this.onClosed(event) };
this._isSocketInit = true;
}
}
protected updateNetTips(tipsType: NetTipsType, isShow: boolean) {
if (this._networkTips) {
if (tipsType == NetTipsType.Requesting) {
this._networkTips.requestTips(isShow);
}
else if (tipsType == NetTipsType.Connecting) {
this._networkTips.connectTips(isShow);
}
else if (tipsType == NetTipsType.ReConnecting) {
this._networkTips.reconnectTips(isShow);
}
}
}
/** 网络连接成功 */
protected onConnected(event: any) {
Logger.instance.logNet("网络已连接")
this._isSocketOpen = true;
// 如果设置了鉴权回调,在连接完成后进入鉴权阶段,等待鉴权结束
if (this._connectedCallback !== null) {
this._state = NetNodeState.Checking;
this._connectedCallback(() => { this.onChecked() });
}
else {
this.onChecked();
}
Logger.instance.logNet(`网络已连接当前状态为【${NetNodeStateStrs[this._state]}`);
}
/** 连接验证成功,进入工作状态 */
protected onChecked() {
Logger.instance.logNet("连接验证成功,进入工作状态");
this._state = NetNodeState.Working;
// 关闭连接或重连中的状态显示
this.updateNetTips(NetTipsType.Connecting, false);
this.updateNetTips(NetTipsType.ReConnecting, false);
// 重发待发送信息
var requests = this._requests.concat();
if (requests.length > 0) {
Logger.instance.logNet(`请求【${this._requests.length}】个待发送的信息`);
for (var i = 0; i < requests.length;) {
let req = requests[i];
this._socket!.send(req.buffer);
if (req.rspObject == null || req.rspCmd != "") {
requests.splice(i, 1);
}
else {
++i;
}
}
// 如果还有等待返回的请求,启动网络请求层
this.updateNetTips(NetTipsType.Requesting, this._requests.length > 0);
}
}
/** 接收到一个完整的消息包 */
protected onMessage(msg: any): void {
// Logger.logNet(`接受消息状态为【${NetNodeStateStrs[this._state]}】`);
var json = JSON.parse(msg);
// 进行头部的校验(实际包长与头部长度是否匹配)
if (!this._protocolHelper!.checkResponsePackage(json)) {
error(`校验接受消息数据异常`);
return;
}
// 处理相应包数据
if (!this._protocolHelper!.handlerResponsePackage(json)) {
if (this._networkTips)
this._networkTips.responseErrorCode(json.code);
}
// 接受到数据,重新定时收数据计时器
this.resetReceiveMsgTimer();
// 重置心跳包发送器
this.resetHearbeatTimer();
// 触发消息执行
let rspCmd = this._protocolHelper!.getPackageId(json);
Logger.instance.logNet(`接受到命令【${rspCmd}】的消息`);
// 优先触发request队列
if (this._requests.length > 0) {
for (let reqIdx in this._requests) {
let req = this._requests[reqIdx];
if (req.rspCmd == rspCmd && req.rspObject) {
Logger.instance.logNet(`触发请求命令【${rspCmd}】的回调`);
this._callbackExecuter!(req.rspObject, json.data);
this._requests.splice(parseInt(reqIdx), 1);
break;
}
}
if (this._requests.length == 0) {
this.updateNetTips(NetTipsType.Requesting, false);
}
else {
Logger.instance.logNet(`请求队列中还有【${this._requests.length}】个请求在等待`);
}
}
let listeners = this._listener[rspCmd];
if (null != listeners) {
for (const rsp of listeners) {
Logger.instance.logNet(`触发监听命令【${rspCmd}】的回调`);
this._callbackExecuter!(rsp, json.data);
}
}
}
protected onError(event: any) {
error(event);
}
protected onClosed(event: any) {
this.clearTimer();
// 执行断线回调返回false表示不进行重连
if (this._disconnectCallback && !this._disconnectCallback()) {
Logger.instance.logNet(`断开连接`);
return;
}
// 自动重连
if (this.isAutoReconnect()) {
this.updateNetTips(NetTipsType.ReConnecting, true);
this._reconnectTimer = setTimeout(() => {
this._socket!.close();
this._state = NetNodeState.Closed;
this.connect(this._connectOptions!);
if (this._autoReconnect > 0) {
this._autoReconnect -= 1;
}
}, this._reconnetTimeOut);
}
else {
this._state = NetNodeState.Closed;
}
}
/**
* 断开网络
* @param code 关闭码
* @param reason 关闭原因
*/
close(code?: number, reason?: string) {
this.clearTimer();
this._listener = {};
this._requests.length = 0;
if (this._networkTips) {
this._networkTips.connectTips(false);
this._networkTips.reconnectTips(false);
this._networkTips.requestTips(false);
}
if (this._socket) {
this._socket.close(code, reason);
}
else {
this._state = NetNodeState.Closed;
}
}
/**
* 只是关闭Socket套接字仍然重用缓存与当前状态
* @param code 关闭码
* @param reason 关闭原因
*/
closeSocket(code?: number, reason?: string) {
if (this._socket) {
this._socket.close(code, reason);
}
}
/**
* 发起请求,如果当前处于重连中,进入缓存列表等待重连完成后发送
* @param buf 网络数据
* @param force 是否强制发送
*/
send(buf: NetData, force: boolean = false): number {
if (this._state == NetNodeState.Working || force) {
return this._socket!.send(buf);
}
else if (this._state == NetNodeState.Checking ||
this._state == NetNodeState.Connecting) {
this._requests.push({
buffer: buf,
rspCmd: "",
rspObject: null
});
Logger.instance.logNet(`当前状态为【${NetNodeStateStrs[this._state]}】,繁忙并缓冲发送数据`);
return 0;
}
else {
error(`当前状态为【${NetNodeStateStrs[this._state]}】,请求错误`);
return -1;
}
}
/**
* 发起请求,并进入缓存列表
* @param reqProtocol 请求协议
* @param rspObject 回调对象
* @param showTips 是否触发请求提示
* @param force 是否强制发送
*/
request<T>(reqProtocol: IRequestProtocol, rspObject: CallbackObject, showTips: boolean = true, force: boolean = false) {
var rspCmd = this._protocolHelper!.handlerRequestPackage(reqProtocol);
this.base_request(reqProtocol, rspCmd, rspObject, showTips, force);
}
/**
* 唯一request确保没有同一响应的请求避免一个请求重复发送netTips界面的屏蔽也是一个好的方法
* @param reqProtocol 请求协议
* @param rspObject 回调对象
* @param showTips 是否触发请求提示
* @param force 是否强制发送
*/
requestUnique(reqProtocol: IRequestProtocol, rspObject: CallbackObject, showTips: boolean = true, force: boolean = false): boolean {
var rspCmd = this._protocolHelper!.handlerRequestPackage(reqProtocol);
for (let i = 0; i < this._requests.length; ++i) {
if (this._requests[i].rspCmd == rspCmd) {
Logger.instance.logNet(`命令【${rspCmd}】重复请求`);
return false;
}
}
this.base_request(reqProtocol, rspCmd, rspObject, showTips, force);
return true;
}
private base_request(reqProtocol: IRequestProtocol, rspCmd: string, rspObject: CallbackObject, showTips: boolean = true, force: boolean = false) {
var buf: NetData = JSON.stringify(reqProtocol); // 转为二进制流发送
if (this._state == NetNodeState.Working || force) {
this._socket!.send(buf);
}
Logger.instance.logNet(`队列命令为【${rspCmd}】的请求,等待请求数据的回调`);
// 进入发送缓存列表
this._requests.push({
buffer: buf, rspCmd, rspObject
});
// 启动网络请求层
if (showTips) {
this.updateNetTips(NetTipsType.Requesting, true);
}
}
/********************** 回调相关处理 *********************/
/**
* 设置一个唯一的服务器推送监听
* @param cmd 命令字串
* @param callback 回调方法
* @param target 目标对象
*/
setResponeHandler(cmd: string, callback: NetCallFunc, target?: any): boolean {
if (callback == null) {
error(`命令为【${cmd}】设置响应处理程序错误`);
return false;
}
this._listener[cmd] = [{ target, callback }];
return true;
}
/**
* 可添加多个同类返回消息的监听
* @param cmd 命令字串
* @param callback 回调方法
* @param target 目标对象
* @returns
*/
addResponeHandler(cmd: string, callback: NetCallFunc, target?: any): boolean {
if (callback == null) {
error(`命令为【${cmd}】添加响应处理程序错误`);
return false;
}
let rspObject = { target, callback };
if (null == this._listener[cmd]) {
this._listener[cmd] = [rspObject];
}
else {
let index = this.getNetListenersIndex(cmd, rspObject);
if (-1 == index) {
this._listener[cmd]!.push(rspObject);
}
}
return true;
}
/**
* 删除一个监听中指定子回调
* @param cmd 命令字串
* @param callback 回调方法
* @param target 目标对象
*/
removeResponeHandler(cmd: string, callback: NetCallFunc, target?: any) {
if (null != this._listener[cmd] && callback != null) {
let index = this.getNetListenersIndex(cmd, { target, callback });
if (-1 != index) {
this._listener[cmd]!.splice(index, 1);
}
}
}
/**
* 清除所有监听或指定命令的监听
* @param cmd 命令字串(默认不填为清除所有)
*/
cleanListeners(cmd: string = "") {
if (cmd == "") {
this._listener = {}
}
else {
delete this._listener[cmd];
}
}
protected getNetListenersIndex(cmd: string, rspObject: CallbackObject): number {
let index = -1;
for (let i = 0; i < this._listener[cmd]!.length; i++) {
let iterator = this._listener[cmd]![i];
if (iterator.callback == rspObject.callback
&& iterator.target == rspObject.target) {
index = i;
break;
}
}
return index;
}
/********************** 心跳、超时相关处理 *********************/
protected resetReceiveMsgTimer() {
if (this._receiveMsgTimer !== null) {
clearTimeout(this._receiveMsgTimer);
}
this._receiveMsgTimer = setTimeout(() => {
warn("接收消息定时器关闭网络连接");
this._socket!.close();
}, this._receiveTime);
}
protected resetHearbeatTimer() {
if (this._keepAliveTimer !== null) {
clearTimeout(this._keepAliveTimer);
}
this._keepAliveTimer = setTimeout(() => {
Logger.instance.logNet("网络节点保持活跃发送心跳信息");
this.send(this._protocolHelper!.getHearbeat());
}, this._heartTime);
}
protected clearTimer() {
if (this._receiveMsgTimer !== null) {
clearTimeout(this._receiveMsgTimer);
}
if (this._keepAliveTimer !== null) {
clearTimeout(this._keepAliveTimer);
}
if (this._reconnectTimer !== null) {
clearTimeout(this._reconnectTimer);
}
}
/** 是否自动重连接 */
isAutoReconnect() {
return this._autoReconnect != 0;
}
/** 拒绝重新连接 */
rejectReconnect() {
this._autoReconnect = 0;
this.clearTimer();
}
}

View File

@@ -0,0 +1,11 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "57f0f07d-d243-4150-9f7c-cb2bbe8f18f1",
"files": [],
"subMetas": {},
"userData": {
"simulateGlobals": []
}
}

View File

@@ -0,0 +1,69 @@
/*
* @Author: dgflash
* @Date: 2022-04-21 13:45:51
* @LastEditors: dgflash
* @LastEditTime: 2022-04-21 13:51:33
*/
import { IProtocolHelper, IRequestProtocol, IResponseProtocol, NetData } from "./NetInterface";
var unzip = function (str: string) {
let charData = str.split('').map(function (x) {
return x.charCodeAt(0);
});
let binData = new Uint8Array(charData);
//@ts-ignore
let data = pako.inflate(binData, { to: 'string' });
return data;
}
var zip = function (str: string) {
//@ts-ignore
let binaryString = pako.gzip(str, { to: 'string' });
return binaryString;
}
/** Pako.js 数据压缩协议 */
export class NetProtocolPako implements IProtocolHelper {
getHeadlen(): number {
return 0;
}
getHearbeat(): NetData {
return "";
}
getPackageLen(msg: NetData): number {
return msg.toString().length;
}
checkResponsePackage(respProtocol: IResponseProtocol): boolean {
return true;
}
handlerResponsePackage(respProtocol: IResponseProtocol): boolean {
if (respProtocol.code == 1) {
if (respProtocol.isCompress) {
respProtocol.data = unzip(respProtocol.data);
}
respProtocol.data = JSON.parse(respProtocol.data);
return true;
}
else {
return false;
}
}
handlerRequestPackage(reqProtocol: IRequestProtocol): string {
var rspCmd = reqProtocol.cmd;
reqProtocol.callback = rspCmd;
if (reqProtocol.isCompress) {
reqProtocol.data = zip(reqProtocol.data);
}
return rspCmd;
}
getPackageId(respProtocol: IResponseProtocol): string {
return respProtocol.callback!;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "88ae0948-8390-4559-bd4e-d44f3f12dc22",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,84 @@
/*
* @Author: dgflash
* @Date: 2021-07-03 16:13:17
* @LastEditors: dgflash
* @LastEditTime: 2022-09-09 17:42:19
*/
import { Logger } from "../../core/common/log/Logger";
import { ISocket, MessageFunc, NetData } from "./NetInterface";
type Connected = (event: any) => void;
/**
* WebSocket 封装
* 1. 连接/断开相关接口
* 2. 网络异常回调
* 3. 数据发送与接收
*/
export class WebSock implements ISocket {
private _ws: WebSocket | null = null; // websocket对象
/** 网络连接成功事件 */
onConnected: ((this: WebSocket, ev: Event) => any) | null = null;
/** 接受到网络数据事件 */
onMessage: MessageFunc | null = null;
/** 网络错误事件 */
onError: ((this: WebSocket, ev: Event) => any) | null = null;
/** 网络断开事件 */
onClosed: ((this: WebSocket, ev: CloseEvent) => any) | null = null;
/** 请求连接 */
connect(options: any) {
if (this._ws) {
if (this._ws.readyState === WebSocket.CONNECTING) {
Logger.logNet("websocket connecting, wait for a moment...")
return false;
}
}
let url = null;
if (options.url) {
url = options.url;
}
else {
let ip = options.ip;
let port = options.port;
let protocol = options.protocol;
url = `${protocol}://${ip}:${port}`;
}
this._ws = new WebSocket(url);
this._ws.binaryType = options.binaryType ? options.binaryType : "arraybuffer";
this._ws.onmessage = (event) => {
let onMessage: MessageFunc = this.onMessage!;
onMessage(event.data);
};
this._ws.onopen = this.onConnected;
this._ws.onerror = this.onError;
this._ws.onclose = this.onClosed;
return true;
}
/**
* 发送数据
* @param buffer 网络数据
*/
send(buffer: NetData): number {
if (this._ws && this._ws.readyState == WebSocket.OPEN) {
this._ws.send(buffer);
return 1;
}
return -1;
}
/**
* 网络断开
* @param code 关闭码
* @param reason 关闭原因
*/
close(code?: number, reason?: string) {
if (this._ws) {
this._ws.close(code, reason);
}
}
}

View File

@@ -0,0 +1,11 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "70df255b-214f-41eb-a16b-fa8a7c14a269",
"files": [],
"subMetas": {},
"userData": {
"simulateGlobals": []
}
}

View File

@@ -0,0 +1,12 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "922e2501-4a6b-4f54-852c-28e24c3fc5b8",
"files": [],
"subMetas": {},
"userData": {
"compressionType": {},
"isRemoteBundle": {}
}
}

3424
assets/libs/network/protocol/pako.min.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,17 @@
{
"ver": "4.0.24",
"importer": "javascript",
"imported": true,
"uuid": "60e799a8-4bb9-4e3d-8b8c-ab86909f9eb8",
"files": [
".js"
],
"subMetas": {},
"userData": {
"isPlugin": true,
"loadPluginInWeb": true,
"loadPluginInNative": false,
"loadPluginInEditor": false,
"loadPluginInMiniGame": true
}
}