From c7e7f82a957b15979ed3f9e52d195850be179f88 Mon Sep 17 00:00:00 2001 From: Anhgelus Morhtuuzh Date: Tue, 17 Mar 2026 17:57:12 +0100 Subject: refactor(game): use ticks for timer --- .../java/world/anhgelus/molehunt/game/Game.java | 127 +++++++++------------ .../anhgelus/molehunt/mixin/WorldTimerAccess.java | 45 ++++++++ .../world/anhgelus/molehunt/timer/TickTask.java | 72 ++++++++++++ .../world/anhgelus/molehunt/timer/TimerAccess.java | 65 +++++++++++ src/main/resources/molehunt.mixins.json | 3 +- 5 files changed, 239 insertions(+), 73 deletions(-) create mode 100644 src/main/java/world/anhgelus/molehunt/mixin/WorldTimerAccess.java create mode 100644 src/main/java/world/anhgelus/molehunt/timer/TickTask.java create mode 100644 src/main/java/world/anhgelus/molehunt/timer/TimerAccess.java diff --git a/src/main/java/world/anhgelus/molehunt/game/Game.java b/src/main/java/world/anhgelus/molehunt/game/Game.java index 6ab5a4a..98fe2b8 100644 --- a/src/main/java/world/anhgelus/molehunt/game/Game.java +++ b/src/main/java/world/anhgelus/molehunt/game/Game.java @@ -15,6 +15,8 @@ import net.minecraft.text.Text; import net.minecraft.world.GameMode; import net.minecraft.world.rule.GameRules; import world.anhgelus.molehunt.Molehunt; +import world.anhgelus.molehunt.timer.TickTask; +import world.anhgelus.molehunt.timer.TimerAccess; import world.anhgelus.molehunt.utils.TimeUtils; import java.util.*; @@ -23,16 +25,11 @@ import java.util.stream.Collectors; public class Game { - private Timer timer = new Timer(); - public final int defaultTime = Molehunt.CONFIG.getGameDuration()*60; - private int remaining = defaultTime; - + public final int defaultTime = Molehunt.CONFIG.getGameDuration() * 60; private final MinecraftServer server; - private final List moles = new ArrayList<>(); - private final TitleFadeS2CPacket timing = new TitleFadeS2CPacket(20, 40, 20); - + private int remaining = defaultTime; private boolean started = false; public Game(MinecraftServer server) { @@ -62,21 +59,17 @@ public class Game { // gamerules for the start gamerules.setValue(GameRules.DO_IMMEDIATE_RESPAWN, true, server); + final var timer = TimerAccess.getTimerFromOverworld(server); + final var worldBorder = server.getOverworld().getWorldBorder(); worldBorder.setSize(Molehunt.CONFIG.getInitialWorldSize()); if (Molehunt.CONFIG.getBorderShrinkingStartingTimeOffset() < Molehunt.CONFIG.getGameDuration()) { - timer.schedule(new TimerTask() { - @Override - public void run() { - final var worldBorder = server.getOverworld().getWorldBorder(); - worldBorder.interpolateSize( - Molehunt.CONFIG.getInitialWorldSize(), - Molehunt.CONFIG.getFinalWorldSize(), - (long) (Molehunt.CONFIG.getGameDuration() - Molehunt.CONFIG.getBorderShrinkingStartingTimeOffset()) * 60 * 1000, - 0L - ); - } - }, (long) Molehunt.CONFIG.getBorderShrinkingStartingTimeOffset() * 60 * 1000); + timer.dds_runTask(new TickTask(() -> worldBorder.interpolateSize( + Molehunt.CONFIG.getInitialWorldSize(), + Molehunt.CONFIG.getFinalWorldSize(), + (long) (Molehunt.CONFIG.getGameDuration() - Molehunt.CONFIG.getBorderShrinkingStartingTimeOffset()) * 60 * 1000, + 0L + ), (long) Molehunt.CONFIG.getBorderShrinkingStartingTimeOffset() * 60 * 1000)); } final var title = new TitleS2CPacket(Text.translatable("molehunt.game.start.suspense")); @@ -91,45 +84,38 @@ public class Game { server.setDefaultGameMode(GameMode.SPECTATOR); - timer.schedule(new TimerTask() { - @Override - public void run() { - playerManager.getPlayerList().forEach(p -> { - p.networkHandler.sendPacket(timing); - if (moles.contains(p.getUuid())) { - p.networkHandler.sendPacket(new TitleS2CPacket(Text.translatable("molehunt.game.start.mole.title"))); - p.networkHandler.sendPacket(new SubtitleS2CPacket(Text.translatable("molehunt.game.start.mole.subtitle"))); - } else { - p.networkHandler.sendPacket(new TitleS2CPacket(Text.translatable("molehunt.game.start.survivor.title"))); - p.networkHandler.sendPacket(new SubtitleS2CPacket(Text.translatable("molehunt.game.start.survivor.subtitle"))); + timer.dds_runTask(new TickTask(() -> { + playerManager.getPlayerList().forEach(p -> { + p.networkHandler.sendPacket(timing); + if (moles.contains(p.getUuid())) { + p.networkHandler.sendPacket(new TitleS2CPacket(Text.translatable("molehunt.game.start.mole.title"))); + p.networkHandler.sendPacket(new SubtitleS2CPacket(Text.translatable("molehunt.game.start.mole.subtitle"))); + } else { + p.networkHandler.sendPacket(new TitleS2CPacket(Text.translatable("molehunt.game.start.survivor.title"))); + p.networkHandler.sendPacket(new SubtitleS2CPacket(Text.translatable("molehunt.game.start.survivor.subtitle"))); + } + // reset health and food level + p.setHealth(p.getMaxHealth()); + p.getHungerManager().setFoodLevel(20); + p.getHungerManager().setSaturationLevel(5.0f); + }); + // reset gamerules after the start + gamerules.setValue(GameRules.DO_IMMEDIATE_RESPAWN, false, server); + // reset time and weather + server.getOverworld().setTimeOfDay(0); + server.getOverworld().resetWeather(); + changeState(true); + timer.dds_runTask(new TickTask(() -> { + remaining--; + playerManager.getPlayerList().forEach(player -> { + if (Molehunt.timerVisibility.getOrDefault(player.getUuid(), true)) { + player.networkHandler.sendPacket(new OverlayMessageS2CPacket(Text.of(getRemainingText()))); } - // reset health and food level - p.setHealth(p.getMaxHealth()); - p.getHungerManager().setFoodLevel(20); - p.getHungerManager().setSaturationLevel(5.0f); }); - // reset gamerules after the start - gamerules.setValue(GameRules.DO_IMMEDIATE_RESPAWN, false, server); - // reset time and weather - server.getOverworld().setTimeOfDay(0); - server.getOverworld().resetWeather(); - changeState(true); - - timer.scheduleAtFixedRate(new TimerTask() { - @Override - public void run() { - remaining--; - playerManager.getPlayerList().forEach(player -> { - if (Molehunt.timerVisibility.getOrDefault(player.getUuid(), true)) { - player.networkHandler.sendPacket(new OverlayMessageS2CPacket(Text.of(getRemainingText()))); - } - }); - playerManager.sendToAll(timing); - if (remaining == 0) end(); - } - }, 5*1000, 1000); - } - }, 4*1000); + playerManager.sendToAll(timing); + if (remaining == 0) end(); + }, 5 * 1000, 1000)); + }, 4 * 1000)); } public void stop() { @@ -138,8 +124,8 @@ public class Game { } public void end() { - timer.cancel(); - timer = new Timer(); + final var timer = TimerAccess.getTimerFromOverworld(server); + timer.dds_cancel(); final var worldBorder = server.getOverworld().getWorldBorder(); // Stops the border shrinking. @@ -153,21 +139,18 @@ public class Game { p.networkHandler.sendPacket(winnerSuspense); p.changeGameMode(GameMode.CREATIVE); }); - timer.schedule(new TimerTask() { - @Override - public void run() { - TitleS2CPacket winner; - if (wonByMoles()) { - winner = new TitleS2CPacket(Text.translatable("molehunt.game.end.winners.moles.title")); - } else { - winner = new TitleS2CPacket(Text.translatable("molehunt.game.end.winners.survivors.title")); - } - pm.sendToAll(new SubtitleS2CPacket(Text.translatable("molehunt.game.end.winners.subtitle", getMolesAsString()))); - pm.sendToAll(winner); - pm.sendToAll(timing); - moles.clear(); + timer.dds_runTask(new TickTask(() -> { + TitleS2CPacket winner; + if (wonByMoles()) { + winner = new TitleS2CPacket(Text.translatable("molehunt.game.end.winners.moles.title")); + } else { + winner = new TitleS2CPacket(Text.translatable("molehunt.game.end.winners.survivors.title")); } - }, 4*1000); + pm.sendToAll(new SubtitleS2CPacket(Text.translatable("molehunt.game.end.winners.subtitle", getMolesAsString()))); + pm.sendToAll(winner); + pm.sendToAll(timing); + moles.clear(); + }, 4 * 1000)); } public Text getRemainingText() { diff --git a/src/main/java/world/anhgelus/molehunt/mixin/WorldTimerAccess.java b/src/main/java/world/anhgelus/molehunt/mixin/WorldTimerAccess.java new file mode 100644 index 0000000..cd8a1b4 --- /dev/null +++ b/src/main/java/world/anhgelus/molehunt/mixin/WorldTimerAccess.java @@ -0,0 +1,45 @@ +package world.anhgelus.molehunt.mixin; + +import net.minecraft.server.world.ServerWorld; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import world.anhgelus.molehunt.timer.TimerAccess; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BooleanSupplier; + +@Mixin(ServerWorld.class) +public class WorldTimerAccess implements TimerAccess { + @Unique + private final List tasks = new ArrayList<>(); + + @Unique + private final List tasksToAdd = new ArrayList<>(); + + @Inject(method = "tick", at = @At("TAIL")) + private void onTick(BooleanSupplier shouldKeepTicking, CallbackInfo ci) { + tasks.stream().filter(TickTask::isRunning).forEach(TickTask::tick); + tasks.addAll(tasksToAdd); + tasksToAdd.clear(); + } + + @Override + public void dds_runTask(TimerAccess.TickTask task) { + tasksToAdd.add(task); + } + + @Override + public void dds_cancel() { + tasks.stream().filter(TickTask::isRunning).forEach(TickTask::cancel); + tasks.clear(); + } + + @Override + public List dds_getTasks() { + return tasks.stream().filter(TickTask::isRunning).toList(); + } +} \ No newline at end of file diff --git a/src/main/java/world/anhgelus/molehunt/timer/TickTask.java b/src/main/java/world/anhgelus/molehunt/timer/TickTask.java new file mode 100644 index 0000000..8c74b89 --- /dev/null +++ b/src/main/java/world/anhgelus/molehunt/timer/TickTask.java @@ -0,0 +1,72 @@ +package world.anhgelus.molehunt.timer; + +/** + * Represents a complete task called each tick + */ +public class TickTask implements TimerAccess.TickTask { + public final long ticksDelay; + public final long ticksRepeat; + public final boolean repeating; + public final TimerAccess.Task task; + private boolean cancelled = false; + private long currentTicking; + + /** + * Create a new repeating TickTask + * + * @param task Task to run after the delay or the repeat time + * @param ticksDelay Delay before the first task's run + * @param ticksRepeat Repeat each tick (if the repeat is 0, it will repeat each tick, if it is below 0, it will not repeat) + * @throws IllegalArgumentException if ticksDelay is below 0 + */ + public TickTask(TimerAccess.Task task, long ticksDelay, long ticksRepeat) { + if (ticksDelay < 0) throw new IllegalArgumentException("Ticks delay must be non-negative"); + this.ticksDelay = ticksDelay; + this.ticksRepeat = ticksRepeat; + this.task = task; + repeating = ticksRepeat >= 0; + currentTicking = ticksDelay; + } + + /** + * Create a new delayed TickTask + * + * @param task Task to run after the delay or the repeat time + * @param ticksDelay Delay before the first task's run + * @throws IllegalArgumentException if ticksDelay or if ticksRepeat is below 0 + */ + public TickTask(TimerAccess.Task task, long ticksDelay) { + if (ticksDelay < 0) throw new IllegalArgumentException("Ticks delay must be non-negative"); + this.ticksDelay = ticksDelay; + this.ticksRepeat = -1; + this.task = task; + repeating = false; + currentTicking = ticksDelay; + } + + public void tick() { + if (--currentTicking > 0) return; + task.run(); + if (repeating) { + currentTicking = ticksRepeat; + } else { + cancel(); + } + } + + public long cancel() { + if (cancelled) throw new IllegalStateException("Task already cancelled"); + cancelled = true; + return currentTicking; + } + + public boolean isRunning() { + return !cancelled; + } + + @Override + public long getTickingBeforeRun() { + if (cancelled) return -1; + return currentTicking; + } +} \ No newline at end of file diff --git a/src/main/java/world/anhgelus/molehunt/timer/TimerAccess.java b/src/main/java/world/anhgelus/molehunt/timer/TimerAccess.java new file mode 100644 index 0000000..2e02b12 --- /dev/null +++ b/src/main/java/world/anhgelus/molehunt/timer/TimerAccess.java @@ -0,0 +1,65 @@ +package world.anhgelus.molehunt.timer; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.World; + +import java.util.List; + +public interface TimerAccess { + /** + * Get the timer linked to the overworld + * + * @param server Current server + * @return TimerAccess linked to the overworld + */ + static TimerAccess getTimerFromOverworld(MinecraftServer server) { + final var timer = (TimerAccess) server.getWorld(World.OVERWORLD); + if (timer == null) + throw new NullPointerException("Impossible to get TimerAccess from the overworld (it is null)"); + return timer; + } + + /** + * Run a task (called each tick ticked) + * + * @param task Task to run + */ + void dds_runTask(TimerAccess.TickTask task); + + void dds_cancel(); + + /** + * @return All non-cancelled tasks + */ + List dds_getTasks(); + + interface TickTask { + /** + * Tick the task + */ + void tick(); + + /** + * Cancel the task + * + * @return the remaining ticks before the run of the Task + * @throws IllegalStateException if the task is already cancelled + */ + long cancel(); + + boolean isRunning(); + + /** + * @return the number of ticks before run of the task (if the task is cancelled, returns -1) + */ + long getTickingBeforeRun(); + } + + /** + * Represents a task to run after ticking + */ + @FunctionalInterface + interface Task { + void run(); + } +} diff --git a/src/main/resources/molehunt.mixins.json b/src/main/resources/molehunt.mixins.json index e9ec77e..2dc5bb9 100644 --- a/src/main/resources/molehunt.mixins.json +++ b/src/main/resources/molehunt.mixins.json @@ -6,7 +6,8 @@ "mixins": [ "NoJoinLeaveMessage", "NoMsgCommand", - "NoPortals" + "NoPortals", + "WorldTimerAccess" ], "injectors": { "defaultRequire": 1 -- cgit v1.2.3