Refactor pomodoro
Some checks failed
/ build (push) Failing after 10m2s

Signed-off-by: Minecon724 <minecon724@noreply.git.m724.eu>
This commit is contained in:
Minecon724 2025-05-14 19:06:11 +02:00
commit e3f34ec46f
Signed by untrusted user who does not match committer: m724
GPG key ID: A02E6E67AB961189
8 changed files with 244 additions and 158 deletions

View file

@ -6,70 +6,77 @@
package eu.m724.tweaks.module.pomodoro; package eu.m724.tweaks.module.pomodoro;
import java.util.concurrent.TimeUnit;
public class PlayerPomodoro { public class PlayerPomodoro {
private int pomodori = 0; private int intervalsDone = 0;
private boolean isBreak = false; private boolean breaktime = false;
// this is for both break and not break private long currentIntervalStartedAtNanos = -1;
private long intervalStart = -1;
/** public int getIntervalsDone() {
* A "pomodoro" is the 25-minute cycle you take breaks after<br> return intervalsDone;
* This returns how many cycles already elapsed, so if this is the first cycle this is 0<br> }
* The break after the "pomodoro," so if it's breaktime after the first "pomodoro" it stays at 0
*/ public boolean isBreaktime() {
public int getPomodori() { return breaktime;
return pomodori; }
public boolean isLongBreak() {
return (intervalsDone + 1) % PomodoroModule.INTERVALS_BEFORE_LONG_BREAK == 0;
} }
/** /**
* When did the current interval start<br> * @return The time the current interval or break started, in milliseconds
* Or when did the break start
*
* @see PlayerPomodoro#isBreak()
*/ */
public long getIntervalStart() { public long getCurrentIntervalStartedAtNanos() {
return intervalStart; return currentIntervalStartedAtNanos;
} }
public int getCycleDurationSeconds() { public int getCurrentIntervalDurationSeconds() {
return isBreak ? (pomodori < 3 ? 300 : 1200) : 1500; if (breaktime) {
if (isLongBreak()) {
return PomodoroModule.LONG_BREAK_DURATION_SECONDS;
}
return PomodoroModule.SHORT_BREAK_DURATION_SECONDS;
} else {
return PomodoroModule.INTERVAL_DURATION_SECONDS;
}
} }
public long getRemainingSeconds(long now) { public long getCurrentIntervalDurationNanos() {
return getCycleDurationSeconds() - (now - getIntervalStart()) / 1000000000; return TimeUnit.SECONDS.toNanos(getCurrentIntervalDurationSeconds());
} }
/** public long getCurrentIntervalRemainingNanos(long nowNanos) {
* Is it a break currently long elapsed = nowNanos - getCurrentIntervalStartedAtNanos();
*/ return getCurrentIntervalDurationNanos() - elapsed;
public boolean isBreak() {
return isBreak;
} }
public boolean isCycleComplete() { public boolean isIntervalComplete(long nowNanos) {
return intervalStart + getCycleDurationSeconds() * 1000000000L < System.nanoTime(); return currentIntervalStartedAtNanos + getCurrentIntervalDurationNanos() < nowNanos;
} }
/** /**
* Resets and starts the timer * Resets and starts the timer
*/ */
public void start() { public void start() {
this.pomodori = 0; this.intervalsDone = 0;
this.isBreak = false; this.breaktime = false;
this.intervalStart = System.nanoTime(); this.currentIntervalStartedAtNanos = System.nanoTime();
} }
/** /**
* Completes a cycle * Completes a cycle
*/ */
public void next() { public void startBreakOrNextInterval() {
if (isBreak) { // from break to interval if (breaktime) { // from break to interval
this.pomodori++; this.intervalsDone++;
this.pomodori %= 4; this.intervalsDone %= PomodoroModule.INTERVALS_BEFORE_LONG_BREAK;
} }
this.intervalStart = System.nanoTime(); this.currentIntervalStartedAtNanos = System.nanoTime();
isBreak = !isBreak; breaktime = !breaktime;
} }
} }

View file

@ -0,0 +1,29 @@
/*
* Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
package eu.m724.tweaks.module.pomodoro;
import org.bukkit.entity.Player;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class PlayerPomodoroTracker {
static final Map<UUID, PlayerPomodoro> timers = new HashMap<>();
public static PlayerPomodoro get(Player player) {
return timers.get(player.getUniqueId());
}
public static PlayerPomodoro create(Player player) {
return timers.computeIfAbsent(player.getUniqueId(), (k) -> new PlayerPomodoro());
}
public static boolean remove(Player player) {
return timers.remove(player.getUniqueId()) != null;
}
}

View file

@ -6,6 +6,9 @@
package eu.m724.tweaks.module.pomodoro; package eu.m724.tweaks.module.pomodoro;
import eu.m724.tweaks.Language;
import eu.m724.tweaks.config.TweaksConfig;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.command.Command; import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
@ -18,25 +21,34 @@ public class PomodoroCommands implements CommandExecutor {
Player player = (Player) sender; Player player = (Player) sender;
String action = args.length > 0 ? args[0] : null; String action = args.length > 0 ? args[0] : null;
PlayerPomodoro pomodoro = Pomodoros.get(player); PlayerPomodoro pomodoro = PlayerPomodoroTracker.get(player);
long now = System.nanoTime();
if (pomodoro != null) { if (pomodoro != null) {
if ("stop".equals(action)) { if ("stop".equals(action)) {
Pomodoros.remove(player); PlayerPomodoroTracker.remove(player);
sender.sendMessage("Pomodoro disabled"); sender.spigot().sendMessage(Language.getComponent("pomodoroStopped", ChatColor.GREEN, label));
} else { } else {
if (pomodoro.isCycleComplete()) { if (pomodoro.isIntervalComplete(now)) {
pomodoro.next(); pomodoro.startBreakOrNextInterval();
if (pomodoro.isBreaktime() && TweaksConfig.getConfig().pomodoroForce()) {
player.kickPlayer(PomodoroModule.formatTimer(pomodoro, now).toLegacyText());
}
} }
sender.spigot().sendMessage(Pomodoros.formatTimer(pomodoro, pomodoro.getRemainingSeconds(System.nanoTime())));
sender.spigot().sendMessage(PomodoroModule.formatTimer(pomodoro, now));
} }
} else { } else {
if ("start".equals(action)) { if ("start".equals(action)) {
pomodoro = Pomodoros.create(player); pomodoro = PlayerPomodoroTracker.create(player);
pomodoro.start(); pomodoro.start();
sender.spigot().sendMessage(Pomodoros.formatTimer(pomodoro, pomodoro.getCycleDurationSeconds()));
sender.spigot().sendMessage(PomodoroModule.formatTimer(pomodoro, now));
} else { } else {
sender.sendMessage("Start pomodoro with /pom start"); // TODO help?
sender.spigot().sendMessage(Language.getComponent("pomodoroStart", ChatColor.GOLD, label));
} }
} }

View file

@ -7,56 +7,68 @@
package eu.m724.tweaks.module.pomodoro; package eu.m724.tweaks.module.pomodoro;
import eu.m724.tweaks.config.TweaksConfig; import eu.m724.tweaks.config.TweaksConfig;
import net.md_5.bungee.api.chat.ComponentBuilder;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.player.*; import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerToggleSneakEvent;
public class PomodoroListener implements Listener { public class PomodoroListener implements Listener {
private final boolean force = TweaksConfig.getConfig().pomodoroForce(); private final TweaksConfig config = TweaksConfig.getConfig();
@EventHandler @EventHandler
public void onPlayerLogin(PlayerLoginEvent event) { public void onPlayerLogin(PlayerLoginEvent event) {
// Joining ends break
Player player = event.getPlayer(); Player player = event.getPlayer();
PlayerPomodoro pomodoro = Pomodoros.get(player); PlayerPomodoro pomodoro = PlayerPomodoroTracker.get(player);
if (pomodoro == null) return; if (pomodoro == null) return;
long remaining = pomodoro.getRemainingSeconds(System.nanoTime()); long now = System.nanoTime();
long remaining = pomodoro.getCurrentIntervalRemainingNanos(now);
if (pomodoro.isBreak()) { if (pomodoro.isBreaktime()) {
if (pomodoro.isCycleComplete()) { if (remaining > 0 && config.pomodoroForce()) {
pomodoro.next(); player.kickPlayer(PomodoroModule.formatTimer(pomodoro, now).toLegacyText());
} else { } else {
if (force) { pomodoro.startBreakOrNextInterval();
event.getPlayer().kickPlayer(
new ComponentBuilder()
.append(Pomodoros.formatTimer(pomodoro, remaining))
.build().toLegacyText()
);
}
} }
} }
} }
@EventHandler @EventHandler
public void onPlayerQuit(PlayerQuitEvent event) { public void onPlayerQuit(PlayerQuitEvent event) {
// Quitting starts break
Player player = event.getPlayer(); Player player = event.getPlayer();
PlayerPomodoro pomodoro = Pomodoros.get(player); PlayerPomodoro pomodoro = PlayerPomodoroTracker.get(player);
if (pomodoro == null) return; if (pomodoro == null) return;
if (!pomodoro.isBreak() && pomodoro.isCycleComplete()) { if (pomodoro.isBreaktime()) return;
pomodoro.next();
long now = System.nanoTime();
long remaining = pomodoro.getCurrentIntervalRemainingNanos(now);
if (remaining <= 0) {
pomodoro.startBreakOrNextInterval();
} }
} }
@EventHandler @EventHandler
public void onPlayerMove(PlayerMoveEvent event) { public void onPlayerToggleSneak(PlayerToggleSneakEvent event) {
Player player = event.getPlayer(); // Sneaking ends break
PlayerPomodoro timer = Pomodoros.get(player); if (!event.isSneaking()) return;
if (timer == null) return;
if (timer.isBreak() && timer.getRemainingSeconds(System.nanoTime()) <= 0) Player player = event.getPlayer();
timer.next(); // resume timer if break ended PlayerPomodoro pomodoro = PlayerPomodoroTracker.get(player);
if (pomodoro == null) return;
if (!pomodoro.isBreaktime()) return;
long now = System.nanoTime();
long remaining = pomodoro.getCurrentIntervalRemainingNanos(now);
if (remaining <= 0) {
pomodoro.startBreakOrNextInterval();
}
} }
} }

View file

@ -6,9 +6,23 @@
package eu.m724.tweaks.module.pomodoro; package eu.m724.tweaks.module.pomodoro;
import eu.m724.tweaks.Language;
import eu.m724.tweaks.config.TweaksConfig;
import eu.m724.tweaks.module.TweaksModule; import eu.m724.tweaks.module.TweaksModule;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.ComponentBuilder;
import java.util.concurrent.TimeUnit;
public class PomodoroModule extends TweaksModule { public class PomodoroModule extends TweaksModule {
static final int INTERVAL_DURATION_SECONDS = 10;
static final int SHORT_BREAK_DURATION_SECONDS = (int) TimeUnit.MINUTES.toSeconds(5);
static final int LONG_BREAK_DURATION_SECONDS = (int) TimeUnit.MINUTES.toSeconds(20);
static final int INTERVALS_BEFORE_LONG_BREAK = 4;
static final int KICK_DELAY_SECONDS = 60;
@Override @Override
protected void onInit() { protected void onInit() {
registerEvents(new PomodoroListener()); registerEvents(new PomodoroListener());
@ -16,4 +30,66 @@ public class PomodoroModule extends TweaksModule {
registerCommand("pomodoro", new PomodoroCommands()); registerCommand("pomodoro", new PomodoroCommands());
} }
/**
* Gets a formatted timer for a player.
*
* @param pomodoro the player's {@link PlayerPomodoro} instance
* @param nowNanos unix now timestamp in nanoseconds
* @return the timer as {@link BaseComponent}
*/
static BaseComponent formatTimer(PlayerPomodoro pomodoro, long nowNanos) {
ComponentBuilder builder = new ComponentBuilder();
long remainingNanos = pomodoro.getCurrentIntervalRemainingNanos(nowNanos);
long remainingSeconds = TimeUnit.NANOSECONDS.toSeconds(remainingNanos);
if (pomodoro.isBreaktime()) {
if (pomodoro.isLongBreak()) {
builder.append(Language.getComponent("pomodoroLongBreak", ChatColor.LIGHT_PURPLE));
} else {
builder.append(Language.getComponent("pomodoroShortBreak", ChatColor.LIGHT_PURPLE));
}
if (remainingNanos > 0) {
builder.append(" %02d:%02d".formatted(remainingSeconds / 60, remainingSeconds % 60))
.color(ChatColor.GOLD);
} else {
builder.append(" 00:00")
.color(ChatColor.GREEN);
if (!TweaksConfig.getConfig().pomodoroForce()) {
builder.append( "/pom").color(ChatColor.GOLD);
}
}
} else {
if (remainingNanos > 0) {
builder
.append("%02d:%02d".formatted(remainingSeconds / 60, remainingSeconds % 60))
.color(ChatColor.GRAY);
} else {
builder
.append("00:00")
.color(remainingSeconds % 2 == 0 ? ChatColor.RED : ChatColor.GRAY);
if (!TweaksConfig.getConfig().pomodoroForce()) {
builder.append( "/pom").color(ChatColor.GOLD);
}
}
}
for (int i=0; i<INTERVALS_BEFORE_LONG_BREAK; i++) {
ChatColor color = ChatColor.GRAY;
if (i == pomodoro.getIntervalsDone()) {
color = ChatColor.LIGHT_PURPLE;
} else if (i > pomodoro.getIntervalsDone()) {
color = ChatColor.DARK_GRAY;
}
builder.append(" o").color(color);
}
return builder.build();
}
} }

View file

@ -9,34 +9,49 @@ package eu.m724.tweaks.module.pomodoro;
import eu.m724.tweaks.TweaksPlugin; import eu.m724.tweaks.TweaksPlugin;
import eu.m724.tweaks.config.TweaksConfig; import eu.m724.tweaks.config.TweaksConfig;
import net.md_5.bungee.api.ChatMessageType; import net.md_5.bungee.api.ChatMessageType;
import org.bukkit.Bukkit;
import org.bukkit.Sound; import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin; import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitRunnable; import org.bukkit.scheduler.BukkitRunnable;
import java.util.concurrent.TimeUnit;
public class PomodoroRunnable extends BukkitRunnable { public class PomodoroRunnable extends BukkitRunnable {
private final boolean force = TweaksConfig.getConfig().pomodoroForce(); private final TweaksConfig config = TweaksConfig.getConfig();
private final Plugin plugin = TweaksPlugin.getInstance(); // used only to kick private final Plugin plugin = TweaksPlugin.getInstance(); // used only to kick
@Override @Override
public void run() { public void run() {
long now = System.nanoTime(); long now = System.nanoTime();
Bukkit.getOnlinePlayers().forEach(player -> { PlayerPomodoroTracker.timers.forEach((uuid, pomodoro) -> {
PlayerPomodoro pomodoro = Pomodoros.get(player); long remainingNanos = pomodoro.getCurrentIntervalRemainingNanos(now);
if (pomodoro == null) return; long remainingSecs = TimeUnit.NANOSECONDS.toSeconds(remainingNanos);
long remaining = pomodoro.getRemainingSeconds(now); // TODO optimize?
// TODO make not always on Player player = plugin.getServer().getPlayer(uuid);
player.spigot().sendMessage(ChatMessageType.ACTION_BAR, Pomodoros.formatTimer(pomodoro, remaining));
if (remaining <= 0) { if (player != null && player.isOnline()) {
player.playSound(player.getLocation(), Sound.BLOCK_ANVIL_FALL, 1.0f, 0.5f); // TODO make not always on
if (remaining < -60 && force) { player.spigot().sendMessage(ChatMessageType.ACTION_BAR, PomodoroModule.formatTimer(pomodoro, now));
plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> {
pomodoro.next(); if (remainingNanos <= 0) {
player.kickPlayer(Pomodoros.formatTimer(pomodoro, pomodoro.getRemainingSeconds(now)).toLegacyText()); player.playSound(player.getLocation(), Sound.BLOCK_ANVIL_FALL, 1.0f, 0.5f);
});
if (remainingSecs < -PomodoroModule.KICK_DELAY_SECONDS && config.pomodoroForce()) {
pomodoro.startBreakOrNextInterval();
plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () ->
player.kickPlayer(PomodoroModule.formatTimer(pomodoro, now).toLegacyText())
);
}
}
} else {
if (remainingNanos <= 0) {
// Start break automatically if the player is offline
if (!pomodoro.isBreaktime()) {
pomodoro.startBreakOrNextInterval();
}
} }
} }
}); });

View file

@ -1,71 +0,0 @@
/*
* Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
package eu.m724.tweaks.module.pomodoro;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.ComponentBuilder;
import org.bukkit.entity.Player;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class Pomodoros {
static final Map<UUID, PlayerPomodoro> timers = new HashMap<>();
public static PlayerPomodoro get(Player player) {
return timers.get(player.getUniqueId());
}
public static PlayerPomodoro create(Player player) {
return timers.computeIfAbsent(player.getUniqueId(), (k) -> new PlayerPomodoro());
}
public static boolean remove(Player player) {
return timers.remove(player.getUniqueId()) != null;
}
static BaseComponent formatTimer(PlayerPomodoro pomodoro, long remaining) {
ComponentBuilder builder = new ComponentBuilder();
if (pomodoro.isBreak()) {
builder.append("Break ").color(ChatColor.LIGHT_PURPLE);
if (remaining > 0) {
builder.append("%02d:%02d".formatted(remaining / 60, remaining % 60))
.color(ChatColor.GOLD);
} else {
builder.append("00:00")
.color(ChatColor.GREEN);
}
} else {
if (remaining > 0) {
builder
.append("%02d:%02d".formatted(remaining / 60, remaining % 60))
.color(ChatColor.GRAY);
} else {
builder
.append("00:00")
.color(remaining % 2 == 0 ? ChatColor.RED : ChatColor.YELLOW);
}
}
for (int i=0; i<4; i++) {
ChatColor color = ChatColor.GRAY;
if (i == pomodoro.getPomodori()) {
color = ChatColor.LIGHT_PURPLE;
} else if (i > pomodoro.getPomodori()) {
color = ChatColor.DARK_GRAY;
}
builder.append(" o").color(color);
}
return builder.build();
}
}

View file

@ -30,7 +30,7 @@ chatAlreadyHere = You're already in this room.
authKickWrongKey = You're connecting to the wrong server address. You must connect to the one you're registered to. authKickWrongKey = You're connecting to the wrong server address. You must connect to the one you're registered to.
# If force is enabled and player is not registered. Changing this reveals you're using this plugin # If force is enabled and player is not registered. Changing this reveals you're using this plugin
authKickUnregistered = You are not whitelisted on this server! authKickUnregistered = You are not whitelisted on this server!
authKickError = An error occured. Please try again. If this persists, contact an administrator. authKickError = An error occurred. Please try again. If this persists, contact an administrator.
redstoneGatewayItem = Redstone gateway redstoneGatewayItem = Redstone gateway
@ -45,3 +45,9 @@ wordCoordsPlayerOnly = Only players can execute this command without arguments.
wordCoordsOutOfRange = Those coordinates are invalid. wordCoordsOutOfRange = Those coordinates are invalid.
wordCoordsInvalidWord = Invalid word: "%s" wordCoordsInvalidWord = Invalid word: "%s"
wordCoordsNoWords = Please provide the Z coordinate. wordCoordsNoWords = Please provide the Z coordinate.
# /pomodoro
pomodoroStopped = Pomodoro stopped. Restart it with /%s start
pomodoroStart = Start pomodoro with /%s start
pomodoroShortBreak = Short break
pomodoroLongBreak = Long break