Persistence
Dokumentace k perzistenci dat v Hytale - BsonUtil, ukládání a načítání.
---
BsonUtil
Utility třída pro práci s BSON dokumenty:
Zápis a Čtení Dokumentů
public class BsonUtil {
// Async čtení JSON dokumentu
public static CompletableFuture readDocument(@Nonnull Path file) {
return readDocument(file, true);
} public static CompletableFuture readDocument(@Nonnull Path file, boolean backup) {
BasicFileAttributes attributes;
try {
attributes = Files.readAttributes(file, BasicFileAttributes.class);
} catch (IOException e) {
// Soubor neexistuje - zkus backup
if (backup) {
return readDocumentBak(file);
}
return CompletableFuture.completedFuture(null);
}
if (attributes.size() == 0L) {
LOGGER.at(Level.WARNING).log("Error loading file %s, file was empty", file);
return backup ? readDocumentBak(file) : CompletableFuture.completedFuture(null);
}
CompletableFuture future = CompletableFuture
.supplyAsync(() -> Files.readString(file))
.thenApply(BsonDocument::parse);
return backup ? future.exceptionallyCompose(t -> readDocumentBak(file)) : future;
}
// Async zápis dokumentu (s automatickou zálohou)
public static CompletableFuture writeDocument(@Nonnull Path file, BsonDocument document) {
return writeDocument(file, document, true);
}
}
Synchronní Operace
// Synchronní čtení (blokující)
public static BsonDocument readDocumentNow(@Nonnull Path file) {
BasicFileAttributes attributes;
try {
attributes = Files.readAttributes(file, BasicFileAttributes.class);
} catch (IOException e) {
return null;
} if (attributes.size() == 0L) {
return null;
}
try {
String contents = Files.readString(file);
return BsonDocument.parse(contents);
} catch (IOException e) {
return null;
}
}
// Synchronní zápis s codecem
public static void writeSync(
@Nonnull Path path,
@Nonnull Codec codec,
T value,
@Nonnull HytaleLogger logger
) throws IOException {
// Vytvoř parent složku
Path parent = PathUtil.getParent(path);
if (!Files.exists(parent)) {
Files.createDirectories(parent);
}
// Vytvoř zálohu existujícího souboru
if (Files.isRegularFile(path)) {
Path backup = path.resolveSibling(path.getFileName() + ".bak");
Files.move(path, backup, StandardCopyOption.REPLACE_EXISTING);
}
// Encode a zapiš
ExtraInfo extraInfo = ExtraInfo.THREAD_LOCAL.get();
BsonValue bsonValue = codec.encode(value, extraInfo);
extraInfo.getValidationResults().logOrThrowValidatorExceptions(logger);
BsonDocument document = bsonValue.asDocument();
try (BufferedWriter writer = Files.newBufferedWriter(path, CREATE, WRITE)) {
BSON_DOCUMENT_CODEC.encode(new JsonWriter(writer, SETTINGS), document, encoderContext);
}
}
Binární Operace
// Zápis do byte[]
public static byte[] writeToBytes(@Nullable BsonDocument document) {
if (document == null) {
return ArrayUtil.EMPTY_BYTE_ARRAY;
} try (BasicOutputBuffer buffer = new BasicOutputBuffer()) {
codec.encode(new BsonBinaryWriter(buffer), document, encoderContext);
return buffer.toByteArray();
}
}
// Čtení z byte[]
public static BsonDocument readFromBytes(@Nullable byte[] buf) {
if (buf == null || buf.length == 0) {
return null;
}
return codec.decode(new BsonBinaryReader(ByteBuffer.wrap(buf)), decoderContext);
}
// Pro síťový přenos
public static BsonDocument readFromBinaryStream(@Nonnull ByteBuf buf) {
return readFromBytes(ByteBufUtil.readByteArray(buf));
}
public static void writeToBinaryStream(@Nonnull ByteBuf buf, BsonDocument doc) {
ByteBufUtil.writeByteArray(buf, writeToBytes(doc));
}
---
Backup Systém
Automatická Záloha
// BsonUtil automaticky vytváří .bak soubory před přepsáním
// Při selhání čtení se automaticky pokusí načíst zálohupublic static CompletableFuture readDocumentBak(@Nonnull Path fileOrig) {
Path file = fileOrig.resolveSibling(fileOrig.getFileName() + ".bak");
BasicFileAttributes attributes;
try {
attributes = Files.readAttributes(file, BasicFileAttributes.class);
} catch (IOException e) {
return CompletableFuture.completedFuture(null);
}
if (attributes.size() == 0L) {
LOGGER.at(Level.WARNING).log("Backup file %s was empty", file);
return CompletableFuture.completedFuture(null);
}
LOGGER.at(Level.WARNING).log("Loading backup file %s for %s!", file, fileOrig);
return CompletableFuture
.supplyAsync(() -> Files.readString(file))
.thenApply(BsonDocument::parse);
}
Manuální Backup
public class BackupManager {
private final Path backupFolder; public BackupManager(Path dataFolder) {
this.backupFolder = dataFolder.resolve("backups");
}
public CompletableFuture createBackup(Path file, String reason) {
return CompletableFuture.runAsync(() -> {
try {
Files.createDirectories(backupFolder);
String timestamp = DateTimeFormatter
.ofPattern("yyyy-MM-dd_HH-mm-ss")
.format(LocalDateTime.now());
String backupName = file.getFileName() + "." + timestamp + "." + reason + ".bak";
Path backupPath = backupFolder.resolve(backupName);
Files.copy(file, backupPath, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
throw new RuntimeException("Failed to create backup", e);
}
});
}
public CompletableFuture restoreBackup(Path backupFile, Path targetFile) {
return CompletableFuture.runAsync(() -> {
try {
Files.copy(backupFile, targetFile, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
throw new RuntimeException("Failed to restore backup", e);
}
});
}
}
---
PlayerData Serializace
Třída s Codec
public class PlayerData {
public static final BuilderCodec CODEC; private UUID uuid;
private int kills;
private int deaths;
private long playTime;
private long firstJoin;
private long lastJoin;
private Map statistics = new HashMap<>();
static {
BuilderCodec.Builder builder = BuilderCodec.builder(
PlayerData.class,
PlayerData::new
);
builder.append(
new KeyedCodec<>("Uuid", Codec.UUID_STRING),
PlayerData::setUuid,
PlayerData::getUuid
).add();
builder.append(
new KeyedCodec<>("Kills", Codec.INTEGER),
PlayerData::setKills,
PlayerData::getKills
).defaultValue(0).add();
builder.append(
new KeyedCodec<>("Deaths", Codec.INTEGER),
PlayerData::setDeaths,
PlayerData::getDeaths
).defaultValue(0).add();
builder.append(
new KeyedCodec<>("PlayTime", Codec.LONG),
PlayerData::setPlayTime,
PlayerData::getPlayTime
).defaultValue(0L).add();
builder.append(
new KeyedCodec<>("FirstJoin", Codec.LONG),
PlayerData::setFirstJoin,
PlayerData::getFirstJoin
).add();
builder.append(
new KeyedCodec<>("LastJoin", Codec.LONG),
PlayerData::setLastJoin,
PlayerData::getLastJoin
).add();
builder.append(
new KeyedCodec<>("Statistics", new MapCodec<>(Codec.INTEGER, HashMap::new, false)),
PlayerData::setStatistics,
PlayerData::getStatistics
).add();
CODEC = builder.build();
}
// Gettery a settery...
}
Použití Codecu pro Ukládání
public class PlayerDataPersistence {
private final Path dataFolder; public PlayerDataPersistence(Path dataFolder) {
this.dataFolder = dataFolder;
}
public CompletableFuture save(UUID uuid, PlayerData data) {
return CompletableFuture.runAsync(() -> {
Path file = dataFolder.resolve(uuid + ".json");
ExtraInfo extraInfo = ExtraInfo.THREAD_LOCAL.get();
BsonDocument doc = PlayerData.CODEC.encode(data, extraInfo).asDocument();
extraInfo.getValidationResults().logOrThrowValidatorExceptions(LOGGER);
BsonUtil.writeDocument(file, doc).join();
});
}
public CompletableFuture load(UUID uuid) {
Path file = dataFolder.resolve(uuid + ".json");
return BsonUtil.readDocument(file).thenApply(doc -> {
if (doc == null) {
// Nový hráč
PlayerData data = new PlayerData();
data.setUuid(uuid);
data.setFirstJoin(System.currentTimeMillis());
data.setLastJoin(System.currentTimeMillis());
return data;
}
ExtraInfo extraInfo = ExtraInfo.THREAD_LOCAL.get();
PlayerData data = PlayerData.CODEC.decode(doc, extraInfo);
extraInfo.getValidationResults().logOrThrowValidatorExceptions(LOGGER);
data.setLastJoin(System.currentTimeMillis());
return data;
});
}
}
---
Thread-Safe Save Pattern
Z TeleportPlugin - Vzor pro Bezpečné Ukládání
public class SafeSaveManager {
private final ReentrantLock saveLock = new ReentrantLock();
private final Path filePath;
private final BuilderCodec codec;
private final HytaleLogger logger;
private final Supplier defaultSupplier; private volatile T currentData;
private volatile boolean dirty;
public SafeSaveManager(
Path filePath,
BuilderCodec codec,
HytaleLogger logger,
Supplier defaultSupplier
) {
this.filePath = filePath;
this.codec = codec;
this.logger = logger;
this.defaultSupplier = defaultSupplier;
}
public void load() {
BsonDocument doc = BsonUtil.readDocumentNow(filePath);
if (doc == null) {
currentData = defaultSupplier.get();
} else {
ExtraInfo extraInfo = ExtraInfo.THREAD_LOCAL.get();
currentData = codec.decode(doc, extraInfo);
extraInfo.getValidationResults().logOrThrowValidatorExceptions(logger);
}
}
public void save() {
if (!dirty) return;
saveLock.lock();
try {
if (!dirty) return;
T dataToSave = currentData;
ExtraInfo extraInfo = ExtraInfo.THREAD_LOCAL.get();
BsonDocument doc = codec.encode(dataToSave, extraInfo).asDocument();
extraInfo.getValidationResults().logOrThrowValidatorExceptions(logger);
BsonUtil.writeDocument(filePath, doc).join();
dirty = false;
} finally {
saveLock.unlock();
}
}
public void modify(Consumer modifier) {
modifier.accept(currentData);
dirty = true;
}
public T getData() {
return currentData;
}
public boolean isDirty() {
return dirty;
}
}
---
Periodické Auto-Save
public class AutoSaveManager {
private final ScheduledExecutorService executor;
private final List saveCallbacks = new CopyOnWriteArrayList<>(); public AutoSaveManager(Duration interval) {
this.executor = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "AutoSave-Thread");
t.setDaemon(true);
return t;
});
executor.scheduleAtFixedRate(
this::saveAll,
interval.toSeconds(),
interval.toSeconds(),
TimeUnit.SECONDS
);
}
public void register(Runnable saveCallback) {
saveCallbacks.add(saveCallback);
}
public void unregister(Runnable saveCallback) {
saveCallbacks.remove(saveCallback);
}
private void saveAll() {
for (Runnable callback : saveCallbacks) {
try {
callback.run();
} catch (Exception e) {
LOGGER.at(Level.SEVERE).withCause(e).log("Auto-save failed:");
}
}
}
public void shutdown() {
// Finální uložení před shutdown
saveAll();
executor.shutdown();
try {
if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
---
Plugin Lifecycle Integration
public class MyPlugin extends JavaPlugin {
private AutoSaveManager autoSaveManager;
private PlayerDataPersistence persistence;
private final Map playerData = new ConcurrentHashMap<>(); @Override
protected void setup() {
instance = this;
// Inicializace persistence
persistence = new PlayerDataPersistence(getDataDirectory().resolve("players"));
// Auto-save každých 5 minut
autoSaveManager = new AutoSaveManager(Duration.ofMinutes(5));
autoSaveManager.register(this::saveAllPlayers);
// Eventy
getEventRegistry().registerGlobal(PlayerReadyEvent.class, this::onPlayerReady);
getEventRegistry().register(PlayerDisconnectEvent.class, this::onPlayerDisconnect);
getEventRegistry().registerGlobal(AllWorldsLoadedEvent.class, e -> loadGlobalData());
}
@Override
protected void shutdown() {
getLogger().atInfo().log("Shutting down...");
// Zastav auto-save a proveď finální uložení
autoSaveManager.shutdown();
// Synchronní uložení všech dat
saveAllPlayersSync();
getLogger().atInfo().log("Shutdown complete");
}
private void onPlayerReady(PlayerReadyEvent event) {
Player player = event.getPlayer();
UUID uuid = player.getUuid();
persistence.load(uuid).thenAccept(data -> {
playerData.put(uuid, data);
// Aplikuj data na world thread
player.getWorld().execute(() -> {
applyPlayerData(player, data);
});
});
}
private void onPlayerDisconnect(PlayerDisconnectEvent event) {
Player player = event.getPlayer();
UUID uuid = player.getUuid();
PlayerData data = playerData.remove(uuid);
if (data != null) {
updatePlayerData(player, data);
persistence.save(uuid, data);
}
}
private void saveAllPlayers() {
for (Map.Entry entry : playerData.entrySet()) {
persistence.save(entry.getKey(), entry.getValue());
}
}
private void saveAllPlayersSync() {
List> futures = new ArrayList<>();
for (Map.Entry entry : playerData.entrySet()) {
futures.add(persistence.save(entry.getKey(), entry.getValue()));
}
CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)).join();
}
}
---
Shrnutí
| Metoda | Účel |
|--------|------|
| BsonUtil.readDocument(path) | Async čtení s backup fallback |
| BsonUtil.writeDocument(path, doc) | Async zápis s automatickou zálohou |
| BsonUtil.readDocumentNow(path) | Synchronní čtení |
| BsonUtil.writeSync(path, codec, value) | Synchronní zápis s codecem |
| BsonUtil.writeToBytes(doc) | Serializace do byte[] |
| BsonUtil.readFromBytes(bytes) | Deserializace z byte[] |
| Pattern | Kdy Použít |
|---------|-----------|
| Async save | Normální operace - neblokuje |
| Sync save | Shutdown - musí dokončit |
| Backup | Automaticky při přepsání souboru |
| Auto-save | Periodické ukládání dirty dat |
| ReentrantLock | Thread-safe save operace |