Files
purpur-stealth/src/main/java/games/dmg/spigotstealth/QualifyingInvisibilityService.java
T
dmg 91c9e7b9ad
CI / build (push) Successful in 2m31s
Release / release (push) Successful in 3m30s
feat(progression): unlock stealth from potion invisibility
2026-08-15 00:01:26 -04:00

123 lines
5.0 KiB
Java

package games.dmg.spigotstealth;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.LongSupplier;
/** Accumulates qualifying potion invisibility using monotonic elapsed time. */
public final class QualifyingInvisibilityService {
private final StealthStateManager stateManager;
private final long unlockThresholdMillis;
private final LongSupplier nanoTime;
private final Consumer<UUID> unlockNotifier;
private final Map<UUID, ActiveInterval> activeIntervals = new HashMap<>();
public QualifyingInvisibilityService(
StealthStateManager stateManager,
Duration unlockThreshold,
LongSupplier nanoTime,
Consumer<UUID> unlockNotifier) {
this.stateManager = Objects.requireNonNull(stateManager, "stateManager");
this.unlockThresholdMillis = Objects.requireNonNull(unlockThreshold, "unlockThreshold").toMillis();
this.nanoTime = Objects.requireNonNull(nanoTime, "nanoTime");
this.unlockNotifier = Objects.requireNonNull(unlockNotifier, "unlockNotifier");
}
public CompletableFuture<Void> begin(UUID playerId, String playerName, Instant startedAt) {
Objects.requireNonNull(playerId, "playerId");
Objects.requireNonNull(startedAt, "startedAt");
long nowNanos = nanoTime.getAsLong();
ActiveInterval previous = activeIntervals.put(playerId, new ActiveInterval(nowNanos, startedAt, playerName));
return record(playerId, playerName, elapsedMillis(previous, nowNanos), startedAt, true);
}
public CompletableFuture<Void> checkpoint(UUID playerId) {
long nowNanos = nanoTime.getAsLong();
ActiveInterval previous = activeIntervals.get(playerId);
if (previous == null) {
return CompletableFuture.completedFuture(null);
}
long elapsedMillis = elapsedMillis(previous, nowNanos);
Instant continuedAt = previous.startedAt().plusMillis(elapsedMillis);
activeIntervals.put(playerId, new ActiveInterval(nowNanos, continuedAt, previous.playerName()));
return record(playerId, previous.playerName(), elapsedMillis, continuedAt, true);
}
public CompletableFuture<Void> stop(UUID playerId) {
long nowNanos = nanoTime.getAsLong();
ActiveInterval previous = activeIntervals.remove(playerId);
if (previous == null) {
return CompletableFuture.completedFuture(null);
}
return record(playerId, previous.playerName(), elapsedMillis(previous, nowNanos), null, false);
}
public boolean isQualifying(UUID playerId) {
return activeIntervals.containsKey(playerId);
}
public Set<UUID> activePlayerIds() {
return Set.copyOf(activeIntervals.keySet());
}
public CompletableFuture<Void> stopAll() {
CompletableFuture<?>[] stops = activePlayerIds().stream()
.map(this::stop)
.toArray(CompletableFuture[]::new);
return CompletableFuture.allOf(stops);
}
public long currentAccumulatedMillis(UUID playerId) {
long persisted = stateManager.snapshot().player(playerId).accumulatedMillis();
return Math.addExact(persisted, elapsedMillis(activeIntervals.get(playerId), nanoTime.getAsLong()));
}
private CompletableFuture<Void> record(
UUID playerId,
String playerName,
long elapsedMillis,
Instant qualifyingSince,
boolean active) {
AtomicBoolean unlockedNow = new AtomicBoolean();
CompletableFuture<Void> saved = stateManager.update(state -> {
PlayerStealthState player = state.player(playerId);
long accumulated = Math.addExact(player.accumulatedMillis(), elapsedMillis);
boolean unlocked = player.unlocked() || accumulated >= unlockThresholdMillis;
unlockedNow.set(unlocked && !player.unlocked());
PlayerStealthState updated = new PlayerStealthState(
playerId,
playerName,
accumulated,
unlocked,
player.preparedLogin(),
player.concealed(),
active ? qualifyingSince : null,
player.unknownFields());
return state.withPlayer(updated);
});
return saved.thenRun(() -> {
if (unlockedNow.get()) {
unlockNotifier.accept(playerId);
}
});
}
private static long elapsedMillis(ActiveInterval interval, long nowNanos) {
if (interval == null) {
return 0L;
}
long elapsedNanos = Math.max(0L, nowNanos - interval.startedNanos());
return Duration.ofNanos(elapsedNanos).toMillis();
}
private record ActiveInterval(long startedNanos, Instant startedAt, String playerName) { }
}