Compare commits

...
Author SHA1 Message Date
imDMK 795017b938 Follow Gemini review and fix build 2026-03-16 12:00:15 +01:00
DMKandgemini-code-assist[bot] 0a5ae2a305 Update eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/tagout/FightTagOutCommand.java
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-03-16 11:50:23 +01:00
DMK 85d20b6a0c Merge branch 'master' into feature/duration-formatter 2026-03-16 11:46:46 +01:00
imDMK aae1f6bf28 follow Rollczi review suggestions 2026-03-16 11:37:29 +01:00
imDMK 96920f023a Merge remote-tracking branch 'origin/feature/duration-formatter' into feature/duration-formatter 2026-03-08 00:14:30 +01:00
imDMK 56815d5155 duration: add ms unit support to duration formatter 2026-03-08 00:14:17 +01:00
DMKandJakub Kędziora 87650c3acb Update eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/tagout/FightTagOutCommand.java
Co-authored-by: Jakub Kędziora <77227023+Jakubk15@users.noreply.github.com>
2026-03-07 11:48:33 +01:00
DMKandJakub Kędziora 01fa48ce83 Update eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/tagout/FightTagOutCommand.java
Co-authored-by: Jakub Kędziora <77227023+Jakubk15@users.noreply.github.com>
2026-03-07 11:48:24 +01:00
DMKandJakub Kędziora 6a02e62815 Update eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/tagout/FightTagOutCommand.java
Co-authored-by: Jakub Kędziora <77227023+Jakubk15@users.noreply.github.com>
2026-03-07 11:48:14 +01:00
imDMK e64f555349 Follow Jakubk15 review suggestions 2026-03-07 11:46:30 +01:00
imDMK 7885b03332 Follow Google Gemini review 2026-03-06 00:43:11 +01:00
imDMK c81f73abc2 Introduce configurable duration formatter with plural forms and separators 2026-03-06 00:26:26 +01:00
14 changed files with 331 additions and 137 deletions

No files matched your search

@@ -1,17 +1,28 @@
package com.eternalcode.combat;
import com.eternalcode.combat.border.BorderTriggerController;
import com.eternalcode.combat.border.BorderService;
import com.eternalcode.combat.border.BorderServiceImpl;
import com.eternalcode.combat.border.BorderTriggerController;
import com.eternalcode.combat.border.animation.block.BorderBlockController;
import com.eternalcode.combat.border.animation.particle.ParticleController;
import com.eternalcode.combat.bridge.BridgeService;
import com.eternalcode.combat.crystalpvp.RespawnAnchorListener;
import com.eternalcode.combat.config.ConfigService;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.crystalpvp.EndCrystalListener;
import com.eternalcode.combat.crystalpvp.RespawnAnchorListener;
import com.eternalcode.combat.event.EventManager;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.fight.FightManagerImpl;
import com.eternalcode.combat.fight.FightTagCommand;
import com.eternalcode.combat.fight.FightTask;
import com.eternalcode.combat.fight.controller.FightActionBlockerController;
import com.eternalcode.combat.fight.controller.FightBypassAdminController;
import com.eternalcode.combat.fight.controller.FightBypassCreativeController;
import com.eternalcode.combat.fight.controller.FightBypassPermissionController;
import com.eternalcode.combat.fight.controller.FightInventoryController;
import com.eternalcode.combat.fight.controller.FightMessageController;
import com.eternalcode.combat.fight.controller.FightTagController;
import com.eternalcode.combat.fight.controller.FightUnTagController;
import com.eternalcode.combat.fight.death.DeathFlareController;
import com.eternalcode.combat.fight.death.DeathLightningController;
import com.eternalcode.combat.fight.drop.DropKeepInventoryService;
@@ -27,29 +38,30 @@ import com.eternalcode.combat.handler.MissingPermissionHandlerImpl;
import com.eternalcode.combat.config.ConfigService;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.drop.DropController;
import com.eternalcode.combat.fight.drop.DropKeepInventoryService;
import com.eternalcode.combat.fight.drop.DropKeepInventoryServiceImpl;
import com.eternalcode.combat.fight.drop.DropService;
import com.eternalcode.combat.fight.drop.DropServiceImpl;
import com.eternalcode.combat.fight.drop.impl.PercentDropModifier;
import com.eternalcode.combat.fight.drop.impl.PlayersHealthDropModifier;
import com.eternalcode.combat.fight.FightTagCommand;
import com.eternalcode.combat.fight.controller.FightActionBlockerController;
import com.eternalcode.combat.fight.controller.FightMessageController;
import com.eternalcode.combat.fight.controller.FightTagController;
import com.eternalcode.combat.fight.controller.FightUnTagController;
import com.eternalcode.combat.fight.effect.FightEffectController;
import com.eternalcode.combat.event.EventManager;
import com.eternalcode.combat.fight.FightManagerImpl;
import com.eternalcode.combat.fight.FightTask;
import com.eternalcode.combat.fight.effect.FightEffectService;
import com.eternalcode.combat.fight.effect.FightEffectServiceImpl;
import com.eternalcode.combat.fight.firework.FireworkController;
import com.eternalcode.combat.fight.knockback.KnockbackRegionController;
import com.eternalcode.combat.fight.knockback.KnockbackService;
import com.eternalcode.combat.fight.logout.LogoutController;
import com.eternalcode.combat.fight.logout.LogoutService;
import com.eternalcode.combat.fight.pearl.FightPearlController;
import com.eternalcode.combat.fight.pearl.FightPearlService;
import com.eternalcode.combat.fight.pearl.FightPearlServiceImpl;
import com.eternalcode.combat.fight.tagout.FightTagOutController;
import com.eternalcode.combat.fight.tagout.FightTagOutServiceImpl;
import com.eternalcode.combat.fight.tagout.FightTagOutCommand;
import com.eternalcode.combat.fight.tagout.FightTagOutController;
import com.eternalcode.combat.fight.tagout.FightTagOutService;
import com.eternalcode.combat.fight.tagout.FightTagOutServiceImpl;
import com.eternalcode.combat.handler.InvalidUsageHandlerImpl;
import com.eternalcode.combat.handler.MissingPermissionHandlerImpl;
import com.eternalcode.combat.notification.NoticeService;
import com.eternalcode.combat.fight.knockback.KnockbackRegionController;
import com.eternalcode.combat.region.RegionProvider;
import com.eternalcode.combat.updater.UpdaterNotificationController;
import com.eternalcode.combat.updater.UpdaterService;
@@ -62,7 +74,6 @@ import dev.rollczi.litecommands.LiteCommands;
import dev.rollczi.litecommands.bukkit.LiteBukkitFactory;
import dev.rollczi.litecommands.bukkit.LiteBukkitMessages;
import dev.rollczi.litecommands.folia.FoliaExtension;
import java.time.Duration;
import net.kyori.adventure.platform.AudienceProvider;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import net.kyori.adventure.text.minimessage.MiniMessage;
@@ -74,6 +85,7 @@ import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
@@ -95,7 +107,6 @@ public final class CombatPlugin extends JavaPlugin implements EternalCombatApi {
private AudienceProvider audienceProvider;
private LiteCommands<CommandSender> liteCommands;
@Override
public void onEnable() {
Stopwatch started = Stopwatch.createStarted();
@@ -182,7 +193,7 @@ public final class CombatPlugin extends JavaPlugin implements EternalCombatApi {
new FightBypassPermissionController(server, pluginConfig),
new FightBypassCreativeController(server, pluginConfig),
new FightActionBlockerController(this.fightManager, noticeService, pluginConfig, server),
new FightPearlController(pluginConfig.pearl, noticeService, this.fightManager, this.fightPearlService),
new FightPearlController(pluginConfig, noticeService, this.fightManager, this.fightPearlService),
new DeathFlareController(pluginConfig, server, scheduler, this),
new DeathLightningController(pluginConfig, server),
new UpdaterNotificationController(updaterService, pluginConfig, this.audienceProvider, miniMessage),
@@ -1,20 +1,21 @@
package com.eternalcode.combat.bridge;
import com.eternalcode.combat.region.lands.LandsRegionProvider;
import com.eternalcode.combat.bridge.placeholder.FightTagPlaceholder;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.region.CompositeRegionProvider;
import com.eternalcode.combat.region.bukkit.DefaultRegionProvider;
import com.eternalcode.combat.region.RegionProvider;
import com.eternalcode.combat.region.bukkit.DefaultRegionProvider;
import com.eternalcode.combat.region.lands.LandsRegionProvider;
import com.eternalcode.combat.region.worldguard.WorldGuardRegionProvider;
import org.bukkit.Server;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginManager;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import org.bukkit.Server;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginManager;
public class BridgeService {
@@ -1,12 +1,9 @@
package com.eternalcode.combat.bridge.placeholder;
import com.eternalcode.combat.config.implementation.PlaceholderSettings;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.fight.FightTag;
import com.eternalcode.combat.util.DurationUtil;
import com.eternalcode.commons.time.DurationParser;
import java.util.Optional;
import com.eternalcode.combat.time.DurationFormatter;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.bukkit.OfflinePlayer;
import org.bukkit.Server;
@@ -14,17 +11,19 @@ import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
public class FightTagPlaceholder extends PlaceholderExpansion {
private static final String IDENTIFIER = "eternalcombat";
private final PlaceholderSettings placeholderSettings;
private final PluginConfig config;
private final FightManager fightManager;
private final Server server;
private final Plugin plugin;
public FightTagPlaceholder(PluginConfig pluginConfig, FightManager fightManager, Server server, Plugin plugin) {
this.placeholderSettings = pluginConfig.placeholders;
public FightTagPlaceholder(PluginConfig config, FightManager fightManager, Server server, Plugin plugin) {
this.config = config;
this.fightManager = fightManager;
this.server = server;
this.plugin = plugin;
@@ -33,8 +32,7 @@ public class FightTagPlaceholder extends PlaceholderExpansion {
@Override
public String onRequest(OfflinePlayer player, String identifier) {
return switch (identifier) {
case "remaining_millis" -> this.handleRemainingMillis(player);
case "remaining_seconds" -> this.handleRemainingSeconds(player);
case "remaining" -> this.handleRemaining(player);
case "opponent" -> this.handleOpponent(player);
case "opponent_health" -> this.handleOpponentHealth(player);
case "isInCombat" -> this.handleIsInCombat(player);
@@ -43,15 +41,9 @@ public class FightTagPlaceholder extends PlaceholderExpansion {
};
}
private String handleRemainingMillis(OfflinePlayer player) {
private String handleRemaining(OfflinePlayer player) {
return this.getFightTag(player)
.map(tag -> DurationParser.TIME_UNITS.format(tag.getRemainingDuration()))
.orElse("");
}
private String handleRemainingSeconds(OfflinePlayer player) {
return this.getFightTag(player)
.map(tag -> DurationUtil.format(tag.getRemainingDuration()))
.map(tag -> DurationFormatter.of(config.durationFormat).format(tag.getRemainingDuration()))
.orElse("");
}
@@ -73,8 +65,8 @@ public class FightTagPlaceholder extends PlaceholderExpansion {
private String handleIsInCombatFormatted(OfflinePlayer player) {
return this.isPlayerInCombat(player)
? this.placeholderSettings.isInCombatFormattedTrue
: this.placeholderSettings.isInCombatFormattedFalse;
? this.config.placeholders.isInCombatFormattedTrue
: this.config.placeholders.isInCombatFormattedFalse;
}
private boolean isPlayerInCombat(OfflinePlayer player) {
@@ -110,7 +102,7 @@ public class FightTagPlaceholder extends PlaceholderExpansion {
@Override
public @NotNull String getAuthor() {
return this.plugin.getDescription().getAuthors().get(0);
return this.plugin.getDescription().getAuthors().getFirst();
}
@Override
@@ -8,7 +8,6 @@ public class CrystalPvpSettings extends OkaeriConfig {
@Comment({"# Should player be tagged when damaged from crystal explosion set by other player"})
public boolean tagFromCrystals = true;
@Comment({"#Should player be tagged when damaged from respawn anchor explosion set by other player"})
@Comment({"# Should player be tagged when damaged from respawn anchor explosion set by other player"})
public boolean tagFromRespawnAnchor = true;
}
@@ -0,0 +1,37 @@
package com.eternalcode.combat.config.implementation;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.Comment;
public class DurationFormatSettings extends OkaeriConfig {
@Comment({
"# Pattern used to format durations.",
"# Placeholders:",
"# %d{singular|plural} - days",
"# %h{singular|plural} - hours",
"# %m{singular|plural} - minutes",
"# %s{singular|plural} - seconds",
"# %ms{singular|plural} - milliseconds",
"# Example:",
"# %d{day|days} %h{hour|hours} %m{minute|minutes} %s{second|seconds} %ms{millisecond|milliseconds}"
})
public String pattern = "%d {day|days} %h {hour|hours} %m {minute|minutes} %s {second|seconds} %ms {millisecond|milliseconds}";
@Comment({
"# Separator used between duration parts.",
"# Example result:",
"# 1 hour, 2 minutes, 5 seconds"
})
public String separator = ", ";
@Comment({
"# Separator used before the last duration part.",
"# Example result:",
"# 1 hour, 2 minutes and 5 seconds"
})
public String lastSeparator = " and ";
@Comment({"# Text displayed when duration is zero or negative."})
public String zero = "<1 second";
}
@@ -23,11 +23,6 @@ public class MessagesSettings extends OkaeriConfig {
.actionBar("Combat ends in: <red>{TIME}</red>")
.build();
@Comment({
"# Would you like to display milliseconds instead of seconds in combat notification "
})
public boolean withoutMillis = true;
@Comment({
"# Message displayed when a player lacks permission to execute a command.",
"# The {PERMISSION} placeholder is replaced with the required permission."
@@ -5,10 +5,10 @@ import eu.okaeri.configs.annotation.Comment;
public class PlaceholderSettings extends OkaeriConfig {
@Comment("Text returned by %eternalcombat_isInCombat_formatted% placeholder when the player is in combat")
@Comment("# Text returned by %eternalcombat_isInCombat_formatted% placeholder when the player is in combat")
public String isInCombatFormattedTrue = "In Combat";
@Comment("Text returned by %eternalcombat_isInCombat_formatted% placeholder when the player is out of combat")
@Comment("# Text returned by %eternalcombat_isInCombat_formatted% placeholder when the player is out of combat")
public String isInCombatFormattedFalse = "Not In Combat";
}
@@ -8,6 +8,7 @@ import com.eternalcode.combat.fight.knockback.KnockbackSettings;
import com.eternalcode.combat.fight.pearl.FightPearlSettings;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.Comment;
import java.time.Duration;
import java.util.List;
@@ -29,6 +30,13 @@ public class PluginConfig extends OkaeriConfig {
})
public Settings settings = new Settings();
@Comment({
" ",
"# Duration formatting settings.",
"# Controls how time values (e.g. cooldowns, timers) are displayed."
})
public DurationFormatSettings durationFormat = new DurationFormatSettings();
@Comment({
" ",
"# Settings related to Ender Pearls.",
@@ -3,7 +3,7 @@ package com.eternalcode.combat.fight;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.event.CauseOfUnTag;
import com.eternalcode.combat.notification.NoticeService;
import com.eternalcode.combat.util.DurationUtil;
import com.eternalcode.combat.time.DurationFormatter;
import org.bukkit.Server;
import org.bukkit.entity.Player;
@@ -17,7 +17,7 @@ public class FightTask implements Runnable {
private final FightManager fightManager;
private final NoticeService noticeService;
public FightTask(Server server, PluginConfig config, FightManager fightManager, NoticeService noticeService) {
public FightTask(Server server, PluginConfig config, FightManager fightManager, NoticeService noticeService) {
this.server = server;
this.config = config;
this.fightManager = fightManager;
@@ -45,7 +45,7 @@ public class FightTask implements Runnable {
this.noticeService.create()
.player(player.getUniqueId())
.notice(this.config.messagesSettings.combatNotification)
.placeholder("{TIME}", DurationUtil.format(remaining, this.config.messagesSettings.withoutMillis))
.placeholder("{TIME}", DurationFormatter.of(config.durationFormat).format(remaining))
.send();
}
@@ -1,10 +1,9 @@
package com.eternalcode.combat.fight.pearl;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.notification.NoticeService;
import com.eternalcode.combat.util.DurationUtil;
import java.time.Duration;
import java.util.UUID;
import com.eternalcode.combat.time.DurationFormatter;
import org.bukkit.Material;
import org.bukkit.entity.EnderPearl;
import org.bukkit.entity.Player;
@@ -14,22 +13,24 @@ import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.ProjectileLaunchEvent;
import org.bukkit.inventory.ItemStack;
import java.time.Duration;
import java.util.UUID;
public class FightPearlController implements Listener {
private final FightPearlSettings settings;
private final PluginConfig config;
private final NoticeService noticeService;
private final FightManager fightManager;
private final FightPearlService fightPearlService;
public FightPearlController(
FightPearlSettings settings,
PluginConfig config,
NoticeService noticeService,
FightManager fightManager,
FightPearlService fightPearlService
) {
this.settings = settings;
this.config = config;
this.noticeService = noticeService;
this.fightManager = fightManager;
this.fightPearlService = fightPearlService;
@@ -51,23 +52,23 @@ public class FightPearlController implements Listener {
return;
}
if (this.settings.pearlThrowDisabledDuringCombat) {
if (this.config.pearl.pearlThrowDisabledDuringCombat) {
event.setCancelled(true);
this.noticeService.create()
.player(playerId)
.notice(this.settings.pearlThrowBlockedDuringCombat)
.notice(this.config.pearl.pearlThrowBlockedDuringCombat)
.send();
return;
}
if (this.settings.pearlCooldownEnabled) {
if (this.config.pearl.pearlCooldownEnabled) {
handlePearlCooldown(event, player, playerId);
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPearlDamage(EntityDamageByEntityEvent event) {
if (this.settings.pearlThrowDamageEnabled) {
if (this.config.pearl.pearlThrowDamageEnabled) {
return;
}
@@ -81,7 +82,7 @@ public class FightPearlController implements Listener {
}
private void handlePearlCooldown(ProjectileLaunchEvent event, Player player, UUID playerId) {
if (this.settings.pearlThrowDelay.isZero()) {
if (this.config.pearl.pearlThrowDelay.isZero()) {
return;
}
@@ -91,14 +92,14 @@ public class FightPearlController implements Listener {
this.noticeService.create()
.player(playerId)
.notice(this.settings.pearlThrowBlockedDelayDuringCombat)
.placeholder("{TIME}", DurationUtil.format(remainingDelay))
.notice(this.config.pearl.pearlThrowBlockedDelayDuringCombat)
.placeholder("{TIME}", DurationFormatter.of(config.durationFormat).format(remainingDelay))
.send();
return;
}
this.fightPearlService.markDelay(playerId);
int cooldownTicks = (int) (this.settings.pearlThrowDelay.toMillis() / 50);
int cooldownTicks = (int) (this.config.pearl.pearlThrowDelay.toMillis() / 50);
player.setCooldown(Material.ENDER_PEARL, cooldownTicks);
}
}
@@ -2,15 +2,16 @@ package com.eternalcode.combat.fight.tagout;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.notification.NoticeService;
import com.eternalcode.combat.util.DurationUtil;
import com.eternalcode.combat.time.DurationFormatter;
import dev.rollczi.litecommands.annotations.argument.Arg;
import dev.rollczi.litecommands.annotations.command.Command;
import dev.rollczi.litecommands.annotations.context.Context;
import dev.rollczi.litecommands.annotations.execute.Execute;
import dev.rollczi.litecommands.annotations.permission.Permission;
import org.bukkit.entity.Player;
import java.time.Duration;
import java.util.UUID;
import org.bukkit.entity.Player;
@Permission("eternalcombat.tagout")
@Command(name = "tagout", aliases = "tagimmunity")
@@ -31,47 +32,42 @@ public class FightTagOutCommand {
}
@Execute
void tagout(@Context Player sender, @Arg Duration time) {
UUID targetUniqueId = sender.getUniqueId();
this.fightTagOutService.tagOut(targetUniqueId, time);
void tagOut(@Context Player sender, @Arg Duration time) {
this.fightTagOutService.tagOut(sender.getUniqueId(), time);
this.noticeService.create()
.notice(this.config.messagesSettings.admin.adminTagOutSelf)
.placeholder("{TIME}", DurationUtil.format(time))
.placeholder("{TIME}", DurationFormatter.of(config.durationFormat).format(time))
.viewer(sender)
.send();
}
@Execute
void tagout(@Context Player sender, @Arg Player target, @Arg Duration time) {
UUID targetUniqueId = target.getUniqueId();
this.fightTagOutService.tagOut(targetUniqueId, time);
void tagOut(@Context Player sender, @Arg Player target, @Arg Duration time) {
this.fightTagOutService.tagOut(target.getUniqueId(), time);
this.noticeService.create()
.notice(this.config.messagesSettings.admin.adminTagOut)
.placeholder("{PLAYER}", target.getName())
.placeholder("{TIME}", DurationUtil.format(time))
.placeholder("{TIME}", DurationFormatter.of(config.durationFormat).format(time))
.viewer(sender)
.send();
this.noticeService.create()
.notice(this.config.messagesSettings.admin.playerTagOut)
.placeholder("{TIME}", DurationUtil.format(time))
.placeholder("{TIME}", DurationFormatter.of(config.durationFormat).format(time))
.player(target.getUniqueId())
.send();
}
@Execute(name = "remove")
void untagout(@Context Player sender, @Arg Player target) {
void unTagOut(@Context Player sender, @Arg Player target) {
UUID targetUniqueId = target.getUniqueId();
this.fightTagOutService.unTagOut(targetUniqueId);
if (!targetUniqueId.equals(sender.getUniqueId())) {
this.noticeService.create()
.notice(this.config.messagesSettings.admin.adminTagOutOff)
@@ -87,10 +83,8 @@ public class FightTagOutCommand {
}
@Execute(name = "remove")
void untagout(@Context Player sender) {
UUID senderUniqueId = sender.getUniqueId();
this.fightTagOutService.unTagOut(senderUniqueId);
void unTagOut(@Context Player sender) {
this.fightTagOutService.unTagOut(sender.getUniqueId());
this.noticeService.create()
.notice(this.config.messagesSettings.admin.playerTagOutOff)
@@ -0,0 +1,166 @@
package com.eternalcode.combat.time;
import com.eternalcode.combat.config.implementation.DurationFormatSettings;
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
public final class DurationFormatter {
private final Token[] tokens;
private final String separator;
private final String lastSeparator;
private final String zero;
DurationFormatter(
String pattern,
String separator,
String lastSeparator,
String zero
) {
this.tokens = parsePattern(pattern);
this.separator = separator;
this.lastSeparator = lastSeparator;
this.zero = zero;
}
public static DurationFormatter of(
@NotNull String pattern,
@NotNull String separator,
@NotNull String lastSeparator,
@NotNull String zero
) {
return new DurationFormatter(pattern, separator, lastSeparator, zero);
}
public static DurationFormatter of(@NotNull DurationFormatSettings settings) {
return of(settings.pattern, settings.separator, settings.lastSeparator, settings.zero);
}
public String format(Duration duration) {
if (duration.isZero() || duration.isNegative()) {
return zero;
}
long[] values = new long[tokens.length];
int count = 0;
for (int i = 0; i < tokens.length; i++) {
long value = tokens[i].unit.getPart(duration);
values[i] = value;
if (value > 0) {
count++;
}
}
if (count == 0) {
return zero;
}
StringBuilder result = new StringBuilder(tokens.length * 16);
int index = 0;
for (int i = 0; i < tokens.length; i++) {
long value = values[i];
if (value <= 0) {
continue;
}
Token token = tokens[i];
if (index++ > 0) {
result.append(index == count ? lastSeparator : separator);
}
result.append(value);
if (token.spaceBetween) {
result.append(' ');
}
result.append(value == 1 ? token.singular : token.plural);
}
return result.toString();
}
private static Token[] parsePattern(String pattern) {
List<Token> tokens = new ArrayList<>(DurationUnit.values().length);
int i = 0;
while (i < pattern.length()) {
if (pattern.charAt(i) != '%') {
i++;
continue;
}
i++; // skip %
int start = i;
while (i < pattern.length() && Character.isLetter(pattern.charAt(i))) {
i++;
}
if (start == i) {
throw new IllegalArgumentException("Missing duration unit after %");
}
if (i >= pattern.length()) {
throw new IllegalArgumentException("Unexpected end of pattern");
}
DurationUnit unit = DurationUnit.fromSymbol(pattern.substring(start, i));
boolean spaceBetween = pattern.charAt(i) == ' ';
if (spaceBetween) {
i++;
}
if (i >= pattern.length() || pattern.charAt(i) != '{') {
throw new IllegalArgumentException("Missing plural definition");
}
i++; // skip {
int blockStart = i;
while (i < pattern.length() && pattern.charAt(i) != '}') {
i++;
}
if (i >= pattern.length()) {
throw new IllegalArgumentException("Unclosed plural definition");
}
String block = pattern.substring(blockStart, i);
i++; // skip }
int split = block.indexOf('|');
if (split == -1) {
throw new IllegalArgumentException("Plural must be singular|plural");
}
tokens.add(new Token(
unit,
block.substring(0, split),
block.substring(split + 1),
spaceBetween
));
}
return tokens.toArray(new Token[0]);
}
private record Token(
DurationUnit unit,
String singular,
String plural,
boolean spaceBetween
) {}
}
@@ -0,0 +1,40 @@
package com.eternalcode.combat.time;
import java.time.Duration;
import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
enum DurationUnit {
DAYS("d", d -> Math.toIntExact(d.toDaysPart())),
HOURS("h", Duration::toHoursPart),
MINUTES("m", Duration::toMinutesPart),
SECONDS("s", Duration::toSecondsPart),
MILLIS("ms", d -> d.getSeconds() == 0 ? d.toMillisPart() : 0);
private static final Map<String, DurationUnit> BY_SYMBOL = Arrays.stream(values())
.collect(Collectors.toUnmodifiableMap(unit -> unit.symbol, unit -> unit));
private final String symbol;
private final Function<Duration, Integer> extractor;
DurationUnit(String symbol, Function<Duration, Integer> extractor) {
this.symbol = symbol;
this.extractor = extractor;
}
long getPart(Duration duration) {
return extractor.apply(duration);
}
static DurationUnit fromSymbol(String symbol) {
DurationUnit unit = BY_SYMBOL.get(symbol);
if (unit == null) {
throw new IllegalArgumentException("Unknown duration unit: " + symbol);
}
return unit;
}
}
@@ -1,50 +0,0 @@
package com.eternalcode.combat.util;
import com.eternalcode.commons.time.DurationParser;
import com.eternalcode.commons.time.TemporalAmountParser;
import java.math.RoundingMode;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
public class DurationUtil {
private static final TemporalAmountParser<Duration> WITHOUT_MILLIS_FORMAT = new DurationParser()
.withUnit("s", ChronoUnit.SECONDS)
.withUnit("m", ChronoUnit.MINUTES)
.withUnit("h", ChronoUnit.HOURS)
.withUnit("d", ChronoUnit.DAYS)
.withRounded(ChronoUnit.MILLIS, RoundingMode.UP);
private static final TemporalAmountParser<Duration> STANDARD_FORMAT = new DurationParser()
.withUnit("d", ChronoUnit.DAYS)
.withUnit("h", ChronoUnit.HOURS)
.withUnit("m", ChronoUnit.MINUTES)
.withUnit("s", ChronoUnit.SECONDS)
.withUnit("ms", ChronoUnit.MILLIS);
public static final Duration ONE_SECOND = Duration.ofSeconds(1);
public DurationUtil() {
throw new UnsupportedOperationException("This class cannot be instantiated");
}
public static String format(Duration duration, boolean removeMillis) {
if (removeMillis) {
if (duration.toMillis() < ONE_SECOND.toMillis()) {
return "0s";
}
return WITHOUT_MILLIS_FORMAT.format(duration);
}
if (duration.toMillis() > ONE_SECOND.toMillis()) {
return WITHOUT_MILLIS_FORMAT.format(duration);
}
return STANDARD_FORMAT.format(duration);
}
public static String format(Duration duration) {
return format(duration, true);
}
}