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í |