From 75ffad74cd5b45585222ac2d0f3b7ec7c6cba861 Mon Sep 17 00:00:00 2001 From: Joltras Date: Sun, 9 Aug 2026 14:47:18 +0200 Subject: [PATCH 1/4] chore(profile): improve runnable cancellation and add missing docs --- .../bounce/profile/ProfileService.java | 78 +++++++++++++++++-- 1 file changed, 73 insertions(+), 5 deletions(-) diff --git a/src/main/java/net/theevilreaper/bounce/profile/ProfileService.java b/src/main/java/net/theevilreaper/bounce/profile/ProfileService.java index 2355e50..e8b6605 100644 --- a/src/main/java/net/theevilreaper/bounce/profile/ProfileService.java +++ b/src/main/java/net/theevilreaper/bounce/profile/ProfileService.java @@ -5,61 +5,129 @@ import net.theevilreaper.bounce.common.map.GameMap; import org.jetbrains.annotations.Nullable; -import java.util.*; +import java.util.Collections; +import java.util.Comparator; +import java.util.Map; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; +/** + * Manages player profiles ({@link BounceProfile}) during the lifecycle of the game. + * + *

Provides concurrent lookup, creation, removal, lifecycle startup triggers, + * winner determination based on profile score, and cleanup methods.

+ + * @author theEvilReaper + * @version 1.1.0 + * @since 0.1.0 + */ public class ProfileService { private final Map profileMap; + /** + * Constructs a new {@link ProfileService} backed by a concurrent hash map. + */ public ProfileService() { - profileMap = new ConcurrentHashMap<>(); + this.profileMap = new ConcurrentHashMap<>(); } + /** + * Adds or retrieves the {@link BounceProfile} for the specified player. + * + * @param player whose profile should be retrieved or created + * @return existing or newly created profile + */ public BounceProfile add(Player player) { return this.profileMap.computeIfAbsent(player.getUuid(), uuid -> new BounceProfile(player)); } + /** + * Removes the profile associated with the specified player. + * + * @param player whose profile should be removed + * @return removed profile, or {@code null} if no profile was stored + */ public @Nullable BounceProfile remove(Player player) { return this.profileMap.remove(player.getUuid()); } + /** + * Retrieves the profile associated with the specified player. + * + * @param player whose profile to retrieve + * @return player's profile, or {@code null} if not found + */ public @Nullable BounceProfile get(Player player) { return this.profileMap.get(player.getUuid()); } - public @Nullable BounceProfile get(UUID uuid) { + /** + * Retrieves the profile associated with the specified UUID. + * + * @param uuid unique identifier of the player + * @return player's profile, or {@code null} if not found + */ + public @Nullable BounceProfile get(@Nullable UUID uuid) { + if (uuid == null) return null; return this.profileMap.get(uuid); } + /** + * Starts the jump task for all stored profiles on the given map and invokes the consumer callback. + * + * @param gameMap active game map + * @param consumer action executed for each online player profile + */ public void start(GameMap gameMap, PlayerConsumer consumer) { for (BounceProfile value : this.profileMap.values()) { - value.getJumpRunnable().start(gameMap); + if (value.getJumpRunnable() != null) { + value.getJumpRunnable().start(gameMap); + } consumer.accept(value.getPlayer()); } } + /** + * Determines and returns the winning profile (the profile with the highest points). + * + * @return winning profile, or {@code null} if no profiles are registered + */ public @Nullable BounceProfile getWinner() { if (this.profileMap.isEmpty()) return null; return profileMap.values().stream() - .min(Comparator.naturalOrder()) // because "highest points" sorts first + .max(Comparator.naturalOrder()) .orElse(null); } + /** + * Clears all stored player profiles. + */ public void clear() { if (this.profileMap.isEmpty()) return; this.profileMap.clear(); } + /** + * Executes the given callback consumer for every profile and clears the profile map. + * + * @param callback action to execute for each profile before clearing + */ public void clear(Consumer callback) { if (this.profileMap.isEmpty()) return; for (BounceProfile value : this.profileMap.values()) { callback.accept(value); } + this.profileMap.clear(); } + /** + * Returns an unmodifiable view of the internal profile map. + * + * @return unmodifiable map mapping player UUIDs to their {@link BounceProfile} + */ public Map getProfileMap() { return Collections.unmodifiableMap(profileMap); } From a0172db35db081a0d34df44a177ba9c5803c615c Mon Sep 17 00:00:00 2001 From: Joltras Date: Sun, 9 Aug 2026 14:47:27 +0200 Subject: [PATCH 2/4] test(profile): add integration test --- .../ProfileServiceIntegrationTest.java | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 src/test/java/net/theevilreaper/bounce/profile/ProfileServiceIntegrationTest.java diff --git a/src/test/java/net/theevilreaper/bounce/profile/ProfileServiceIntegrationTest.java b/src/test/java/net/theevilreaper/bounce/profile/ProfileServiceIntegrationTest.java new file mode 100644 index 0000000..03449c5 --- /dev/null +++ b/src/test/java/net/theevilreaper/bounce/profile/ProfileServiceIntegrationTest.java @@ -0,0 +1,79 @@ +package net.theevilreaper.bounce.profile; + +import net.minestom.server.entity.Player; +import net.minestom.server.instance.Instance; +import net.minestom.testing.Env; +import net.minestom.testing.extension.MicrotusExtension; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MicrotusExtension.class) +class ProfileServiceIntegrationTest { + + @Test + void testProfileServiceAddAndGet(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player player = env.createPlayer(instance); + + ProfileService service = new ProfileService(); + assertTrue(service.getProfileMap().isEmpty()); + + BounceProfile profile = service.add(player); + assertNotNull(profile); + assertEquals(player.getUuid(), profile.getPlayer().getUuid()); + + assertEquals(profile, service.get(player)); + assertEquals(profile, service.get(player.getUuid())); + assertEquals(1, service.getProfileMap().size()); + + env.destroyInstance(instance, true); + } + + @Test + void testProfileServiceClearWithCallbackClearsMap(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player player1 = env.createPlayer(instance); + Player player2 = env.createPlayer(instance); + + ProfileService service = new ProfileService(); + service.add(player1); + service.add(player2); + + assertEquals(2, service.getProfileMap().size()); + + AtomicInteger processedCount = new AtomicInteger(0); + service.clear(profile -> processedCount.incrementAndGet()); + + assertEquals(2, processedCount.get(), "Callback should be executed for each profile"); + assertTrue(service.getProfileMap().isEmpty(), "Profile map should be cleared after clear(callback)"); + + env.destroyInstance(instance, true); + } + + @Test + void testProfileServiceGetWinnerReturnsHighestScoringProfile(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player player1 = env.createPlayer(instance); + Player player2 = env.createPlayer(instance); + + ProfileService service = new ProfileService(); + assertNull(service.getWinner(), "Winner should be null when profile service is empty"); + + BounceProfile profile1 = service.add(player1); + BounceProfile profile2 = service.add(player2); + + profile1.addPoints(20); + profile2.addPoints(5); + + BounceProfile winner = service.getWinner(); + assertNotNull(winner, "Winner profile should not be null"); + assertEquals(profile1, winner, "Winner should be the profile with the highest score"); + + env.destroyInstance(instance, true); + } +} From 0a52a67ed86a484fe660b807b390e82fed09bccf Mon Sep 17 00:00:00 2001 From: Joltras Date: Sun, 9 Aug 2026 14:47:40 +0200 Subject: [PATCH 3/4] chore(message): add win component --- src/main/java/net/theevilreaper/bounce/util/GameMessages.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/net/theevilreaper/bounce/util/GameMessages.java b/src/main/java/net/theevilreaper/bounce/util/GameMessages.java index 9fd5dd2..b85dc8c 100644 --- a/src/main/java/net/theevilreaper/bounce/util/GameMessages.java +++ b/src/main/java/net/theevilreaper/bounce/util/GameMessages.java @@ -19,6 +19,8 @@ public class GameMessages extends Messages { public static final Component INVALID_PLAYER_NAME; public static final Component PLAYER_NOT_FOUND; + public static final Component WON_COMPONENT; + public static final Component STATS_LINE; private static final Component LEAVE_PART; @@ -46,6 +48,8 @@ public class GameMessages extends Messages { POINT_PART = Component.space().append(Component.text("Points", NamedTextColor.GRAY)); STATS_LINE = Component.text("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n", NamedTextColor.GRAY); + + WON_COMPONENT = Component.text("won the game", NamedTextColor.GRAY); } @Contract From 4362f8492f31ec87635982640e61413bb7b6c752 Mon Sep 17 00:00:00 2001 From: Joltras Date: Sun, 9 Aug 2026 14:53:51 +0200 Subject: [PATCH 4/4] chore(game): improve finish usage --- src/main/java/net/theevilreaper/bounce/Bounce.java | 2 +- .../bounce/listener/game/GameFinishListener.java | 11 ++++++----- .../theevilreaper/bounce/profile/BounceProfile.java | 10 ++++++++++ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/main/java/net/theevilreaper/bounce/Bounce.java b/src/main/java/net/theevilreaper/bounce/Bounce.java index e31d22d..8483692 100644 --- a/src/main/java/net/theevilreaper/bounce/Bounce.java +++ b/src/main/java/net/theevilreaper/bounce/Bounce.java @@ -145,7 +145,7 @@ private void handleGameLeave(Player player) { if (profile == null) return; - profile.getJumpRunnable().cancel(); + profile.stopJumpTask(); this.scoreboard.removeViewer(player); } diff --git a/src/main/java/net/theevilreaper/bounce/listener/game/GameFinishListener.java b/src/main/java/net/theevilreaper/bounce/listener/game/GameFinishListener.java index c302ad9..f8e1660 100644 --- a/src/main/java/net/theevilreaper/bounce/listener/game/GameFinishListener.java +++ b/src/main/java/net/theevilreaper/bounce/listener/game/GameFinishListener.java @@ -1,7 +1,6 @@ package net.theevilreaper.bounce.listener.game; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.title.Title; import net.kyori.adventure.title.TitlePart; import net.minestom.server.entity.Player; @@ -9,6 +8,7 @@ import net.theevilreaper.bounce.event.BounceGameFinishEvent; import net.theevilreaper.bounce.profile.BounceProfile; import net.theevilreaper.bounce.profile.ProfileService; +import net.theevilreaper.bounce.util.GameMessages; import java.util.function.Consumer; @@ -26,17 +26,18 @@ public void accept(BounceGameFinishEvent event) { BounceProfile winnerProfile = profileService.getWinner(); if (winnerProfile == null) { - profileService.clear(bounceProfile -> bounceProfile.getJumpRunnable().cancel()); + profileService.clear(BounceProfile::stopJumpTask); return; } profileService.clear(profile -> { - profile.getJumpRunnable().cancel(); - Player player = winnerProfile.getPlayer(); + profile.stopJumpTask(); + Player player = profile.getPlayer(); AttributeHelper.resetJumpStrength(player); boolean isWinner = profile.equals(winnerProfile); profile.sendStats(isWinner); - var title = Title.title(player.getDisplayName(), Component.text("wons the game", NamedTextColor.GRAY), Title.DEFAULT_TIMES); + Component displayName = winnerProfile.getPlayer().getDisplayName(); + Title title = Title.title(displayName, GameMessages.WON_COMPONENT, Title.DEFAULT_TIMES); player.sendTitlePart(TitlePart.TITLE, title.title()); player.sendTitlePart(TitlePart.SUBTITLE, title.subtitle()); player.sendTitlePart(TitlePart.TIMES, title.times()); diff --git a/src/main/java/net/theevilreaper/bounce/profile/BounceProfile.java b/src/main/java/net/theevilreaper/bounce/profile/BounceProfile.java index e021590..9be0e62 100644 --- a/src/main/java/net/theevilreaper/bounce/profile/BounceProfile.java +++ b/src/main/java/net/theevilreaper/bounce/profile/BounceProfile.java @@ -169,6 +169,16 @@ public Player getPlayer() { return player; } + /** + * Stops and cancels the active jump task for this profile, if registered. + */ + public void stopJumpTask() { + if (this.jumpRunnable != null) { + this.jumpRunnable.cancel(); + this.jumpRunnable = null; + } + } + /** * Returns the jump runnable associated with this profile's player. *