From 78be44807e93cd08d8292c736304a3a2b4e5e8ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Zieli=C5=84ski?= <111442524+ChudziudgiToJa@users.noreply.github.com> Date: Wed, 1 Jul 2026 16:57:06 +0200 Subject: [PATCH 1/2] feature: add death teleport (back to death location --- .../implementation/PluginConfiguration.java | 8 ++ .../deathteleport/DeathTeleportCommand.java | 84 ++++++++++++++++ .../deathteleport/DeathTeleportConfig.java | 25 +++++ .../DeathTeleportController.java | 97 +++++++++++++++++++ .../deathteleport/DeathTeleportService.java | 42 ++++++++ .../deathteleport/DeathTeleportSettings.java | 12 +++ .../deathteleport/DeathTeleportState.java | 11 +++ .../DeathTeleportStateTable.java | 26 +++++ .../DeathTeleportToggleRepository.java | 11 +++ .../DeathTeleportToggleRepositoryOrmLite.java | 34 +++++++ .../DeathTeleportToggleService.java | 13 +++ .../DeathTeleportToggleServiceImpl.java | 54 +++++++++++ .../messages/DeathTeleportMessages.java | 16 +++ .../messages/ENDeathTeleportMessages.java | 29 ++++++ .../messages/PLDeathTeleportMessages.java | 29 ++++++ .../core/translation/Translation.java | 3 + .../implementation/ENTranslation.java | 4 + .../implementation/PLTranslation.java | 4 + .../DeathTeleportServiceTest.java | 61 ++++++++++++ .../DeathTeleportToggleServiceImplTest.java | 82 ++++++++++++++++ 20 files changed, 645 insertions(+) create mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportCommand.java create mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportConfig.java create mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportController.java create mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportService.java create mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportSettings.java create mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportState.java create mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportStateTable.java create mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportToggleRepository.java create mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportToggleRepositoryOrmLite.java create mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportToggleService.java create mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportToggleServiceImpl.java create mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/messages/DeathTeleportMessages.java create mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/messages/ENDeathTeleportMessages.java create mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/messages/PLDeathTeleportMessages.java create mode 100644 eternalcore-core/src/test/java/com/eternalcode/core/feature/deathteleport/DeathTeleportServiceTest.java create mode 100644 eternalcore-core/src/test/java/com/eternalcode/core/feature/deathteleport/DeathTeleportToggleServiceImplTest.java diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/configuration/implementation/PluginConfiguration.java b/eternalcore-core/src/main/java/com/eternalcode/core/configuration/implementation/PluginConfiguration.java index a9dc88b2b..716d0da8b 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/configuration/implementation/PluginConfiguration.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/configuration/implementation/PluginConfiguration.java @@ -21,6 +21,8 @@ import com.eternalcode.core.feature.chat.ChatSettings; import com.eternalcode.core.feature.deathmessage.config.DeathMessageConfig; import com.eternalcode.core.feature.deathmessage.config.DeathMessageSettings; +import com.eternalcode.core.feature.deathteleport.DeathTeleportConfig; +import com.eternalcode.core.feature.deathteleport.DeathTeleportSettings; import com.eternalcode.core.feature.enchant.EnchantConfig; import com.eternalcode.core.feature.enchant.EnchantSettings; import com.eternalcode.core.feature.give.GiveConfig; @@ -244,6 +246,12 @@ public static class Format extends OkaeriConfig { @Comment("# Settings for the /back command functionality") BackConfig back = new BackConfig(); + @Bean(proxied = DeathTeleportSettings.class) + @Comment("") + @Comment("# Death Teleport Configuration") + @Comment("# Automatically teleports players back to their death location after respawning") + DeathTeleportConfig deathTeleport = new DeathTeleportConfig(); + @Override public File getConfigFile(File dataFolder) { return new File(dataFolder, "config.yml"); diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportCommand.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportCommand.java new file mode 100644 index 000000000..d1daf1a50 --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportCommand.java @@ -0,0 +1,84 @@ +package com.eternalcode.core.feature.deathteleport; + +import com.eternalcode.annotations.scan.command.DescriptionDocs; +import com.eternalcode.core.injector.annotations.Inject; +import com.eternalcode.core.notice.NoticeService; +import dev.rollczi.litecommands.annotations.argument.Arg; +import dev.rollczi.litecommands.annotations.command.Command; +import dev.rollczi.litecommands.annotations.context.Sender; +import dev.rollczi.litecommands.annotations.execute.Execute; +import dev.rollczi.litecommands.annotations.permission.Permission; +import java.util.UUID; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +@Command(name = "deathteleport", aliases = "deathtp") +@Permission("eternalcore.deathteleport") +class DeathTeleportCommand { + + private final DeathTeleportToggleService toggleService; + private final NoticeService noticeService; + + @Inject + DeathTeleportCommand(DeathTeleportToggleService toggleService, NoticeService noticeService) { + this.toggleService = toggleService; + this.noticeService = noticeService; + } + + @Execute + @DescriptionDocs(description = "Toggle automatic teleportation to your last death location after respawning") + void execute(@Sender Player sender) { + UUID player = sender.getUniqueId(); + this.toggleService.toggleState(player) + .thenAccept(state -> this.noticePlayer(state, player)); + } + + @Execute(name = "enable") + @DescriptionDocs(description = "Enable automatic teleportation to your last death location after respawning") + void executeEnable(@Sender Player sender) { + UUID player = sender.getUniqueId(); + this.toggleService.setState(player, DeathTeleportState.ENABLED) + .thenAccept(ignored -> this.noticePlayer(DeathTeleportState.ENABLED, player)); + } + + @Execute(name = "disable") + @DescriptionDocs(description = "Disable automatic teleportation to your last death location after respawning") + void executeDisable(@Sender Player sender) { + UUID player = sender.getUniqueId(); + this.toggleService.setState(player, DeathTeleportState.DISABLED) + .thenAccept(ignored -> this.noticePlayer(DeathTeleportState.DISABLED, player)); + } + + @Execute + @Permission("eternalcore.deathteleport.other") + @DescriptionDocs(description = "Toggle automatic death teleportation for another player", arguments = "") + void executeOther(@Sender CommandSender sender, @Arg Player target) { + UUID player = target.getUniqueId(); + this.toggleService.toggleState(player) + .thenAccept(state -> this.noticeOtherPlayer(sender, state, target)); + } + + private void noticeOtherPlayer(CommandSender sender, DeathTeleportState state, Player target) { + this.noticePlayer(state, target.getUniqueId()); + if (sender.equals(target)) { + return; + } + + this.noticeService.create() + .sender(sender) + .notice(translation -> state == DeathTeleportState.ENABLED + ? translation.deathTeleport().deathTeleportOtherEnabled() + : translation.deathTeleport().deathTeleportOtherDisabled()) + .placeholder("{PLAYER}", target.getName()) + .send(); + } + + private void noticePlayer(DeathTeleportState state, UUID player) { + this.noticeService.create() + .player(player) + .notice(translation -> state == DeathTeleportState.ENABLED + ? translation.deathTeleport().deathTeleportSelfEnabled() + : translation.deathTeleport().deathTeleportSelfDisabled()) + .send(); + } +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportConfig.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportConfig.java new file mode 100644 index 000000000..65a513c86 --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportConfig.java @@ -0,0 +1,25 @@ +package com.eternalcode.core.feature.deathteleport; + +import eu.okaeri.configs.OkaeriConfig; +import eu.okaeri.configs.annotation.Comment; +import java.time.Duration; +import lombok.Getter; +import lombok.experimental.Accessors; + +@Getter +@Accessors(fluent = true) +public class DeathTeleportConfig extends OkaeriConfig implements DeathTeleportSettings { + + @Comment({ + "# Automatically teleport players back to their last death location after they respawn.", + "# This helps new players who don't know the /back command to quickly return to where they died.", + "# Players can opt out for themselves with the /deathteleport command." + }) + public boolean deathTeleportEnabled = true; + + @Comment("# Delay after respawn before the player is teleported back to their death location.") + public Duration deathTeleportDelay = Duration.ofSeconds(3); + + @Comment("# How long a death location is remembered and eligible for automatic teleportation.") + public Duration deathLocationCacheDuration = Duration.ofHours(1); +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportController.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportController.java new file mode 100644 index 000000000..8101f3d39 --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportController.java @@ -0,0 +1,97 @@ +package com.eternalcode.core.feature.deathteleport; + +import com.eternalcode.commons.bukkit.position.Position; +import com.eternalcode.commons.bukkit.position.PositionAdapter; +import com.eternalcode.commons.bukkit.scheduler.MinecraftScheduler; +import com.eternalcode.core.feature.teleport.TeleportService; +import com.eternalcode.core.injector.annotations.Inject; +import com.eternalcode.core.injector.annotations.component.Controller; +import com.eternalcode.core.notice.NoticeService; +import com.eternalcode.core.util.DurationUtil; +import java.util.Optional; +import java.util.UUID; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.PlayerDeathEvent; +import org.bukkit.event.player.PlayerRespawnEvent; + +@Controller +class DeathTeleportController implements Listener { + + private final DeathTeleportService deathTeleportService; + private final DeathTeleportToggleService toggleService; + private final DeathTeleportSettings settings; + private final TeleportService teleportService; + private final NoticeService noticeService; + private final MinecraftScheduler scheduler; + + @Inject + DeathTeleportController( + DeathTeleportService deathTeleportService, + DeathTeleportToggleService toggleService, + DeathTeleportSettings settings, + TeleportService teleportService, + NoticeService noticeService, + MinecraftScheduler scheduler + ) { + this.deathTeleportService = deathTeleportService; + this.toggleService = toggleService; + this.settings = settings; + this.teleportService = teleportService; + this.noticeService = noticeService; + this.scheduler = scheduler; + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + void onPlayerDeath(PlayerDeathEvent event) { + Player player = event.getEntity(); + + this.deathTeleportService.markDeathLocation(player.getUniqueId(), PositionAdapter.convert(player.getLocation())); + } + + @EventHandler + void onPlayerRespawn(PlayerRespawnEvent event) { + if (!this.settings.deathTeleportEnabled()) { + return; + } + + Player player = event.getPlayer(); + UUID playerId = player.getUniqueId(); + + Optional deathLocation = this.deathTeleportService.getDeathLocation(playerId); + if (deathLocation.isEmpty()) { + return; + } + + this.toggleService.getState(playerId).thenAccept(state -> { + if (state == DeathTeleportState.DISABLED) { + return; + } + + this.noticeService.create() + .player(playerId) + .notice(translation -> translation.deathTeleport().deathTeleportStarted()) + .placeholder("{TIME}", DurationUtil.format(this.settings.deathTeleportDelay(), true)) + .send(); + + this.scheduler.runLater( + () -> { + if (!player.isOnline() || player.isDead()) { + return; + } + + this.deathTeleportService.clearDeathLocation(playerId); + this.teleportService.teleport(player, PositionAdapter.convert(deathLocation.get())); + + this.noticeService.create() + .player(playerId) + .notice(translation -> translation.deathTeleport().deathTeleportSuccess()) + .send(); + }, + this.settings.deathTeleportDelay() + ); + }); + } +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportService.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportService.java new file mode 100644 index 000000000..d0b52a336 --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportService.java @@ -0,0 +1,42 @@ +package com.eternalcode.core.feature.deathteleport; + +import com.eternalcode.commons.bukkit.position.Position; +import com.eternalcode.core.injector.annotations.Inject; +import com.eternalcode.core.injector.annotations.component.Service; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import java.util.Optional; +import java.util.UUID; + +@Service +class DeathTeleportService { + + private final Cache deathLocations; + + @Inject + DeathTeleportService( + DeathTeleportSettings settings + ) { + this.deathLocations = Caffeine.newBuilder() + .expireAfterWrite(settings.deathLocationCacheDuration()) + .build(); + } + + void markDeathLocation( + UUID playerId, Position position + ) { + this.deathLocations.put(playerId, position); + } + + Optional getDeathLocation( + UUID playerId + ) { + return Optional.ofNullable(this.deathLocations.getIfPresent(playerId)); + } + + void clearDeathLocation( + UUID playerId + ) { + this.deathLocations.invalidate(playerId); + } +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportSettings.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportSettings.java new file mode 100644 index 000000000..bcb6f4c94 --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportSettings.java @@ -0,0 +1,12 @@ +package com.eternalcode.core.feature.deathteleport; + +import java.time.Duration; + +public interface DeathTeleportSettings { + + boolean deathTeleportEnabled(); + + Duration deathTeleportDelay(); + + Duration deathLocationCacheDuration(); +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportState.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportState.java new file mode 100644 index 000000000..c388e1fb4 --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportState.java @@ -0,0 +1,11 @@ +package com.eternalcode.core.feature.deathteleport; + +public enum DeathTeleportState { + ENABLED, + DISABLED; + + + DeathTeleportState invert() { + return this == ENABLED ? DISABLED : ENABLED; + } +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportStateTable.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportStateTable.java new file mode 100644 index 000000000..e09afd30d --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportStateTable.java @@ -0,0 +1,26 @@ +package com.eternalcode.core.feature.deathteleport; + +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; +import java.util.UUID; + +@DatabaseTable(tableName = "eternal_core_death_teleport_state") +class DeathTeleportStateTable { + + @DatabaseField(columnName = "id", id = true) + private UUID uniqueId; + + @DatabaseField(columnName = "state") + private DeathTeleportState state; + + DeathTeleportStateTable(UUID uniqueId, DeathTeleportState state) { + this.uniqueId = uniqueId; + this.state = state; + } + + DeathTeleportStateTable() {} + + DeathTeleportState state() { + return this.state; + } +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportToggleRepository.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportToggleRepository.java new file mode 100644 index 000000000..da4fab007 --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportToggleRepository.java @@ -0,0 +1,11 @@ +package com.eternalcode.core.feature.deathteleport; + +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +interface DeathTeleportToggleRepository { + + CompletableFuture getState(UUID uuid); + + CompletableFuture setState(UUID uuid, DeathTeleportState state); +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportToggleRepositoryOrmLite.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportToggleRepositoryOrmLite.java new file mode 100644 index 000000000..b8e0dd665 --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportToggleRepositoryOrmLite.java @@ -0,0 +1,34 @@ +package com.eternalcode.core.feature.deathteleport; + +import com.eternalcode.commons.scheduler.Scheduler; +import com.eternalcode.core.database.AbstractRepositoryOrmLite; +import com.eternalcode.core.database.DatabaseManager; +import com.eternalcode.core.injector.annotations.Inject; +import com.eternalcode.core.injector.annotations.component.Repository; +import com.j256.ormlite.table.TableUtils; +import java.sql.SQLException; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +@Repository +class DeathTeleportToggleRepositoryOrmLite extends AbstractRepositoryOrmLite implements DeathTeleportToggleRepository { + + @Inject + private DeathTeleportToggleRepositoryOrmLite(DatabaseManager databaseManager, Scheduler scheduler) throws SQLException { + super(databaseManager, scheduler); + TableUtils.createTableIfNotExists(databaseManager.connectionSource(), DeathTeleportStateTable.class); + } + + @Override + public CompletableFuture getState(UUID uuid) { + return this.selectSafe(DeathTeleportStateTable.class, uuid) + .thenApply(optional -> optional.map(DeathTeleportStateTable::state).orElse(DeathTeleportState.ENABLED)) + .exceptionally(throwable -> DeathTeleportState.ENABLED); + } + + @Override + public CompletableFuture setState(UUID uuid, DeathTeleportState state) { + return this.save(DeathTeleportStateTable.class, new DeathTeleportStateTable(uuid, state)) + .thenApply(status -> null); + } +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportToggleService.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportToggleService.java new file mode 100644 index 000000000..5bae826ab --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportToggleService.java @@ -0,0 +1,13 @@ +package com.eternalcode.core.feature.deathteleport; + +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public interface DeathTeleportToggleService { + + CompletableFuture getState(UUID playerUniqueId); + + CompletableFuture setState(UUID playerUniqueId, DeathTeleportState state); + + CompletableFuture toggleState(UUID playerUniqueId); +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportToggleServiceImpl.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportToggleServiceImpl.java new file mode 100644 index 000000000..76f2bfd1a --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportToggleServiceImpl.java @@ -0,0 +1,54 @@ +package com.eternalcode.core.feature.deathteleport; + +import com.eternalcode.core.injector.annotations.Inject; +import com.eternalcode.core.injector.annotations.component.Service; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; + +@Service +class DeathTeleportToggleServiceImpl implements DeathTeleportToggleService { + + private final DeathTeleportToggleRepository repository; + private final ConcurrentHashMap cachedStates; + + @Inject + DeathTeleportToggleServiceImpl(DeathTeleportToggleRepository repository) { + this.repository = repository; + this.cachedStates = new ConcurrentHashMap<>(); + } + + @Override + public CompletableFuture getState(UUID playerUniqueId) { + DeathTeleportState cached = this.cachedStates.get(playerUniqueId); + if (cached != null) { + return CompletableFuture.completedFuture(cached); + } + + return this.repository.getState(playerUniqueId) + .thenApply(state -> { + this.cachedStates.put(playerUniqueId, state); + return state; + }); + } + + @Override + public CompletableFuture setState(UUID playerUniqueId, DeathTeleportState state) { + this.cachedStates.put(playerUniqueId, state); + + return this.repository.setState(playerUniqueId, state) + .exceptionally(throwable -> { + this.cachedStates.remove(playerUniqueId); + return null; + }); + } + + @Override + public CompletableFuture toggleState(UUID playerUniqueId) { + return this.getState(playerUniqueId).thenCompose(state -> { + DeathTeleportState newState = state.invert(); + return this.setState(playerUniqueId, newState) + .thenApply(ignored -> newState); + }); + } +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/messages/DeathTeleportMessages.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/messages/DeathTeleportMessages.java new file mode 100644 index 000000000..1507f69e3 --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/messages/DeathTeleportMessages.java @@ -0,0 +1,16 @@ +package com.eternalcode.core.feature.deathteleport.messages; + +import com.eternalcode.multification.notice.Notice; + +public interface DeathTeleportMessages { + + Notice deathTeleportStarted(); + + Notice deathTeleportSuccess(); + + Notice deathTeleportSelfEnabled(); + Notice deathTeleportSelfDisabled(); + + Notice deathTeleportOtherEnabled(); + Notice deathTeleportOtherDisabled(); +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/messages/ENDeathTeleportMessages.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/messages/ENDeathTeleportMessages.java new file mode 100644 index 000000000..847fb497b --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/messages/ENDeathTeleportMessages.java @@ -0,0 +1,29 @@ +package com.eternalcode.core.feature.deathteleport.messages; + +import com.eternalcode.multification.notice.Notice; +import eu.okaeri.configs.OkaeriConfig; +import eu.okaeri.configs.annotation.Comment; +import lombok.Getter; +import lombok.experimental.Accessors; + +@Getter +@Accessors(fluent = true) +public class ENDeathTeleportMessages extends OkaeriConfig implements DeathTeleportMessages { + + @Comment("# {TIME} - Time left before the teleportation") + public Notice deathTeleportStarted = Notice.actionbar( + "Teleporting you back to your death location in {TIME}..."); + + public Notice deathTeleportSuccess = Notice.chat( + "You have been teleported back to your death location!"); + + public Notice deathTeleportSelfEnabled = Notice.chat( + "Automatic teleportation to your death location is now enabled!"); + public Notice deathTeleportSelfDisabled = Notice.chat( + "Automatic teleportation to your death location is now disabled!"); + + public Notice deathTeleportOtherEnabled = Notice.chat( + "Automatic teleportation to death location has been enabled for {PLAYER}!"); + public Notice deathTeleportOtherDisabled = Notice.chat( + "Automatic teleportation to death location has been disabled for {PLAYER}!"); +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/messages/PLDeathTeleportMessages.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/messages/PLDeathTeleportMessages.java new file mode 100644 index 000000000..fd2504fbb --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/messages/PLDeathTeleportMessages.java @@ -0,0 +1,29 @@ +package com.eternalcode.core.feature.deathteleport.messages; + +import com.eternalcode.multification.notice.Notice; +import eu.okaeri.configs.OkaeriConfig; +import eu.okaeri.configs.annotation.Comment; +import lombok.Getter; +import lombok.experimental.Accessors; + +@Getter +@Accessors(fluent = true) +public class PLDeathTeleportMessages extends OkaeriConfig implements DeathTeleportMessages { + + @Comment("# {TIME} - Czas pozostały do teleportacji") + public Notice deathTeleportStarted = Notice.actionbar( + "Za {TIME} zostaniesz przeteleportowany do miejsca swojej śmierci..."); + + public Notice deathTeleportSuccess = Notice.chat( + "Zostałeś przeteleportowany do miejsca swojej śmierci!"); + + public Notice deathTeleportSelfEnabled = Notice.chat( + "Automatyczna teleportacja do miejsca śmierci jest teraz włączona!"); + public Notice deathTeleportSelfDisabled = Notice.chat( + "Automatyczna teleportacja do miejsca śmierci jest teraz wyłączona!"); + + public Notice deathTeleportOtherEnabled = Notice.chat( + "Automatyczna teleportacja do miejsca śmierci została włączona dla gracza {PLAYER}!"); + public Notice deathTeleportOtherDisabled = Notice.chat( + "Automatyczna teleportacja do miejsca śmierci została wyłączona dla gracza {PLAYER}!"); +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/translation/Translation.java b/eternalcore-core/src/main/java/com/eternalcode/core/translation/Translation.java index f4228143e..54e2b41c2 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/translation/Translation.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/translation/Translation.java @@ -1,6 +1,7 @@ package com.eternalcode.core.translation; import com.eternalcode.core.feature.back.messages.BackMessages; +import com.eternalcode.core.feature.deathteleport.messages.DeathTeleportMessages; import com.eternalcode.core.feature.freeze.messages.FreezeMessages; import com.eternalcode.core.feature.playtime.messages.PlaytimeMessages; import com.eternalcode.core.feature.clear.messages.ClearMessages; @@ -104,6 +105,8 @@ interface Format { BackMessages back(); + DeathTeleportMessages deathTeleport(); + TeleportToRandomPlayerMessages teleportToRandomPlayer(); RandomTeleportMessages randomTeleport(); diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/translation/implementation/ENTranslation.java b/eternalcore-core/src/main/java/com/eternalcode/core/translation/implementation/ENTranslation.java index 9cf7a1e0e..37be98a3e 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/translation/implementation/ENTranslation.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/translation/implementation/ENTranslation.java @@ -10,6 +10,7 @@ import com.eternalcode.core.feature.chat.messages.ENChatMessages; import com.eternalcode.core.feature.clear.messages.ENClearMessages; import com.eternalcode.core.feature.container.messages.ENContainerMessages; +import com.eternalcode.core.feature.deathteleport.messages.ENDeathTeleportMessages; import com.eternalcode.core.feature.deathmessage.messages.ENDeathMessages; import com.eternalcode.core.feature.disposal.messages.ENDisposalMessages; import com.eternalcode.core.feature.enchant.messages.ENEnchantMessages; @@ -196,6 +197,9 @@ public static class ENFormatSection extends OkaeriConfig implements Format { @Comment("# Back") public ENBackMessages back = new ENBackMessages(); + @Comment("# Automatic teleportation back to the last death location after respawning") + public ENDeathTeleportMessages deathTeleport = new ENDeathTeleportMessages(); + @Comment("# Teleport to offline players") public ENTeleportOfflineMessages teleportToOfflinePlayer = new ENTeleportOfflineMessages(); diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/translation/implementation/PLTranslation.java b/eternalcore-core/src/main/java/com/eternalcode/core/translation/implementation/PLTranslation.java index ee828da82..e7108c095 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/translation/implementation/PLTranslation.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/translation/implementation/PLTranslation.java @@ -1,6 +1,7 @@ package com.eternalcode.core.translation.implementation; import com.eternalcode.core.feature.back.messages.PLBackMessages; +import com.eternalcode.core.feature.deathteleport.messages.PLDeathTeleportMessages; import com.eternalcode.core.feature.freeze.messages.PLFreezeMessages; import com.eternalcode.core.feature.near.messages.PLNearMessages; import com.eternalcode.core.feature.playtime.messages.PLPlaytimeMessages; @@ -197,6 +198,9 @@ public static class PLFormatSection extends OkaeriConfig implements Format { @Comment("# Powrót do poprzedniej lokalizacji") public PLBackMessages back = new PLBackMessages(); + @Comment("# Automatyczna teleportacja do miejsca śmierci po odrodzeniu") + public PLDeathTeleportMessages deathTeleport = new PLDeathTeleportMessages(); + @Comment("# Losowa teleportacja") public PLRandomTeleportMessages randomTeleport = new PLRandomTeleportMessages(); diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/feature/deathteleport/DeathTeleportServiceTest.java b/eternalcore-core/src/test/java/com/eternalcode/core/feature/deathteleport/DeathTeleportServiceTest.java new file mode 100644 index 000000000..66142078e --- /dev/null +++ b/eternalcore-core/src/test/java/com/eternalcode/core/feature/deathteleport/DeathTeleportServiceTest.java @@ -0,0 +1,61 @@ +package com.eternalcode.core.feature.deathteleport; + +import com.eternalcode.commons.bukkit.position.Position; +import java.time.Duration; +import java.util.UUID; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class DeathTeleportServiceTest { + + private static final DeathTeleportSettings SETTINGS = new DeathTeleportSettings() { + @Override + public boolean deathTeleportEnabled() { + return true; + } + + @Override + public Duration deathTeleportDelay() { + return Duration.ofSeconds(3); + } + + @Override + public Duration deathLocationCacheDuration() { + return Duration.ofHours(1); + } + }; + + private static Position position() { + return new Position(1.0, 64.0, 2.0, 0.0F, 0.0F, "world"); + } + + @Test + void shouldReturnEmptyWhenNoDeathLocationMarked() { + DeathTeleportService service = new DeathTeleportService(SETTINGS); + + assertThat(service.getDeathLocation(UUID.randomUUID())).isEmpty(); + } + + @Test + void shouldStoreAndReturnDeathLocation() { + DeathTeleportService service = new DeathTeleportService(SETTINGS); + UUID player = UUID.randomUUID(); + Position position = position(); + + service.markDeathLocation(player, position); + + assertThat(service.getDeathLocation(player)).contains(position); + } + + @Test + void shouldClearDeathLocation() { + DeathTeleportService service = new DeathTeleportService(SETTINGS); + UUID player = UUID.randomUUID(); + + service.markDeathLocation(player, position()); + service.clearDeathLocation(player); + + assertThat(service.getDeathLocation(player)).isEmpty(); + } +} diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/feature/deathteleport/DeathTeleportToggleServiceImplTest.java b/eternalcore-core/src/test/java/com/eternalcode/core/feature/deathteleport/DeathTeleportToggleServiceImplTest.java new file mode 100644 index 000000000..8a2746467 --- /dev/null +++ b/eternalcore-core/src/test/java/com/eternalcode/core/feature/deathteleport/DeathTeleportToggleServiceImplTest.java @@ -0,0 +1,82 @@ +package com.eternalcode.core.feature.deathteleport; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class DeathTeleportToggleServiceImplTest { + + private static class InMemoryRepository implements DeathTeleportToggleRepository { + + private final Map states = new HashMap<>(); + private final AtomicInteger reads = new AtomicInteger(); + + @Override + public CompletableFuture getState(UUID uuid) { + this.reads.incrementAndGet(); + return CompletableFuture.completedFuture(this.states.getOrDefault(uuid, DeathTeleportState.ENABLED)); + } + + @Override + public CompletableFuture setState(UUID uuid, DeathTeleportState state) { + this.states.put(uuid, state); + return CompletableFuture.completedFuture(null); + } + } + + @Test + void shouldReturnEnabledByDefault() { + DeathTeleportToggleServiceImpl service = new DeathTeleportToggleServiceImpl(new InMemoryRepository()); + UUID player = UUID.randomUUID(); + + assertThat(service.getState(player).join()).isEqualTo(DeathTeleportState.ENABLED); + } + + @Test + void shouldToggleStateFromEnabledToDisabled() { + DeathTeleportToggleServiceImpl service = new DeathTeleportToggleServiceImpl(new InMemoryRepository()); + UUID player = UUID.randomUUID(); + + DeathTeleportState toggled = service.toggleState(player).join(); + + assertThat(toggled).isEqualTo(DeathTeleportState.DISABLED); + assertThat(service.getState(player).join()).isEqualTo(DeathTeleportState.DISABLED); + } + + @Test + void shouldToggleBackAndForth() { + DeathTeleportToggleServiceImpl service = new DeathTeleportToggleServiceImpl(new InMemoryRepository()); + UUID player = UUID.randomUUID(); + + assertThat(service.toggleState(player).join()).isEqualTo(DeathTeleportState.DISABLED); + assertThat(service.toggleState(player).join()).isEqualTo(DeathTeleportState.ENABLED); + } + + @Test + void shouldPersistExplicitState() { + InMemoryRepository repository = new InMemoryRepository(); + DeathTeleportToggleServiceImpl service = new DeathTeleportToggleServiceImpl(repository); + UUID player = UUID.randomUUID(); + + service.setState(player, DeathTeleportState.DISABLED).join(); + + assertThat(repository.states.get(player)).isEqualTo(DeathTeleportState.DISABLED); + } + + @Test + void shouldCacheStateAfterFirstReadFromRepository() { + InMemoryRepository repository = new InMemoryRepository(); + DeathTeleportToggleServiceImpl service = new DeathTeleportToggleServiceImpl(repository); + UUID player = UUID.randomUUID(); + + service.getState(player).join(); + service.getState(player).join(); + + assertThat(repository.reads.get()).isEqualTo(1); + } +} From 24515a584b0f202692bb8b69cfde090fa4da8077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Zieli=C5=84ski?= <111442524+ChudziudgiToJa@users.noreply.github.com> Date: Thu, 2 Jul 2026 00:54:40 +0200 Subject: [PATCH 2/2] fix code to new gemini ideas --- .../feature/deathteleport/DeathTeleportController.java | 9 +++++++++ .../deathteleport/DeathTeleportToggleServiceImpl.java | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportController.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportController.java index 8101f3d39..51de795a4 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportController.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportController.java @@ -70,6 +70,10 @@ void onPlayerRespawn(PlayerRespawnEvent event) { return; } + if (!player.isOnline()) { + return; + } + this.noticeService.create() .player(playerId) .notice(translation -> translation.deathTeleport().deathTeleportStarted()) @@ -82,6 +86,11 @@ void onPlayerRespawn(PlayerRespawnEvent event) { return; } + Optional currentDeathLocation = this.deathTeleportService.getDeathLocation(playerId); + if (currentDeathLocation.isEmpty() || !currentDeathLocation.get().equals(deathLocation.get())) { + return; + } + this.deathTeleportService.clearDeathLocation(playerId); this.teleportService.teleport(player, PositionAdapter.convert(deathLocation.get())); diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportToggleServiceImpl.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportToggleServiceImpl.java index 76f2bfd1a..05bd964a2 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportToggleServiceImpl.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/deathteleport/DeathTeleportToggleServiceImpl.java @@ -38,7 +38,7 @@ public CompletableFuture setState(UUID playerUniqueId, DeathTeleportState return this.repository.setState(playerUniqueId, state) .exceptionally(throwable -> { - this.cachedStates.remove(playerUniqueId); + this.cachedStates.remove(playerUniqueId, state); return null; }); }