Skip to main content
Hytale is a tick-based server. Many game systems — ECS processors, entity spawning, damage application, loot drops — only take effect after one or more world ticks. If your test spawns an entity and immediately checks its state, the entity may not be fully initialized yet. HRTK provides tick-aware waiting primitives to handle this correctly.

Why Tick Waiting Matters

Consider this naive test:
@WorldTest
void brokenTest(WorldTestContext ctx) {
    Object entity = ctx.spawnEntity("hytale:kweebec", 0, 64, 0);
    // BUG: The entity was just created. ECS processors haven't run yet.
    // HealthComponent may not be initialized until the next tick.
    StatsAssert.assertAlive(ctx.getStore(), entity); // May fail!
}
The fix is to wait for the server to tick, giving ECS processors time to run:
@WorldTest
void correctTest(WorldTestContext ctx) {
    Object entity = ctx.spawnEntity("hytale:kweebec", 0, 64, 0);
    ctx.waitTicks(1); // Let the world tick once
    StatsAssert.assertAlive(ctx.getStore(), entity); // Now safe
}

waitTicks()

Blocks the test thread until the specified number of world ticks have elapsed. Available on both EcsTestContext and WorldTestContext.
// Wait for 1 tick (minimum for most ECS operations to take effect)
ctx.waitTicks(1);

// Wait for 5 ticks (enough for most multi-step processes)
ctx.waitTicks(5);

// Wait for 20 ticks (1 second at 20 TPS -- use for slow animations or timers)
ctx.waitTicks(20);
waitTicks() uses World.getTick() for counting and world.execute() for scheduling. The test thread blocks (via a latch) while the world thread advances. This is safe because HRTK runs world-bound tests inside world.execute().

Async variant

If you need non-blocking tick waiting (for concurrent operations), use waitTicksAsync():
CompletableFuture<Void> future = ctx.waitTicksAsync(5);
// Do other setup while ticks elapse
future.join(); // Block until 5 ticks have passed

awaitCondition()

Polls a condition every tick until it returns a non-null value, or times out after a maximum number of ticks. This is the preferred way to wait for something to happen without hardcoding tick counts.
@WorldTest
void waitForEntityToDie(WorldTestContext ctx) {
    Object entity = ctx.spawnEntity("hytale:kweebec", 0, 64, 0);
    ctx.waitTicks(1);

    // Apply lethal damage (implementation depends on your game logic)
    applyDamage(ctx, entity, 9999);

    // Wait up to 100 ticks for the entity to die
    Boolean isDead = ctx.awaitCondition(
        () -> hasDeathComponent(ctx.getStore(), entity) ? true : null,
        100
    );

    HytaleAssert.assertNotNull("Entity should have died", isDead);
}

With custom failure message

Object loot = ctx.awaitCondition(
    () -> findDroppedItem(ctx, "hytale:gold_coin"),
    60,
    "Expected gold coin to drop within 60 ticks"
);
HytaleAssert.assertNotNull(loot);
If the condition never returns a non-null value within maxTicks, awaitCondition() throws a RuntimeException with the failure message. The test is reported as ERRORED.

@AsyncTest

Marks a test as asynchronous. The runner will wait up to timeoutTicks server ticks for the test to complete.
import com.frotty27.hrtk.api.annotation.AsyncTest;

@WorldTest
@AsyncTest(timeoutTicks = 200)
void testDelayedSpawn(WorldTestContext ctx) {
    // Schedule something to happen in 100 ticks
    scheduleDelayedSpawn(ctx, 100);

    // Wait for it
    Object entity = ctx.awaitCondition(
        () -> findEntityByType(ctx, "hytale:delayed_mob"),
        150,
        "Delayed mob should have spawned"
    );

    HytaleAssert.assertNotNull(entity);
}
@AsyncTest is useful for tests that depend on game timers, scheduled tasks, or multi-tick processes. The timeoutTicks value (default 200, which is 10 seconds at 20 TPS) acts as a safety net to prevent indefinite waiting.

Common Patterns

@WorldTest
void spawnAndVerify(WorldTestContext ctx) {
    Object entity = ctx.spawnEntity("hytale:kweebec", 0, 64, 0);
    ctx.waitTicks(1);
    HytaleAssert.assertTrue(ctx.entityExists(entity));
    StatsAssert.assertAlive(ctx.getStore(), entity);
}
@WorldTest
void applyPoisonAndWait(WorldTestContext ctx) {
    Object entity = ctx.spawnEntity("hytale:kweebec", 0, 64, 0);
    ctx.waitTicks(1);
    applyEffect(ctx, entity, POISON_EFFECT_INDEX);
    ctx.waitTicks(1);
    EffectAssert.assertHasEffect(ctx.getStore(), entity, POISON_EFFECT_INDEX);
}
@FlowTest(timeoutTicks = 200)
void killAndLoot(WorldTestContext ctx) {
    Object entity = ctx.spawnEntity("hytale:kweebec", 0, 64, 0);
    ctx.waitTicks(1);
    killEntity(ctx, entity);

    List<?> drops = ctx.awaitCondition(
        () -> collectDrops(ctx, 0, 64, 0),
        100,
        "Expected loot to drop after kill"
    );
    LootAssert.assertDropsContain(drops, "hytale:kweebec_hide");
}

Next Steps

  • Flow Tests — multi-step integration tests using tick waiting
  • World Surface — world context and block/entity operations
  • ECS Surface — ECS assertions and entity queries