Files
spigot-heights/src/main/java/games/dmg/spigotheights/HeightStore.java
T
dmg 47d5060dac
Release / release (push) Successful in 2m36s
CI / build (push) Successful in 1m1s
feat(heights): add durable temporary throwable stature
2026-09-10 22:41:20 -04:00

127 lines
4.6 KiB
Java

package games.dmg.spigotheights;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.time.Instant;
import java.time.format.DateTimeParseException;
import java.util.UUID;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.YamlConfiguration;
public final class HeightStore {
private final Path statePath;
private volatile YamlConfiguration state;
public record Sequence(double baseline, Instant expiresAt) {}
public HeightStore(File dataFolder) {
statePath = dataFolder.toPath().resolve("state.yml");
state = YamlConfiguration.loadConfiguration(statePath.toFile());
}
public Double find(UUID playerId) {
YamlConfiguration snapshot = state;
String path = path(playerId);
return snapshot.contains(path) ? snapshot.getDouble(path) : null;
}
public Sequence sequence(UUID playerId) {
YamlConfiguration snapshot = state;
String path = temporaryPath(playerId);
if (!snapshot.isDouble(path + ".baseline") && !snapshot.isInt(path + ".baseline")) {
return null;
}
double baseline = snapshot.getDouble(path + ".baseline");
if (!Double.isFinite(baseline) || baseline < 0.0625 || baseline > 16.0) {
return null;
}
try {
return new Sequence(baseline, Instant.parse(snapshot.getString(path + ".expires-at", "")));
} catch (DateTimeParseException exception) {
return null;
}
}
/** A permanent write also invalidates any outstanding temporary sequence. */
public void save(UUID playerId, double scale) throws IOException {
save(playerId, scale, null);
}
public boolean hasReceipt(UUID playerId, UUID source) {
return state.getBoolean(receiptPath(playerId, source));
}
public void save(UUID playerId, double scale, Sequence sequence) throws IOException {
save(playerId, scale, sequence, null);
}
public synchronized void save(UUID playerId, double scale, Sequence sequence, UUID source) throws IOException {
YamlConfiguration next = copy();
next.set(path(playerId), scale);
next.set(temporaryPath(playerId), null);
if (sequence != null) {
next.set(temporaryPath(playerId) + ".baseline", sequence.baseline());
next.set(temporaryPath(playerId) + ".expires-at", sequence.expiresAt().toString());
}
if (source != null) {
next.set(receiptPath(playerId, source), true);
}
replace(next);
}
public synchronized void forgetSource(UUID source) throws IOException {
var players = state.getConfigurationSection("players");
if (players == null) {
return;
}
java.util.List<String> paths = players.getKeys(false).stream()
.map(player -> "players." + player + ".throwables." + source)
.filter(state::contains).toList();
if (!paths.isEmpty()) {
YamlConfiguration next = copy();
paths.forEach(path -> next.set(path, null));
replace(next);
}
}
private YamlConfiguration copy() throws IOException {
YamlConfiguration next = new YamlConfiguration();
try {
next.loadFromString(state.saveToString());
} catch (InvalidConfigurationException exception) {
throw new IOException("Could not copy player state", exception);
}
return next;
}
private void replace(YamlConfiguration next) throws IOException {
Files.createDirectories(statePath.getParent());
Path temporary = statePath.resolveSibling("state.yml.tmp");
Files.writeString(temporary, next.saveToString(), StandardCharsets.UTF_8);
try {
Files.move(temporary, statePath, StandardCopyOption.ATOMIC_MOVE,
StandardCopyOption.REPLACE_EXISTING);
} catch (AtomicMoveNotSupportedException exception) {
Files.move(temporary, statePath, StandardCopyOption.REPLACE_EXISTING);
}
state = next;
}
private static String receiptPath(UUID playerId, UUID source) {
return "players." + playerId + ".throwables." + source;
}
private static String temporaryPath(UUID playerId) {
return "players." + playerId + ".temporary";
}
private static String path(UUID playerId) {
return "players." + playerId + ".scale";
}
}