EasyBox3Lib.js

// ==UserScript==
const gameConfig = {
    type: "EasyBox3Lib",
    title: "EasyBox3Lib",
    doc: `一个提供了很多实用功能的库。目前已实现:storage缓存&写入队列、物品系统、基于对话框的菜单和翻页、自定义事件等功能。通过安装配置文件来调整其功能。`,
    help: "https://qndm.github.io/EasyBox3Lib",
    file: true,
    isClient: false
}
// ==UserScript==

/**
 * EasyBox3Lib库  
 * 一个适用于大部分地图的通用代码库
 * @module EasyBox3Lib
 * @version 0.1.6
 * @author qndm Nomen
 * @license MIT
 */
/**
 * 配置文件
 */
const CONFIG = require('./EasyBox3Lib.config.js');

if (global.EasyBox3Lib) {
    throw '请勿重复加载 EasyBox3Lib';
}

if (!CONFIG) {
    console.warn('警告:未找到配置文件\n请检查config.js文件');
} else {
    if (global['GameEntity']) {
        console.log('正在自动创建Box3xxx');
        Object.keys(global).forEach(v => {
            if (v.startsWith("Game")) {
                global['Box3' + v.slice(4)] = global[v];
            }
        })
    }
}
/**
 * 空值合并
 * @param {*} a 
 * @param {*} b 
 * @returns {*}
 */
function nullc(a, b) {
    if (a === null || a === undefined)
        return b;
    else return a;
}
const DEBUGMODE = nullc(CONFIG.EasyBox3Lib.debug, false),
    BLACKLIST = nullc(CONFIG.EasyBox3Lib.getFunctionNameBlackList, ['eval', 'getTheCodeExecutionLocation', 'output', 'executeSQLCode', 'tryExecuteSQL', 'throwError']),
    ENABLE_ABBREVIATIONS = nullc(CONFIG.EasyBox3Lib.enableAbbreviations, true);

/**
 * 事件回调函数
 * @callback EventCallBack
 * @param {any} data 传递的参数
 * @returns {void}
 */

/**
 * onTick事件回调函数
 * @callback onTickEventCallBack
 * @returns {void}
 */

/**
 * 对话框回调函数
 * @callback dialogCallBack
 * @param {Box3Entity} entity 打开对话框的实体
 * @param {Box3DialogResponse} value 玩家选择/输入的结果
 * @returns {void}
 */

/**
 * `DataStorage.update`的回调函数
 * @callback dataStorageUpdateCallBack
 * @param {ReturnValue} prevValue 更改前的数据
 * @returns {JSONValue}
 */

/**
 * `onTick`事件令牌
 * @typedef onTickEventToken
 * @private
 * @property {OnTickHandlerToken[]} events 该tick的所有监听器
 * @property {number} timeSpent 该tick预计花费的时间
 */

/**
 * 预处理函数回调函数
 * @callback preprocessCallback
 * @returns {void}
 */

/**
 * 预处理函数令牌
 * @typedef preprocessToken
 * @private
 * @property {eventCallBack} callbackfn 预处理函数
 * @property {number} priority 预处理函数优先级
 */

/**
 * 用于选择/测试物品用的选择器  
 * 可以一次使用多个选择器,使用`,`分隔多个选择器,只需满足任意一个选择器  
 * 前缀为`#`,则检测`id`是否满足  
 * 前缀为`.`,则检测`tag`是否满足  
 * 前缀为`!`,则该选择器结果取反  
 * 添加前缀`&`,代表该选择器必须满足(必须是第一个字符)  
 * 如果为`null`,则检测物品是否为`null`;前缀为`!`时,返回所有非`null`物品  
 * 选择器前后不能有其他字符  
 * `id`的优先级比`tag`高  
 * 不允许有任何无意义的空格和非法字符
 * @typedef {string} ItemSelectorString
 */

/**
 * `ThingStorage.forEach`回调函数
 * @callback thingStorageForEachCallback
 * @param {Thing} thing 物品
 * @param {number} index 编号
 * @param {Thing[]} array 原数组
 */

/**
 * @typedef Output
 * @property {string} type 类型
 * @property {string} data 内容
 * @property {any[]} location 代码执行位置
 */

/**
 * 物品(`Thing`)转换为`string`的结果  
 * 格式为:
 * ```text
 * i[id](|n[name])|s[stackSize]|d[data](|w)
 * ```
 * 例如:  
 * itest1|n测试物品1|s114514|d{}  
 * itest2|s1919810|d{hello: 'world!'}
 * @typedef {string} ThingString
 */

/**
 * Storage任务  
 * 用于Storage Queue
 * @typedef StorageTask
 * @property {"set" | "remove"} type 任务类型
 * @property {string} storageKey 数据储存空间名称
 * @property {string} key 数据键
 * @property {any} value 数据值
 */

/**
 * 物品对话框正文内容
 * @callback ThingDialogCallback
 * @param {Thing} thing 物品
 * @returns {string} 对话框正文内容
 */

/**
 * 储存物品的格子
 * @typedef {?Thing} Tartan
 * @private
 */
/**
 * 物品被使用回调函数
 * @callback ThingUseCallback
 * @async
 * @param {Box3Entity} entity 使用物品的玩家
 * @param {Thing} thing 使用的物品
 * @param {boolean} onlyUpdate 是否只是为了更新穿戴状态而触发(`updateWear`方法)
 */
const
    /**
     * 一些常用的时间单位
     */
    TIME = {
        /**
         * 一秒对应的毫秒数
         */
        SECOND: 1e3,
        /**
         * 一分钟对应的毫秒数
         */
        MINUTE: 6e4,
        /**
         * 一小时对应的毫秒数
         */
        HOUR: 36e5,
        /**
         * 一天对应的毫秒数
         */
        DAY: 864e5,
        /**
         * 一周对应的毫秒数
         */
        WEEK: 6048e5,
        /**
         * 一年对应的毫秒数(按365天算)
         */
        YEAR: 315576e5,
        /**
         * 一`tick`对应的毫秒数(不计误差等意外情况)
         */
        TICK: 64,
        /**
         * 16`tick`对应的毫秒数(不计误差等意外情况)
         */
        SIXTEEN_TICK: 1024
    }, STATUS = {
        NOT_RUNNING: 'notRunning',
        RUNNING: 'running',
        FAILED: 'failed',
        SKIP: 'skip',
        FREE: 'free'
    },
    TRANSLATION_REGEXPS = [['too much recursion', '太多递归'], [/Permission denied to access property "(\w+)"/ig, '尝试访问无权访问的对象:$1'], [/Invalid code point (\w+)/ig, '无效码位:$1'], ['Invalid array length', '无效的数组长度'], ['Division by zero', '除以0'], ['Exponent must be positive', '指数必须为正数'], ['Invalid time value', '非法时间值'], [/(\w+\(\)) argument must be between (\d+) and (\d+)/ig, '$1 的参数必须在 $2 和 $3 之间'], [/(\w+\(\)) (\w+) argument must be between (\d+) and (\d+)/ig, '$1 的 $2 参数必须在 $3 和 $4 之间'], ['Invalid count value', '无效的计数参数'], [/The number (\d.+) cannot be converted to a BigInt because it is not an integer/ig, '数字 $1 不能被转换成 BigInt 类型,因为它不是整数'], [/(\w+) is not defined/ig, '$1 未定义'], [/Cannot access '(\w+)' before initialization/ig, '初始化前无法访问 $1'], [/'(\w+)', '(\w+)', and '(\w+)' properties may not be accessed on strict mode functions or the arguments objects for calls to them/ig, '$1、$2、$3 属性不能在严格模式函数或调用它们的参数对象上访问'], [/(\w+) literals are not allowed in strict mode./ig, '严格模式下不允许使用$1字面量。'], ['Illegal \'use strict\' directive in function with non-simple parameter list', '带有非简单参数列表的函数中的非法 "use strict" 指令'], ['Unexpected reserved word', '意外的保留字'], [/(\S+) loop variable declaration may not have an initializer./ig, '$1语句的变量声明不能有初始化表达式。'], ['Delete of an unqualified identifier in strict mode.', '在严格模式下,无法对标识符调用 "delete"。'], ['Function statements require a function name', '函数声明需要提供函数名称'], ['await is only valid in async functions and the top level bodies of modules', 'await 仅在异步函数和模块的顶层主体中有效'], [/Unexpected token '(\S+)'/ig, '意外标记 $1(不能在不使用括号的情况下混用 "||" 和 "??" 操作)'], ['Illegal continue statement: no surrounding iteration statement', '非法 continue 语句:周围没有迭代语句("continue" 语句只能在封闭迭代语句内使用)'], ['Invalid or unexpected token', '无效或意外的标识符'], ['Invalid left-hand side in assignment', '赋值中的左值无效'], ['Invalid regular expression flags', '正则表达式修饰符无效'], [/Cannot convert (\d.+) to a BigInt/ig, '不能将 $1 转换成 BigInt 类型'], ['Unexpected identifier', '意外标识符'], [/(\w+) is not iterable/ig, '$1 是不可迭代的'], [/(\w+) has no properties/ig, '$1 没有属性'], [/Cannot read properties of (\w+) (reading '(\w+)')/ig, '不能从 $1 中读取属性 $2'], [/(\w+) is not a constructor/ig, '$1 不是构造器'], [/(\w+) is not a function/ig, '$1 不是函数'], [/Property description must be an object: (\w+)/ig, '属性描述必须是一个对象:$1'], [/Cannot assign to read only property '(\w+)' of object '(\S+)'/ig, '无法为对象\'$2\'的只读属性\'$1\'赋值'], [/Cannot create property '(\w+)' on string '(\S+)'/ig, '无法在字符串 \'$2\' 上创建属性 $1'], ['Cannot mix BigInt and other types, use explicit conversions', '不能混合 BigInt 和其他类型,应该使用显式转换'], ['Warning', '警告'], ['Reference', '引用'], ['Type', '类型'], ['Syntax', '语法'], ['Range', '范围'], ['Internal', '内部'], ['Error', '错误'], ['Uncaught', '未捕获的'], [/(at\b)/g, '在'], ['Octal', '八进制'], [/Unexpected/ig, '意外的'], [/Invalid/ig, '无效的'], [/token/ig, '标识符']];

var
    /**
     * 日志内容
     * @type {Output[]}
     * @private
     */
    logs = [],
    /**
     * @type {Map<string, DataStorage>}
     * @private
     */
    dataStorages = new Map(),
    /**
     * @type {Map<string, {statu: "running" | "stop" | "awaiting_deletion" | "awaiting_stop", init: EventCallBack, times: number}>}
     */
    gameLoops = new Map(),
    events = {
        /**@type {OnTickHandlerToken[]} */
        onTick: [],
        /**@type {EventHandlerToken[]} */
        onStart: [],
        /**@type {EventHandlerToken[]} */
        onPlayerJoin: []
    },
    /**
     * @private
     * @type {onTickEventToken[]}
     */
    onTickHandlers = new Array(nullc(CONFIG.EasyBox3Lib.onTickCycleLength, 16)).fill({ events: [], timeSpent: 0 }),
    /**
     * 预处理时调用的函数
     * @private
     * @type {preprocessToken[]}
     */
    preprocessFunctions = [],
    /**
    * 当前周期的第几个tick
    * @private
    */
    tick = 0,
    /**
     * 当前已完成多少个周期
     * @private
     */
    cycleNumber = 0,
    /**
     * 要处理的玩家队列
     * @private
     * @type {Map<string, Box3PlayerEntity>}
     */
    players = new Map(),
    /** 地图是否完全启动(预处理函数执行完成)@type {boolean} */
    started = false,
    /** 物品注册表 @private @type {Map<string, Item>} */
    itemRegistry = new Map(),
    /** 
     * Storage Queue - Storage任务队列
     * 队列只会在`start`方法调用时才会开始,或者手动调用`startStorageQueue`函数
     * @type {Map<string, StorageTask>} 
     */
    storageQueue = new Map(),
    /**@type {boolean} */
    storageQueueStarted = false,
    /**
     * 注册函数类别索引
     * @type {Map<any, Function>}
     */
    registryClassIndex = new Map();
/**
 * 日志信息
 * @private
 */
class Output {
    type = "log";
    data = "";
    location = [];
    /**
     * 定义一条日志信息
     * @param {string} type 类型
     * @param {string} data 内容
     * @param {any[]} location 代码执行位置
     */
    constructor(type, data, location) {
        this.type = type;
        this.data = data;
        this.location = location;
    }
    get file() {
        return this.location[0];
    }
    get line() {
        return this.location[1];
    }
    get lineLocation() {
        return this.location[2];
    }
}
/**
 * 键值对查询列表,用于批量获取键值对,通过 `DataStorage.list` 方法返回。  
 * 列表根据配置项被划分为一个或多个分页,每个分页最多包含 `ListPageOptions` | `pageSize` 个键值对。  
 * 数据以 `DataStorage.list` 方法调用时的数据为准
 * @private
 */
class StorageQueryList {
    /**
     * 数据
     * @type {ReturnValue[]}
     */
    data = [];
    /**
     * 页数
     * @type {number}
     */
    page = 0;
    cursor = 0;
    length = 0;
    /**
     * 分页大小
     * @type {number}
     */
    pageSize = 100;
    constraintTarget = null;
    ascending = false;
    max = null;
    min = null;
    /**
     * 键值对查询列表,用于批量获取键值对,通过 `DataStorage.list` 方法返回。
     * 列表根据配置项被划分为一个或多个分页,每个分页最多包含 `ListPageOptions` | `pageSize` 个键值对。
     * @param {ReturnValue[]} data 数据
     * @param {number} cursor 起始位置
     * @param {number} pageSize 分页大小
     */
    constructor(data, cursor, pageSize = 100, constraintTarget = null, ascending = false, max = null, min = null) {
        this.data = data;
        /**当前页码 @type {number}*/
        this.page = 0;
        this.cursor = cursor;
        this.length = data.length;
        this.pageSize = pageSize;
        this.constraintTarget = constraintTarget;
        this.ascending = ascending;
        this.max = max;
        this.min = min;
        if (constraintTarget) {
            this.data.sort((a, b) => (a[constraintTarget] - b[constraintTarget]) * (this.ascending - 0.5))
            if (this.max !== null)
                this.data = this.data.filter(a => a[constraintTarget] <= this.max);
            if (this.min !== null)
                this.data = this.data.filter(a => a[constraintTarget] >= this.min);
        }
    }
    get isLastPage() {
        return this.length - 1 <= (this.page + 1) * this.pageSize + this.cursor;
    }
    /**
     * 获取当前页的键值对
     */
    getCurrentPage() {
        return this.data.slice(this.page * this.pageSize + this.cursor, (this.page + 1) * this.pageSize + this.cursor);
    }
    /**
     * 翻到下一页
     */
    nextPage() {
        this.page++;
    }
}
/**
 * 代表数据存储空间的类。仅能通过 `getDataStorage` 创建。能够以键值对的形式存储数据,提供方法处理空间内键值对相关的操作。  
 * 和官方的`GameDataStorage`不同,`DataStorage`自带缓存  
 * 也可以直接在Pro编辑器使用
 */
class DataStorage {
    /**
     * 数据储存空间名称 
     * @type {string} 
     */
    key = "";
    /**
     * 数据储存空间缓存
     * @type {Map<string, ResultValue> | undefined}
     * @private
     */
    _data = undefined;
    /**
     * 实际数据储存空间
     * @type {GameDataStorage | undefined}
     * @private
     */
    #gameDataStorage = undefined;
    /**
     * 是否已被完全缓存
     * @type {boolean}
     * @private
     */
    #fullCached = false;
    /**
     * 定义一个DataStorage
     * @param {string} key 空间名称(只读)
     * @param {GameDataStorage} gameDataStorage 对应的`GameDataStorage`
     */
    constructor(key, gameDataStorage) {
        Object.defineProperty(this, 'key', {
            value: key,
            writable: false,
        });
        if (nullc(CONFIG.EasyBox3Lib.enableSQLCache, false))
            this._data = new Map();
        if (nullc(CONFIG.EasyBox3Lib.inArena, false))
            this.#gameDataStorage = gameDataStorage;
    }
    /**
     * 获取指定键对应的值  
     * 如果没有指定的键,返回`undefined`  
     * 注意:非Pro地图`version`将会返回空字符串
     * @async
     * @param {string} key 指定的键
     * @returns {ReturnValue | undefined}
     */
    async get(key) {
        output('log', '获取数据', this.key, ':', key);
        if (nullc(CONFIG.EasyBox3Lib.enableSQLCache, false) && this._data.has(key)) {
            return copyObject(this._data.get(key));
        } else {
            if (!nullc(CONFIG.EasyBox3Lib.inArena, false)) {
                result = await tryExecuteSQL(async () => {
                    return await executeSQLCode(`SELECT * FROM "${encodeURIComponent(this.key)}" WHERE "key" == '${key}'`, '获取数据失败')[0]
                });
                if (result instanceof Array && result.length > 0) {
                    this._data.set(key, result);
                    return copyObject(this._data[key]);
                } else
                    return;
            }
            if (DEBUGMODE)
                output('log', '使用Pro数据库');
            let result = await this.#gameDataStorage.get(key);
            this._data.set(key, result);
            if (DEBUGMODE)
                output('log', this.key, '读取完成,key:', key)
            return result;
        }
    }
    /**
     * 传入指定键与值,无论该键是否存在,均将值设置到此键上。
     * @async
     * @param {string} key 需要设置的键
     * @param {JSONValue} value 需要设置的值
     */
    async set(key, value) {
        output('log', '设置数据', this.key, ':', key, '=', value);
        if (nullc(CONFIG.EasyBox3Lib.inArena, false)) {
            await tryExecuteSQL(async () => {
                var data = await tryExecuteSQL(async () => await this.get(key), '获取数据失败') || { errorInfo: '无数据' };
                this._data.set(key, {
                    metadata: {}, key, value, updateTime: Date.now(), createTime: data.createTime || Date.now(), version: data.version || ""
                });
                await this.#gameDataStorage.set(key, value);
            }, '设置数据失败');
            return;
        } else {
            var data = await tryExecuteSQL(async () => await this.get(key), '获取数据失败');//由于需要更新版本,所以先获取一遍旧数据
            if (data) {
                data.updateTime = Date.now();
                data.value = JSON.stringify(value);
                await tryExecuteSQL(async () => await executeSQLCode(`UPDATE "${encodeURIComponent(this.key)}" SET ("value" = '${sqlAntiInjection(data.value)}', "updateTime" = ${data.updateTime}) WHERE "key" == '${sqlAntiInjection(key)}'`), '更新数据失败');
            } else {
                data = {
                    metadata: {},
                    key,
                    value,
                    createTime: Date.now(),
                    updateTime: Date.now(),
                    version: ""
                };
                await tryExecuteSQL(async () => await executeSQLCode(`INSERT INTO "${encodeURIComponent(this.key)}" ("key", "value", "createTime", "updateTime", "version") VALUES ('${sqlAntiInjection(data.key)}', '${sqlAntiInjection(data.value)}', ${data.createTime}, ${data.updateTime}, '${sqlAntiInjection(data.version)}')`), '插入数据失败');
            }
        }
        output('log', this.key, ':', key, ':', this._data[key].value, '->', data.value);
        this._data.set(key, data);
    }
    /**
     * 使用传入的方法更新键值对
     * @async
     * @param {string} key 需要更新的键
     * @param {dataStorageUpdateCallBack} handler 处理更新的方法,接受一个参数,为当前键的值,返回一个更新后的值
     */
    async update(key, handler) {
        if (nullc(CONFIG.EasyBox3Lib.inArena, false)) {
            await this.#gameDataStorage.update(key, handler);
            return;
        }
        var value = await handler(await this.get(key));
        await this.set(key, value);
    }
    /**
     * 批量获取键值对  
     * ~~注意:该方法不会创建缓存和读取缓存,所以比`get`更慢~~
     * 目前在完全缓存的情况下可以在此使用缓存,需要更改配置文件
     * @param {ListPageOptions} options 批量获取键值对的配置项
     * @returns {QueryList | ReturnValue[]}
     */
    async list(options) {
        output('log', '获取数据', this.key, ':', options.cursor, '-', options.cursor + (options.pageSize || 100));
        if (!nullc(CONFIG.EasyBox3Lib.inArena, false)) {
            if (this.#fullCached && nullc(CONFIG.EasyBox3Lib.enableStorageListCache, false)) {
                let data = [];
                this._data.forEach((value) => data.push(value));
                return new StorageQueryList(data, options.cursor, options.pageSize, options.constraintTarget, options.ascending, options.max, options.min);
            }
            return await tryExecuteSQL(async () => await this.#gameDataStorage.list(options), '获取数据失败');
        } else {
            let data = await tryExecuteSQL(async () => await executeSQLCode(`SELECT * FROM "${this.key}"`), '获取数据失败');
            return new StorageQueryList(data, options.cursor, options.pageSize, options.constraintTarget, options.ascending, options.max, options.min);
        }
    }
    /**
     * 删除键值对
     * @param {string} key 需要删除的键
     */
    async remove(key) {
        output('log', '删除数据', this.key, ':', key);
        this._data.delete(key);
        if (!nullc(CONFIG.EasyBox3Lib.inArena, false))
            await tryExecuteSQL(async () => await executeSQLCode(`DELETE FROM ${this.key} WHERE "key" == '${key}'`), '删除数据失败');
        else
            await this.#gameDataStorage.remove(key);
    }
    /**
     * 删除表格  
     * 警告:删除之后无法恢复,请谨慎使用!!!
     */
    async drop() {
        if (nullc(CONFIG.EasyBox3Lib.inArena, false))
            output("error", '暂不支持Pro地图');
        else {
            output('warn', '删除表', this.key);
            if (CONFIG.EasyBox3Lib.enableSQLCache)
                delete this._data;
            await tryExecuteSQL(async () => await executeSQLCode(`DROP TABLE ${this.key}`));
        }
    }
    /**
     * 创建数据储存空间缓存  
     * 会缓存全部数据  
     * 对于有大量数据的数据库来说,不建议这么做,因为会消耗大量内存
     */
    async createCache() {
        var page = await tryExecuteSQL(async () => await this.list({ cursor: 0 }));
        do {
            let currentPage = page.getCurrentPage();
            currentPage.forEach(data => this._data.set(data.key, data));
            page.nextPage();
        } while (page.isLastPage);
        this.#fullCached = true;
    }
}
/**
 * 菜单
 */
class Menu {
    /**
     * 菜单的标题,同时也作为父菜单选项的标题
     * @type {string} 
     */
    title = "";
    /**
     * 菜单的标题,同时也作为父菜单选项的标题 
     * @type {string[]} 
     */
    content = "";
    /**
     * 该菜单的选项 
     * @type {Menu[]}
     */
    options = [];
    /**
     * 该菜单的父菜单。如果没有,为`undefined` 
     * @type {Menu | undefined} 
     */
    previousLevelMenu = undefined;
    /**
     * 事件监听器 
     */
    handler = {
        /**
         * 当该页被打开时执行的操作 
         * @type {dialogCallBack[]}
         */
        onOpen: [],
        /**
         * 当该菜单被关闭时执行的操作 
         * @type {dialogCallBack[]}
         */
        onClose: []
    };
    /**
     * 创建一个`Menu`
     * @param {string} title 菜单的标题,同时也作为父菜单选项的标题
     * @param {...string} content 菜单正文内容,可以输入多个值,显示时用`\n`分隔
     */
    constructor(title, ...content) {
        this.title = title;
        this.content = content.join('\n');
        this.options = [];
        this.previousLevelMenu = undefined;
        this.handler = {
            onOpen: [],
            onClose: []
        };
    }
    /**
     * 添加子菜单  
     * 返回该菜单本身
     * @param {Menu | Menu[]} submenu 要添加的子菜单
     * @returns {Menu}
     */
    addSubmenu(submenu) {
        var submenus = [];
        if (submenu instanceof Menu)
            submenus = [submenu];
        else
            submenus = submenu;
        for (let menu of submenus) {
            menu.previousLevelMenu = this;
            this.options.push(menu);
        }
        return this;
    }
    /**
     * 打开该菜单  
     * 如果没有任何子菜单,则直接调用`this.handler.onOpen`,并返回`true`  
     * 如果关闭该菜单,则尝试打开上一级菜单
     * @async
     * @param {Box3PlayerEntity} entity 要打开该菜单的玩家
     * @returns {boolean} 是否完成了该菜单。如果关闭了该菜单,返回`false`;否则返回`true`
     */
    async open(entity) {
        if (this.options.length <= 0) {
            await this.handler.onOpen.forEach(async callback => await callback(entity, value))
            return true;
        }
        var value = await selectDialog(entity, this.title, this.content, this.options.map(option => option.title));
        if (value) {
            await this.handler.onOpen.forEach(async callback => await callback(entity, value));
            await this.options[value.index].open(entity);
            return true;
        } else {
            await this.handler.onClose.forEach(async callback => await callback(entity, value));
            if (this.previousLevelMenu) //打开上一级菜单
                this.previousLevelMenu.open(entity);
            return false;
        }
    }
    /**
     * 当该菜单被打开时执行的操作
     * @param {dialogCallBack} handler 当该菜单被打开时执行的操作。
     * @return {Menu} 该菜单本身
     */
    onOpen(handler) {
        this.handler.onOpen.push(handler);
        return this;
    }
    /**
     * 当该菜单被关闭时执行的操作
     * @param {dialogCallBack} handler 当该菜单被关闭时执行的操作。
     * @return {Menu} 该菜单本身
     */
    onClose(handler) {
        this.handler.onClose.push(handler);
        return this;
    }
}
class Pages {
    /**
     * 页标题 
     * @type {string} 
     */
    title = "";
    /**
     * 每页内容 
     * @type {string[]} 
     */
    contents = "";
    /**
     * 当前页码 
     * @type {number} 
     */
    page = 0;
    /**
     * 事件监听器 
     */
    handler = {
        /**
         * 当该页被打开时执行的操作 
         * @type {dialogCallBack[]}
         */
        onOpen: [],
        /**
         * 当该菜单被关闭时执行的操作 
         * @type {dialogCallBack[]}
         */
        onClose: []
    };
    /**
     * 创建一个`Pages`  
     * 用于一段需要分页的文字  
     * ps: 知道为什么叫`Pages`而不是`Page`吗?因为不止一个页!
     * @param {string} title 页的标题
     * @param  {...string} contents 页的内容,每一项就是一页
     */
    constructor(title, ...contents) {
        this.title = title;
        this.contents = contents;
        this.page = 0;
        this.handler = {
            onOpen: [],
            onClose: []
        };
    }
    get isFirstPage() {
        return this.page <= 0;
    }
    get isLastPage() {
        return this.page >= this.contents.length - 1;
    }
    nextPage() {
        if (this.isLastPage)
            output('warn', '已经是最后一页');
        else
            this.page++;
        return this.page;
    }
    previousPage() {
        if (this.isLastPage)
            output('warn', '已经是最后一页');
        else
            this.page++;
        return this.page;
    }
    /**
     * 打开当前页面  
     * 会自动根据玩家的选择来切换到上一页/下一页并打开
     * @async
     * @param {Box3PlayerEntity} entity 要打开该页的玩家
     * @returns {boolean} 是否完成了该页。如果关闭了该页,返回`false`;否则返回`true`
     */
    async open(entity) {
        var value = await selectDialog(entity, this.title, this.contents[this.page], [this.isFirstPage ? '上一页' : '返回', this.isLastPage ? '关闭' : '下一页']);
        if (value) {
            await this.handler.onOpen.forEach(async callback => await callback(entity, value));
            switch (value.value) {
                case '下一页':
                    this.nextPage();
                    await this.open();
                    break;
                case '上一页':
                    this.previousPage();
                    await this.open();
                    break;
            }
            return true;
        } else {
            await this.handler.onClose.forEach(async callback => await callback(entity, value))
            return false;
        }
    }
    /**
     * 当该页被打开时执行的操作
     * @param {dialogCallBack} handler 当该页被打开时执行的操作。
     * @returns {Pages} 自身
     */
    onOpen(handler) {
        this.handler.onOpen.push(handler);
        return this;
    }
    /**
     * 当该页被关闭时执行的操作
     * @param {dialogCallBack} handler 当该页被关闭时执行的操作。
     * @returns {Pages} 自身
     */
    onClose(handler) {
        this.handler.onClose.push(handler);
        return this;
    }
}
/**
 * 事件令牌
 * @private
 */
class EventHandlerToken {
    /**
     * 事件监听器
     * @type {onTickEventCallBack}
     */
    handler = () => { };
    /**
     * 事件状态
     * @type {string}
     */
    statu = STATUS.FREE;
    /**
     * 创建一个事件令牌
     * @param {onTickEventCallBack} handler 监听器
     */
    constructor(handler) {
        this.handler = handler;
        this.statu = STATUS.FREE;
    }
    /**
     * 取消订阅该事件
     */
    cancel() {
        this.statu = STATUS.NOT_RUNNING;
    }
    resume() {
        this.statu = STATUS.FREE;
    }
    async run(data = {}) {
        if (this.statu != STATUS.NOT_RUNNING && (nullc(CONFIG.EasyBox3Lib.disableEventOptimization, false) || this.statu == STATUS.FREE)) {
            this.statu = STATUS.RUNNING;
            await this.handler(Object.assign({ tick: world.currentTick }, data));
            this.statu = STATUS.FREE;
        }
    }
}
/**
 * `onTick`监听器事件令牌
 * @private
 */
class OnTickHandlerToken extends EventHandlerToken {
    /**
     * 每周期运行多少次,最大为`config.EasyBox3Lib.onTickCycleLength`,最小为`1`
     * @type {number}
     */
    tpc = 1;
    /**
     * 性能影响程度
     * @type {number}
     */
    enforcement = 1;
    /**
     * 是否强制运行,如果为true,则会在每个tick都运行
     * @type {boolean}
     */
    enforcement = false;
    /**
     * 定义一个`onTick`监听器事件
     * @param {onTickEventCallBack} handler 监听器
     * @param {number} tpc 每周期运行多少次,最大为`config.EasyBox3Lib.onTickCycleLength`,最小为`1`
     * @param {number} performanceImpact 性能影响程度
     * @param {boolean} enforcement 是否强制运行,如果为true,则会在每个tick都运行
     */
    constructor(handler, tpc, performanceImpact = 1, enforcement = false) {
        super(handler);
        this.tpc = Math.max(Math.min(tpc, nullc(CONFIG.EasyBox3Lib.onTickCycleLength, 16)), 1);
        this.enforcement = enforcement;
        this.performanceImpact = performanceImpact;
    }
    async run() {
        if (this.statu == STATUS.NOT_RUNNING)
            return;
        if (this.enforcement || this.statu == STATUS.FREE) {
            this.statu = STATUS.RUNNING;
            await this.handler({ tick: world.currentTick });
            this.statu = STATUS.FREE;
        }
    }
}
class EntityGroup {
    /**
     * 实体组内的实体
     * @type {Box3Entity[]}
     */
    entities = [];
    /**
     * 实体组中心位置
     * @type {Box3Vector3}
     */
    position = new Box3Vector3(0, 0, 0);
    /**
     * 定义一个实体组  
     * 更改`entities`中每个实体的`indexInEntityGroup`为该实体在实体组内的编号
     * @param {Box3Entity[]} entities 实体组内的实体
     * @param {Box3Vector3} position 实体组中心位置
     */
    constructor(entities, position) {
        this.entities = entities;
        if (position)
            this.position = position;
        else if (this.entities.length > 0) {
            this.position = new Box3Vector3(0, 0, 0);
            for (let entity of this.entities)
                this.position.addEq(entity.position);
            this.position.divEq(new Box3Vector3(this.entities.length, this.entities.length, this.entities.length));
        } else {
            this.position = new Box3Vector3(0, 0, 0);
            output('warn', '实体组未指定中心位置,自动设为', this.position.toString());
        }
        for (let entity of this.entities) {
            this.adjustmentEntityPosition(entity);
        }
    }
    /**
     * 调整实体组中指定实体的位置  
     * 会更改该实体的`position`和`meshOffest`  
     * 更改实体的`offest`属性为该实体调整位置时的`meshOffest`
     * @param {Box3Entity} entity 要调整的实体
     */
    adjustmentEntityPosition(entity) {
        entity.offset = entity.meshOffset.clone();
        entity.meshOffset.addEq(entity.position.sub(this.position).mul(new Box3Vector3(16, 16, 16)));
        entity.position = this.position;
    }
    /**
     * 在实体组内移除实体
     * @param {number} index 该实体在实体组内的编号,一般为`entity.indexInEntityGroup`
     */
    removeEntity(index) {
        var entity = this.entities.filter(entity => entity.indexInEntityGroup == index)[0];
        entity.position = this.position.add(entity.meshOffset).sub(entity.offset);
        entity.meshOffset.copy(entity.offset);
        delete entity.offset;
        this.entities.splice(index, 1);
        this.entities.forEach((entity, index) => entity.indexInEntityGroup = index);
    }
    /**
     * 实体组动画
     * @param {Box3EntityKeyframe[]} keyframes 动画关键帧
     * @param {Box3AnimationPlaybackConfig} playbackInfo 动画配置信息
     */
    animate(keyframes, playbackInfo) {
        var position = this.position.clone();
        for (const keyframe of keyframes)
            if (Object.keys(keyframe).includes('position'))
                position.copy(keyframe.position);
        this.entities.forEach((entity) => {
            entity.animate(keyframes, playbackInfo).onFinish(() => {
                entity.position = position;
            });
        });
        this.position = position;
    }
}

class Item {
    /**
     * 该物品的id
     * @type {string} 
     */
    id = "";
    /**
     * 该物品的显示名称
     * @type {string}
     */
    name = "";
    /**
     * 该物品的最大堆叠数量 
     * @type {number}
     */
    maxStackSize = Infinity;
    /**
     * 该物品的模型  
     * 如果这里未指定且`wearable`类型为`Box3Wearable`,那么会自动读取`wearable.mesh`作为`mesh`
     * @type {string}
     */
    mesh = "";
    /**
     * 该物品的默认数据
     * @type {object}
     */
    data = {};
    /**
     * 该物品的标签
     * @type {string[]}
     */
    tags = [];
    /**
     * 该物品的静态数据
     * @type {object}
     */
    staticData = {};
    /**
     * 该物品的穿戴配件
     * @type {Box3Wearable | boolean}
     */
    wearable = false;
    /**
     * 该物品打开对话框时,物品的默认对话框正文内容
     */
    content = undefined;
    /**
     * 当物品被使用时,调用的函数
     * @type {ThingUseCallback[]}
     */
    _onUse = [];
    /**
     * 当物品被穿戴时,调用的函数
     * @type {ThingUseCallback[]}
     */
    _onWear = [];
    /**
     * 当物品被卸下时,调用的函数
     * @type {ThingUseCallback[]}
     */
    _onDiswear = [];
    /**
     * 当物品被更新穿戴状态时,调用的函数
     * @type {ThingUseCallback[]}
     */
    __onUpdateWear = [];
    /**
     * 定义一**种**物品
     * @param {string} id 该物品的id
     * @param {string} name 该物品的显示名称,默认和id相同
     * @param {string} mesh 该物品的模型。如果这里未指定且`wearable`类型为`Box3Wearable`,那么会自动读取`wearable.mesh`作为`mesh`
     * @param {number} maxStackSize 该物品的最大堆叠数量,默认为`Infinity`。最小值为`1`。当`data`不为空对象时,为`1`.
     * @param {string[]} tags 该物品的标签
     * @param {object} data 该物品的默认数据
     * @param {object} staticData 该物品的静态数据
     * @param {Box3Wearable | boolean} wearable 该实体是否可穿戴。如果可以穿戴,那么填入一个`Box3Wearable`,表示玩家穿戴的部件;如果填入`true`,代表该实体可以穿戴但是没有模型;填入`false`,表示该物品不可穿戴
     * @param {string | ThingDialogCallback} content 该物品打开对话框时,物品的默认对话框正文内容
     */
    constructor(id, name = id, maxStackSize = Infinity, tags = [], data = {}, staticData = {}, wearable = undefined, mesh = '', content = undefined) {
        this.id = encodeURI(id);
        this.name = name;
        this.maxStackSize = Math.max(Object.keys(data).length ? 1 : maxStackSize, 1);
        this.tags = tags.map(tag => encodeURI(tag));
        this.data = data;
        this.staticData = staticData;
        this.wearable = wearable;
        this.mesh = mesh;
        if (!this.mesh && typeof this.wearable == "object" && this.mesh) {
            this.mesh = this.wearable.mesh;
        }
        this.content = content;
        this._onUse = [];
        this._onWear = [];
        this._onDiswear = [];
        this.__onUpdateWear = [];
    }
    /**
     * 当物品被使用时,调用的函数
     * @param {ThingUseCallback} callback 监听器回调函数
     * @returns {Item} 自身
     */
    onUse(callback) {
        this._onUse.push(callback);
        return this;
    }
    /**
     * 当物品被穿戴时,调用的函数
     * @param {ThingUseCallback} callback 监听器回调函数
     * @returns {Item} 自身
     */
    onWear(callback) {
        this._onWear.push(callback);
        return this;
    }
    /**
     * 当物品被卸下时,调用的函数
     * @param {ThingUseCallback} callback 监听器回调函数
     * @returns {Item} 自身
     */
    onDiswear(callback) {
        this._onDiswear.push(callback);
        return this;
    }
    /**
     * 当物品被更新穿戴状态时,调用的函数  
     * 不仅会在`Thing.updateWear`调用时触发,也会在`onWear`、`onDiswear`调用时触发
     * @param {ThingUseCallback} callback 监听器回调函数
     * @returns {Item} 自身
     */
    onUpdateWear(callback) {
        this.__onUpdateWear.push(callback);
        return this;
    }
}
/**
 * 一个(或一组)物品
 */
class Thing {
    /**
     * 物品id 
     * @type {string} 
    */
    id = "";
    /**
     * 物品堆叠数量,如果有数据将不能再堆叠
     * @type {number} 
     */
    stackSize = 1;
    /**
     * **该物品**显示名称 
     * @type {string} 
     */
    name = "";
    /**
     * 物品数据
     * @type {object}
     */
    data = {};
    /**
     * 该物品是否处于穿戴状态
     * @type {boolean}
     */
    _wearing = false;
    /**
     * 定义一个(或一组)物品
     * @param {string} id 物品的id。应该是已经注册的物品id。如果`data`不为空对象时,最大为`1`
     * @param {number} stackSize 该物品的堆叠数量,最大为该物品的最大堆叠数量
     * @param {object} data 该物品的数据
     */
    constructor(id, stackSize = 1, data = {}) {
        if (DEBUGMODE)
            output('log', '创建物品', id, '*', stackSize, 'data: ', JSON.stringify(data));
        if (!itemRegistry.has(encodeURI(id))) {
            throwError('[THING] 未注册的物品:', id, '编码:', encodeURI(id));
        }
        var item = itemRegistry.get(encodeURI(id));
        this.id = encodeURI(id);
        this.stackSize = Math.min(stackSize, item.maxStackSize);
        this.name = item.name;
        this.data = copyObject(item.data);
        this._wearing = false;
        Thing.setData(this, copyObject(data));
        Object.seal(this.data);
    }
    /**
     * 该物品的类型
     * @returns {Item} 
     */
    get item() {
        return itemRegistry.get(this.id);
    }
    /**
     * 该物品的静态数据
     * @returns {object} 
     */
    get staticData() {
        return this.item.staticData;
    }
    /**
     * 该物品的标签
     * @returns {string[]}
     */
    get tags() {
        return this.item.tags;
    }
    /**
     * 该物品的最大堆叠数量 
     * @returns {number} 
     */
    get maxStackSize() {
        return this.item.maxStackSize;
    }
    /**
     * 该物品是否可堆叠 
     * @returns {boolean} 
     */
    get stackable() {
        return Object.keys(this.data) <= 0;
    }
    /**
     * 该物品的穿戴部件
     * 如果为`false`,则该部件不可穿戴
     * @returns {Box3Wearable | true | false}
     */
    get wearable() {
        return this.item.wearable;
    }
    /**
     * 该物品是否处于穿戴状态
     * @returns {boolean} 是否处于穿戴状态
     */
    get wearing() {
        return this._wearing && this.wearable;
    }
    /**
     * 设置物品穿戴状态
     */
    set wearing(value) {
        if (this.wearable)
            this._wearing = value;
    }
    /**
     * 对实体更新穿戴状态  
     * 更新后的实体会显示穿戴部件
     * @param {Box3Entity} entity 要更新的实体
     */
    async updateWear(entity) {
        if (!this.wearable)
            output("warn", '该物品不可穿戴:', decodeURI(this.id));
        if (this.wearable === true)
            return;
        if (this.wearing)
            entity.player.addWearable(this.wearable);
        else
            entity.player.removeWearable(this.wearable);//qndm: 笑死,现在才知道removeWearable填的是穿戴部件而不是编号
        await this.item.__onUpdateWear.forEach(async callback => await callback(entity, this, true));
    }
    /**
     * 测试该物品是否满足选择器的条件
     * @param {ItemSelectorString} selectorString 选择器
     * @returns {boolean}
     */
    testSelector(selectorString) {
        return Thing.testSelector(this, selectorString);
    }
    /**
     * 为一个物品设置`data`
     * @param {Thing} thing 要设置的物品
     * @param {object} data 要设置的数据。如果数据中包含该种物品中没有的键值对或者数据类型不符,将被忽略
     * @returns {Thing}
     */
    static setData(thing, data) {
        var item = itemRegistry.get(thing.id), keys = Object.keys(item.data);
        for (let key in data)
            if (keys.includes(key) && typeof item.data[key] == typeof data[key])
                thing.data = data;
        return thing;
    }
    /**
     * 为该物品设置`data`
     * @param {object} data 要设置的数据。如果数据中包含该种物品中没有的键值对或者数据类型不符,将被忽略
     * @returns {Thing}
     */
    setData(data) {
        var item = this.item, keys = Object.keys(item.data);
        for (let key in data)
            if (keys.includes(key) && typeof item.data[key] == typeof data[key])
                this.data = data;
        return this;
    }
    /**
     * 测试物品是否满足选择器的条件
     * @param {Thing | object} thing 要测试的物品
     * @param {ItemSelectorString} selectorString 选择器
     * @returns {boolean}
     */
    static testSelector(thing, selectorString) {
        var selectors = encodeURI(selectorString).split(','), result = false;
        for (let selector of selectors) {
            let test = false, must = false, not = false, index = 0;
            if (selector[index] == '&') {
                must = true;
                index++;
            }
            if (selector[index] == '!') {
                not = true;
                index++;
            }
            test = (thing === null || thing === undefined) ? selector.substring(index) == 'null' :
                ((selector[index] == '#') ?
                    (thing.id == encodeURI(selector.substring(index + 1))) :
                    (thing.tags.includes(encodeURI(selector.substring(index + 1)))));
            test ^= not;//虽然是number类型,不过无所谓了()
            if (test)
                result = true;
            else if (must) //test为false且must为true时,就没有必要再往下测试了
                break;
        }
        return result;
    }
    /**
     * 从一个`object`中生成一个`Thing`  
     * 这个`object`必须包含`id`、`stackingNumber`、`data`属性  
     * 可以用此方法复制物品
     * @param {object} source 源对象
     * @returns {Thing | null | undefined}
     */
    static from(source) {
        if (source === null)
            return null;
        if (source === undefined)
            return;
        return new Thing(decodeURI(source.id), source.stackingNumber, copyObject(source.data));
    }
    /**
     * 为该物品创建对应的实体
     * @param {Box3Vector3} position 实体位置
     * @param {Box3Vector3} meshScale 实体尺寸
     * @param {boolean} allowedPickups 是否允许拾取
     * @returns {Box3Entity}
     */
    createThingEntity(position, meshScale = nullc(CONFIG.EasyBox3Lib.defaultMeshScale, new Box3Vector3(1 / 16, 1 / 16, 1 / 16)), allowedPickups = true, interactRadius = 4, interactColor = new Box3RGBColor(1, 1, 1)) {
        var entity = createEntity(this.item.mesh, position, true, true, meshScale);
        entity.thing = this;
        if (allowedPickups) {
            entity.enableInteract = true;
            entity.interactHint = this.name;
            entity.interactColor = interactColor;
            entity.interactRadius = interactRadius;
        }
        return entity;
    }
    /**
     * 将物品转换为字符串
     * @returns {ThingString}
     */
    toString() {
        return `i${this.id}${this.name != this.item.name ? ('|n' + this.name) : ''}|s${this.stackSize.toString(16)}|d${encodeURIComponent(JSON.stringify(this.data))}${this.wearing ? 'w' : ''}`;
    }
    /**
     * 从字符串中获取物品数据
     * @param {ThingString} string 物品数据的字符串
     */
    static fromString(string) {
        var /**@type {string[]}*/source = string.split('|'), id = '', name = '', stackSize = 1, data = {}, wearing = false;
        for (const item of source) {
            switch (item[0]) {
                case 'i':
                    id = item.substring(1);
                    break;
                case 'n':
                    name = item.substring(1);
                    break;
                case 's':
                    stackSize = parseInt(item.substring(1), 16);
                    break;
                case 'd':
                    data = JSON.parse(decodeURIComponent(item.substring(1)));
                    break;
                case 'w':
                    wearing = true;
                    break;
            }
        }
        if (!itemRegistry.has(id))
            throwError('未知的物品id', id);
        if (!name)
            name = itemRegistry.get(id).name;
        var thing = new Thing(decodeURI(id), Math.max(Math.min(stackSize, itemRegistry.get(id)).maxStackSize, 1), data);
        thing.wearing = wearing;
        thing.name = name || thing.name;
        return thing;
    }
    /**
     * 打开物品对话框
     * @param {Box3Entity} entity 打开对话框的实体
     * @param {string | ThingDialogCallback} content 对话框正文内容。如果为空,则读取对应`Item`的内容
     * @param {string[]} options 对话框选项
     * @param {Box3SelectDialogParams} otherOptions 可选,对话框的其他选项
     * @returns {Box3DialogSelectResponse} 对话框选择结果
     */
    async dialog(entity, content, options = [], otherOptions = {}) {
        return await selectDialog(entity, this.name, nullc(
            typeof content == "function" ? content(this) : content,
            typeof this.item.content == "function" ? content(this) : content
        ), options, otherOptions);
    }
    /**
     * 使用/穿戴/卸下物品  
     * 需要自行使用`takeOut`取出`ThingStorage`(如果有)的物品
     * @param {Box3Entity} entity 使用物品的玩家
     * @returns {void}
     */
    async use(entity) {
        if (!this.wearable) {
            await this.item._onUse.forEach(async callback => await callback(entity, this, false));
        }
        if (this.wearing) {
            this.wearing = false;
            this.updateWear(entity);
            await this.item._onDiswear.forEach(async callback => await callback(entity, this, false));
        } else {
            this.wearing = true;
            this.updateWear(entity);
            await this.item._onWear.forEach(async callback => await callback(entity, this, false));
        }
    }
}
/**
 * 物品储存空间  
 * 用于存放一些物品  
 * 用于制作背包/箱子等
 */
class ThingStorage {
    /**
     * 储存容量,决定了该储存空间能储存多少组物品
     * @type {number}
     */
    size = 1;
    /**
     * 堆叠数量倍率。默认为1
     * @type {number}
     */
    stackSizeMultiplier = 1;
    /**
     * 黑名单,在黑名单内的物品不能放入该储存空间
     * @type {ItemSelectorString[]}
     */
    blacklist = [];
    /**
     * 物品实际储存空间
     * @type {Tartan[]} 
     */
    thingStorage = [];
    /**
     * 定义一个物品储存空间
     * @param {number} size 储存容量,决定了该储存空间能储存多少组物品
     * @param {number} stackSizeMultiplier 堆叠数量倍率。默认为1
     * @param {ItemSelectorString[]} blacklist 黑名单,在黑名单内的物品不能放入该储存空间
     */
    constructor(size, stackSizeMultiplier = 1, blacklist = []) {
        this.size = size;
        this.stackSizeMultiplier = stackSizeMultiplier;
        this.blacklist = blacklist;
        this.thingStorage = new Array(size).fill(null);
    }
    /**
     * 向该物品储存空间的指定位置放入物品  
     * @param {Tartan} source 要放入的物品
     * @param {number} index 放入物品的位置
     * @returns {?Thing} 剩余的物品。如果没有剩余物品,返回`null`
     */
    putTo(source, index) {
        if (index < 0 || index >= this.size || !Number.isInteger(index)) {
            output("error", '未知的index', index);
            return source;
        }
        /**@type {Thing} */
        var thing = Thing.from(source),
            maxStackSize = thing.maxStackSize * this.stackSizeMultiplier;
        if (thing === null || thing.stackSize <= 0)
            return null;
        for (let itemSelector of this.blacklist)
            if (Thing.testSelector(source, itemSelector))
                return thing;
        if (this.thingStorage[index] === null) {
            if (DEBUGMODE)
                output('log', '[LOG][THINGSTORAGE]', '槽位为空');
            this.thingStorage[index] = Thing.from(thing);
            if (thing.stackSize > maxStackSize) {
                this.thingStorage[index].stackSize = maxStackSize;
                thing.stackSize -= maxStackSize;
                return thing;
            }
            return null;
        }
        if (this.thingStorage[index].id == thing.id && this.thingStorage[index].stackable && thing.stackable) {
            if (DEBUGMODE)
                output('log', '[LOG][THINGSTORAGE]', '槽位不为空');
            if (this.thingStorage[index].stackSize + thing.stackSize <= maxStackSize) {
                this.thingStorage[index].stackSize += thing.stackSize;
                return null;
            }
            let x = maxStackSize - this.thingStorage[index].stackSize;
            this.thingStorage[index].stackSize += x;
            thing.stackSize -= x;
        }
        return thing.stackSize > 0 ? thing : null;
    }
    /**
     * 向该物品储存空间中放入一个(或一些)物品
     * @param {...?Thing} source 要放入的物品
     * @returns {Thing[]} 剩余的物品
     */
    putInto(...source) {
        if (DEBUGMODE)
            output('log', '[LOG][THINGSTORAGE] 放入', source.length, '件物品');
        /**@type {Thing[]} */
        var things = source.filter(thing => thing !== null && thing !== undefined && thing.id !== undefined).map(thing => Thing.from(thing)), surplus = [];
        for (let thing of things) {
            if (DEBUGMODE)
                output('log', '[LOG][THINGSTORAGE] 放入物品', decodeURI(thing.id), '*', thing.stackSize);
            if (thing.stackSize <= 0 || thing === undefined) {
                if (DEBUGMODE)
                    output('warn', '[WARN][THINGSTORAGE] 物品无效');
                continue;
            }
            for (let i = 0; i < this.size; i++) {
                thing = this.putTo(thing, i);
                if (thing === null || thing.stackSize <= 0)
                    break;
            }
            if (thing !== null && thing.stackSize > 0)
                surplus.push(thing);
        }
        return surplus;
    }
    /**
     * 从该物品储存空间中拿取物品
     * @param {number} index 拿取物品的位置
     * @param {number} quantity 拿取数量
     * @returns {?Thing} 成功拿取的物品
     */
    takeOut(index, quantity = 1) {
        if (!Number.isInteger(index) || index < 0 || index >= this.size) {
            output("error", '未知的index', JSON.stringify(index), '类型', typeof index);
            return null;
        }
        /**@type {?Thing} */
        var thing = Thing.from(this.thingStorage[index]);
        if (thing === null)
            return null;
        var x = Math.min(quantity, this.thingStorage[index].stackSize);
        thing.stackSize = x;
        this.thingStorage[index].stackSize -= x;
        if (this.thingStorage[index].stackSize <= 0)
            this.thingStorage[index] = null;
        return thing;
    }
    /**
     * 整理该物品储存空间  
     * 同时清除数量小于`0`的物品
     */
    collation() {
        var oldThingStorage = copyObject(this.thingStorage.filter(thing => thing.stackSize > 0));
        this.thingStorage.fill(null);
        this.putInto(...oldThingStorage);
    }
    /**
     * 检查物品是否在该物品储存空间中  
     * 会忽略`null`和数量小于`0`的物品
     * @param {ItemSelectorString} selector 一个物品选择器
     * @returns {boolean}
     */
    includes(selector) {
        for (let index in this.thingStorage) {
            if (this.thingStorage[index] === null || this.thingStorage[index].stackSize <= 0)
                continue;
            if (Thing.testSelector(this.thingStorage[index], selector))
                return true;
        }
        return false;
    }
    /**
     * 搜索满足条件的第一个物品的位置  
     * 如果没有搜索到,返回`-1`  
     * 会忽略`null`和数量小于`0`的物品
     * @param {ItemSelectorString} selector 一个物品选择器
     * @returns {number}
     */
    querySelector(selector) {
        for (let index in this.thingStorage) {
            if (this.thingStorage[index] === null || this.thingStorage[index].stackSize <= 0)
                continue;
            if (Thing.testSelector(this.thingStorage[index], selector))
                return Number(index);
        }
        return -1;
    }
    /**
     * 搜索满足条件的所有物品的位置,返回一个列表  
     * 会忽略`null`和数量小于`0`的物品
     * @param {ItemSelectorString} selector 一个物品选择器
     * @returns {number[]}
     */
    querySelectorAll(selector) {
        var result = [];
        for (let index in this.thingStorage) {
            if (this.thingStorage[index] === null || this.thingStorage[index].stackSize <= 0)
                continue;
            if (Thing.testSelector(this.thingStorage[index], selector))
                result.push(Number(index));
        }
        return result;
    }
    /**
     * 对该物品储存空间中的每个物品调用给定的函数  
     * 会忽略`null`
     * @param {thingStorageForEachCallback} callbackfn 回调函数
     */
    forEach(callbackfn) {
        this.thingStorage.forEach((thing, index, array) => {
            if (thing !== null)
                callbackfn(thing, index, array);
        });
    }
    /**
     * 为指定玩家打开该物品储存空间的对话框
     * @async
     * @param {Box3PlayerEntity} entity 打开对话框的实体
     * @param {string} title 对话框的标题
     * @param {string} content 对话框的正文
     * @param {ItemSelectorString} filter 用来筛选的选择器,默认为`!null`
     * @param {object} otherOptions 对话框的其他选项
     * @returns {number | null} 玩家选择的物品在该物品储存空间的位置。没有选择,返回`null`
     */
    async dialog(entity, title, content, filter = '!null', otherOptions) {
        var arr = [];
        this.querySelectorAll(filter).forEach(index => arr.push([index, this.thingStorage[index].name]));
        var result = await selectDialog(entity, title, content, arr.map(x => x[1]), otherOptions);
        if (result)
            return arr[result.index][0];
        else
            return null;
    }
    /**
     * 将物品储存空间转换成字符串  
     * 空物品和数量小于0的物品会被忽略
     * @returns {string} 转换结果
     */
    toString() {
        return this.thingStorage.filter(thing => thing !== null && thing.stackSize > 0).map(thing => thing.toString()).join(',');
    }
    /**
     * 从字符串中读取物品储存空间
     * @param {string} str 要读取的字符串
     * @param {number} size 储存空间大小。如果存储空间不能放下,则剩余的数据会被舍弃
     * @param {number} stackSizeMultiplier 堆叠数量倍率。默认为1
     */
    static fromString(str, size, stackSizeMultiplier = 1) {
        var thingStorage = new ThingStorage(size, stackSizeMultiplier),
            things = (str ? str.split(',') : []).filter(thing => thing).map(thing => Thing.fromString(thing));
        if (things.length)
            thingStorage.putInto(...things);
        return thingStorage;
    }
}
/**
 * 复制一个`object`
 * @param {any} obj 要复制的`object`
 * @returns {any}
 */
function copyObject(obj) {
    if (typeof obj !== 'object')
        return obj;
    var newObj = newObj instanceof Array ? [] : {};
    for (let key in obj) {
        newObj[key] = copyObject(obj[key]);
    }
    return newObj;
}
/**
 * 使用实体ID获取一个实体
 * @param {string} id 实体ID
 * @returns {Box3Entity}
 */
function getEntity(id) {
    var entity = world.querySelector('#' + id);
    if (entity) return entity;
    else output('warn', '错误:没有ID为', id, '的实体');
}
/**
 * 使用实体标签获取一组实体
 * @param {string} tag 实体标签
 * @returns {Box3Entity[]}
 */
function getEntities(tag) {
    var entities = world.querySelectorAll('.' + tag);
    if (entities.length > 0) return entities;
    else output('warn', '错误:没有标签为', tag, '的实体');
}
/**
 * 通过玩家的昵称/BoxID/userKey找到一个玩家  
 * 如果没有找到,返回`undefined`  
 * 也可以在key参数中填入其他的东西,但不建议,因为可能有多个玩家满足该条件  
 * 判断方法:
 * ```javascript
 * entity.player[key] == value
 * ```
 * @param {*} value 见上
 * @param {string} key 见上,一般填入`name`、`boxid`、`userKey`,默认为`name`
 * @returns {Box3PlayerEntity | undefined}
 */
function getPlayer(value, key = 'name') {
    return world.querySelectorAll('player').filter(entity => entity.player[key] == value)[0];
}
function getAllLogs() {
    return copyObject(logs);
}
/**
 * 获取当前代码的执行位置
 * @returns {{locations: string[], functions: string[]}}
 */
function getTheCodeExecutionLocation() {
    var stack = new Error().stack.match(/[\w$]+ \([\w$]+\.js:\d+:\d+\)/g);
    if (!stack) return { locations: ['unknown:-1:-1'], functions: ['unknown'] };
    var locations = stack.map(location => location.match(/[\w$]+\.js:\d+:\d+/g)[0]),
        functions = stack.map(location => location.split(' ')[0]);
    return { locations, functions };
}
/**
 * 输出一段日志,并记录到日志文件中
 * @param {'log' | 'warn' | 'error'} type 输出类型
 * @param {...string} data 要输出的内容
 * @returns {string}
 */
function output(type, ...data) {
    let str = data.join(' ');
    if (nullc(CONFIG.EasyBox3Lib.getCodeExecutionLocationOnOutput, true)) {
        let locations = getTheCodeExecutionLocation();
        let location = (locations.locations.filter((location, index) => !BLACKLIST.includes(locations.functions[index]))[0] || (locations.locations[1] || locations.locations[0])).split(':');
        console[type](`(${location[0]}:${location[1]}) -> ${locations.functions.filter(func => !BLACKLIST.includes(func))[0] || 'unknown'}`, nullc(CONFIG.EasyBox3Lib.enableAutoTranslation, false) ? translationError(str) : str);
        if (nullc(CONFIG.EasyBox3Lib.automaticLoggingOfOutputToTheLog, true) && (!nullc(CONFIG.EasyBox3Lib.logOnlyWarningsAndErrors, false) || type == 'warn' || type == 'error'))
            logs.push(new Output(type, str, locations));
        return `(${location[0]}:${location[1]}) [${type}] ${str}`;
    } else {
        console[type](str);
        if (nullc(CONFIG.EasyBox3Lib.automaticLoggingOfOutputToTheLog, true) && !nullc(CONFIG.EasyBox3Lib.logOnlyWarningsAndErrors, false))
            logs.push(new Output(type, str, ['unknown', -1, -1]));
        return `[${type}] ${str}`;
    }
}
/**
 * 抛出错误
 * @param {...string} data 错误内容
 */
function throwError(...data) {
    let str = data.join(' ');
    if (nullc(CONFIG.EasyBox3Lib.getCodeExecutionLocationOnOutput, true)) {
        let locations = getTheCodeExecutionLocation();
        if (nullc(CONFIG.EasyBox3Lib.automaticLoggingOfOutputToTheLog, true) && !nullc(CONFIG.EasyBox3Lib.logOnlyWarningsAndErrors, false))
            logs.push(new Output('error', str, locations));
        throw `[THROW] ${nullc(CONFIG.EasyBox3Lib.enableAutoTranslation, false) ? translationError(str) : str} ` + locations.locations.map((loc, index) => `在 ${loc} -> ${locations.functions[index]}`).join(', ');
    } else {
        if (nullc(CONFIG.EasyBox3Lib.automaticLoggingOfOutputToTheLog, true) && !nullc(CONFIG.EasyBox3Lib.logOnlyWarningsAndErrors, false))
            logs.push(new Output('error', str, ['unknown', -1, -1]));
        throw `[THROW] ${nullc(CONFIG.EasyBox3Lib.enableAutoTranslation, false) ? translationError(str) : str}`;
    }
}
/**
 * 通过管理员列表,判断一个玩家是不是管理员  
 * 注意:对于在运行过程中添加的管理员(即`entity.player.isAdmin`为`true`但管理员列表中没有该玩家的`userKey`,返回`false`  
 * 对于这种情况,应该使用:
 * ```javascript
 * entity.player.isAdmin
 * ```
 * @param {Box3PlayerEntity} entity 要判断的实体
 * @returns {boolean}
 */
function isAdmin(entity) {
    return nullc(CONFIG.admin, []).includes(entity.player.userKey);
}
/**
 * 设置一个玩家是否是管理员
 * @param {Box3PlayerEntity} entity 要设置的玩家
 * @param {boolean} type 是否设置为管理员
 */
function setAdminStatus(entity, type) {
    output('log', '管理员', `${type ? '' : '取消'}设置玩家 ${entity.player.name} (${entity.player.userKey}) 为管理员`);
    entity.player.isAdmin = type;
}
/**
 * 缩放一个玩家,包括玩家的移动速度&跳跃高度
 * @param {Box3PlayerEntity} entity 要缩放的玩家
 * @param {number} size 缩放尺寸
 */
function resizePlayer(entity, size) {
    entity.player.scale *= size;
    entity.player.jumpPower *= size;
    entity.player.jumpAccelerationFactor *= size;
    entity.player.doubleJumpPower *= size;
    entity.player.walkSpeed *= size;
    entity.player.walkAcceleration *= size;
    entity.player.runSpeed *= size;
    entity.player.runAcceleration *= size;
    entity.player.crouchSpeed *= size;
    entity.player.crouchAcceleration *= size
    entity.player.swimSpeed *= size;
    entity.player.swimAcceleration *= size;
    output('log', `缩放玩家尺寸 ${entity.player.name} (${entity.player.userKey}) 为 ${size} `);
}
/**
 * 简单创建一个实体(其实也没简单到哪去)
 * @param {string} mesh 实体外形
 * @param {Box3Vector3} position 实体位置
 * @param {boolean} collides 实体是否可碰撞
 * @param {boolean} gravity 实体是否会下落
 * @param {Box3Vector3} meshScale 实体的缩放比例
 * @param {Box3Quaternion} meshOrientation 实体的旋转角度
 * @returns {Box3Entity | null}
 */
function createEntity(mesh, position, collides, gravity, meshScale = nullc(CONFIG.EasyBox3Lib.defaultMeshScale, new Box3Vector3(1 / 16, 1 / 16, 1 / 16)), meshOrientation = nullc(CONFIG.EasyBox3Lib.defaultMeshOrientation, new Box3Quaternion(0, 0, 0, 1))) {
    if (world.entityQuota() >= 1) {
        if (DEBUGMODE)
            output('log', '创建实体', mesh, position, collides, gravity);
        if (world.entityQuota() <= nullc(CONFIG.EasyBox3Lib.numberOfEntitiesRemainingToBeCreatedForSecurity, 500))
            output('warn', '实体创建超出安全上限', `剩余可创建实体数量:${world.entityQuota()} `);
        return world.createEntity({
            mesh,
            position,
            collides,
            fixed: !gravity,
            gravity,
            meshScale,
            meshOrientation,
            friction: 1
        });
    } else {
        throwError('实体创建失败', '实体数量超过上限');
        return null;
    }
}
/**
 * 弹出一个/若干个文本对话框
 * @async
 * @param {Box3PlayerEntity} entity 要弹出对话框的玩家
 * @param {string} title 对话框的标题
 * @param {string | string[]} content 对话框的正文,也可以输入一个列表来实现多个对话框依次弹出(相当于一个低配版的`Pages`)
 * @param {'auto' | boolean} hasArrow 是否显示箭头提示,`auto`表示自动
 * @param {object} otherOptions 对话框的其他选项
 * @returns {'success' | number | null} 如果完成了所有对话,则返回`success`(只有一个对话框)或者完成对话框的数量(有多个对话框);否则返回`null`(只有一个对话框)
 */
async function textDialog(entity, title, content, hasArrow = nullc(CONFIG.EasyBox3Lib.defaultHasArrow, 'auto'), otherOptions = nullc(CONFIG.EasyBox3Lib.defaultDialogOtherOptions, {})) {
    if (typeof content == "string") {
        return await entity.player.dialog(Object.assign({
            type: Box3DialogType.TEXT,
            content,
            title,
            hasArrow: typeof hasArrow == "boolean" ? hasArrow : false,
            titleTextColor: nullc(CONFIG.EasyBox3Lib.defaultTitleTextColor, new Box3RGBAColor(0, 0, 0, 1)),
            titleBackgroundColor: nullc(CONFIG.EasyBox3Lib.defaultTitleBackgroundColor, new Box3RGBAColor(0.968, 0.702, 0.392, 1)),
            contentTextColor: nullc(CONFIG.EasyBox3Lib.defaultContentTextColor, new Box3RGBAColor(0, 0, 0, 1)),
            contentBackgroundColor: nullc(CONFIG.EasyBox3Lib.defaultContentBackgroundColor, new Box3RGBAColor(1, 1, 1, 1))
        }, otherOptions));
    } else {
        var cnt = 0, length = content.length - 1;
        for (let index in content) {
            let result = await entity.player.dialog(Object.assign({
                type: Box3DialogType.TEXT,
                content: content[index],
                title,
                hasArrow: index < length,
                titleTextColor: nullc(CONFIG.EasyBox3Lib.defaultTitleTextColor, new Box3RGBAColor(0, 0, 0, 1)),
                titleBackgroundColor: nullc(CONFIG.EasyBox3Lib.defaultTitleBackgroundColor, new Box3RGBAColor(0.968, 0.702, 0.392, 1)),
                contentTextColor: nullc(CONFIG.EasyBox3Lib.defaultContentTextColor, new Box3RGBAColor(0, 0, 0, 1)),
                contentBackgroundColor: nullc(CONFIG.EasyBox3Lib.defaultContentBackgroundColor, new Box3RGBAColor(1, 1, 1, 1))
            }, otherOptions));
            if (result == 'success') cnt++;
        }
        return cnt;
    }
}
/**
 * 弹出一个输入对话框
 * @async
 * @param {Box3PlayerEntity} entity 要弹出对话框的玩家
 * @param {string} title 对话框的标题
 * @param {string} content 对话框的正文
 * @param {undefined | string} confirmText 可选,确认按钮文字
 * @param {undefined | string} placeholder 可选,输入框提示文字
 * @param {object} otherOptions 对话框的其他选项
 * @returns {string | null} 输入框填写的内容字符串
 */
async function inputDialog(entity, title, content, confirmText = undefined, placeholder = undefined, otherOptions = CONFIG.EasyBox3Lib.defaultDialogOtherOptions) {
    return await entity.player.dialog(Object.assign({
        type: Box3DialogType.INPUT,
        content,
        title,
        confirmText,
        placeholder,
        titleTextColor: nullc(CONFIG.EasyBox3Lib.defaultTitleTextColor, new Box3RGBAColor(0, 0, 0, 1)),
        titleBackgroundColor: nullc(CONFIG.EasyBox3Lib.defaultTitleBackgroundColor, new Box3RGBAColor(0.968, 0.702, 0.392, 1)),
        contentTextColor: nullc(CONFIG.EasyBox3Lib.defaultContentTextColor, new Box3RGBAColor(0, 0, 0, 1)),
        contentBackgroundColor: nullc(CONFIG.EasyBox3Lib.defaultContentBackgroundColor, new Box3RGBAColor(1, 1, 1, 1))
    }, otherOptions));
}
/**
 * 弹出一个选项对话框
 * @async
 * @param {Box3PlayerEntity} entity 要弹出对话框的玩家
 * @param {string} title 对话框的标题
 * @param {string} content 对话框的正文
 * @param {string[]} options 选项列表
 * @param {object} otherOptions 对话框的其他选项
 * @returns {Box3DialogSelectResponse} 玩家选择的选项
 */
async function selectDialog(entity, title, content, options, otherOptions = CONFIG.EasyBox3Lib.defaultDialogOtherOptions) {
    return await entity.player.dialog(Object.assign({
        type: Box3DialogType.SELECT,
        content,
        title,
        options: options.map(text => {
            if (typeof text != "string") {
                return JSON.stringify(text);
            }
            return text;
        }),
        titleTextColor: nullc(CONFIG.EasyBox3Lib.defaultTitleTextColor, new Box3RGBAColor(0, 0, 0, 1)),
        titleBackgroundColor: nullc(CONFIG.EasyBox3Lib.defaultTitleBackgroundColor, new Box3RGBAColor(0.968, 0.702, 0.392, 1)),
        contentTextColor: nullc(CONFIG.EasyBox3Lib.defaultContentTextColor, new Box3RGBAColor(0, 0, 0, 1)),
        contentBackgroundColor: nullc(CONFIG.EasyBox3Lib.defaultContentBackgroundColor, new Box3RGBAColor(1, 1, 1, 1))
    }, otherOptions));
}
/**
 * 打乱一个列表
 * @param {any[]} oldList 要打乱的列表
 * @returns {any[]}
 */
function shuffleTheList(oldList) {
    var list = [];
    while (oldList.length > 0) {
        let index = random(0, oldList.length, true);
        list.push(oldList[index]);
    }
    return list;
}
/**
 * 随机生成一个数
 * @param {number} min 生成范围的最小值
 * @param {number} max 生成范围的最大值
 * @param {boolean} integer 是否生成一个整数
 * @returns {number}
 */
function random(min = 0, max = 1, integer = false) {
    var result = min + (max - min) * Math.random();
    if (integer)
        return Math.round(result);
    else
        return result;
}
/**
 * 将Unix时间戳转换为中文日期
 * @example
 * toChineseDate(Date.now())
 * @param {number} date Unix时间戳
 * @return {string}
 */
function toChineseDate(date) {
    var time = new Date(date + 28800 * 1000);
    var chineseDate = `${time.getFullYear()} 年 ${time.getMonth() + 1} 月 ${time.getDate()} 日 ${time.getHours()} 时 ${time.getMinutes()} 分 ${time.getSeconds()} 秒`;
    return chineseDate;
}
/**
 * 执行一段SQL命令  
 * 仅在旧岛中使用
 * @async
 * @param {string} code 
 * @returns {any}
 */
async function executeSQLCode(code) {
    output('log', `执行SQL命令 ${code} `);
    var result = await db.sql([code]);
    output('log', `SQL运行结果:${JSON.stringify(result)} `);
    return result;
}
/**
 * SQL防注入  
 * 对输入的值进行加工,防止SQL注入
 * @param {string} value 
 * @private
 * @returns {any}
 */
function sqlAntiInjection(value) {
    if (!typeof value != "string") {
        return value;
    }
    var result = '';
    for (let char of value) {
        if (['\'', '\\'].includes(char)) {
            result += `\\${char}`;
        } else result += char;
    }
    return result;
}
/**
 * 尝试执行SQL代码
 * @param {Function} func 要执行的代码
 * @param {string} msg 当代码执行失败时,输出的信息
 */
async function tryExecuteSQL(func, msg = '') {
    var count = 0, lastError = '', data;
    while (count <= nullc(CONFIG.EasyBox3Lib.maximumDatabaseRetries, 5))
        try {
            data = await func();
            return data;
        } catch (error) {
            output("warn", msg, error);
            count++;
            lastError = error;
        }
    if (count > nullc(CONFIG.EasyBox3Lib.maximumDatabaseRetries, 5)) {
        throwError(msg, lastError);
    }
}
/**
 * 连接指定数据存储空间,如果不存在则创建一个新的空间。
 * @async
 * @param {string} key 指定空间的名称,长度不超过50个字符
 * @returns {DataStorage}
 * @example
 * (async () => {
 *     await EasyBox3Lib.storage.getDataStorage('test');
 * })();
 */
async function getDataStorage(key) {
    if (dataStorages.get(key)) {
        output('log', '检测到已连接的数据存储空间', key);
        return dataStorages.get(key);
    }
    output('log', '连接数据存储空间', key);
    var gameDataStorage;
    if (nullc(CONFIG.EasyBox3Lib.inArena, false))
        gameDataStorage = await tryExecuteSQL(() => storage.getDataStorage(key), '数据存储空间连接失败');
    else
        await tryExecuteSQL(async () => await executeSQLCode(`CREATE TABLE IF NOT EXISTS "${key}"("key" TEXT PRIMARY KEY, "value" TEXT NOT NULL, "version" TEXT NOT NULL, "updateTime" INTEGER NOT NULL, "createTime" INTEGER NOT NULL)`), '数据存储空间连接失败');
    dataStorages.set(key, new DataStorage(key, gameDataStorage))
    return dataStorages.get(key);
}
/**
 * 在缓存中直接获取指定数据存储空间  
 * 比`getDataStorage`更快,但不能创建数据存储空间
 * @param {string} storageKey 指定数据存储空间名称
 * @returns {DataStorage}
 * @example
 * await EasyBox3Lib.storage.getDataStorage('test');
 * var playerStorage = EasyBox3Lib.storage.getDataStorageInCache('test');
 */
function getDataStorageInCache(storageKey) {
    if (!dataStorages.has(storageKey)) {
        throwError('未找到数据储存空间', storageKey);
    }
    return dataStorages.get(storageKey);
}
/**
 * 设置一个键值对  
 * 与`DataStorage.set`方法不同,该方法调用后不会立即写入数据,而是移动到`Storage Queue`中  
 * 建议添加 `await`
 * @async
 * @param {string} storageKey 指定空间的名称,不需要提前获取空间
 * @param {string} key 需要设置的键
 * @param {string} value 需要设置的值
 * @example
 * await EasyBox3Lib.storage.setData('test', entity.player.userKey, {
 *     money: entity.player.money,
 *     experience: entity.player.experience,
 *     itemList: entity.player.itemList.toString(),
 * });
 */
async function setData(storageKey, key, value) {
    /**@type {StorageTask} */
    var task = { type: "set", storageKey, key, value }, dataStorage = await getDataStorage(storageKey);
    var data = await dataStorage.get(key) || { errorInfo: '无数据' };
    if (CONFIG.EasyBox3Lib.enableSQLCache) {
        dataStorage._data.set(key, {
            key, value, updateTime: Date.now(), createTime: data.createTime || Date.now(), version: data.version || "", metadata: {}
        });
    }
    storageQueue.set(`s-${storageKey}:${key}`, task);
    startStorageQueue();
}
/**
 * 查找一个键值对
 * @async
 * @param {string} storageKey 指定空间的名称,不需要提前获取空间
 * @param {string} key 指定的键
 * @returns {ReturnValue}
 * @example
 * var data = await EasyBox3Lib.storage.getData('test', entity.player.userKey);
 */
async function getData(storageKey, key) {
    var dataStorage = await getDataStorage(storageKey), result;
    try {
        result = await dataStorage.get(key);
    } catch (error) {
        output('warn', error);
    }
    return result;
}
/**
 * 批量获取键值对  
 * ~~注意:该方法不会创建缓存和读取缓存,所以比`get`更慢~~
 * 目前在完全缓存的情况下可以在此使用缓存,需要更改配置文件
 * @param {string} storageKey 指定空间的名称,不需要提前获取空间
 * @param {ListPageOptions} options 批量获取键值对的配置项
 * @returns {QueryList}
 * @example
 * var datas = await EasyBox3Lib.storage.listData('test', {cursor: 0});
 */
async function listData(storageKey, options) {
    var dataStorage = await getDataStorage(storageKey);
    return dataStorage.list(options);
}
/**
 * 删除表中的键值对
 * 与`DataStorage.remove`方法不同,该方法调用后不会立即写入数据,而是移动到Storage Queue中  
 * 建议添加 `await`
 * @async
 * @param {string} storageKey 指定空间的名称,不需要提前获取空间
 * @param {string} key 指定的键
 * @example
 * await EasyBox3Lib.storage.removeData('test', entity.player.userKey);
 */
async function removeData(storageKey, key) {
    /**@type {StorageTask} */
    var task = { type: "remove", storageKey, key, value }, dataStorage = await getDataStorage(storageKey);
    storageQueue.set(`r-${storageKey}:${key}`, task);
    if (CONFIG.EasyBox3Lib.enableSQLCache)
        dataStorage._data.delete(key);
    startStorageQueue();
}
/**
 * 删除指定数据存储空间  
 * 警告:删除之后无法恢复,请谨慎使用!!!  
 * 不支持Arena,只支持旧岛
 * @async
 * @param {string} storageKey 指定数据存储空间,不需要提前获取空间
 * @example
 * await EasyBox3Lib.storage.dropDataStorage('player');
 */
async function dropDataStorage(storageKey) {
    await getDataStorage(storageKey).drop();
}
/**
 * 创建游戏循环  
 * 需要手动调用`runGameLoop`运行
 * @param {string} name 循环名称
 * @param {EventCallBack} callbackfn 要执行的函数。提供一个参数:time,代表循环执行的次数
 * @example
 * EasyBox3Lib.createGameLoop('test1', async (time) => {
 *     await world.nextChat();
 *     EasyBox3Lib.output('log', '第', time, '次收到消息');
 * });
 */
function createGameLoop(name, callbackfn) {
    if (gameLoops.has(name)) {
        output('error', name, '循环已存在');
        return;
    }
    if (callbackfn === undefined) {
        output('error', name, '循环为空');
        return;
    }
    output('log', '创建游戏循环', name);
    gameLoops.set(name, { statu: "stop", init: callbackfn, times: 0 });
}
/**
 * 创建并运行游戏循环  
 * 如果循环已存在,则直接运行循环
 * @async
 * @param {string} name 循环名称
 * @param {EventCallBack | undefined} callbackfn 要执行的函数。提供一个参数:time,代表循环执行的次数。如果为空且循环存在,则继续循环
 * @example
 * EasyBox3Lib.startGameLoop('test1', async (time) => {
 *     await world.nextChat();
 *     EasyBox3Lib.output('log', '第', time, '次收到消息');
 * });
 * @example
 * EasyBox3Lib.createGameLoop('test1', async (time) => {
 *     await world.nextChat();
 *     EasyBox3Lib.output('log', '第', time, '次收到消息');
 * });
 * EasyBox3Lib.startGameLoop('test1');
 */
async function startGameLoop(name, callbackfn = undefined) {
    if (gameLoops[name]) {
        await runGameLoop(name);
        return;
    }
    if (callbackfn === undefined) {
        output('error', name, '循环为空');
        return;
    }
    createGameLoop(name, callbackfn);
    await runGameLoop(name);
}
/**
 * 停止游戏循环  
 * 该游戏循环可能不会立即停止,而是在当前循环运行完后真正停止
 * @param {string} name 循环名称
 * @example
 * EasyBox3Lib.startGameLoop('test1', async (time) => {
 *     await world.nextChat();
 *     EasyBox3Lib.output('log', '第', time, '次收到消息');
 * });
 * EasyBox3Lib.stopGameLoop('test1');
 * //这个例子可能不是很好,因为设计的时候就没想过这么用,比这个复杂多了,参见Storage Queue的实现
 */
function stopGameLoop(name) {
    output('log', '停止游戏循环', name);
    gameLoops.get(name).statu = 'awaiting_stop';
}
/**
 * 运行游戏循环
 * @param {string} name 循环名称
 * @example
 * EasyBox3Lib.createGameLoop('test1', async (time) => {
 *     await world.nextChat();
 *     EasyBox3Lib.output('log', '第', time, '次收到消息');
 * });
 * EasyBox3Lib.runGameLoop('test1');
 */
async function runGameLoop(name) {
    if (!gameLoops.has(name)) {
        output('error', '未知游戏循环', name);
        return;
    }
    var gameLoop = gameLoops.get(name);
    if (gameLoop.statu != 'stop') {
        if (name != '$StorageQueue')
            output('warn', '该游戏循环已经在运行中或等待删除/停止,当前状态:', gameLoop.statu);
        return;
    }
    output('log', '运行游戏循环', name);
    gameLoop.statu = 'running';
    while (true) {
        gameLoop = gameLoops.get(name);
        if (DEBUGMODE)
            output("log", '[LOG][GAMELOOP]', name, gameLoop.statu);
        if (!gameLoops.has(name) || gameLoop.statu != 'running')
            break;
        await gameLoop.init(++gameLoop.times);
    }
    if (gameLoops.has(name))
        if (gameLoop.statu == 'awaiting_deletion')
            gameLoops.delete(name);
        else if (gameLoop.statu == 'awaiting_stop')
            gameLoop.statu == 'stop';
}
/**
 * 删除游戏循环,之后不能再对该循环进行操作  
 * 如果循环正在运行中,那么循环结束后才会删除
 * @param {string} name 循环名称
 * @example
 * EasyBox3Lib.createGameLoop('test1', async (time) => {
 *     await world.nextChat();
 *     EasyBox3Lib.output('log', '第', time, '次收到消息');
 * });
 * EasyBox3Lib.deleteGameLoop('test1');//刚创建还没运行就删除()
 * //这个例子可能不是很好,因为设计的时候就没想过这么用,比这个复杂多了,参见Storage Queue的实现
 */
function deleteGameLoop(name) {
    if (['$StorageQueue'].includes(name))
        throwError('受保护的GameLoop:', name);
    output('log', '停止游戏循环', name);
    var gameLoop = gameLoops.get(name);
    if (gameLoop.statu == 'stop') {
        gameLoops.delete(name);
        return;
    }
    gameLoop.statu = 'awaiting_deletion';
}
/**
 * 注册一个事件
 * @example
 * registerEvent('testEvent');
 * @param {string} name 事件名称
 */
function registerEvent(name) {
    if (!events[name]) {
        events[name] = [];
        output('log', name, '事件注册成功');
    }
    else
        output('warn', name, '事件已存在');
}
/**
 * 添加事件监听器  
 * 不可使用此方法注册`onTick`事件
 * @example
 * EasyBox3Lib.registerEvent('testEvent');
 * EasyBox3Lib.addEventHandler('testEvent', () => {
 *     output("log", '触发事件');
 * });
 * @example
 * EasyBox3Lib.addEventHandler('onStart', () => {
 *     EasyBox3Lib.output('地图启动成功');
 * });
 * EasyBox3Lib.addEventHandler('onPlayerJoin', ({ entity }) => {
 *     world.say('欢迎', entity.player.name, '加入地图!');
 * });
 * @param {string} eventName 事件名称
 * @param {EventCallBack} handler 事件监听器
 */
function addEventHandler(eventName, handler) {
    if (eventName == 'onTick') {
        throwError('不可使用 addEventHandler 方法注册 onTick 事件,请使用 onTick 方法');
    }
    var event = new EventHandlerToken(handler);
    if (events[eventName]) {
        events[eventName].push(event);
        return event;
    } else
        throwError(eventName, '事件不存在');
}
/**
 * 触发一个事件
 * @async
 * @example
 * EasyBox3Lib.registerEvent('testEvent');
 * EasyBox3Lib.addEventHandler('testEvent', () => {
 *     EasyBox3Lib.output("log", '触发事件');
 * });
 * EasyBox3Lib.triggerEvent('testEvent');
 * @example
 * EasyBox3Lib.registerEvent('testEvent');
 * EasyBox3Lib.addEventHandler('testEvent', ({ data }) => {
 *     EasyBox3Lib.output("log", '触发事件 data =', data);
 * });
 * EasyBox3Lib.triggerEvent('testEvent', { data: 114514 });
 * @param {string} eventName 事件名称
 * @param {object} data 监听器使用的参数
 */
async function triggerEvent(eventName, data) {
    if (DEBUGMODE)
        output('log', '触发事件', eventName);
    for (let event of events[eventName])
        await event.run(data);
}
/**
 * 移除一个事件,之后不能监听该事件
 * @param {string} eventName 事件名称
 */
function removeEvent(eventName) {
    if (events[eventName]) {
        output('warn', `移除事件 ${eventName}\n该事件有${events[eventName].length} 个监听器`);
        delete events[eventName];
    }
}
/**
 * 注册一个`onTick`事件,类似于`Box3`中的`onTick`事件,但有一些优化  
 * 需要在预处理时注册
 * @example
 * EasyBox3Lib.onTick(({ tick }) => {
 *     EasyBox3Lib.output("log", 'tick', tick);
 * }, 16);
 * @param {onTickEventCallBack} handler 要执行的函数
 * @param {number} tpc 每个循环的执行次数(times per cycles)
 * @param {number} performanceImpact 性能影响程度
 * @param {boolean} enforcement 如果为false,则如果上一次未执行完,则不会执行
 * @returns {OnTickHandlerToken}
 */
function onTick(handler, tpc, performanceImpact = 1, enforcement = false) {
    var event = new OnTickHandlerToken(handler, tpc, performanceImpact, enforcement);
    events.onTick.push(event);
    return event;
}
/**
 * 添加预处理函数
 * @param {preprocessCallback} callbackfn 要执行的函数
 * @param {number} priority 优先级,值越大越先执行
 * @example
 * EasyBox3Lib.preprocess(() => {
 *     EasyBox3Lib.output("log", '这是一个预处理函数,会第一个执行');
 * }, 2);
 * EasyBox3Lib.preprocess(() => {
 *     EasyBox3Lib.output("log", '这是一个预处理函数,会第二个执行');
 * }, 1);
 * EasyBox3Lib.preprocess(() => {
 *     EasyBox3Lib.output("log", '优先级甚至可以是负数和小数!');
 * }, -114.514);
 */
function preprocess(callbackfn, priority) {
    preprocessFunctions.push({ callbackfn, priority });
    if (DEBUGMODE)
        output('log', '[LOG][PREPROCESS]', '注册预处理函数成功');
}
/**
 * 规划onTick事件  
 * 一般不需要自行调用
 */
async function planningOnTickEvents() {
    output('log', '开始规划onTick……');
    onTickHandlers = new Array(nullc(CONFIG.EasyBox3Lib.onTickCycleLength, 16)).fill({ events: [], performanceImpact: 0 });
    for (let event of events.onTick) { //先运行一次,记录时间
        await event.run();
        onTickHandlers.sort((a, b) => a.performanceImpact - b.performanceImpact);
        for (let i = 0; i < event.tpc; i++) { //tpc为1~16之间,且一个tick不能用两个相同的事件
            onTickHandlers[i].events.push(event);
            onTickHandlers[i].performanceImpact += event.performanceImpact;
        }
    }
    output('log', 'onTick事件规划完成');
}
/**
 * 启动地图(执行预处理函数)
 * @example
 * EasyBox3Lib.start();
 * @example
 * EasyBox3Lib.addEventHandler('onStart', () => {
 *     EasyBox3Lib.output('地图启动成功');
 * });
 * EasyBox3Lib.addEventHandler('onPlayerJoin', ({ entity }) => {
 *     world.say('欢迎', entity.player.name, '加入地图!');
 * });
 * EasyBox3Lib.start();
 */
async function start() {
    preprocessFunctions.sort((a, b) => b.priority - a.priority);
    for (let func of preprocessFunctions) {
        try {
            await func.callbackfn();
        } catch (error) {
            throwError('预处理函数发生错误', error);
        }
    }
    output('log', '预处理函数执行完成');
    triggerEvent('onStart');
    await planningOnTickEvents();
    world.onTick(({ skip, elapsedTimeMS }) => {
        onTickHandlers[tick].events.forEach(async token => {
            token.run();
        });
        tick = (tick + 1) % nullc(CONFIG.EasyBox3Lib.onTickCycleLength, 16);
        if (tick <= 0) {
            ++cycleNumber;
            if (DEBUGMODE)
                output('log', '开始新的onTick周期', cycleNumber);
        }
        if (DEBUGMODE && skip) {
            output("warn", '延迟', elapsedTimeMS, 'ms');
        }
    });
    output('log', '开始处理玩家', '玩家数量:', players.size)
    for (let [userKey, entity] of players) {
        if (DEBUGMODE)
            output('log', '处理玩家', userKey);
        await triggerEvent('onPlayerJoin', { entity });
    }
    players.clear();
    started = true;
    output('log', '地图启动完成');
}
/**
 * 转换速度单位  
 * 把`velocity`/`time`转换为`result`/`64ms`  
 * 公式为:
 * ```javascript
 * result = velocity * time / 64
 * ```
 * @example
 * changeVelocity(1, 1000); // 1格/s
 * @example
 * changeVelocity(299792458, 1000); // 光速,299792458格/s
 * @param {number} velocity 移动的速度,每`time`应该移动`velocity`格
 * @param {number} time 速度的单位时间,单位:ms。默认为1s
 */
function changeVelocity(velocity, time = TIME.SECOND) {
    return velocity * time / TIME.TICK;
}

/**
 * 注册一种物品  
 * 如果成功注册物品,返回`true`,否则返回`false`
 * @param {Item} item 要注册的物品
 * @returns {boolean}
 */
function registerItem(item) {
    if (itemRegistry.has(item.id)) {
        output('error', '[ERROR][ITEM]', decodeURI(item.id), '注册失败');
        return false;
    }
    itemRegistry.set(item.id, item);
    if (DEBUGMODE)
        output('log', '[LOG][ITEM]', decodeURI(item.id), '注册成功');
    return true;
}
/**
 * 启动Storage Queue  
 * 只有启动了Storage Queue,`setData`和`removeData`的任务才会被处理
 */
function startStorageQueue() {
    if (storageQueueStarted)
        return;
    output('log', '启动Storage Queue');
    if (gameLoops.has('$StorageQueue')) {
        runGameLoop('$StorageQueue');
        return;
    }
    startGameLoop('$StorageQueue', async () => {
        storageQueueStarted = true;
        if (storageQueue.size <= 0) {
            output("log", '无任务,停止Storage Queue');
            stopStorageQueue();
            return;
        }
        var task = storageQueue.values().next().value, taskId = storageQueue.keys().next().value;
        await tryExecuteSQL(async () => {
            var dataStorage = await getDataStorage(task.storageKey);
            switch (task.type) {
                case "set":
                    await dataStorage.set(task.key, task.value);
                    break;
                case "remove":
                    await dataStorage.remove(task.key);
                    break;
                default:
                    output("warn", '未知的type', task.type, `\n数据:${task.storageKey}.${task.key}=${task.value}`);
            }
        });
        storageQueue.delete(taskId);
        if (DEBUGMODE)
            output("log", 'Storage Queue完成任务', taskId, '还剩', storageQueue.size, '个任务');
        await sleep(CONFIG.EasyBox3Lib.storageQueueCooldown);
    });
    output("log", '已启动Storage Queue');
}
/**
 * 停止Storage Queue  
 * `setData`和`removeData`仍会将任务放到队列中,但不会再次处理,除非运行`startStorageQueue`
 * @example
 * EasyBox3Lib.stopStorageQueue();
 */
function stopStorageQueue() {
    if (!storageQueueStarted) {
        output("warn", 'Storage Queue未启动,无法停止');
        return;
    }
    storageQueueStarted = false;
    stopGameLoop('$StorageQueue');
    output("log", '已停止Storage Queue');
}
/**
 * 翻译报错信息
 * @param {string} message 报错内容
 * @returns {string} 翻译后的信息
 */
function translationError(msg) {
    var message = msg;
    for (let tr of TRANSLATION_REGEXPS) {
        message = message.replaceAll(...tr);
    }
    return message;
}
/**
 * 万用注册函数
 * @param {any} any 你要注册的东西
 * @example
 * EasyBox3Lib.register(new EasyBox3Lib.Item('豆奶', '豆奶', 16, ['食物'], undefined, { recovery: 6 }));
 * @example
 * EasyBox3Lib.register(new BehaviorLib.Behavior("findAttackTarget", (self, data) => {
 *     self.attackTarget = world.querySelectorAll(data.selector).filter(data.filter)[0];
 * }, 1));
 */
function register(any) {
    registryClassIndex.forEach((fn, type) => {
        if (any instanceof type) {
            fn(any);
            if (DEBUGMODE)
                output("log", '向', type.name, '注册', JSON.stringify(any));
        }
    });
}
/**
 * 注册注册函数类别索引  
 * 满足`any instanceof type`时调用`fn(any)`
 * @param {Function} fn
 * @example
 * EasyBox3Lib.registerRegistryClassIndex(EasyBox3Lib.Item, registerItem);
 */
function registerRegistryClassIndex(type, fn) {
    registryClassIndex.set(type, fn);
}
/**
 * 根据大小写和符号,拆分单词
 * @param {string} word
 * @return {string[]}
 */
function spiltWord(word) {
    function getType(char) {
        if (char >= 'A' && char <= 'Z')
            return 0;
        if (char >= 'a' && char <= 'z')
            return 1;
        if (char >= '0' && char <= '9')
            return 2;
        else
            return 3;
    }
    var result = [], now = '', nowType = -1;//nowType -1=>空 0=>大写字母 1=>小写字母 2=>数字 3=>符号
    for (let char of word) {
        let type = getType(char);
        if (type != nowType && nowType != 0 && type != 1) {
            if (now !== '') {
                result.push(now);
                now = '';
            }
        }
        if (char === '3' && result[result.length - 1] === 'Box') {
            result[result.length - 1] += char;
            nowType = -1;
            continue;
        }
        now += char;
        nowType = type;
    }
    result.push(now);
    return result;
}
/**
 * 创建一个实体组  
 * 更改`entities`中每个实体的`indexInEntityGroup`属性为该实体在实体组内的编号
 * @param {Box3Entity[]} entities 实体组内的实体
 * @param {Box3Vector3} position 实体组中心位置.
 * @return {EntityGroup}
 */
function createEntityGroup(entities, position) {
    return new EntityGroup(entities, position);
}
const EasyBox3Lib = {
    copyObject,
    copy: copyObject,
    getEntity,
    gete: getEntity,
    getEntities,
    getes: getEntities,
    getPlayer,
    getp: getPlayer,
    getAllLogs,
    getlog: getAllLogs,
    output,
    o: output,
    isAdmin,
    isa: isAdmin,
    resizePlayer,
    resp: resizePlayer,
    setAdminStatus,
    setas: setAdminStatus,
    createEntity,
    ce: createEntity,
    textDialog,
    td: textDialog,
    selectDialog,
    sd: selectDialog,
    inputDialog,
    id: inputDialog,
    shuffleTheList,
    sl: shuffleTheList,
    toChineseDate,
    tcd: toChineseDate,
    random,
    ran: random,
    Menu,
    M: Menu,
    Pages,
    P: Pages,
    getTheCodeExecutionLocation,
    getl: getTheCodeExecutionLocation,
    storage: {
        executeSQLCode,
        getDataStorage,
        getDataStorageInCache,
        setData,
        getData,
        listData,
        removeData,
        dropDataStorage,
        tryExecuteSQL,
        startStorageQueue,
        stopStorageQueue
    },
    s: {
        eSQL: executeSQLCode,
        getds: getDataStorage,
        getdsc: getDataStorageInCache,
        setd: setData,
        getd: getData,
        ld: listData,
        remd: removeData,
        dds: dropDataStorage,
        tSQL: tryExecuteSQL,
        startSQ: startStorageQueue,
        stopSQ: stopStorageQueue
    },
    createGameLoop,
    startGameLoop,
    deleteGameLoop,
    stopGameLoop,
    runGameLoop,
    cGL: createGameLoop,
    startGL: startGameLoop,
    dGL: deleteGameLoop,
    stopGL: stopGameLoop,
    rGL: runGameLoop,
    onTick,
    ot: onTick,
    preprocess,
    pre: preprocess,
    start,
    addEventHandler,
    addEH: addEventHandler,
    triggerEvent,
    triE: triggerEvent,
    registerEvent,
    regE: registerEvent,
    removeEvent,
    remE: removeEvent,
    createEntityGroup,
    cEG: createEntityGroup,
    changeVelocity,
    changeV: changeVelocity,
    registerItem,
    regI: registerItem,
    Item,
    I: Item,
    Thing,
    T: Thing,
    ThingStorage,
    TS: ThingStorage,
    throwError,
    thr: throwError,
    translationError,
    te: translationError,
    register,
    reg: register,
    registerRegistryClassIndex,
    regIndex: registerRegistryClassIndex,
    spiltWord,
    TIME,
    version: [0, 1, 6]
};
Object.defineProperty(EasyBox3Lib, 'started', {
    get: () => started,
    set: () => {
        output("error", 'EasyBox3Lib.started 是只读属性,无法写入');
    }
});
if (nullc(CONFIG.EasyBox3Lib.exposureToGlobal, false)) {
    Object.assign(global, EasyBox3Lib);
    output('log', '已成功暴露到全局');
}
if (nullc(CONFIG.EasyBox3Lib.enableOnPlayerJoin, false)) {
    world.onPlayerJoin(({ entity }) => {
        if (started)
            triggerEvent('onPlayerJoin', { entity });
        else
            players.set(entity.player.userKey, entity);
        output("log", '进入玩家', entity.player.name);
    });
}
registerRegistryClassIndex(Item, registerItem);
/**
 * EasyBox3Lib的全局对象
 * @global
 */
global.EasyBox3Lib = EasyBox3Lib;
console.log('EasyBox3Lib', EasyBox3Lib.version.join('.'));
module.exports = EasyBox3Lib;