HyCodeYourTale

Hytale Plugin Thread Safety Guidelines

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: PlayerChatEvent a dalsi async eventy pouzivaji registerAsync

  • [ ] ECS Eventy: BreakBlockEvent, DeathEvent atd. pouzivaji EntityEventSystem

  • [ ] world.execute(): Pouzito pro async -> sync prechody

  • [ ] Zadne Blocking: Zadne Thread.sleep(), blocking I/O na world threadu

  • [ ] Thread-safe kolekce: ConcurrentHashMap, AtomicBoolean pro sdilena data

  • [ ] Null kontroly: Vsechny getComponent() jsou kontrolovany na null

  • [ ] BreakBlockEvent: Kontroluje BlockType.EMPTY
  • Varovne Znaky:

  • store.getComponent() bez world.execute() v async kontextu

  • Thread.sleep() kdekoli v kodu

  • HashMap pro sdilena data mezi vlakny

  • CommandBase s pristupem ke componentum

  • register() pro PlayerChatEvent
  • ---

    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

  • Thread Safety Documentation

  • Commands Documentation

  • Event System Documentation

  • ECS Architecture Documentation

  • Dekompilovany kod: TeraFlex/decompiled/com/hypixel/hytale/

Last updated: 20. ledna 2026