yoni-mcscripts-lib
Version:
为 Minecraft Script API 中的部分接口创建了 wrapper,并提供简单的事件管理器和任务管理器,另附有一些便于代码编写的一些小工具。
544 lines (543 loc) • 18.5 kB
JavaScript
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)));
}
}
});