Hytale Plugin Thread Safety Guidelines
Tento dokument obsahuje komplexní pravidla a vzory pro psaní thread-safe pluginů v Hytale.
Obsah
1. Architektura Threadingu v Hytale
2. Hlavni Pravidla
3. Vzory pro Prikazy (Commands)
4. Vzory pro Eventy
5. ECS Systemy
6. Debug Mechanismy
7. Caste Chyby
8. Checklist pro Code Review
---
Architektura Threadingu v Hytale
Rozdil oproti Minecraftu
| Minecraft | Hytale |
|-----------|--------|
| Single-threaded | Multi-threaded |
| Jeden hlavni tick thread | Kazdy World ma vlastni TickingThread |
| Vsechno na jednom vlakne | Paralelni zpracovani svetu |
| Jednodussi, ale pomalejsi | Rychlejsi, ale vyzaduje synchronizaci |
Vlakna v Hytale
Main Thread
├── World Thread (default) <- ECS operace pro "default" svet
├── World Thread (nether) <- ECS operace pro "nether" svet
├── World Thread (custom) <- ECS operace pro vlastni svety
├── Scheduler Thread <- Async tasky a nektere eventy
└── Network Thread <- Zpracovani paketu
World jako TickingThread
Kazdy World v Hytale dedi z TickingThread:
public class World extends TickingThread implements Executor {
// Kazdy svet bezi na vlastnim vlakne
// Component operace MUSI bezet na tomto vlakne
}
---
Hlavni Pravidla
Pravidlo #1: Component Operace na Spravnem Vlakne
// SPATNE - Zpusobi "Assert not in thread!" error
public void handleAsync(SomeEvent event) {
Store store = ref.getStore();
Player player = store.getComponent(ref, Player.getComponentType()); // CRASH!
}// SPRAVNE - Pouzij world.execute()
public void handleAsync(SomeEvent event) {
Player player = event.getPlayer();
World world = player.getWorld();
world.execute(() -> {
Store store = player.getRef().getStore();
Player p = store.getComponent(player.getRef(), Player.getComponentType()); // OK
});
}
Pravidlo #2: Nikdy Neblokuj World Thread
// SPATNE - Blokuje cely svet!
world.execute(() -> {
Thread.sleep(5000); // NIKDY!
database.saveAllPlayers(); // Blocking I/O
});// SPRAVNE - Async operace, pak sync vysledek
CompletableFuture.runAsync(() -> {
database.saveAllPlayers(); // Async
}).thenRun(() -> {
world.execute(() -> {
// Rychly update na world thread
});
});
Pravidlo #3: Thread-Safe Kolekce pro Sdilena Data
// SPATNE - HashMap neni thread-safe
private final Map data = new HashMap<>();// SPRAVNE - ConcurrentHashMap
private final Map data = new ConcurrentHashMap<>();
// SPRAVNE - AtomicBoolean pro flagy
private final AtomicBoolean loaded = new AtomicBoolean(false);
// SPRAVNE - ReentrantLock pro slozitejsi synchronizaci
private final ReentrantLock saveLock = new ReentrantLock();
---
Vzory pro Prikazy (Commands)
Kdyz Pouzit Jakou Tridu
| Potreba | Pouzij |
|---------|--------|
| Jednoduchy prikaz bez componentu | CommandBase |
| Prikaz potrebujici pristup ke componentum | AbstractPlayerCommand |
| Tezke/blocking operace | AbstractAsyncCommand |
| Vice sub-prikazu | AbstractCommandCollection |
| Jen konzole | CommandBase s !context.isPlayer() |
AbstractPlayerCommand (Doporuceno pro component pristup)
// DOPORUCENO pro prikazy pristupujici ke componentum
public class SafeStatsCommand extends AbstractPlayerCommand { public SafeStatsCommand() {
super("stats", "myplugin.commands.stats.desc");
}
@Override
protected void execute(
@Nonnull CommandContext context,
@Nonnull Store store,
@Nonnull Ref ref,
@Nonnull PlayerRef playerRef,
@Nonnull World world
) {
// BEZPECNE - AbstractPlayerCommand zajistuje spravny thread context
Player player = store.getComponent(ref, Player.getComponentType());
if (player != null) {
CustomComponent custom = store.getComponent(ref, CustomComponent.getComponentType());
context.sendMessage(Message.raw("Value: " + custom.getValue()));
}
}
}
CommandBase s world.execute() (Alternativa)
public class ManualSafeCommand extends CommandBase { public ManualSafeCommand() {
super("manual", "myplugin.commands.manual.desc");
}
@Override
protected void executeSync(@Nonnull CommandContext context) {
if (!context.isPlayer()) {
context.sendMessage(Message.raw("Pouze pro hrace."));
return;
}
Player player = context.senderAs(Player.class);
World world = player.getWorld();
// Rucne naplanovani na world thread
world.execute(() -> {
Ref playerRef = context.senderAsPlayerRef();
Store store = playerRef.getStore();
// Ted bezpecne
CustomComponent component = store.getComponent(playerRef, CustomComponent.getComponentType());
if (component != null) {
context.sendMessage(Message.raw("Value: " + component.getValue()));
}
});
}
}
AbstractAsyncCommand pro Tezke Operace
public class ExportCommand extends AbstractAsyncCommand { public ExportCommand() {
super("export", "myplugin.commands.export.desc");
}
@Override
protected CompletableFuture executeAsync(@Nonnull CommandContext context) {
return CompletableFuture.runAsync(() -> {
// Tezka prace (soubory, databaze...)
exportData();
// Sync zpet pro component pristup
Player player = context.senderAs(Player.class);
player.getWorld().execute(() -> {
Ref ref = context.senderAsPlayerRef();
Store store = ref.getStore();
// Bezpecny pristup ke componentum
});
context.sendMessage(Message.raw("Export dokoncen."));
});
}
}
---
Vzory pro Eventy
Typy Registrace Eventu
| Metoda | Thread | Pouziti |
|--------|--------|---------|
| register() | World thread | Sync eventy, component pristup bezpecny |
| registerAsync() | Async thread | Async eventy, BEZ primeho component pristupu |
| registerAsyncGlobal() | Async thread | Async eventy, globalni (vsechny svety) |
| registerGlobal() | World thread | Sync eventy, globalni |
Sync Eventy (Bezpecne)
@Override
protected void setup() {
// PlayerReadyEvent je synchronni - bezpecny pristup
getEventRegistry().registerGlobal(PlayerReadyEvent.class, event -> {
Player player = event.getPlayer();
player.sendMessage(Message.raw("Vitej!"));
});
}
Async Eventy (Pozor na Thread!)
@Override
protected void setup() {
// PlayerChatEvent je ASYNCHRONNI - MUSI pouzit registerAsync
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
future.thenAccept(event -> {
Player player = event.getPlayer();
World world = player.getWorld(); // BEZ pristupu ke componentum zde!
// Pro component pristup pouzij world.execute()
world.execute(() -> {
Store store = player.getRef().getStore();
CustomComponent comp = store.getComponent(
player.getRef(),
CustomComponent.getComponentType()
);
// Ted bezpecne
});
});
});
}
ECS Eventy (EntityEventSystem)
// Pro ECS eventy jako BreakBlockEvent, DeathEvent, atd.
public class BlockBreakSystem extends EntityEventSystem { public BlockBreakSystem() {
super(BreakBlockEvent.class);
}
@Override
public void handle(
int i,
@Nonnull ArchetypeChunk chunk,
@Nonnull Store store,
@Nonnull CommandBuffer commandBuffer,
@Nonnull BreakBlockEvent event
) {
// DULEZITE: Preskoc prazdne bloky!
if (event.getBlockType() == BlockType.EMPTY) return;
Ref ref = chunk.getReferenceTo(i);
// BEZPECNE - EntityEventSystem zajistuje spravny thread
Player player = store.getComponent(ref, Player.getComponentType());
if (player != null) {
player.sendMessage(Message.raw("Rozbit: " + event.getBlockType().getId()));
}
}
@Nullable
@Override
public Query getQuery() {
return PlayerRef.getComponentType();
}
}
// Registrace v setup()
@Override
protected void setup() {
getEntityStoreRegistry().registerSystem(new BlockBreakSystem());
}
---
ECS Systemy
Registrace Componentu
public class MyPlugin extends JavaPlugin {
private static MyPlugin instance;
private ComponentType myComponentType; @Override
protected void setup() {
instance = this;
// Registrace vlastniho componentu
this.myComponentType = getEntityStoreRegistry().registerComponent(
MyComponent.class,
MyComponent::new
);
// Registrace systemu
getEntityStoreRegistry().registerSystem(new MySystem());
}
public static MyPlugin get() {
return instance;
}
public ComponentType getMyComponentType() {
return myComponentType;
}
}
Bezpecny Pristup ke Componentum
// Vzdy over existenci componentu
Player player = store.getComponent(ref, Player.getComponentType());
if (player == null) {
return; // Entita nema Player component
}// Minimalizuj pocet lookup operaci
// SPATNE - dvojity lookup
if (store.hasComponent(ref, Player.getComponentType())) {
Player player = store.getComponent(ref, Player.getComponentType());
}
// SPRAVNE - jediny lookup
Player player = store.getComponent(ref, Player.getComponentType());
if (player != null) {
// pouzij player
}
---
Debug Mechanismy
Kontrola Aktualniho Threadu
public void debugThreadContext() {
Thread current = Thread.currentThread();
getLogger().atInfo().log("Aktualni thread: " + current.getName());
getLogger().atInfo().log("Thread ID: " + current.getId());
}
Overeni World Threadu
// Overeni ze jsme na spravnem vlakne
world.execute(() -> {
getLogger().atInfo().log("Bezim na world thread: " + Thread.currentThread().getName());
});
Interni Kontroly (z dekompilovaneho kodu)
V dekompilovanych zdrojovych kodech Hytale najdeme tyto kontrolni metody:
// Kontrola ze jsme ve spravnem vlakne
public boolean isInThread() {
return Thread.currentThread() == this.thread;
}// Debug assert - haze vyjimku pokud nejsme ve spravnem vlakne
public void debugAssertInTickingThread() {
if (!this.isInThread()) {
throw new IllegalStateException(
"Assert not in thread! " + this.thread +
" but was in " + Thread.currentThread()
);
}
}
Tyto kontroly muzete vyuzit pro debug:
// Pokud jste si jisti ze mate pristup k World objektu:
if (!world.isInThread()) {
getLogger().atWarning().log("VAROVANI: Nejsme na world threadu!");
}
---
Caste Chyby
Chyba #1: Vnorene world.execute()
// SPATNE - zbytecne vnoreni
world.execute(() -> {
world.execute(() -> { // Nedelej tohle - uz jsme na world threadu
// ...
});
});// SPRAVNE - jedine volani
world.execute(() -> {
// Vsechny operace zde
});
Chyba #2: Blokujici Operace na World Threadu
// SPATNE - blokuje svet
world.execute(() -> {
Thread.sleep(5000); // NIKDY
database.save(); // Blocking I/O
httpClient.sendRequest(); // Blocking network
});// SPRAVNE - async prace, pak sync vysledek
CompletableFuture.runAsync(() -> {
database.save(); // Async
}).thenRun(() -> {
world.execute(() -> {
// Rychly update
});
});
Chyba #3: Ignorovani Navratove Hodnoty
// world.execute() se vrati OKAMZITE - prace je naplanovana
int result;
world.execute(() -> {
result = calculate(); // NEFUNGUJE - result neni dostupny
});
// result NENI k dispozici zde!// SPRAVNE - pouzij CompletableFuture
CompletableFuture future = CompletableFuture.supplyAsync(() -> {
return heavyCalculation(); // Async
});
future.thenAccept(result -> {
world.execute(() -> {
// Pouzij result na world threadu
});
});
Chyba #4: Pouziti register() pro Async Eventy
// SPATNE - PlayerChatEvent je asynchronni!
getEventRegistry().register(PlayerChatEvent.class, event -> {
// Toto se nikdy nevola
});// SPRAVNE - pouzij registerAsync
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
future.thenAccept(event -> {
// Spravne
});
});
Chyba #5: Primý Component Pristup v ECS Eventech
// SPATNE - nebude fungovat
getEventRegistry().register(BreakBlockEvent.class, event -> {
// Toto se nikdy nevola - BreakBlockEvent je ECS event
});// SPRAVNE - pouzij EntityEventSystem
public class MySystem extends EntityEventSystem {
// ...
}
getEntityStoreRegistry().registerSystem(new MySystem());
---
Checklist pro Code Review
Pred Commitem Over:
- [ ] Commands: Prikazy pristupujici ke componentum pouzivaji
AbstractPlayerCommand - [ ] Async Eventy:
PlayerChatEventa dalsi async eventy pouzivajiregisterAsync - [ ] ECS Eventy:
BreakBlockEvent,DeathEventatd. pouzivajiEntityEventSystem - [ ] world.execute(): Pouzito pro async -> sync prechody
- [ ] Zadne Blocking: Zadne
Thread.sleep(), blocking I/O na world threadu - [ ] Thread-safe kolekce:
ConcurrentHashMap,AtomicBooleanpro sdilena data - [ ] Null kontroly: Vsechny
getComponent()jsou kontrolovany na null - [ ] BreakBlockEvent: Kontroluje
BlockType.EMPTY store.getComponent()bezworld.execute()v async kontextuThread.sleep()kdekoli v koduHashMappro sdilena data mezi vlaknyCommandBases pristupem ke componentumregister()proPlayerChatEvent
Varovne Znaky:
---
Priklad: Kompletni Thread-Safe Plugin
package com.teraflex.example;import com.hypixel.hytale.server.core.plugin.JavaPlugin;
import com.hypixel.hytale.server.core.plugin.JavaPluginInit;
import com.hypixel.hytale.server.core.command.system.CommandContext;
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand;
import com.hypixel.hytale.server.core.entity.entities.Player;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import com.hypixel.hytale.server.core.Message;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import javax.annotation.Nonnull;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CompletableFuture;
import java.util.Map;
import java.util.UUID;
public class ThreadSafePlugin extends JavaPlugin {
private static ThreadSafePlugin instance;
// Thread-safe kolekce pro sdilena data
private final Map playerDataCache = new ConcurrentHashMap<>();
public ThreadSafePlugin(JavaPluginInit init) {
super(init);
}
public static ThreadSafePlugin get() {
return instance;
}
@Override
protected void setup() {
instance = this;
// Registrace prikazu
getCommandRegistry().registerCommand(new SafeDataCommand());
// Registrace sync eventu
getEventRegistry().registerGlobal(PlayerReadyEvent.class, event -> {
Player player = event.getPlayer();
loadPlayerDataAsync(player);
});
// Registrace async eventu
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
future.thenAccept(event -> {
// BEZ component pristupu zde
logChat(event.getMessage());
// Pro component pristup:
Player player = event.getPlayer();
player.getWorld().execute(() -> {
// Ted bezpecne
});
});
});
}
private void loadPlayerDataAsync(Player player) {
UUID uuid = player.getUuid();
World world = player.getWorld();
// Async nacteni dat
CompletableFuture.runAsync(() -> {
PlayerData data = database.loadPlayerData(uuid);
playerDataCache.put(uuid, data);
// Sync zpet pro oznameni hraci
world.execute(() -> {
player.sendMessage(Message.raw("Data nactena!"));
});
});
}
private void logChat(String message) {
// Async operace - zadny component pristup
getLogger().atInfo().log("Chat: " + message);
}
// Thread-safe prikaz s pristupem ke componentum
public class SafeDataCommand extends AbstractPlayerCommand {
public SafeDataCommand() {
super("mydata", "myplugin.commands.mydata.desc");
}
@Override
protected void execute(
@Nonnull CommandContext context,
@Nonnull Store store,
@Nonnull Ref ref,
@Nonnull PlayerRef playerRef,
@Nonnull World world
) {
// BEZPECNE - AbstractPlayerCommand zajistuje spravny thread
Player player = store.getComponent(ref, Player.getComponentType());
if (player != null) {
UUID uuid = playerRef.getUuid();
PlayerData data = playerDataCache.get(uuid);
if (data != null) {
context.sendMessage(Message.raw("Tvoje data: " + data.toString()));
} else {
context.sendMessage(Message.raw("Data se nacitaji..."));
}
}
}
}
}
---
Zdroje
TeraFlex/decompiled/com/hypixel/hytale/