Translation notice
This page was translated with machine translation and may contain inaccuracies. If you can help improve it, please open an issue or submit a pull request.
Have you ever encountered: all scheduled tasks disappeared after the server restarted, tasks were not executed after the player went offline, and there was no chance to retry after the scheduling failed...
/scheduleThe limitations don't stop there. doom.schedule uses four queues, UUID positioning and offline recovery to make up for all these shortcomings - pure data pack, zero dependencies.
Doom_Flare
doom.schedule is a data pack scheduling framework for the Minecraft vanilla server. It provides features such as game time-based task queue, execution context freezing, target offline detection and automatic recovery, and failure retry without relying on Mods.
from/scheduleSpeaking of the limitations of
vanillacommand /schedule functionProvides the most basic delayed call. It is sufficient in simple machinery or small-scale scenarios - but when it comes to persistence, cross-dimension execution, and player-oriented logic, its shortcomings will be quickly exposed:
- No persistence: All schedules to be executed disappear after the server is restarted.
- No Target Tracking:
/scheduleOnly one function name can be remembered and cannot be associated with the player or entity during execution. - No context: The entity may be offline or replaced during execution, so there is no way to judge.
- No Cancellation or Suspension: Once issued it cannot be revoked
- No retry: Failure to execute is a failure
The goal of doom.schedule is to fill these gaps and provide a task scheduling system that is durable, trackable, and manageable. It's completely built into the data pack, no external tools or mods required.
core design
doom.schedule uses game time as the time base to organize tasks into a first-in, first-out queue. Each task records the expected execution time when it is added to the queue. The tick loop traverses all expired tasks and executes them when they expire.
┌─────────────────────┐
│ queue[] │
│ [task, task, ...] │
└──────┬──────────────┘
│ tick: 移动到 processing[]
▼
┌─────────────────────┐
│ looper_scan │
│ 遍历 processing[] │
└──────┬──────────────┘
│
┌───────┴───────────────┐
▼ ▼
exec_time 到? 不到期
│ 放回队尾
▼
looper_exec 决策分流The data structure of each task is as follows:
{
"run": "say hello",
"time": 100,
"unit": "t",
"id": "my_task_001",
"exec_time": 12345,
"by": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"dim": "minecraft:overworld",
"posX": 0.0, "posY": 64.0, "posZ": 0.0,
"rotX": 0.0, "rotY": 0.0,
"is_player": true,
"retry": 3,
"retry_delay": 20
}exec_timeis the reason for joining the teamget_timeplustimeTarget game time converted (multiplied by unit.scale). tick in looplooper_scanCompareexec_timeWith the current game time, it will be executed when it expires.
Detailed explanation of joining process
scheduleThe execution path of a function is the entrance to understanding the entire system. It receives as macro argument{run, time, unit, id}, and then complete the following process:
1. Unit legality verification
$data modify storage doom.schedule:ctx _.unitEntry set from storage doom.schedule:const units[{name:'$(unit)'}]
execute unless data storage doom.schedule:ctx _.unitEntry run return failWill pass in the unit name (such as"s") and constant tableconst.unitsmatch. If no match is found, directlyreturn fail. This lookup table is in__load__Medium initialization:
[
{name:"t", scale:1}, {name:"tick", scale:1},
{name:"s", scale:20}, {name:"second", scale:20},
{name:"m", scale:1200}, {name:"minute", scale:1200},
{name:"h", scale:72000}, {name:"hour", scale:72000},
{name:"d", scale:1728000}, {name:"day", scale:1728000}
]Why not use if-else chains? The storage filter is an O(1) hash search, and adding a new unit only requiresconst.units[]Add a record without changing the function.
2. Delayed calculation
# 提取 unit 对应的倍率
execute store result score #scale doom.schedule run data get storage doom.schedule:ctx _.unitEntry.scale
# 计算 delay = time × scale
execute if score #scale doom.schedule matches 1.. store result score #delay doom.schedule run data get storage doom.schedule:ctx _.time
execute if score #scale doom.schedule matches 1.. run scoreboard players operation #delay doom.schedule *= #scale doom.scheduletime × unit.scaleGet tick-level latency.
3. Context freeze
function doom.schedule:internal/schedule/contextread@sUUID, dimension, location, orientation, storedctx._. UUID is concatenated into a string through 4 int → 16 byte → 16 hex →. Dimension reads entity NBT firstDimension, if it fails, fallback toexecute if dimensionDetect Mihara dimension, and if it fails, useknown_dimensions function tag。
4. Execution time
execute store result score #time doom.schedule run function doom.schedule:get_time
scoreboard players operation #time doom.schedule += #delay doom.schedule
execute store result storage doom.schedule:ctx _.exec_time int 1 run scoreboard players get #time doom.schedulegame_time + delay = exec_time. After joining the teamlooper_scanCompare at every tickgame_time >= exec_time, decide whether to execute.
5. Retry parameter transparent transmission
schedule_with_retryInstead of repeating the enqueuing logic, temporarily store the retry parameterctx.temp_retry, then callschedule:
$data modify storage doom.schedule:ctx temp_retry set value {retry:$(retry),retry_delay:$(retry_delay)}
$function doom.schedule:schedule {run:'$(run)',time:$(time),unit:'$(unit)',id:'$(id)'}scheduleAfter calculatingexec_timepost-testtemp_retryDoes it exist? If it exists, transfer to_.retry/_.retry_delay, then deletetemp_retry. This is a parameter transfer mode between macros——schedule_with_retrypreprocessing,scheduleConsumption.
6. Cleaning and joining the team
function doom.schedule:internal/cleanup_temp
data modify storage doom.schedule:data queue append from storage doom.schedule:ctx _
data remove storage doom.schedule:ctx _cleanup_tempdeletectx._All UUID intermediate fields (b0..bf, h0..hf, uuid0..uuid3) in the queue ensure that the queued tasks do not contain transient data. Then add the clean taskappendarrivedata.queue[], and finally deletectx._。
looper_exec offload
looper_execSeparate target detection and command execution into two stages,looper_execInternally cleared#target_onlineand#success, and then process it by path:
# 检测目标是否在线
scoreboard players set #target_online doom.schedule 0
$execute if entity $(by) run scoreboard players set #target_online doom.schedule 1
# 仅在线时才执行,并记录 success
$execute if score #target_online doom.schedule matches 1 store success score #success doom.schedule in $(dim) positioned $(posX) $(posY) $(posZ) rotated $(rotX) $(rotY) as $(by) at @s run $(run)Diversion decision:
| path | #target_online | #success | Yesretry | is_player | Results |
|---|---|---|---|---|---|
Yesby | 0 | — | — | true | move inoffline[] |
Yesby | 0 | — | — | false | Silently discard |
Yesby | 1 | 1 | — | — | complete, discard |
Yesby | 1 | 0 | true | — | retryDecrease, re-enqueue |
Yesby | 1 | 0 | false | true | move inoffline[] |
Yesby | 1 | 0 | false | false | discard |
Noneby | — | 0 | true | — | Retry |
Noneby | — | 0 | false | — | discard |
nonebyPath consists ofinternal/execute/run_noentityProcessing, the command is only executed under the saved dimension/coordinate/orientation, and the executor is not bound.
This separation solves the ambiguity of the old version: the old version could not distinguish between "target offline" and "target online but command execution failed", both of which would trigger a retry or offline.
Four queue architecture
The system maintains four parallel queues, each with different responsibilities:
| Queue | Purpose | Dequeue mechanism |
|---|---|---|
data.queue[] | Tasks waiting for scheduling (FIFO) | Move in as a whole every tickprocessing[] |
ctx.processing[] | Tasks being processed at the current tick | looper_scanCheck items one by one, execute when expired/return if not expired queue |
data.offline[] | Tasks frozen when player is offline | restoreRecover up to 10 per tick |
data.paused[] | Tasks paused manually by the user | OnlyresumeManual recovery |
tick loop(tick.mcfunction):
execute if data storage doom.schedule:data queue[0] run function doom.schedule:internal/looper
scoreboard players set #restore_count doom.schedule 0
execute if data storage doom.schedule:data offline[0] run function doom.schedule:internal/restorelooperwill the entirequeue[]Move toprocessing[]and clear the queue, followed bylooper_scanProcess each task recursively. The purpose of this design is: if a new schedule (enqueue) is generated during the execution of an expired task, it will not cause the queue of the current tick to expand infinitely.
looper_scanEach step of:
data modify storage doom.schedule:ctx task set from storage doom.schedule:ctx processing[0]
data remove storage doom.schedule:ctx processing[0]
execute store result score #exec_now doom.schedule run data get storage doom.schedule:ctx task.exec_time
execute if score #time_now doom.schedule >= #exec_now doom.schedule run function doom.schedule:internal/looper_exec
execute unless score #time_now doom.schedule >= #exec_now doom.schedule run data modify storage doom.schedule:data queue append from storage doom.schedule:ctx taskExpiration →looper_exec(Execute + Triage). Not expired → put backqueue[]. Regardless of whether it is due or not,processing[]Delete this entry. whenprocessing[]The recursion ends when it is empty,looperat the enddata remove storage doom.schedule:ctx processingClean empty arrays.
Transfer between queues:
入队 → queue[] ──tick──→ processing[] ──到期──→ 执行
│
├── 不到期 → queue[]
│
└── 离线 → offline[]
│
restore ──online──→ queue[]
│
offline → 等待
pause → queue[] → paused[]
resume → paused[] → queue[]Offline recovery
If the target is offline, the task is moved tooffline[]queue. per tickrestorefunction scans at a rate of up to 10offline[]:
# doom.schedule:internal/restore — 每 tick 恢复离线任务
data modify storage doom.schedule:ctx task set from storage doom.schedule:data offline[0]
data remove storage doom.schedule:data offline[0]
function doom.schedule:internal/restore_check with storage doom.schedule:ctx task
scoreboard players set #restore_online doom.schedule 0
execute if data storage doom.schedule:ctx task.online run scoreboard players set #restore_online doom.schedule 1
data remove storage doom.schedule:ctx task.online
execute if score #restore_online doom.schedule matches 1 run data modify storage doom.schedule:data queue append from storage doom.schedule:ctx task
execute unless score #restore_online doom.schedule matches 1 run data modify storage doom.schedule:data offline append from storage doom.schedule:ctx task
data remove storage doom.schedule:ctx task
scoreboard players add #restore_count doom.schedule 1
execute if data storage doom.schedule:data offline[0] if score #restore_count doom.schedule matches ..9 run function doom.schedule:internal/restore#restore_countIncreasing from 0,matches ..9A total of 10 recursions from 0 to 9 are allowed.
Retry mechanism
When command execution fails (#success = 0), and the task is definedretryandretry_delayWhen, enter the retry process:
# doom.schedule:internal/retry — 重试逻辑
execute store result score #retry doom.schedule run data get storage doom.schedule:ctx task.retry
scoreboard players remove #retry doom.schedule 1
execute if score #retry doom.schedule matches 0.. store result storage doom.schedule:ctx task.retry int 1 run scoreboard players get #retry doom.schedule
execute if score #retry doom.schedule matches 0.. run scoreboard players set #delay doom.schedule 1
execute if score #retry doom.schedule matches 0.. store result score #delay doom.schedule run data get storage doom.schedule:ctx task.retry_delay
execute if score #retry doom.schedule matches 0.. if score #delay doom.schedule matches ..0 run scoreboard players set #delay doom.schedule 1
execute if score #retry doom.schedule matches 0.. store result score #now doom.schedule run function doom.schedule:get_time
execute if score #retry doom.schedule matches 0.. run scoreboard players operation #now doom.schedule += #delay doom.schedule
execute if score #retry doom.schedule matches 0.. store result storage doom.schedule:ctx task.exec_time int 1 run scoreboard players get #now doom.schedule
execute if score #retry doom.schedule matches 0.. run data modify storage doom.schedule:data queue append from storage doom.schedule:ctx task
execute if score #retry doom.schedule matches ..-1 run tellraw @a [{"text":"[doom.schedule] Retry exhausted: ","color":"red"},{"nbt":"task.id","storage":"doom.schedule:ctx"}]retryRepresents the number of additional attempts (retry:3=Try 3 more times after failure, for a total of 4 executions).retry_delayDefault is 1, ≤0 is automatically corrected to 1. Output a warning and discard the task when exhausted.
context freeze
Freeze execution context when enqueuing: current dimension, coordinate, orientation, executor UUID. UUID passedUUID[0..3]Read 4 ints, decompose them byte by byte, look up the table and concatenate them into a hex string:
# doom.schedule:internal/schedule/context — 冻结上下文
execute if entity @s store result storage doom.schedule:ctx _.uuid0 int 1 run data get entity @s UUID[0]
execute if entity @s store result storage doom.schedule:ctx _.uuid1 int 1 run data get entity @s UUID[1]
execute if entity @s store result storage doom.schedule:ctx _.uuid2 int 1 run data get entity @s UUID[2]
execute if entity @s store result storage doom.schedule:ctx _.uuid3 int 1 run data get entity @s UUID[3]
execute if entity @s[type=player] run data modify storage doom.schedule:ctx _.is_player set value 1b
execute if entity @s run function doom.schedule:internal/schedule/uuid_hex
execute if data storage doom.schedule:ctx _.b0 run function doom.schedule:internal/schedule/uuid_join with storage doom.schedule:ctx _uuid_hexBreak 4 ints into 16 bytes (b0..bf),uuid_joinHongchahex_chars[]Get 16 hex pairs,uuid_concatMacros are concatenated into UUID strings:
$data modify storage doom.schedule:ctx _.by set value "$(h3)$(h2)$(h1)$(h0)-$(h7)$(h6)-$(h5)$(h4)-$(hb)$(ha)-$(h9)$(h8)$(hf)$(he)$(hd)$(hc)"Notice
uuid_hexThere is a negative value overflow fix. when#byteWhen negative, except the correction#byte(add 256), it also needs to be corrected#temp(remove 1), otherwise subsequent divisions are offset by 1:
execute if score #byte doom.schedule matches ..-1 run scoreboard players remove #temp doom.schedule 1
execute if score #byte doom.schedule matches ..-1 run scoreboard players add #byte doom.schedule 256After joining the queue, all UUID intermediate fields (b0..bf, h0..hf, uuid0..uuid3) are automatically cleaned, and no storage remains.
dimension scheme:
| Level | Detection method | Scope of application |
|---|---|---|
| entitydimension | data get entity @s Dimension | All entities - automatically support any dimension |
| dim_scan | execute if dimension | 3 native dimension, used for entityless executors |
known_dimensionstag | function tag | user-defined dimension |
Entity scheduling automatically supports any dimension. Only command block/console scheduling requires manual registration of custom dimensions:
# 检测自定义维度
execute if dimension mymod:void run data modify storage doom.schedule:ctx _.dim set value "mymod:void"// data/doom.schedule/tags/function/known_dimensions.json
{"values": ["your_datapack:detect_void"]}API reference
All APIs are functions and support macro parameters. removeschedule_dynamicexistapi/below, and the rest are at the root level.
basic scheduling
function doom.schedule:schedule {run:'say hello',time:5,unit:'s',id:'hello_world'}| Parameters | Type | Required | Description |
|---|---|---|---|
run | string | ✅ | executed command |
time | int | ✅ | Delay value |
unit | string | ✅ | Unit:t/tick、s/second、m/minute、h/hour、d/day |
id | string | ✅ | Task identifier, used to cancel/pause/resume |
Invalid unit will report an error andreturn fail。
with retry
function doom.schedule:schedule_with_retry {run:'say hi',time:20,unit:'t',id:'rt',retry:3,retry_delay:5}retry— Number of additional attempts (3 = maximum 4 executions).retry_delay— Retry interval tick (≤0 automatically corrected to 1).
Cancel
function doom.schedule:cancel_one {id:'hello_world'} # 推荐
function doom.schedule:cancel_all {id:'group_a'} # 全取消
function doom.schedule:clear # 清空所有cancel_one
- according to
queue[]→offline[]→paused[]Scan sequentially, delete only the first match (excluding this tickprocessing[])。
cancel_all— Exact ID match, deleteprocessing[]+queue[]+offline[]+paused[]All matching items in , return the cumulative count.
clear— Clear all queues unconditionally.
Pause and resume
function doom.schedule:pause {id:'hello_world'} # 从 queue 移入 paused
function doom.schedule:resume {id:'hello_world'} # 从 paused 移回 queuepaused[]independent fromoffline[], not interfered by restore. only handlequeue[], excluding this tickprocessing[]。
Quick dispatch
function doom.schedule:api/schedule_dynamic {run:'say hi',time:20,unit:'t',prefix:'demo'}byprefixdirectly asid. If you need a unique ID, please use it directly.schedule。
other
function doom.schedule:get_time # 返回当前 game time
function doom.schedule:__help__ # 聊天栏帮助Scan mode
cancel_one、pause、resumeShare a Scan-Rebuild mode. bycancel_oneFor example:
# cancel_one.mcfunction (简化)
data modify storage doom.schedule:ctx scan set from storage doom.schedule:data queue
data remove storage doom.schedule:data queue
data modify storage doom.schedule:data queue set value []
$execute if data storage doom.schedule:ctx scan[0] run function doom.schedule:internal/scan/cancel_queue {id:'$(id)'}Step 3: Copy the source queue toctx.scan[]→ Clear the source queue → Check each item.
scan/cancel_queueFor each task:
data modify storage doom.schedule:ctx current set from storage doom.schedule:ctx scan[0]
data remove storage doom.schedule:ctx scan[0]
execute if score #removed doom.schedule matches 1.. run data modify ... queue append ... # 已找到目标,剩余全部保留
$execute if score #removed doom.schedule matches 0 unless data ... current{id:'$(id)'} run data modify ... queue append ... # 未找到且不匹配,保留
$execute if score #removed doom.schedule matches 0 if data ... current{id:'$(id)'} run scoreboard players set #removed 1 # 找到匹配,标记移除
$execute if data ... scan[0] run function ... cancel_queue {id:'$(id)'} # 递归This mode maintains FIFO order at the cost of O(n) copying the entire queue.
cancel_oneScan in sequencequeue[]→offline[]→paused[], it will stop when it finds the first one.pausescanningqueue[]Move matching tasks intopaused[]。resumescanningpaused[]move backqueue[]。
Exact filtering for cancel_all
cancel_allAdopt a completely different strategy - don't scan, but use NBT filter to get it right in one step:
$execute store result score #removed_queue doom.schedule run data remove storage doom.schedule:data queue[{id:"$(id)"}]
$execute store result score #removed_offline doom.schedule run data remove storage doom.schedule:data offline[{id:"$(id)"}]
$execute store result score #removed_paused doom.schedule run data remove storage doom.schedule:data paused[{id:"$(id)"}]
$execute store result score #removed_processing doom.schedule run data remove storage doom.schedule:ctx processing[{id:"$(id)"}]data remove ... [{id:"$(id)"}]Find all elements matching id in storage and delete them,execute store result scoreCapture the actual number of deletions. Each of the four queues has one line, and is returned after accumulation. This is a typical application of NBT filter - search, delete and count in one step.
clearMore directly - unconditionally reset all queues to empty arrays.
Execution path
When the task is due,looper_execAccording to whether there isbyThe fields are scattered into two execution paths:
has target entity (execute/run):
$execute if entity $(by) run scoreboard players set #target_online doom.schedule 1
$execute if score #target_online doom.schedule matches 1 store success score #success doom.schedule in $(dim) positioned $(posX) $(posY) $(posZ) rotated $(rotX) $(rotY) as $(by) at @s run $(run)Two stages: detect first$(by)Online, only executed when online.store success score #successCapture command execution success/failure to provide a basis for retry diversion.
Noticeas $(by) at @sinat @swill cover the previouspositioned/rotated——This means that when there is an entity target, the current position of the entity is used for execution, and the coordinate saved when joining the queue is ignored. savedposX/Y/ZandrotX/YOnly if there is no entity path (run_noentity), used for scheduling issued by command block/console.
No target entity(execute/run_noentity):
$execute store success score #success doom.schedule in $(dim) positioned $(posX) $(posY) $(posZ) rotated $(rotX) $(rotY) at @s run $(run)Execute directly under the saved dimension, coordinate, and orientation without binding.@s. Suitable for scheduling issued by command block or console.
after executionlooper_execaccording to#target_online、#success、retry、is_playerThe four-dimensional combination is used to make diversion decisions. This is equivalent to a state machine implemented in mcfunction, with four Boolean values determining the direction of the 8 exits.
Performance considerations
The core loop is intick.mcfunctionRunning in:
execute if data storage doom.schedule:data queue[0] run function doom.schedule:internal/looper
execute if data storage doom.schedule:data offline[0] run function doom.schedule:internal/restorelooperwill the entirequeue[]move toprocessing[],Depend onlooper_scanRecursively traverse all expired tasks and put those that have not expired at the end of the queue. Execute all due tasks in a single tick instead of polling one by one. Offline recovery is limited to 10 per tick.
The UUID temporary field is automatically cleared before joining the queue, leaving no storage residue.
Queue operations (cancel_one/pause/resume) using scan mode: copy the source queue toctx.scan[], clear the source queue, check each item one by one and decide to keep or remove it. Maintain FIFO order but copy the entire queue.
mcdoc autocomplete
doom.schedule provides 3 mcdoc files to support storage completion of Spyglass / Misode's mcdoc plug-in:
| File | Completion Scene |
|---|---|
mcdoc/doom.schedule.mcdoc | data modify storage doom.schedule:data ... |
data modify storage doom.schedule:const ... | |
data modify storage doom.schedule:ctx ... |
doom.schedule:dataCompletequeue[]、offline[]、paused[]Task field in (run, time, unit, id, exec_time, by, dim, posX/Y/Z, rotX/Y, is_player, retry, retry_delay, online)。
doom.schedule:constCompleteunits[](name + scale) andhex_chars[]。
doom.schedule:ctxCompletetask、processing[]、scan[]、current、unitEntry、temp_retryWait for runtime fields.
For example usage seefunction/mcdoc.mcfunction. After installing the mcdoc plug-in, in.mcfunctionEnter:
data modify storage doom.schedule:data queue append value {run:"say hi",time:20,unit:"t",id:"demo",exec_time:0}input to{All task fields will be automatically prompted.
Practice: Integrate into existing data pack
Typical usage of doom.schedule is to replace/schedulecommand, especially when you need to track the player:
# 代替 scoreboard timer 循环
function doom.schedule:schedule {
run:'function your_pack:do_something',
time:2,unit:'t',
id:'task_$(unique_id)'
}Advantages: No need to occupy the scoreboard loop, tasks are automatically frozen when the player is offline, and automatically restored when online.
Limitations and prospects
- Task data is saved in storage: It disappears after the server restarts. Passable
data modifyPersistence to file, but this is not the scope of data pack - Accuracy ±1 tick: compared with game time, no drift will accumulate, but will be affected by the execution order within a single tick
- The retry mechanism is synchronous: retries are also requeued in the same tick chain, without skipping tasks in front of the queue
cancel_one/pauseDoes not apply to this tickprocessing[]: The task is in itruncall withincancel_oneinvalidoffline[]Manual writes are not accepted: only bylooper_execAutomatic management of offline offloading
Compare with similar solutions:
| Features | bs.schedule | D-Better-Schedule | doom.schedule |
|---|---|---|---|
| Number of functions | ~20 | ~60 | 35 |
| Scheduling | /schedulefire-once | #tickPoll | #tickPoll |
| UUID | score + predicate | guhex →execute as | 4int → hex →execute as $(by) |
| Offline | ❌ | ✅offline[] | ✅offline[] + restore |
| Try again | ❌ | ✅ | ✅schedule_with_retry |
| Pause | ❌ | ✅ | ✅paused[]Independent Queue |
| External dependencies | None | guLibrary | None |