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