Data Storage
Dokumentace k ukládání dat hráčů v Hytale.
---
PlayerStorage Interface
Z dekompilovaného kódu:
public interface PlayerStorage {
@Nonnull
CompletableFuture> load(@Nonnull UUID uuid); @Nonnull
CompletableFuture save(@Nonnull UUID uuid, @Nonnull Holder holder);
@Nonnull
CompletableFuture remove(@Nonnull UUID uuid);
@Nonnull
Set getPlayers() throws IOException;
}
Klíčové Metody
| Metoda | Popis |
|--------|-------|
| load(uuid) | Async načtení dat hráče jako Holder
| save(uuid, holder) | Async uložení dat hráče |
| remove(uuid) | Async smazání dat hráče |
| getPlayers() | Synchronní získání všech UUID uložených hráčů |
---
PlayerStorageProvider
public interface PlayerStorageProvider {
BuilderCodecMapCodec CODEC =
new BuilderCodecMapCodec<>("Type", true); PlayerStorage getPlayerStorage();
}
Vestavěné Providery
#### DefaultPlayerStorageProvider
public class DefaultPlayerStorageProvider implements PlayerStorageProvider {
public static final DefaultPlayerStorageProvider INSTANCE = new DefaultPlayerStorageProvider();
public static final String ID = "Hytale";
public static final DiskPlayerStorageProvider DEFAULT = new DiskPlayerStorageProvider(); @Override
public PlayerStorage getPlayerStorage() {
return DEFAULT.getPlayerStorage();
}
}
#### DiskPlayerStorageProvider
public class DiskPlayerStorageProvider implements PlayerStorageProvider {
public static final String ID = "Disk";
private Path path = Constants.UNIVERSE_PATH.resolve("players"); public static final BuilderCodec CODEC =
BuilderCodec.builder(DiskPlayerStorageProvider.class, DiskPlayerStorageProvider::new)
.append(new KeyedCodec<>("Path", Codec.STRING),
(o, s) -> o.path = PathUtil.get(s),
o -> o.path.toString())
.add()
.build();
@Override
public PlayerStorage getPlayerStorage() {
return new DiskPlayerStorage(this.path);
}
}
---
DiskPlayerStorage
Z dekompilovaného kódu - implementace pro ukládání na disk:
public static class DiskPlayerStorage implements PlayerStorage {
public static final String FILE_EXTENSION = ".json";
private final Path path; public DiskPlayerStorage(@Nonnull Path path) {
this.path = path;
if (!Options.getOptionSet().has(Options.BARE)) {
try {
Files.createDirectories(path);
} catch (IOException e) {
throw new RuntimeException("Failed to create players directory", e);
}
}
}
@Override
public CompletableFuture> load(@Nonnull UUID uuid) {
Path file = this.path.resolve(uuid + ".json");
return BsonUtil.readDocument(file).thenApply(bsonDocument -> {
if (bsonDocument == null) {
bsonDocument = new BsonDocument();
}
return EntityStore.REGISTRY.deserialize(bsonDocument);
});
}
@Override
public CompletableFuture save(@Nonnull UUID uuid, @Nonnull Holder holder) {
Path file = this.path.resolve(uuid + ".json");
BsonDocument document = EntityStore.REGISTRY.serialize(holder);
return BsonUtil.writeDocument(file, document);
}
@Override
public CompletableFuture remove(@Nonnull UUID uuid) {
Path file = this.path.resolve(uuid + ".json");
try {
Files.deleteIfExists(file);
return CompletableFuture.completedFuture(null);
} catch (IOException e) {
return CompletableFuture.failedFuture(e);
}
}
@Override
public Set getPlayers() throws IOException {
try (Stream stream = Files.list(this.path)) {
return stream
.map(p -> {
String fileName = p.getFileName().toString();
if (!fileName.endsWith(".json")) return null;
try {
return UUID.fromString(fileName.substring(0, fileName.length() - 5));
} catch (IllegalArgumentException e) {
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toSet());
}
}
}
---
Holder - ECS Kontejner
Holder je kontejner pro všechny komponenty entity:
public class Holder {
private Archetype archetype;
private Component[] components;
private final StampedLock lock = new StampedLock(); // Získání komponenty
public > T getComponent(
@Nonnull ComponentType componentType
) {
long stamp = this.lock.readLock();
try {
if (this.archetype.contains(componentType)) {
return (T) this.components[componentType.getIndex()];
}
return null;
} finally {
this.lock.unlockRead(stamp);
}
}
// Přidání komponenty
public > void addComponent(
@Nonnull ComponentType componentType,
@Nonnull T component
) {
long stamp = this.lock.writeLock();
try {
if (this.archetype.contains(componentType)) {
throw new IllegalArgumentException("Entity contains component type");
}
this.archetype = Archetype.add(this.archetype, componentType);
this.components[componentType.getIndex()] = component;
} finally {
this.lock.unlockWrite(stamp);
}
}
// Zajištění a získání komponenty
public > T ensureAndGetComponent(
@Nonnull ComponentType componentType
) {
this.ensureComponent(componentType);
return this.getComponent(componentType);
}
// Klonování
public Holder clone() {
long stamp = this.lock.readLock();
try {
Component[] componentsClone = new Component[this.components.length];
for (int i = 0; i < this.components.length; i++) {
Component component = this.components[i];
if (component != null) {
componentsClone[i] = component.clone();
}
}
return this.registry.newHolder(this.archetype, componentsClone);
} finally {
this.lock.unlockRead(stamp);
}
}
}
---
Serializace/Deserializace
ComponentRegistry
public class ComponentRegistry {
// Deserializace z BsonDocument
public Holder deserialize(@Nonnull BsonDocument entityDocument) {
ExtraInfo extraInfo = ExtraInfo.THREAD_LOCAL.get();
Holder holder = this.data.getEntityCodec().decode(entityDocument, extraInfo);
extraInfo.getValidationResults().logOrThrowValidatorExceptions(LOGGER);
return holder;
} // Serializace do BsonDocument
public BsonDocument serialize(@Nonnull Holder holder) {
ExtraInfo extraInfo = ExtraInfo.THREAD_LOCAL.get();
BsonDocument document = this.data.getEntityCodec().encode(holder, extraInfo).asDocument();
extraInfo.getValidationResults().logOrThrowValidatorExceptions(LOGGER);
return document;
}
}
Použití v Universe
// Získání PlayerStorage
PlayerStorage storage = Universe.get().getPlayerStorage();// Načtení dat hráče
CompletableFuture> future = storage.load(playerUuid);
future.thenAccept(holder -> {
// holder obsahuje všechny komponenty hráče
});
// Uložení dat hráče
storage.save(playerUuid, holder);
---
Player.saveConfig()
Z dekompilovaného kódu - metoda pro uložení konfigurace hráče:
public CompletableFuture saveConfig(@Nonnull World world, @Nonnull Holder holder) {
// Uložení movement states před uložením
MovementStatesComponent movementStatesComponent = holder.getComponent(MovementStatesComponent.getComponentType());
UUIDComponent uuidComponent = holder.getComponent(UUIDComponent.getComponentType()); this.data.getPerWorldData(world.getName())
.setLastMovementStates(movementStatesComponent.getMovementStates(), false);
// Uložení přes Universe PlayerStorage
return Universe.get().getPlayerStorage().save(uuidComponent.getUuid(), holder);
}
---
Vlastní PlayerStorageProvider
Implementace
public class MyDatabaseStorageProvider implements PlayerStorageProvider {
public static final String ID = "MyDatabase"; public static final BuilderCodec CODEC =
BuilderCodec.builder(MyDatabaseStorageProvider.class, MyDatabaseStorageProvider::new)
.append(new KeyedCodec<>("ConnectionString", Codec.STRING),
(o, s) -> o.connectionString = s,
o -> o.connectionString)
.add()
.build();
private String connectionString;
private DatabaseConnection connection;
@Override
public PlayerStorage getPlayerStorage() {
return new MyDatabaseStorage(connection);
}
public class MyDatabaseStorage implements PlayerStorage {
private final DatabaseConnection connection;
public MyDatabaseStorage(DatabaseConnection connection) {
this.connection = connection;
}
@Override
public CompletableFuture> load(@Nonnull UUID uuid) {
return CompletableFuture.supplyAsync(() -> {
byte[] data = connection.loadPlayer(uuid);
if (data == null || data.length == 0) {
return EntityStore.REGISTRY.deserialize(new BsonDocument());
}
BsonDocument doc = BsonUtil.readFromBytes(data);
return EntityStore.REGISTRY.deserialize(doc);
});
}
@Override
public CompletableFuture save(@Nonnull UUID uuid, @Nonnull Holder holder) {
return CompletableFuture.runAsync(() -> {
BsonDocument doc = EntityStore.REGISTRY.serialize(holder);
byte[] data = BsonUtil.writeToBytes(doc);
connection.savePlayer(uuid, data);
});
}
@Override
public CompletableFuture remove(@Nonnull UUID uuid) {
return CompletableFuture.runAsync(() -> {
connection.deletePlayer(uuid);
});
}
@Override
public Set getPlayers() throws IOException {
return connection.getAllPlayerUuids();
}
}
}
Registrace Providera
// V BSON konfiguraci
// universe.json nebo podobně:
{
"PlayerStorageProvider": {
"Type": "MyDatabase",
"ConnectionString": "jdbc:mysql://localhost/hytale"
}
}
---
Struktura Uloženého Souboru
Příklad: players/{uuid}.json
{
"Components": {
"Player": {
"PlayerData": {
"BlockIdVersion": 1,
"World": "overworld",
"KnownRecipes": ["stone_pickaxe", "wooden_sword"],
"PerWorldData": {
"overworld": {
"LastPosition": {"X": 100.5, "Y": 64.0, "Z": 200.5},
"LastRotation": {"X": 0.0, "Y": 90.0, "Z": 0.0},
"RespawnPoints": []
}
}
},
"GameMode": "Survival",
"HotbarManager": {
"SelectedSlot": 0
}
},
"LivingEntity": {
"Health": 20.0,
"MaxHealth": 20.0,
"Inventory": {
"Slots": [...]
}
},
"Transform": {
"Position": {"X": 100.5, "Y": 64.0, "Z": 200.5},
"Rotation": {"X": 0.0, "Y": 90.0, "Z": 0.0}
}
}
}
---
Shrnutí
| Třída | Účel |
|-------|------|
| PlayerStorage | Interface pro načítání/ukládání dat hráčů |
| PlayerStorageProvider | Factory pro vytváření PlayerStorage |
| DiskPlayerStorageProvider | Vestavěný provider pro disk |
| Holder | Kontejner pro všechny komponenty |
| ComponentRegistry | Serializace/deserializace |
| Operace | Metoda |
|---------|--------|
| Získání storage | Universe.get().getPlayerStorage() |
| Načtení hráče | storage.load(uuid) |
| Uložení hráče | storage.save(uuid, holder) |
| Smazání hráče | storage.remove(uuid) |
| Seznam hráčů | storage.getPlayers() |