feat(progress): track persistent creeper kills
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
package games.dmg.creeperfear;
|
||||
|
||||
import games.dmg.creeperfear.listener.CreeperDeathListener;
|
||||
import games.dmg.creeperfear.progress.ProgressService;
|
||||
import games.dmg.creeperfear.progress.SqliteProgressRepository;
|
||||
import java.nio.file.Path;
|
||||
import java.util.logging.Level;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
public final class CreeperFearPlugin extends JavaPlugin {
|
||||
private ProgressService progressService;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
try {
|
||||
Path databasePath = getDataFolder().toPath().resolve("player-progress.sqlite3");
|
||||
progressService = new ProgressService(new SqliteProgressRepository(databasePath));
|
||||
getServer().getPluginManager().registerEvents(
|
||||
new CreeperDeathListener(progressService, getLogger()), this);
|
||||
getLogger().info("Creeper Fear enabled");
|
||||
} catch (RuntimeException exception) {
|
||||
getLogger().log(Level.SEVERE, "Creeper Fear could not initialize its progress storage", exception);
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
if (progressService == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
progressService.close();
|
||||
} catch (RuntimeException exception) {
|
||||
getLogger().log(Level.SEVERE, "Creeper Fear could not close its progress storage cleanly", exception);
|
||||
} finally {
|
||||
progressService = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package games.dmg.creeperfear.listener;
|
||||
|
||||
import games.dmg.creeperfear.progress.PlayerProgress;
|
||||
import games.dmg.creeperfear.progress.ProgressService;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import org.bukkit.entity.Creeper;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.EntityDeathEvent;
|
||||
|
||||
public final class CreeperDeathListener implements Listener {
|
||||
private static final int RECENT_DEATH_LIMIT = 4096;
|
||||
|
||||
private final BiFunction<UUID, String, CompletableFuture<PlayerProgress>> killRecorder;
|
||||
private final Logger logger;
|
||||
private final Map<UUID, Boolean> recentDeaths = new LinkedHashMap<>(128, 0.75f, true) {
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<UUID, Boolean> eldest) {
|
||||
return size() > RECENT_DEATH_LIMIT;
|
||||
}
|
||||
};
|
||||
|
||||
public CreeperDeathListener(ProgressService progressService, Logger logger) {
|
||||
this(progressService::recordCreeperKill, logger);
|
||||
}
|
||||
|
||||
CreeperDeathListener(
|
||||
BiFunction<UUID, String, CompletableFuture<PlayerProgress>> killRecorder,
|
||||
Logger logger) {
|
||||
this.killRecorder = killRecorder;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onEntityDeath(EntityDeathEvent event) {
|
||||
if (!(event.getEntity() instanceof Creeper creeper)) {
|
||||
return;
|
||||
}
|
||||
Player player = CreeperKillAttributor.findPlayer(creeper).orElse(null);
|
||||
if (player == null || !markNewDeath(creeper.getUniqueId())) {
|
||||
return;
|
||||
}
|
||||
|
||||
UUID playerId = player.getUniqueId();
|
||||
String playerName = player.getName();
|
||||
killRecorder.apply(playerId, playerName).whenComplete((progress, failure) -> {
|
||||
if (failure != null) {
|
||||
logger.log(Level.SEVERE,
|
||||
"Could not persist creeper progress for " + playerName + " (" + playerId + ")",
|
||||
failure);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private synchronized boolean markNewDeath(UUID creeperId) {
|
||||
return recentDeaths.putIfAbsent(creeperId, Boolean.TRUE) == null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package games.dmg.creeperfear.listener;
|
||||
|
||||
import java.util.Optional;
|
||||
import org.bukkit.entity.Creeper;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.Projectile;
|
||||
import org.bukkit.entity.Tameable;
|
||||
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||
import org.bukkit.projectiles.ProjectileSource;
|
||||
|
||||
public final class CreeperKillAttributor {
|
||||
private CreeperKillAttributor() {
|
||||
}
|
||||
|
||||
public static Optional<Player> findPlayer(Creeper creeper) {
|
||||
Player bukkitKiller = creeper.getKiller();
|
||||
if (bukkitKiller != null) {
|
||||
return Optional.of(bukkitKiller);
|
||||
}
|
||||
if (!(creeper.getLastDamageCause() instanceof EntityDamageByEntityEvent damageEvent)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return playerResponsibleFor(damageEvent.getDamager());
|
||||
}
|
||||
|
||||
private static Optional<Player> playerResponsibleFor(Entity damager) {
|
||||
if (damager instanceof Player player) {
|
||||
return Optional.of(player);
|
||||
}
|
||||
if (damager instanceof Projectile projectile) {
|
||||
ProjectileSource shooter = projectile.getShooter();
|
||||
if (shooter instanceof Player player) {
|
||||
return Optional.of(player);
|
||||
}
|
||||
}
|
||||
if (damager instanceof Tameable tameable
|
||||
&& tameable.isTamed()
|
||||
&& tameable.getOwner() instanceof Player player) {
|
||||
return Optional.of(player);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package games.dmg.creeperfear.progress;
|
||||
|
||||
public enum AuraRank {
|
||||
LOCKED,
|
||||
I,
|
||||
II,
|
||||
III,
|
||||
IV,
|
||||
V,
|
||||
VI;
|
||||
|
||||
public boolean isMaximum() {
|
||||
return this == VI;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package games.dmg.creeperfear.progress;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
public record PlayerProgress(UUID playerId, String lastKnownName, AuraRank rank, int tierKills) {
|
||||
public PlayerProgress {
|
||||
Objects.requireNonNull(playerId, "playerId");
|
||||
Objects.requireNonNull(lastKnownName, "lastKnownName");
|
||||
Objects.requireNonNull(rank, "rank");
|
||||
if (lastKnownName.isBlank()) {
|
||||
throw new IllegalArgumentException("lastKnownName must not be blank");
|
||||
}
|
||||
if (tierKills < 0) {
|
||||
throw new IllegalArgumentException("tierKills must not be negative");
|
||||
}
|
||||
if (rank.isMaximum() && tierKills != 0) {
|
||||
throw new IllegalArgumentException("rank VI cannot retain tier progress");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package games.dmg.creeperfear.progress;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface ProgressRepository extends AutoCloseable {
|
||||
PlayerProgress recordCreeperKill(UUID playerId, String playerName);
|
||||
|
||||
Optional<PlayerProgress> find(UUID playerId);
|
||||
|
||||
PlayerProgress save(PlayerProgress progress);
|
||||
|
||||
@Override
|
||||
void close();
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package games.dmg.creeperfear.progress;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class ProgressService implements AutoCloseable {
|
||||
private final ProgressRepository repository;
|
||||
private final ExecutorService executor;
|
||||
|
||||
public ProgressService(ProgressRepository repository) {
|
||||
this.repository = repository;
|
||||
this.executor = Executors.newSingleThreadExecutor(runnable -> {
|
||||
Thread thread = new Thread(runnable, "creeper-fear-progress");
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
}
|
||||
|
||||
public CompletableFuture<PlayerProgress> recordCreeperKill(UUID playerId, String playerName) {
|
||||
return CompletableFuture.supplyAsync(
|
||||
() -> repository.recordCreeperKill(playerId, playerName), executor);
|
||||
}
|
||||
|
||||
public CompletableFuture<Optional<PlayerProgress>> find(UUID playerId) {
|
||||
return CompletableFuture.supplyAsync(() -> repository.find(playerId), executor);
|
||||
}
|
||||
|
||||
public CompletableFuture<PlayerProgress> save(PlayerProgress progress) {
|
||||
return CompletableFuture.supplyAsync(() -> repository.save(progress), executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
executor.shutdown();
|
||||
try {
|
||||
if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException exception) {
|
||||
executor.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
} finally {
|
||||
repository.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package games.dmg.creeperfear.progress;
|
||||
|
||||
public final class ProgressStorageException extends RuntimeException {
|
||||
public ProgressStorageException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package games.dmg.creeperfear.progress;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class SqliteProgressRepository implements ProgressRepository {
|
||||
private final Connection connection;
|
||||
|
||||
public SqliteProgressRepository(Path databasePath) {
|
||||
try {
|
||||
Path parent = databasePath.toAbsolutePath().getParent();
|
||||
if (parent != null) {
|
||||
Files.createDirectories(parent);
|
||||
}
|
||||
connection = DriverManager.getConnection("jdbc:sqlite:" + databasePath.toAbsolutePath());
|
||||
initialize();
|
||||
} catch (IOException | SQLException exception) {
|
||||
throw new ProgressStorageException("Could not initialize player progress database", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void initialize() throws SQLException {
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
statement.execute("PRAGMA journal_mode = WAL");
|
||||
statement.execute("PRAGMA synchronous = NORMAL");
|
||||
statement.execute("""
|
||||
CREATE TABLE IF NOT EXISTS player_progress (
|
||||
player_uuid TEXT PRIMARY KEY NOT NULL,
|
||||
last_known_name TEXT NOT NULL,
|
||||
rank TEXT NOT NULL CHECK (rank IN ('LOCKED', 'I', 'II', 'III', 'IV', 'V', 'VI')),
|
||||
tier_kills INTEGER NOT NULL CHECK (tier_kills >= 0),
|
||||
updated_at INTEGER NOT NULL
|
||||
)
|
||||
""");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized PlayerProgress recordCreeperKill(UUID playerId, String playerName) {
|
||||
String sql = """
|
||||
INSERT INTO player_progress(player_uuid, last_known_name, rank, tier_kills, updated_at)
|
||||
VALUES (?, ?, 'LOCKED', 1, ?)
|
||||
ON CONFLICT(player_uuid) DO UPDATE SET
|
||||
last_known_name = excluded.last_known_name,
|
||||
tier_kills = CASE
|
||||
WHEN player_progress.rank = 'VI' THEN 0
|
||||
ELSE player_progress.tier_kills + 1
|
||||
END,
|
||||
updated_at = excluded.updated_at
|
||||
""";
|
||||
try {
|
||||
connection.setAutoCommit(false);
|
||||
try (PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setString(1, playerId.toString());
|
||||
statement.setString(2, playerName);
|
||||
statement.setLong(3, System.currentTimeMillis());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
PlayerProgress progress = findRequired(playerId);
|
||||
connection.commit();
|
||||
return progress;
|
||||
} catch (SQLException | RuntimeException exception) {
|
||||
rollbackAfterFailure(exception);
|
||||
throw storageFailure("Could not record creeper kill for " + playerId, exception);
|
||||
} finally {
|
||||
restoreAutoCommit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized Optional<PlayerProgress> find(UUID playerId) {
|
||||
try {
|
||||
return findInternal(playerId);
|
||||
} catch (SQLException | RuntimeException exception) {
|
||||
throw storageFailure("Could not load progress for " + playerId, exception);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized PlayerProgress save(PlayerProgress progress) {
|
||||
String sql = """
|
||||
INSERT INTO player_progress(player_uuid, last_known_name, rank, tier_kills, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(player_uuid) DO UPDATE SET
|
||||
last_known_name = excluded.last_known_name,
|
||||
rank = excluded.rank,
|
||||
tier_kills = excluded.tier_kills,
|
||||
updated_at = excluded.updated_at
|
||||
""";
|
||||
try (PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setString(1, progress.playerId().toString());
|
||||
statement.setString(2, progress.lastKnownName());
|
||||
statement.setString(3, progress.rank().name());
|
||||
statement.setInt(4, progress.tierKills());
|
||||
statement.setLong(5, System.currentTimeMillis());
|
||||
statement.executeUpdate();
|
||||
return progress;
|
||||
} catch (SQLException exception) {
|
||||
throw storageFailure("Could not save progress for " + progress.playerId(), exception);
|
||||
}
|
||||
}
|
||||
|
||||
private PlayerProgress findRequired(UUID playerId) throws SQLException {
|
||||
return findInternal(playerId).orElseThrow(
|
||||
() -> new ProgressStorageException("Progress disappeared for " + playerId, null));
|
||||
}
|
||||
|
||||
private Optional<PlayerProgress> findInternal(UUID playerId) throws SQLException {
|
||||
String sql = """
|
||||
SELECT player_uuid, last_known_name, rank, tier_kills
|
||||
FROM player_progress
|
||||
WHERE player_uuid = ?
|
||||
""";
|
||||
try (PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setString(1, playerId.toString());
|
||||
try (ResultSet result = statement.executeQuery()) {
|
||||
if (!result.next()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(new PlayerProgress(
|
||||
UUID.fromString(result.getString("player_uuid")),
|
||||
result.getString("last_known_name"),
|
||||
AuraRank.valueOf(result.getString("rank")),
|
||||
result.getInt("tier_kills")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void rollbackAfterFailure(Throwable original) {
|
||||
try {
|
||||
connection.rollback();
|
||||
} catch (SQLException rollbackFailure) {
|
||||
original.addSuppressed(rollbackFailure);
|
||||
}
|
||||
}
|
||||
|
||||
private void restoreAutoCommit() {
|
||||
try {
|
||||
connection.setAutoCommit(true);
|
||||
} catch (SQLException exception) {
|
||||
throw new ProgressStorageException("Could not restore database transaction state", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private ProgressStorageException storageFailure(String message, Throwable cause) {
|
||||
if (cause instanceof ProgressStorageException storageException) {
|
||||
return storageException;
|
||||
}
|
||||
return new ProgressStorageException(message, cause);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() {
|
||||
try {
|
||||
connection.close();
|
||||
} catch (SQLException exception) {
|
||||
throw new ProgressStorageException("Could not close player progress database", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
name: CreeperFear
|
||||
version: '${version}'
|
||||
main: games.dmg.creeperfear.CreeperFearPlugin
|
||||
api-version: '26.2'
|
||||
author: dmg.games
|
||||
description: Unlock Creeper Aura ranks by defeating creepers.
|
||||
@@ -0,0 +1,43 @@
|
||||
package games.dmg.creeperfear.listener;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import games.dmg.creeperfear.progress.AuraRank;
|
||||
import games.dmg.creeperfear.progress.PlayerProgress;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.logging.Logger;
|
||||
import org.bukkit.entity.Creeper;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.entity.EntityDeathEvent;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class CreeperDeathListenerTest {
|
||||
@Test
|
||||
void recordsOnePointWhenTheSameDeathIsObservedMoreThanOnce() {
|
||||
UUID creeperId = UUID.randomUUID();
|
||||
UUID playerId = UUID.randomUUID();
|
||||
Creeper creeper = mock(Creeper.class);
|
||||
Player player = mock(Player.class);
|
||||
EntityDeathEvent event = mock(EntityDeathEvent.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
BiFunction<UUID, String, CompletableFuture<PlayerProgress>> recorder = mock(BiFunction.class);
|
||||
PlayerProgress progress = new PlayerProgress(playerId, "Player", AuraRank.LOCKED, 1);
|
||||
when(event.getEntity()).thenReturn(creeper);
|
||||
when(creeper.getUniqueId()).thenReturn(creeperId);
|
||||
when(creeper.getKiller()).thenReturn(player);
|
||||
when(player.getUniqueId()).thenReturn(playerId);
|
||||
when(player.getName()).thenReturn("Player");
|
||||
when(recorder.apply(playerId, "Player")).thenReturn(CompletableFuture.completedFuture(progress));
|
||||
CreeperDeathListener listener = new CreeperDeathListener(recorder, Logger.getAnonymousLogger());
|
||||
|
||||
listener.onEntityDeath(event);
|
||||
listener.onEntityDeath(event);
|
||||
|
||||
verify(recorder, times(1)).apply(playerId, "Player");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package games.dmg.creeperfear.listener;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.bukkit.entity.Creeper;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.Projectile;
|
||||
import org.bukkit.entity.Tameable;
|
||||
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class CreeperKillAttributorTest {
|
||||
@Test
|
||||
void attributesDirectPlayerKills() {
|
||||
Creeper creeper = mock(Creeper.class);
|
||||
Player player = mock(Player.class);
|
||||
when(creeper.getKiller()).thenReturn(player);
|
||||
|
||||
assertEquals(player, CreeperKillAttributor.findPlayer(creeper).orElseThrow());
|
||||
}
|
||||
|
||||
@Test
|
||||
void attributesProjectileKillsToTheShooter() {
|
||||
Creeper creeper = mock(Creeper.class);
|
||||
Projectile projectile = mock(Projectile.class);
|
||||
Player player = mock(Player.class);
|
||||
EntityDamageByEntityEvent damage = mock(EntityDamageByEntityEvent.class);
|
||||
when(creeper.getLastDamageCause()).thenReturn(damage);
|
||||
when(damage.getDamager()).thenReturn(projectile);
|
||||
when(projectile.getShooter()).thenReturn(player);
|
||||
|
||||
assertEquals(player, CreeperKillAttributor.findPlayer(creeper).orElseThrow());
|
||||
}
|
||||
|
||||
@Test
|
||||
void attributesTamedEntityKillsToTheOwner() {
|
||||
Creeper creeper = mock(Creeper.class);
|
||||
Tameable tameable = mock(Tameable.class);
|
||||
Player player = mock(Player.class);
|
||||
EntityDamageByEntityEvent damage = mock(EntityDamageByEntityEvent.class);
|
||||
when(creeper.getLastDamageCause()).thenReturn(damage);
|
||||
when(damage.getDamager()).thenReturn(tameable);
|
||||
when(tameable.isTamed()).thenReturn(true);
|
||||
when(tameable.getOwner()).thenReturn(player);
|
||||
|
||||
assertEquals(player, CreeperKillAttributor.findPlayer(creeper).orElseThrow());
|
||||
}
|
||||
|
||||
@Test
|
||||
void ignoresKillsWithoutAPlayerAttribution() {
|
||||
Creeper creeper = mock(Creeper.class);
|
||||
|
||||
assertTrue(CreeperKillAttributor.findPlayer(creeper).isEmpty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package games.dmg.creeperfear.progress;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ProgressServiceTest {
|
||||
@Test
|
||||
void recordsKillsAwayFromTheCallingThread() throws Exception {
|
||||
BlockingRepository repository = new BlockingRepository();
|
||||
Thread caller = Thread.currentThread();
|
||||
|
||||
try (ProgressService service = new ProgressService(repository)) {
|
||||
var result = service.recordCreeperKill(UUID.randomUUID(), "Player");
|
||||
|
||||
assertTrue(repository.started.await(2, TimeUnit.SECONDS));
|
||||
assertFalse(result.isDone());
|
||||
assertNotEquals(caller, repository.worker.get());
|
||||
|
||||
repository.release.countDown();
|
||||
result.get(2, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class BlockingRepository implements ProgressRepository {
|
||||
private final CountDownLatch started = new CountDownLatch(1);
|
||||
private final CountDownLatch release = new CountDownLatch(1);
|
||||
private final AtomicReference<Thread> worker = new AtomicReference<>();
|
||||
|
||||
@Override
|
||||
public PlayerProgress recordCreeperKill(UUID playerId, String playerName) {
|
||||
worker.set(Thread.currentThread());
|
||||
started.countDown();
|
||||
try {
|
||||
assertTrue(release.await(2, TimeUnit.SECONDS));
|
||||
} catch (InterruptedException exception) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException(exception);
|
||||
}
|
||||
return new PlayerProgress(playerId, playerName, AuraRank.LOCKED, 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<PlayerProgress> find(UUID playerId) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlayerProgress save(PlayerProgress progress) {
|
||||
return progress;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package games.dmg.creeperfear.progress;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class SqliteProgressRepositoryTest {
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void recordsCurrentTierProgressAndPersistsItAcrossRestart() {
|
||||
UUID playerId = UUID.randomUUID();
|
||||
Path database = tempDir.resolve("progress.sqlite3");
|
||||
|
||||
try (SqliteProgressRepository repository = new SqliteProgressRepository(database)) {
|
||||
PlayerProgress first = repository.recordCreeperKill(playerId, "FirstName");
|
||||
PlayerProgress second = repository.recordCreeperKill(playerId, "NewName");
|
||||
|
||||
assertEquals(AuraRank.LOCKED, first.rank());
|
||||
assertEquals(1, first.tierKills());
|
||||
assertEquals(2, second.tierKills());
|
||||
assertEquals("NewName", second.lastKnownName());
|
||||
}
|
||||
|
||||
try (SqliteProgressRepository repository = new SqliteProgressRepository(database)) {
|
||||
PlayerProgress persisted = repository.find(playerId).orElseThrow();
|
||||
|
||||
assertEquals(AuraRank.LOCKED, persisted.rank());
|
||||
assertEquals(2, persisted.tierKills());
|
||||
assertEquals("NewName", persisted.lastKnownName());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void storesRankSeparatelyAndDoesNotAdvanceProgressAtRankSix() {
|
||||
UUID playerId = UUID.randomUUID();
|
||||
Path database = tempDir.resolve("maximum-rank.sqlite3");
|
||||
|
||||
try (SqliteProgressRepository repository = new SqliteProgressRepository(database)) {
|
||||
repository.save(new PlayerProgress(playerId, "Player", AuraRank.VI, 0));
|
||||
|
||||
PlayerProgress progress = repository.recordCreeperKill(playerId, "RenamedPlayer");
|
||||
|
||||
assertEquals(AuraRank.VI, progress.rank());
|
||||
assertEquals(0, progress.tierKills());
|
||||
assertEquals("RenamedPlayer", progress.lastKnownName());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user