UNPKG

yoni-mcscripts-lib

Version:

为 Minecraft Script API 中的部分接口创建了 wrapper,并提供简单的事件管理器和任务管理器,另附有一些便于代码编写的一些小工具。

544 lines (543 loc) 18.5 kB
import { MinecraftSystem } from "./basis.js"; import { Logger } from "./util/Logger.js"; import { isDebugMode } from "./debug.js"; import { config } from "./config.js"; const logger = new Logger("Schedule"); const scheduleCallbacks = new WeakMap(); /** * 任务类型。 */ export var ScheduleType; (function (ScheduleType) { /** * 以真实时间为间隔重复运行的任务。 */ ScheduleType["cycleTimerSchedule"] = "0"; /** * 以真实时间为延迟运行的任务。 */ ScheduleType["delayTimerSchedule"] = "1"; /** * 以游戏刻为间隔重复运行的任务。 */ ScheduleType["cycleTickSchedule"] = "2"; /** * 以游戏刻为延迟运行的任务。 */ ScheduleType["delayTickSchedule"] = "3"; })(ScheduleType || (ScheduleType = {})); function isCycleScheduleType(type) { return type === Schedule.cycleTickSchedule || type === Schedule.cycleTimerSchedule; } /** * 任务类。 */ export class Schedule { /** * 以真实时间为间隔重复运行的任务。 */ static cycleTimerSchedule = ScheduleType.cycleTimerSchedule; /** * 以真实时间为延迟运行的任务。 */ static delayTimerSchedule = ScheduleType.delayTimerSchedule; /** * 以游戏刻为间隔重复运行的任务。 */ static cycleTickSchedule = ScheduleType.cycleTickSchedule; /** * 以游戏刻为延迟运行的任务。 */ static delayTickSchedule = ScheduleType.delayTickSchedule; static #scheduleCurrentIndex = 0; /** * 任务的内部ID。 */ id; /** * 任务类型。 */ "type"; /** * 是否为异步任务。 * 对于异步任务,如果任务回调返回了 Promise 对象, * 在其状态从 pending 离开时才视为执行结束。 */ "async"; /** * 任务将以此值指定的间隔重复运行。 * 此值在非重复运行的任务当中没有意义。 */ period; /** * 任务将在放入队列多长时间后运行。 */ delay; /** * 任务是否已经添加到执行队列。 */ isQueued() { return scheduleQueue.has(this); } get startInQueueTime() { return scheduleStartInQueueTime.get(this) ?? -1; } /** * 任务是否正在执行。 * @returns 在同步任务的回调当中会返回 `true`, * 异步任务中,任务回调返回的 Promise 处于 pending 状态时返回 `true`。 * 其余情况返回 `false`。 */ isRunning() { if (this.async) return runningAsyncSchedule.has(this); return executingSchedule === this; } /** * 最后一次执行此任务时是否正常退出。 * @returns 此任务曾被执行,且最后一次执行时任务回调正常退出的情况下返回 `true`, * 其余情况返回 `false`。 */ get isSuccessInLastExecute() { return isSuccessInLastExecute(this); } /** * 此任务最后一次执行成功的时间。 * @returns 此任务曾被执行,且最后一次执行时任务回调正常退出的情况下返回毫秒级别的 unix 时间戳, * 其余情况返回 `-1`。 */ get lastSuccessTime() { return getLastSuccess(this) ?? -1; } /** * 此任务最后一次执行失败的时间。 * @returns 此任务曾被执行,且最后一次执行时任务回调抛出了错误的情况下返回毫秒级别的 unix 时间戳, * 其余情况返回 `-1`。 */ get lastFailTime() { return getLastFail(this) ?? -1; } /** * 此任务最后一次执行结束的时间。 * @returns 如果此任务曾被执行,返回毫秒级别的 unix 时间戳, * 其余情况返回 `-1`。 */ get lastExecuteTime() { return getLastExecute(this) ?? -1; } constructor(props, callback) { let { async, period, delay, type } = props; this.async = (!!async); if (isCycleScheduleType(type) && !isFinite(period)) throw new TypeError(`period ${period} not finite`); this.period = period ?? 0; if (!isFinite(delay)) throw new TypeError(`delay ${delay} not finite`); this.delay = delay ?? 1; this.type = type; this.id = Schedule.#scheduleCurrentIndex++; scheduleCallbacks.set(this, callback); Object.freeze(this); } run() { const fn = scheduleCallbacks.get(this); fn(); } async runAsync() { const fn = scheduleCallbacks.get(this); await fn(); } } const lastTimeExecute = new WeakMap(); const lastTimeExecuteSuccess = new WeakMap(); const lastTimeExecuteFail = new WeakMap(); /** * @param {Schedule} schedule - 任务 * @returns {number|undefined} 时间 */ function getLastExecute(schedule) { return lastTimeExecute.get(schedule); } /** * @param {Schedule} schedule - 任务 * @returns {number|undefined} 时间 */ function getLastSuccess(schedule) { return lastTimeExecuteSuccess.get(schedule); } /** * @param {Schedule} schedule - 任务 * @returns {number|undefined} 时间 */ function getLastFail(schedule) { return lastTimeExecuteFail.get(schedule); } /** * @param {Schedule} schedule - 任务 * @param {number} time - 当前时间 */ function lastExecute(schedule, time) { lastTimeExecute.set(schedule, time); } /** * @param {Schedule} schedule - 任务 * @param {number} time - 当前时间 */ function lastSuccess(schedule, time) { lastExecute(schedule, time); lastTimeExecuteSuccess.set(schedule, time); } /** * @param {Schedule} schedule - 任务 * @param {number} time - 当前时间 */ function lastFail(schedule, time) { lastExecute(schedule, time); lastTimeExecuteFail.set(schedule, time); } /** * @param {Schedule} schedule - 任务 * @returns {boolean} */ function isSuccessInLastExecute(schedule) { const lastExecuteTime = getLastExecute(schedule); if (lastExecuteTime === undefined) return false; else return lastExecuteTime === getLastSuccess(schedule); } const runningAsyncSchedule = new WeakSet(); let executingSchedule = null; /** * @param schedule 任务 */ function executeSchedule(schedule) { if (executingSchedule !== null) { logger.warn("上一个任务没有正常结束,id: {}", executingSchedule.id); executingSchedule = null; } if (schedule.async) { runningAsyncSchedule.add(schedule); schedule.runAsync().then(onSuccess, onFail); function onSuccess(result) { lastSuccess(schedule, Date.now()); runningAsyncSchedule.delete(schedule); } function onFail(error) { lastFail(schedule, Date.now()); runningAsyncSchedule.delete(schedule); logger.error("async schedule {} 运行时出现错误 {}", schedule.id, error); } } else { executingSchedule = schedule; //这样即使在出现无法捕获的错误的时候也可以标记任务执行失败。 lastFail(schedule, Date.now()); try { //do task schedule.run(); lastSuccess(schedule, Date.now()); } catch (err) { lastFail(schedule, Date.now()); logger.error(`schedule {} 运行时出现错误 {}`, schedule.id, err); } executingSchedule = null; } } function shouldExecuteOneTimeDelaySchedule(schedule, curTick) { return scheduleAddToQueueGameTick.get(schedule) !== curTick; } const scheduleExecuteTimer = new WeakMap(); const scheduleAddToQueueTime = new WeakMap(); const scheduleStartInQueueTime = new WeakMap(); /** 仅用于 {@link shouldExecuteOneTimeDelaySchedule} */ const scheduleAddToQueueGameTick = new WeakMap(); const queueSchedulesTypedRecord = {}; /** 仅用于 {@link Schedule#isQueue} */ const scheduleQueue = new WeakSet(); function removeScheduleFromQueue(schedule) { let queue = queueSchedulesTypedRecord[schedule.type]; if (queue === undefined) { return false; } const location = queue.indexOf(schedule); if (location === -1) { return false; } queue.splice(location, 1); scheduleStartInQueueTime.delete(schedule); scheduleAddToQueueTime.delete(schedule); scheduleExecuteTimer.delete(schedule); scheduleQueue.delete(schedule); scheduleAddToQueueGameTick.delete(schedule); return true; } function addScheduleToQueue(schedule) { let queue = queueSchedulesTypedRecord[schedule.type]; if (queue === undefined) { queue = []; queueSchedulesTypedRecord[schedule.type] = queue; } if (queue.includes(schedule)) { return false; } else { queue.push(schedule); scheduleStartInQueueTime.set(schedule, Date.now()); scheduleAddToQueueTime.set(schedule, Date.now()); scheduleExecuteTimer.set(schedule, schedule.delay); scheduleQueue.add(schedule); scheduleAddToQueueGameTick.set(schedule, MinecraftSystem.currentTick); return true; } } MinecraftSystem.runInterval(executeTasks, 1); const taskList = []; let legacyTasks = []; function executeTasks() { if (taskList.length > 0) { legacyTasks = taskList.concat(legacyTasks); taskList.length = 0; } if (config.getInt("scheduler.maxLegacyTaskCount", 0) < legacyTasks.length) { logger.warn("遗留任务过多,已跳过 {} 个遗留任务", legacyTasks.length); legacyTasks.length = 0; } for (let i = legacyTasks.length; i > 0; i--) { executeSchedule(legacyTasks.pop()); } addTimeCycleScheduleToTasks(taskList); addTickCycleScheduleToTasks(taskList); addTimeDelayScheduleToTasks(taskList); addTickDelayScheduleToTasks(taskList); for (let i = taskList.length; i > 0; i--) { executeSchedule(taskList.pop()); } } //处理只执行一次的tick任务 tickdelay function addTickDelayScheduleToTasks(tasks) { let schedules = queueSchedulesTypedRecord[Schedule.delayTickSchedule]; if (schedules === undefined) return; const curTick = MinecraftSystem.currentTick; for (let idx = schedules.length - 1; idx >= 0; idx -= 1) { const schedule = schedules[idx]; let lessTime = scheduleExecuteTimer.get(schedule); if (lessTime === 1 && !shouldExecuteOneTimeDelaySchedule(schedule, curTick)) { continue; } if (--lessTime <= 0) { tasks.push(schedule); removeScheduleFromQueue(schedule); } else { scheduleExecuteTimer.set(schedule, lessTime); } } } //处理只执行一次的time任务 timedelay function addTimeDelayScheduleToTasks(tasks) { let schedules = queueSchedulesTypedRecord[Schedule.delayTimerSchedule]; if (schedules === undefined) return; for (let idx = schedules.length - 1; idx >= 0; idx -= 1) { const schedule = schedules[idx]; const time = Date.now(); const lastInQueueTime = scheduleAddToQueueTime.get(schedule); const passedTime = time - lastInQueueTime; let interval = scheduleExecuteTimer.get(schedule); let lessTime = interval - passedTime; if (--lessTime <= 0) { tasks.push(schedule); removeScheduleFromQueue(schedule); } } } //处理重复执行的tick任务 tickcycle function addTickCycleScheduleToTasks(tasks) { let schedules = queueSchedulesTypedRecord[Schedule.cycleTickSchedule]; if (schedules === undefined) return; const curTick = MinecraftSystem.currentTick; for (let idx = schedules.length - 1; idx >= 0; idx -= 1) { const schedule = schedules[idx]; //一般情况下,异步任务才会出现这种情况 if (schedule.isRunning()) { continue; } let lessTime = scheduleExecuteTimer.get(schedule); if (lessTime === 1 && !shouldExecuteOneTimeDelaySchedule(schedule, curTick)) { continue; } if (--lessTime <= 0) { tasks.push(schedule); scheduleExecuteTimer.set(schedule, schedule.period); } else { scheduleExecuteTimer.set(schedule, lessTime); } } } //处理重复执行的time任务 timecycle function addTimeCycleScheduleToTasks(tasks) { let schedules = queueSchedulesTypedRecord[Schedule.cycleTimerSchedule]; if (schedules === undefined) return; for (let idx = schedules.length - 1; idx >= 0; idx -= 1) { const schedule = schedules[idx]; //一般情况下,异步任务才会出现这种情况 if (schedule.isRunning()) { continue; } //计算lessTime,即此任务距离下次执行还有多久 //负数代表任务应该即刻执行 //这里考虑到任务执行结束的时间可能比较长(特别是异步任务),所以它也作为一个变量参与到运算 const time = Date.now(); const lastInQueueTime = scheduleAddToQueueTime.get(schedule); const lastScheduleChangeTime = Math.max(getLastExecute(schedule), lastInQueueTime); const passedTime = time - lastScheduleChangeTime; const interval = scheduleExecuteTimer.get(schedule); let lessTime = interval - passedTime; if (--lessTime <= 0) { tasks.push(schedule); scheduleAddToQueueTime.set(schedule, time); } } } /** * 你可以使用它创建任务 */ export class YoniScheduler { /** * @param schedule 要添加到队列的任务。 * @returns 操作是否成功。 */ static addSchedule(schedule) { if (!(schedule instanceof Schedule)) throw new TypeError("Not a Schedule"); if (addScheduleToQueue(schedule)) { logger.trace("增加了新的任务, id: {}, async: {}, type: {}, period: {}, delay: {}", schedule.id, schedule["async"], schedule.type.toString(), schedule.period, schedule.delay); return true; } return false; } /** * @param schedule 要从队列中移除的任务。 * @returns 操作是否成功。 */ static removeSchedule(schedule) { if (typeof schedule === "number") { OuterForCycle: for (const ischedules of Object.values(queueSchedulesTypedRecord)) { for (const ischedule of ischedules) { if (ischedule.id === schedule) { schedule = ischedule; break OuterForCycle; } } } } if (!(schedule instanceof Schedule)) throw new TypeError("Not a Schedule"); if (removeScheduleFromQueue(schedule)) { logger.trace("移除了任务, id: {}", schedule.id); return true; } return false; } /** * 创建任务并使其运行指定的回调函数。 * @param callback 需要执行的函数。 * @param async 是否异步执行。 * @returns scheduleId */ static runTask(callback, async = false) { let schedule = new Schedule({ async, delay: 0, type: Schedule.delayTickSchedule }, callback); YoniScheduler.addSchedule(schedule); return schedule.id; } /** * 在 `delay` 毫秒之后调用一个函数。 * @param callback 需要执行的函数。 * @param delay 延迟的毫秒数。 * @param async 是否异步执行。 * @returns scheduleId */ static runDelayTimerTask(callback, delay, async = false) { let schedule = new Schedule({ async, delay, type: Schedule.delayTimerSchedule }, callback); YoniScheduler.addSchedule(schedule); return schedule.id; } /** * 在 `delay` 个游戏刻之后调用一个函数。 * @param callback 需要执行的函数。 * @param delay 延迟的游戏刻。 * @param async 是否异步执行。 * @returns scheduleId */ static runDelayTickTask(callback, delay, async = false) { let schedule = new Schedule({ async, delay, type: Schedule.delayTickSchedule }, callback); YoniScheduler.addSchedule(schedule); return schedule.id; } /** * 在 `delay` 毫秒之后,开始以 `period` 毫秒的间隔重复调用一个函数。 * @param callback 需要执行的函数。 * @param delay 延迟的毫秒树。 * @param period 每次调用间隔的毫秒数。 * @param async 是否异步执行。 * @returns scheduleId */ static runCycleTimerTask(callback, delay, period, async = false) { let schedule = new Schedule({ async, delay, period, type: Schedule.cycleTimerSchedule }, callback); YoniScheduler.addSchedule(schedule); return schedule.id; } /** * 在 `delay` 个游戏刻之后,开始以 `period` 个游戏刻的间隔重复调用一个函数。 * 需要注意的是,间隔时间只在每游戏刻计算一次,小于游戏刻间隔的时间没有意义。 * @param callback 需要执行的函数。 * @param delay 延迟的游戏刻。 * @param period 每次调用间隔的游戏刻。 * @param async 是否异步执行。 * @returns scheduleId */ static runCycleTickTask(callback, delay, period, async = false) { let schedule = new Schedule({ async, delay, period, type: Schedule.cycleTickSchedule }, callback); YoniScheduler.addSchedule(schedule); return schedule.id; } } //对于异常挂断的特殊处理,但是没见他触发过一次 MinecraftSystem.beforeEvents.watchdogTerminate.subscribe((event) => { if (executingSchedule !== null) { logger.warn("在执行一个任务的过程中碰到了脚本挂断事件,事件id: {}, 类型: {}, 挂断原因: {}", executingSchedule.id, String(executingSchedule.type), event.terminateReason); if (isDebugMode()) { logger.warn("正在输出相关任务的回调代码,请在trace中查看"); logger.trace(String(scheduleCallbacks.get(executingSchedule))); } } });