HyCodeYourTale

Caching

Caching

Dokumentace k cachování dat hráčů v Hytale.

---

PlayerConfigData

Z dekompilovaného kódu - hlavní třída pro konfigurační data hráče:

public final class PlayerConfigData {
// Dirty tracking
private final transient AtomicBoolean hasChanged = new AtomicBoolean();

// Per-world data cache
private Map perWorldData = new ConcurrentHashMap<>();

// Známé recepty
private Set knownRecipes = new HashSet<>();

// Reputation data
private Object2IntMap reputationData = new Object2IntOpenHashMap();

// Aktivní úkoly
private Set activeObjectiveUUIDs = ConcurrentHashMap.newKeySet();

// Pozice - cachované pro rychlý přístup
public final Vector3d lastSavedPosition = new Vector3d();
public final Vector3f lastSavedRotation = new Vector3f();
}

Dirty Tracking

// Označení změny
public void markChanged() {
this.hasChanged.set(true);
}

// Konzumace dirty flagu
public boolean consumeHasChanged() {
return this.hasChanged.getAndSet(false);
}

// Použití v Player
@Override
public void markNeedsSave() {
this.data.markChanged();
}

---

Per-World Data

PlayerWorldData

// Získání per-world dat (automaticky vytvoří pokud neexistuje)
public PlayerWorldData getPerWorldData(@Nonnull String worldName) {
return this.perWorldData.computeIfAbsent(worldName, s -> new PlayerWorldData(this));
}

Cleanup Instance Světů

public void cleanup(@Nonnull Universe universe) {
Set keySet = this.perWorldData.keySet();
Iterator iterator = keySet.iterator();

while (iterator.hasNext()) {
String worldName = iterator.next();
// Odstraň data instančních světů které už neexistují
if (worldName.startsWith("instance-") && universe.getWorld(worldName) == null) {
iterator.remove();
}
}
}

---

Cache Pattern pro Plugin

Základní Implementace

public class PlayerDataManager {
private final Map cache = new ConcurrentHashMap<>();
private final Path dataFolder;

public PlayerDataManager(Path dataFolder) {
this.dataFolder = dataFolder;
}

// Získej z cache nebo null
@Nullable
public PlayerData getFromCache(UUID uuid) {
return cache.get(uuid);
}

// Načti s cache-aside pattern
public CompletableFuture load(UUID uuid) {
return CompletableFuture.supplyAsync(() -> {
// Check cache first
PlayerData cached = cache.get(uuid);
if (cached != null) {
return cached;
}

// Load from storage
Path file = dataFolder.resolve(uuid + ".json");
if (Files.exists(file)) {
BsonDocument doc = BsonUtil.readDocumentNow(file);
if (doc != null) {
PlayerData data = PlayerData.fromBson(doc);
cache.put(uuid, data);
return data;
}
}

// Create new
PlayerData data = new PlayerData(uuid);
cache.put(uuid, data);
return data;
});
}

// Ulož a ponech v cache
public CompletableFuture save(UUID uuid) {
PlayerData data = cache.get(uuid);
if (data == null) {
return CompletableFuture.completedFuture(null);
}

return CompletableFuture.runAsync(() -> {
Path file = dataFolder.resolve(uuid + ".json");
BsonDocument doc = data.toBson();
BsonUtil.writeDocument(file, doc).join();
});
}

// Odeber z cache (po odpojení)
public void unload(UUID uuid) {
cache.remove(uuid);
}

// Ulož a odeber z cache
public CompletableFuture saveAndUnload(UUID uuid) {
return save(uuid).thenRun(() -> unload(uuid));
}

// Ulož všechny v cache
public CompletableFuture saveAll() {
List> futures = new ArrayList<>();
for (UUID uuid : cache.keySet()) {
futures.add(save(uuid));
}
return CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new));
}
}

---

Thread-Safe Cache Operace

ConcurrentHashMap Best Practices

public class SafePlayerCache {
private final ConcurrentHashMap cache = new ConcurrentHashMap<>();

// Atomické vložení
public PlayerData getOrCreate(UUID uuid) {
return cache.computeIfAbsent(uuid, PlayerData::new);
}

// Atomická aktualizace
public void updateStats(UUID uuid, int killsToAdd) {
cache.computeIfPresent(uuid, (key, data) -> {
data.addKills(killsToAdd);
return data;
});
}

// Merge operace
public PlayerData merge(UUID uuid, PlayerData newData) {
return cache.merge(uuid, newData, (existing, incoming) -> {
existing.merge(incoming);
return existing;
});
}
}

Read-Write Lock pro Komplexní Operace

public class LockingPlayerCache {
private final Map cache = new HashMap<>();
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

public PlayerData get(UUID uuid) {
lock.readLock().lock();
try {
return cache.get(uuid);
} finally {
lock.readLock().unlock();
}
}

public void put(UUID uuid, PlayerData data) {
lock.writeLock().lock();
try {
cache.put(uuid, data);
} finally {
lock.writeLock().unlock();
}
}

public void processAll(Consumer processor) {
lock.readLock().lock();
try {
for (PlayerData data : cache.values()) {
processor.accept(data);
}
} finally {
lock.readLock().unlock();
}
}
}

---

ECS Komponenta jako Cache

Runtime Data jako Komponenta

public class PlayerStatsComponent implements Component {
private int kills;
private int deaths;
private long sessionStartTime;
private boolean dirty;

public void incrementKills() {
this.kills++;
this.dirty = true;
}

public void incrementDeaths() {
this.deaths++;
this.dirty = true;
}

public boolean consumeDirty() {
boolean was = dirty;
dirty = false;
return was;
}

@Override
public Component clone() {
PlayerStatsComponent copy = new PlayerStatsComponent();
copy.kills = this.kills;
copy.deaths = this.deaths;
copy.sessionStartTime = this.sessionStartTime;
copy.dirty = this.dirty;
return copy;
}
}

Registrace a Použití

@Override
protected void setup() {
// Registrace komponenty
this.playerStatsType = getEntityStoreRegistry().registerComponent(
PlayerStatsComponent.class,
PlayerStatsComponent::new
);

// Event při připojení - načti data do komponenty
getEventRegistry().registerGlobal(PlayerReadyEvent.class, event -> {
Player player = event.getPlayer();
loadPlayerStats(player);
});

// Event při odpojení - ulož data z komponenty
getEventRegistry().register(PlayerDisconnectEvent.class, event -> {
Player player = event.getPlayer();
savePlayerStats(player);
});
}

private void loadPlayerStats(Player player) {
UUID uuid = player.getUuid();

// Async načtení
dataManager.load(uuid).thenAccept(data -> {
// Sync aplikace na world thread
player.getWorld().execute(() -> {
Ref ref = player.getRef();
Store store = ref.getStore();

PlayerStatsComponent stats = store.ensureAndGetComponent(ref, playerStatsType);
stats.setKills(data.getKills());
stats.setDeaths(data.getDeaths());
stats.setSessionStartTime(System.currentTimeMillis());
});
});
}

private void savePlayerStats(Player player) {
Ref ref = player.getRef();
Store store = ref.getStore();
PlayerStatsComponent stats = store.getComponent(ref, playerStatsType);

if (stats != null && stats.consumeDirty()) {
PlayerData data = dataManager.getFromCache(player.getUuid());
if (data != null) {
data.setKills(stats.getKills());
data.setDeaths(stats.getDeaths());
data.addPlayTime(System.currentTimeMillis() - stats.getSessionStartTime());
}

dataManager.saveAndUnload(player.getUuid());
}
}

---

Lazy Loading Pattern

public class LazyPlayerData {
private final UUID uuid;
private volatile PlayerData data;
private volatile CompletableFuture loadingFuture;
private final Object lock = new Object();

public LazyPlayerData(UUID uuid) {
this.uuid = uuid;
}

public CompletableFuture get() {
if (data != null) {
return CompletableFuture.completedFuture(data);
}

synchronized (lock) {
if (data != null) {
return CompletableFuture.completedFuture(data);
}

if (loadingFuture != null) {
return loadingFuture;
}

loadingFuture = loadFromStorage();
loadingFuture.thenAccept(loaded -> {
this.data = loaded;
this.loadingFuture = null;
});

return loadingFuture;
}
}

private CompletableFuture loadFromStorage() {
return CompletableFuture.supplyAsync(() -> {
// Load from disk/database
return PlayerData.load(uuid);
});
}

public boolean isLoaded() {
return data != null;
}

@Nullable
public PlayerData getIfLoaded() {
return data;
}
}

---

Cache Expiration

public class ExpiringPlayerCache {
private final Map cache = new ConcurrentHashMap<>();
private final Duration expiration;
private final ScheduledExecutorService executor;

public ExpiringPlayerCache(Duration expiration) {
this.expiration = expiration;
this.executor = Executors.newSingleThreadScheduledExecutor();

// Periodický cleanup
executor.scheduleAtFixedRate(
this::cleanupExpired,
expiration.toMinutes(),
expiration.toMinutes() / 2,
TimeUnit.MINUTES
);
}

public void put(UUID uuid, PlayerData data) {
cache.put(uuid, new CacheEntry(data, Instant.now()));
}

@Nullable
public PlayerData get(UUID uuid) {
CacheEntry entry = cache.get(uuid);
if (entry == null) return null;

if (entry.isExpired(expiration)) {
cache.remove(uuid);
return null;
}

entry.touch(); // Refresh access time
return entry.data;
}

private void cleanupExpired() {
Instant now = Instant.now();
cache.entrySet().removeIf(entry ->
entry.getValue().isExpired(expiration)
);
}

public void shutdown() {
executor.shutdown();
}

private static class CacheEntry {
final PlayerData data;
volatile Instant lastAccess;

CacheEntry(PlayerData data, Instant lastAccess) {
this.data = data;
this.lastAccess = lastAccess;
}

void touch() {
lastAccess = Instant.now();
}

boolean isExpired(Duration expiration) {
return Duration.between(lastAccess, Instant.now()).compareTo(expiration) > 0;
}
}
}

---

Shrnutí

| Pattern | Použití |
|---------|---------|
| Cache-aside | Základní načítání s cache |
| Dirty tracking | Sledování změn pro optimalizaci ukládání |
| Lazy loading | Načtení až při prvním přístupu |
| Write-through | Okamžité ukládání při změně |
| Write-behind | Periodické ukládání změn |

| Třída | Vlastnost |
|-------|-----------|
| ConcurrentHashMap | Thread-safe, atomické operace |
| ReentrantReadWriteLock | Komplexní read/write operace |
| AtomicBoolean | Dirty tracking |
| volatile | Visibility mezi thready |

| Best Practice | Popis |
|---------------|-------|
| World.execute() | Aplikace dat na world thread |
| CompletableFuture | Async I/O operace |
| computeIfAbsent() | Atomické vytvoření při nepřítomnosti |
| consumeDirty() | Reset dirty flagu při uložení |

Last updated: 20. ledna 2026