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: Minecon724
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;
import java.util.concurrent.TimeUnit;
public class PlayerPomodoro {
private int pomodori = 0;
private int intervalsDone = 0;
private boolean isBreak = false;
// this is for both break and not break
private long intervalStart = -1;
private boolean breaktime = false;
private long currentIntervalStartedAtNanos = -1;
/**
* A "pomodoro" is the 25-minute cycle you take breaks after<br>
* 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 int getPomodori() {
return pomodori;
public int getIntervalsDone() {
return intervalsDone;
}
public boolean isBreaktime() {
return breaktime;
}
public boolean isLongBreak() {
return (intervalsDone + 1) % PomodoroModule.INTERVALS_BEFORE_LONG_BREAK == 0;
}
/**
* When did the current interval start<br>
* Or when did the break start
*
* @see PlayerPomodoro#isBreak()
* @return The time the current interval or break started, in milliseconds
*/
public long getIntervalStart() {
return intervalStart;
public long getCurrentIntervalStartedAtNanos() {
return currentIntervalStartedAtNanos;
}
public int getCycleDurationSeconds() {
return isBreak ? (pomodori < 3 ? 300 : 1200) : 1500;
public int getCurrentIntervalDurationSeconds() {
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) {
return getCycleDurationSeconds() - (now - getIntervalStart()) / 1000000000;
public long getCurrentIntervalDurationNanos() {
return TimeUnit.SECONDS.toNanos(getCurrentIntervalDurationSeconds());
}
/**
* Is it a break currently
*/
public boolean isBreak() {
return isBreak;
public long getCurrentIntervalRemainingNanos(long nowNanos) {
long elapsed = nowNanos - getCurrentIntervalStartedAtNanos();
return getCurrentIntervalDurationNanos() - elapsed;
}
public boolean isCycleComplete() {
return intervalStart + getCycleDurationSeconds() * 1000000000L < System.nanoTime();
public boolean isIntervalComplete(long nowNanos) {
return currentIntervalStartedAtNanos + getCurrentIntervalDurationNanos() < nowNanos;
}
/**
* Resets and starts the timer
*/
public void start() {
this.pomodori = 0;
this.isBreak = false;
this.intervalStart = System.nanoTime();
this.intervalsDone = 0;
this.breaktime = false;
this.currentIntervalStartedAtNanos = System.nanoTime();
}
/**
* Completes a cycle
*/
public void next() {
if (isBreak) { // from break to interval
this.pomodori++;
this.pomodori %= 4;
public void startBreakOrNextInterval() {
if (breaktime) { // from break to interval
this.intervalsDone++;
this.intervalsDone %= PomodoroModule.INTERVALS_BEFORE_LONG_BREAK;
}
this.intervalStart = System.nanoTime();
isBreak = !isBreak;
this.currentIntervalStartedAtNanos = System.nanoTime();
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;
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.CommandExecutor;
import org.bukkit.command.CommandSender;
@ -18,25 +21,34 @@ public class PomodoroCommands implements CommandExecutor {
Player player = (Player) sender;
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 ("stop".equals(action)) {
Pomodoros.remove(player);
sender.sendMessage("Pomodoro disabled");
PlayerPomodoroTracker.remove(player);
sender.spigot().sendMessage(Language.getComponent("pomodoroStopped", ChatColor.GREEN, label));
} else {
if (pomodoro.isCycleComplete()) {
pomodoro.next();
if (pomodoro.isIntervalComplete(now)) {
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 {
if ("start".equals(action)) {
pomodoro = Pomodoros.create(player);
pomodoro = PlayerPomodoroTracker.create(player);
pomodoro.start();
sender.spigot().sendMessage(Pomodoros.formatTimer(pomodoro, pomodoro.getCycleDurationSeconds()));
sender.spigot().sendMessage(PomodoroModule.formatTimer(pomodoro, now));
} 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;
import eu.m724.tweaks.config.TweaksConfig;
import net.md_5.bungee.api.chat.ComponentBuilder;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
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 {
private final boolean force = TweaksConfig.getConfig().pomodoroForce();
private final TweaksConfig config = TweaksConfig.getConfig();
@EventHandler
public void onPlayerLogin(PlayerLoginEvent event) {
// Joining ends break
Player player = event.getPlayer();
PlayerPomodoro pomodoro = Pomodoros.get(player);
PlayerPomodoro pomodoro = PlayerPomodoroTracker.get(player);
if (pomodoro == null) return;
long remaining = pomodoro.getRemainingSeconds(System.nanoTime());
long now = System.nanoTime();
long remaining = pomodoro.getCurrentIntervalRemainingNanos(now);
if (pomodoro.isBreak()) {
if (pomodoro.isCycleComplete()) {
pomodoro.next();
if (pomodoro.isBreaktime()) {
if (remaining > 0 && config.pomodoroForce()) {
player.kickPlayer(PomodoroModule.formatTimer(pomodoro, now).toLegacyText());
} else {
if (force) {
event.getPlayer().kickPlayer(
new ComponentBuilder()
.append(Pomodoros.formatTimer(pomodoro, remaining))
.build().toLegacyText()
);
}
pomodoro.startBreakOrNextInterval();
}
}
}
@EventHandler
public void onPlayerQuit(PlayerQuitEvent event) {
// Quitting starts break
Player player = event.getPlayer();
PlayerPomodoro pomodoro = Pomodoros.get(player);
PlayerPomodoro pomodoro = PlayerPomodoroTracker.get(player);
if (pomodoro == null) return;
if (!pomodoro.isBreak() && pomodoro.isCycleComplete()) {
pomodoro.next();
if (pomodoro.isBreaktime()) return;
long now = System.nanoTime();
long remaining = pomodoro.getCurrentIntervalRemainingNanos(now);
if (remaining <= 0) {
pomodoro.startBreakOrNextInterval();
}
}
@EventHandler
public void onPlayerMove(PlayerMoveEvent event) {
Player player = event.getPlayer();
PlayerPomodoro timer = Pomodoros.get(player);
if (timer == null) return;
public void onPlayerToggleSneak(PlayerToggleSneakEvent event) {
// Sneaking ends break
if (!event.isSneaking()) return;
if (timer.isBreak() && timer.getRemainingSeconds(System.nanoTime()) <= 0)
timer.next(); // resume timer if break ended
Player player = event.getPlayer();
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;
import eu.m724.tweaks.Language;
import eu.m724.tweaks.config.TweaksConfig;
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 {
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
protected void onInit() {
registerEvents(new PomodoroListener());
@ -16,4 +30,66 @@ public class PomodoroModule extends TweaksModule {
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.config.TweaksConfig;
import net.md_5.bungee.api.ChatMessageType;
import org.bukkit.Bukkit;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.concurrent.TimeUnit;
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
@Override
public void run() {
long now = System.nanoTime();
Bukkit.getOnlinePlayers().forEach(player -> {
PlayerPomodoro pomodoro = Pomodoros.get(player);
if (pomodoro == null) return;
PlayerPomodoroTracker.timers.forEach((uuid, pomodoro) -> {
long remainingNanos = pomodoro.getCurrentIntervalRemainingNanos(now);
long remainingSecs = TimeUnit.NANOSECONDS.toSeconds(remainingNanos);
long remaining = pomodoro.getRemainingSeconds(now);
// TODO make not always on
player.spigot().sendMessage(ChatMessageType.ACTION_BAR, Pomodoros.formatTimer(pomodoro, remaining));
// TODO optimize?
Player player = plugin.getServer().getPlayer(uuid);
if (remaining <= 0) {
player.playSound(player.getLocation(), Sound.BLOCK_ANVIL_FALL, 1.0f, 0.5f);
if (remaining < -60 && force) {
plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> {
pomodoro.next();
player.kickPlayer(Pomodoros.formatTimer(pomodoro, pomodoro.getRemainingSeconds(now)).toLegacyText());
});
if (player != null && player.isOnline()) {
// TODO make not always on
player.spigot().sendMessage(ChatMessageType.ACTION_BAR, PomodoroModule.formatTimer(pomodoro, now));
if (remainingNanos <= 0) {
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.
# 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!
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
@ -44,4 +44,10 @@ durabilityDisabled = Disabled durability alert
wordCoordsPlayerOnly = Only players can execute this command without arguments.
wordCoordsOutOfRange = Those coordinates are invalid.
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