HyCodeYourTale

Persistence

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álohu

public 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 |

Last updated: 20. ledna 2026