Scheduling
Hytale provides HytaleServer.SCHEDULED_EXECUTOR for scheduling delayed and repeating tasks outside the world tick loop.
SCHEDULED_EXECUTOR
Section titled “SCHEDULED_EXECUTOR”public static final ScheduledExecutorService SCHEDULED_EXECUTOR = Executors.newSingleThreadScheduledExecutor(ThreadUtil.daemon("Scheduler"));Key characteristics:
- Single-threaded - tasks don’t overlap, execute sequentially
- Daemon thread - won’t prevent JVM shutdown
- Shared across all mods - keep tasks quick
Since tasks run on the scheduler thread (not a world thread), you’ll often need to post results back using world.execute().
Heavy Async Work
Section titled “Heavy Async Work”The scheduler is single-threaded - heavy tasks block other scheduled tasks. For CPU-intensive or blocking I/O work, use ForkJoinPool.commonPool():
CompletableFuture.runAsync(() -> { // runs on ForkJoinPool.commonPool() byte[] data = loadLargeFile();
// post result to world thread world.execute(() -> processData(data));});For custom thread pools, use ThreadUtil:
import com.hypixel.hytale.server.core.util.concurrent.ThreadUtil;
// bounded cached pool with daemon threadsprivate final ExecutorService pool = ThreadUtil.newCachedThreadPool( 4, // max threads ThreadUtil.daemonCounted("MyMod-Worker-%d") // "MyMod-Worker-1", "MyMod-Worker-2", etc.);
CompletableFuture.runAsync(() -> doWork(), pool);Shut down custom executors in shutdown().
Delayed Tasks
Section titled “Delayed Tasks”One-time execution after a delay:
// run once after 5 secondsHytaleServer.SCHEDULED_EXECUTOR.schedule(() -> { getLogger().atInfo().log("5 seconds have passed!");}, 5, TimeUnit.SECONDS);With World Thread Posting
Section titled “With World Thread Posting”// delayed teleportHytaleServer.SCHEDULED_EXECUTOR.schedule(() -> { world.execute(() -> { if (ref.isValid()) { Teleport teleport = Teleport.createForPlayer(targetWorld, spawnPoint); store.addComponent(ref, Teleport.getComponentType(), teleport); } });}, 3, TimeUnit.SECONDS);Repeating Tasks
Section titled “Repeating Tasks”scheduleWithFixedDelay
Section titled “scheduleWithFixedDelay”Waits the delay between task completion and next start. Preferred for most cases:
// auto-save every 5 minutesHytaleServer.SCHEDULED_EXECUTOR.scheduleWithFixedDelay(() -> { saveData();}, 5, 5, TimeUnit.MINUTES); // initial delay, periodscheduleAtFixedRate
Section titled “scheduleAtFixedRate”Runs at fixed intervals regardless of task duration. Use for timing-sensitive tasks:
// ping clients every secondHytaleServer.SCHEDULED_EXECUTOR.scheduleAtFixedRate(() -> { sendPingPackets();}, 1, 1, TimeUnit.SECONDS);Cancellation
Section titled “Cancellation”Store the ScheduledFuture and cancel in shutdown():
public class MyPlugin extends JavaPlugin { private ScheduledFuture<?> saveTask;
@Override protected void setup() { this.saveTask = HytaleServer.SCHEDULED_EXECUTOR.scheduleWithFixedDelay( this::autoSave, 5, 5, TimeUnit.MINUTES ); }
@Override protected void shutdown() { if (this.saveTask != null) { this.saveTask.cancel(false); // false = don't interrupt if running } }
private void autoSave() { // save logic }}Cancellation Parameters
Section titled “Cancellation Parameters”future.cancel(false); // let current execution finish, prevent future runsfuture.cancel(true); // interrupt current execution (use carefully)Self-Cancelling Tasks
Section titled “Self-Cancelling Tasks”A task can cancel itself when a condition is met:
ScheduledFuture<?>[] taskHolder = new ScheduledFuture[1];taskHolder[0] = HytaleServer.SCHEDULED_EXECUTOR.scheduleWithFixedDelay(() -> { if (shouldStop()) { taskHolder[0].cancel(false); return; } doWork();}, 1, 1, TimeUnit.SECONDS);AtomicReference for Safe Replacement
Section titled “AtomicReference for Safe Replacement”When a task might be rescheduled:
private final AtomicReference<ScheduledFuture<?>> currentTask = new AtomicReference<>();
public void scheduleTask() { ScheduledFuture<?> newTask = HytaleServer.SCHEDULED_EXECUTOR.schedule( this::doWork, 10, TimeUnit.SECONDS ); ScheduledFuture<?> oldTask = this.currentTask.getAndSet(newTask); if (oldTask != null) { oldTask.cancel(false); }}Common Patterns
Section titled “Common Patterns”Periodic Save
Section titled “Periodic Save”private ScheduledFuture<?> saveTask;
@Overrideprotected void start() { this.saveTask = HytaleServer.SCHEDULED_EXECUTOR.scheduleWithFixedDelay( () -> this.dataStore.saveToDisk(), 5, 5, TimeUnit.MINUTES );}
@Overrideprotected void shutdown() { if (this.saveTask != null) { this.saveTask.cancel(false); } this.dataStore.saveToDisk(); // final save}Debounced Action
Section titled “Debounced Action”Wait for activity to settle before acting:
private ScheduledFuture<?> debounceTask;
public void onSomethingChanged() { if (this.debounceTask != null) { this.debounceTask.cancel(false); } this.debounceTask = HytaleServer.SCHEDULED_EXECUTOR.schedule( this::processChanges, 1, TimeUnit.SECONDS );}Timed Sequence
Section titled “Timed Sequence”Execute a sequence of actions with delays:
ScheduledFuture<?>[] task = new ScheduledFuture[1];Queue<Runnable> actions = new LinkedList<>(List.of( () -> sendMessage("3..."), () -> sendMessage("2..."), () -> sendMessage("1..."), () -> startGame()));
task[0] = HytaleServer.SCHEDULED_EXECUTOR.scheduleWithFixedDelay(() -> { Runnable action = actions.poll(); if (action == null) { task[0].cancel(false); return; } action.run();}, 0, 1, TimeUnit.SECONDS);Tick-Based Cooldowns
Section titled “Tick-Based Cooldowns”For in-game cooldowns (interactions, abilities), Hytale uses tick-based timing rather than scheduled tasks:
public class CooldownHandler implements Tickable { private final Map<String, Float> cooldowns = new ConcurrentHashMap<>();
public void setCooldown(String key, float seconds) { this.cooldowns.put(key, seconds); }
public boolean isOnCooldown(String key) { return this.cooldowns.containsKey(key); }
@Override public void tick(float dt) { this.cooldowns.entrySet().removeIf(entry -> { entry.setValue(entry.getValue() - dt); return entry.getValue() <= 0; }); }}This approach:
- Respects world pausing and time dilation
- Doesn’t require cancellation management
- Naturally integrates with the game loop
Thread Safety Reminder
Section titled “Thread Safety Reminder”The scheduler thread is not a world thread. Calling world methods directly from scheduled tasks is unsafe:
HytaleServer.SCHEDULED_EXECUTOR.schedule(() -> { world.setBlock(x, y, z, "hytale:stone"); // wrong thread!}, 5, TimeUnit.SECONDS);Post to the world thread instead:
HytaleServer.SCHEDULED_EXECUTOR.schedule(() -> { world.execute(() -> { world.setBlock(x, y, z, "hytale:stone"); });}, 5, TimeUnit.SECONDS);See Threading for the full threading model.