158 lines
5.9 KiB
Java
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;
|
|
}
|
|
}
|