HyCodeYourTale

Scheduled Tasks

Scheduled Tasks

Detailní dokumentace k plánovaným a periodickým úlohám v Hytale.

---

Přehled Architektury

Task System

├── TaskRegistry (per-plugin registrace tasků)
│ └── TaskRegistration (wrapper pro Future)

├── ScheduledExecutorService (Java standard)
│ ├── schedule() - Jednorázový delay
│ ├── scheduleAtFixedRate() - Fixní interval
│ └── scheduleWithFixedDelay() - Delay po dokončení

└── TickingThread (Hytale game loop)
├── 30 TPS default
└── tick(float deltaTime)

---

TaskRegistry

Per-plugin registrace tasků pro automatický cleanup:

public class TaskRegistry extends Registry {

public TaskRegistry(
@Nonnull List registrations,
BooleanSupplier precondition,
String preconditionMessage
) {
super(registrations, precondition, preconditionMessage, TaskRegistration::new);
}

// Registrace CompletableFuture
public TaskRegistration registerTask(@Nonnull CompletableFuture task) {
return this.register(new TaskRegistration(task));
}

// Registrace ScheduledFuture
public TaskRegistration registerTask(@Nonnull ScheduledFuture task) {
return this.register(new TaskRegistration(task));
}
}

TaskRegistration

public class TaskRegistration extends Registration {
private final Future task;

public TaskRegistration(@Nonnull Future task) {
// Při unregister automaticky cancel()
super(() -> true, () -> task.cancel(false));
this.task = task;
}

public Future getTask() {
return this.task;
}
}

---

Použití TaskRegistry

V Pluginu

public class MyPlugin extends JavaPlugin {

private ScheduledExecutorService scheduler;

@Override
protected void setup() {
scheduler = Executors.newSingleThreadScheduledExecutor();

// Registruj periodický task
ScheduledFuture periodicTask = scheduler.scheduleAtFixedRate(
this::performPeriodicWork,
1, 5, TimeUnit.MINUTES
);

// Registrace - automatický cancel při shutdown
getTaskRegistry().registerTask((ScheduledFuture) periodicTask);

// Registruj long-running async task
CompletableFuture asyncTask = CompletableFuture.runAsync(() -> {
while (!Thread.currentThread().isInterrupted()) {
doBackgroundWork();
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});

getTaskRegistry().registerTask(asyncTask);
}

@Override
protected void shutdown() {
// TaskRegistry automaticky zruší všechny registrované tasky
// Ale scheduler je třeba ukončit manuálně
if (scheduler != null) {
scheduler.shutdown();
}
}
}

---

ScheduledExecutorService

Vytvoření Scheduleru

// Single thread - pro sekvenční tasky
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();

// Thread pool - pro paralelní tasky
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(4);

// S pojmenovanými vlákny
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(
runnable -> {
Thread thread = new Thread(runnable, "MyPlugin-Scheduler");
thread.setDaemon(true);
return thread;
}
);

Typy Plánování

#### schedule() - Jednorázový Delay

// Provede jednou za 30 sekund
scheduler.schedule(() -> {
getLogger().info("Delayed task executed!");
}, 30, TimeUnit.SECONDS);

#### scheduleAtFixedRate() - Fixní Interval

// Každých 5 minut (nezávisle na délce běhu tasku)
scheduler.scheduleAtFixedRate(
this::autoSave,
5, // initial delay
5, // period
TimeUnit.MINUTES
);

// Timeline: |--5min--|--5min--|--5min--|
// start start start

#### scheduleWithFixedDelay() - Delay Po Dokončení

// 10 sekund PO dokončení předchozího běhu
scheduler.scheduleWithFixedDelay(
this::cleanupTask,
0, // initial delay
10, // delay after completion
TimeUnit.SECONDS
);

// Timeline: |--run--|--10s--|--run--|--10s--|
// start end start end

---

TickingThread

Hytale game loop abstrakce:

public abstract class TickingThread implements Runnable {
public static final int TPS = 30; // Default 30 ticks per second

private final String threadName;
private final boolean daemon;
private int tps;
private int tickStepNanos; // 1000000000 / tps
private HistoricMetric bufferedTickLengthMetricSet;
@Nullable
private Thread thread;
@Nonnull
private CompletableFuture startedFuture = new CompletableFuture<>();

public TickingThread(String threadName) {
this(threadName, 30, false);
}

public TickingThread(String threadName, int tps, boolean daemon) {
this.threadName = threadName;
this.daemon = daemon;
this.tps = tps;
this.tickStepNanos = 1000000000 / tps;
this.bufferedTickLengthMetricSet = HistoricMetric.builder(
(long)this.tickStepNanos, TimeUnit.NANOSECONDS
)
.addPeriod(10L, TimeUnit.SECONDS)
.addPeriod(1L, TimeUnit.MINUTES)
.addPeriod(5L, TimeUnit.MINUTES)
.build();
}

@Override
public void run() {
try {
this.onStart();
this.startedFuture.complete(null);
long beforeTick = System.nanoTime() - tickStepNanos;

while (this.thread != null && !this.thread.isInterrupted()) {
// Čekej na další tick
long delta;
if (!this.isIdle()) {
while ((delta = System.nanoTime() - beforeTick) < tickStepNanos) {
Thread.onSpinWait();
}
} else {
delta = System.nanoTime() - beforeTick;
}

beforeTick = System.nanoTime();

// Proveď tick
this.tick((float)delta / 1.0E9F);

// Metriky
long tickLength = System.nanoTime() - beforeTick;
this.bufferedTickLengthMetricSet.add(System.nanoTime(), tickLength);

// Sleep do dalšího ticku
long sleepLength = tickStepNanos - tickLength - SLEEP_OFFSET;
if (sleepLength > 0L) {
Thread.sleep(sleepLength / 1000000L);
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (Throwable e) {
HytaleLogger.getLogger().at(Level.SEVERE).withCause(e)
.log("Exception in thread %s:", this.thread);
}

if (this.needsShutdown.getAndSet(false)) {
this.onShutdown();
}
}

// Abstraktní metody
protected abstract void tick(float deltaTime);
protected abstract void onShutdown();
protected void onStart() {}
protected boolean isIdle() { return false; }
}

Použití TickingThread

public class MyGameLoop extends TickingThread {

public MyGameLoop() {
super("MyPlugin-GameLoop", 20, false); // 20 TPS
}

@Override
protected void tick(float deltaTime) {
// deltaTime = čas od posledního ticku v sekundách
updateEntities(deltaTime);
processEvents();
}

@Override
protected void onStart() {
getLogger().info("Game loop started");
}

@Override
protected void onShutdown() {
getLogger().info("Game loop stopped");
saveState();
}
}

// Použití
MyGameLoop gameLoop = new MyGameLoop();
CompletableFuture started = gameLoop.start();

started.thenRun(() -> {
getLogger().info("Game loop is running");
});

// Shutdown
gameLoop.stop();

TPS Management

// Změna TPS za běhu (musí být na ticking thread)
gameLoop.setTps(60); // Zvýšit na 60 TPS

// Kontrola TPS
int currentTps = gameLoop.getTps();

// Metriky délky ticku
HistoricMetric metrics = gameLoop.getBufferedTickLengthMetricSet();

---

Vzory

Auto-Save Manager

public class AutoSaveManager {
private final JavaPlugin plugin;
private ScheduledExecutorService scheduler;
private final int saveIntervalMinutes;

public AutoSaveManager(JavaPlugin plugin, int intervalMinutes) {
this.plugin = plugin;
this.saveIntervalMinutes = intervalMinutes;
}

public void start() {
scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "AutoSave");
t.setDaemon(true);
return t;
});

ScheduledFuture task = scheduler.scheduleAtFixedRate(
this::performSave,
saveIntervalMinutes,
saveIntervalMinutes,
TimeUnit.MINUTES
);

plugin.getTaskRegistry().registerTask((ScheduledFuture) task);
plugin.getLogger().info("Auto-save started (every " + saveIntervalMinutes + " minutes)");
}

private void performSave() {
plugin.getLogger().info("Auto-saving...");

List> saves = new ArrayList<>();
for (Player player : Universe.get().getPlayers()) {
saves.add(CompletableFuture.runAsync(() -> {
savePlayer(player.getUuid());
}));
}

CompletableFuture.allOf(saves.toArray(new CompletableFuture[0]))
.thenRun(() -> plugin.getLogger().info("Auto-save complete"))
.exceptionally(e -> {
plugin.getLogger().at(Level.SEVERE).withCause(e).log("Auto-save failed");
return null;
});
}

public void stop() {
if (scheduler != null) {
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
}
}
}
}

Delayed Teleport

public class TeleportManager {
private final ScheduledExecutorService scheduler;

public TeleportManager() {
this.scheduler = Executors.newSingleThreadScheduledExecutor();
}

public void delayedTeleport(Player player, Vector3d destination, int delaySeconds) {
player.sendMessage(Message.raw("Teleport za " + delaySeconds + " sekund..."));

// Countdown
for (int i = delaySeconds; i > 0; i--) {
final int remaining = i;
scheduler.schedule(() -> {
if (player.isOnline()) {
player.sendMessage(Message.raw(remaining + "..."));
}
}, delaySeconds - i, TimeUnit.SECONDS);
}

// Teleport
scheduler.schedule(() -> {
if (!player.isOnline()) return;

World world = player.getWorld();
world.execute(() -> {
teleportPlayer(player, destination);
player.sendMessage(Message.raw("Teleportován!"));
});
}, delaySeconds, TimeUnit.SECONDS);
}

public void shutdown() {
scheduler.shutdown();
}
}

Cancellable Countdown

public class CountdownTask {
private final ScheduledFuture future;
private final AtomicBoolean cancelled = new AtomicBoolean(false);

public CountdownTask(
ScheduledExecutorService scheduler,
Player player,
int seconds,
Runnable onComplete
) {
AtomicInteger remaining = new AtomicInteger(seconds);

this.future = scheduler.scheduleAtFixedRate(() -> {
if (cancelled.get()) return;

int current = remaining.getAndDecrement();

if (current > 0) {
player.sendMessage(Message.raw("Zbývá: " + current + "s"));
} else {
// Dokončeno
cancel();
World world = player.getWorld();
world.execute(onComplete);
}
}, 0, 1, TimeUnit.SECONDS);
}

public void cancel() {
cancelled.set(true);
future.cancel(false);
}

public boolean isCancelled() {
return cancelled.get();
}
}

Rate Limiter

public class RateLimiter {
private final Map lastAction = new ConcurrentHashMap<>();
private final long cooldownMillis;
private final ScheduledExecutorService cleaner;

public RateLimiter(long cooldownMillis) {
this.cooldownMillis = cooldownMillis;
this.cleaner = Executors.newSingleThreadScheduledExecutor();

// Cleanup starých záznamů každou minutu
cleaner.scheduleAtFixedRate(() -> {
long now = System.currentTimeMillis();
lastAction.entrySet().removeIf(entry ->
now - entry.getValue() > cooldownMillis * 2
);
}, 1, 1, TimeUnit.MINUTES);
}

public boolean tryAction(UUID playerId) {
long now = System.currentTimeMillis();
Long last = lastAction.get(playerId);

if (last != null && now - last < cooldownMillis) {
return false; // Rate limited
}

lastAction.put(playerId, now);
return true;
}

public long getRemainingCooldown(UUID playerId) {
Long last = lastAction.get(playerId);
if (last == null) return 0;

long elapsed = System.currentTimeMillis() - last;
return Math.max(0, cooldownMillis - elapsed);
}

public void shutdown() {
cleaner.shutdown();
}
}

---

World Thread Synchronizace

Periodický Task → World Thread

scheduler.scheduleAtFixedRate(() -> {
// Na scheduler thread

for (Player player : Universe.get().getPlayers()) {
World world = player.getWorld();

// Přepni na world thread pro komponenty
world.execute(() -> {
Ref ref = player.getRef();
if (ref != null && ref.isValid()) {
Store store = ref.getStore();

// Bezpečný přístup ke komponentám
CustomComponent comp = store.getComponent(ref, CustomComponent.getComponentType());
if (comp != null) {
comp.update();
}
}
});
}
}, 0, 1, TimeUnit.MINUTES);

---

Shutdown Pattern

public class MyPlugin extends JavaPlugin {

private ScheduledExecutorService scheduler;
private AutoSaveManager autoSave;

@Override
protected void setup() {
scheduler = Executors.newSingleThreadScheduledExecutor();
autoSave = new AutoSaveManager(this, 5);
}

@Override
protected void start() {
autoSave.start();
}

@Override
protected void shutdown() {
// 1. Zastav auto-save
if (autoSave != null) {
autoSave.stop();
}

// 2. Shutdown scheduler
if (scheduler != null) {
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) {
getLogger().warning("Forcing scheduler shutdown...");
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}

// 3. TaskRegistry automaticky zruší registrované tasky
}
}

---

Shrnutí

| Třída | Účel |
|-------|------|
| TaskRegistry | Per-plugin registrace tasků |
| TaskRegistration | Wrapper pro Future s auto-cancel |
| ScheduledExecutorService | Java scheduler |
| TickingThread | Hytale game loop abstrakce |

| Metoda | Kdy použít |
|--------|------------|
| schedule() | Jednorázový delayed task |
| scheduleAtFixedRate() | Periodický s fixním intervalem |
| scheduleWithFixedDelay() | Periodický s delay po dokončení |
| TickingThread.tick() | Game loop logika |

| TimeUnit | Typické použití |
|----------|-----------------|
| MILLISECONDS | Krátké delay, animace |
| SECONDS | Countdown, cooldown |
| MINUTES | Auto-save, cleanup |
| HOURS | Maintenance, backupy |

| Pattern | Popis |
|---------|-------|
| Auto-Save | Periodické ukládání dat |
| Delayed Action | Akce po delay (teleport) |
| Countdown | Odpočet s cancel |
| Rate Limiter | Omezení frekvence akcí |

Last updated: 20. ledna 2026