Files
spigot-tree-feller/src/main/java/games/dmg/treefeller/YamlPlayerStateRepository.java
T

158 lines
5.9 KiB
Java

package games.dmg.treefeller;
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.util.ArrayList;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
/** UUID-keyed YAML persistence that retains fields it does not own. */
public final class YamlPlayerStateRepository implements PlayerStateCatalog {
private final Path file;
private final YamlConfiguration document;
public YamlPlayerStateRepository(Path file) {
this.file = file;
this.document = Files.exists(file)
? YamlConfiguration.loadConfiguration(file.toFile())
: new YamlConfiguration();
}
@Override
public Optional<PlayerTreeFellerState> load(UUID playerId) {
ConfigurationSection record = document.getConfigurationSection(path(playerId));
if (record == null) {
return Optional.empty();
}
try {
return Optional.of(read(playerId, record));
} catch (IllegalArgumentException exception) {
return Optional.empty();
}
}
@Override
public List<PlayerTreeFellerState> loadAll() {
ConfigurationSection players = document.getConfigurationSection("players");
if (players == null) {
return List.of();
}
List<PlayerTreeFellerState> states = new ArrayList<>();
for (String key : players.getKeys(false)) {
try {
UUID playerId = UUID.fromString(key);
load(playerId).ifPresent(states::add);
} catch (IllegalArgumentException ignored) {
// Invalid records are deliberately excluded rather than trusted.
}
}
return List.copyOf(states);
}
@Override
public void save(PlayerTreeFellerState state) throws IOException {
String root = path(state.playerId());
document.set(root + ".latest-name", state.latestName());
document.set(root + ".known-names", new ArrayList<>(state.knownNames()));
document.set(root + ".enabled", state.enabled());
document.set(root + ".locked", state.locked());
for (TreeSpecies species : TreeSpecies.values()) {
document.set(root + ".progress." + species.id(), state.progress(species));
}
document.set(
root + ".unlocked",
state.unlocked().stream().map(TreeSpecies::id).sorted().toList());
writeAtomically();
}
private PlayerTreeFellerState read(UUID playerId, ConfigurationSection record) {
String latestName = record.getString("latest-name");
if (latestName == null || latestName.isBlank()) {
throw new IllegalArgumentException("missing latest name");
}
if (!record.isBoolean("enabled") || !record.isBoolean("locked")) {
throw new IllegalArgumentException("invalid player flags");
}
List<?> rawNames = record.getList("known-names");
if (rawNames == null) {
throw new IllegalArgumentException("missing known names");
}
LinkedHashSet<String> names = new LinkedHashSet<>();
for (Object rawName : rawNames) {
if (!(rawName instanceof String name) || name.isBlank()) {
throw new IllegalArgumentException("invalid known name");
}
names.add(name);
}
names.add(latestName);
EnumMap<TreeSpecies, Long> progress = new EnumMap<>(TreeSpecies.class);
ConfigurationSection progressSection = record.getConfigurationSection("progress");
for (TreeSpecies species : TreeSpecies.values()) {
long value = progressSection == null ? 0L : progressSection.getLong(species.id(), 0L);
if (value < 0) {
throw new IllegalArgumentException("negative progress");
}
progress.put(species, value);
}
List<?> rawUnlocks = record.getList("unlocked");
if (rawUnlocks == null) {
throw new IllegalArgumentException("missing unlocks");
}
Set<TreeSpecies> unlocked = EnumSet.noneOf(TreeSpecies.class);
for (Object rawUnlock : rawUnlocks) {
if (!(rawUnlock instanceof String identifier)) {
throw new IllegalArgumentException("invalid unlock");
}
TreeSpecies species = TreeSpecies.fromId(identifier)
.orElseThrow(() -> new IllegalArgumentException("unknown unlock"));
unlocked.add(species);
}
return new PlayerTreeFellerState(
playerId,
latestName,
names,
record.getBoolean("enabled"),
record.getBoolean("locked"),
progress,
unlocked);
}
private void writeAtomically() throws IOException {
Path parent = file.toAbsolutePath().getParent();
if (parent != null) {
Files.createDirectories(parent);
}
Path temporary = file.resolveSibling(file.getFileName() + ".tmp");
Files.writeString(temporary, document.saveToString(), StandardCharsets.UTF_8);
try {
Files.move(
temporary,
file,
StandardCopyOption.ATOMIC_MOVE,
StandardCopyOption.REPLACE_EXISTING);
} catch (AtomicMoveNotSupportedException exception) {
Files.move(temporary, file, StandardCopyOption.REPLACE_EXISTING);
}
}
private String path(UUID playerId) {
return "players." + playerId;
}
}