Skip to content

Timers and tasks

Lesson 8.2

“Do this in three seconds.” “Do this every minute.” “Build this tower one block at a time.” All of these need code that waits. In a game you never wait by stopping everything (no sleep and no busy loop); you ask the game to call you again later.

The game counts time in ticks: 20 ticks are one second.

Once, later: cl_sched_run_server

static void say_rules(void* user) {
    (void)user;
    cl_easy_broadcast("Don't forget to read the rules!");
}

// somewhere in server code:
cl_sched_run_server(60, say_rules, NULL, NULL);   // in 3 seconds

cl_sched_run_server(delay, fn, user, out) calls fn(user) once, after delay ticks, on the server’s tick. A delay of 0 means “on the next tick”. out receives a task handle if you want to cancel it; pass NULL if you don’t.

Again and again: cl_sched_repeat_server

static cl_task g_reminder;

static void remind(void* user) {
    (void)user;
    cl_easy_broadcast("Remember to drink water.");
}

// every minute, starting in one minute:
cl_sched_repeat_server(1200, 1200, remind, NULL, &g_reminder);

// stop it again:
cl_task_cancel(g_reminder);

The first number is the delay before the first call, the second the period between calls. The period must be at least 1.

Function Runs
cl_sched_run_server(delay, fn, user, &task) Once, after delay ticks, on the server.
cl_sched_repeat_server(delay, period, fn, user, &task) First after delay ticks, then every period ticks, on the server.
cl_sched_run_client(delay, fn, user, &task) Once, after delay ticks, in the game client.
cl_task_cancel(task) Stops a waiting or repeating task.
cl_task_is_pending(task) true while the task is still waiting.

The server functions only work while a world runs: in singleplayer, or on a server. In singleplayer the world stops while the pause menu is open, and your timers wait too, just like the game’s own.

Taking things along: the user pointer

A task only gets a void* user. To take more than one value along, put them in a struct. The struct must still exist when the task runs, so it can’t be a local variable of the function that schedules it. Allocate it, and free it in the task:

typedef struct {
    cl_entity player;
    int seconds_left;
} countdown;

static void countdown_step(void* user) {
    countdown* c = (countdown*)user;
    if (!cl_entity_is_valid(c->player)) {   // the player left
        free(c);
        return;
    }
    if (c->seconds_left > 0) {
        char text[8];
        snprintf(text, sizeof text, "%d", c->seconds_left);
        cl_easy_send_action_bar(c->player, text);
        c->seconds_left -= 1;
        cl_sched_run_server(20, countdown_step, c, NULL);   // again in one second
    } else {
        cl_easy_send_action_bar(c->player, "Go!");
        free(c);
    }
}

static void start_countdown(cl_entity player) {
    countdown* c = malloc(sizeof *c);
    if (!c) return;
    c->player = player;
    c->seconds_left = 5;
    countdown_step(c);
}

Two habits from this example:

  • Check that things still exist after waiting. The player may have left; cl_entity_is_valid tells you.
  • Every malloc gets exactly one free, on every path out of the task. W3Schools explains both in Allocate memory and Deallocate memory.

A task that schedules itself again, like countdown_step, is a neat way to run a sequence with waits in between.

Big jobs without lag

Doing a slice of the work per tick keeps the game smooth. A tower that grows one block every half second:

typedef struct {
    cl_level level;
    cl_block_pos base;
    int built, height;
} tower;

static void tower_step(void* user) {
    tower* t = (tower*)user;
    t->built += 1;
    cl_block_pos p = {t->base.x, t->base.y + t->built, t->base.z};
    cl_easy_set_block(t->level, p, "minecraft:stone_bricks");
    if (t->built < t->height) cl_sched_run_server(10, tower_step, t, NULL);
    else free(t);
}

This is also the answer to “my mod freezes the game”: a callback that places 100,000 blocks at once makes everyone wait. 1,000 per tick for 100 ticks doesn’t.

Which one to use

  • A sequence with waits in between (a countdown, an animation, a slow build): a task that reschedules itself.
  • Something that repeats forever (a reminder, an autosave): cl_sched_repeat_server.
  • Something every single tick: a handler for CL_EV_TICK_SERVER_POST. Keep it short, it runs 20 times a second.

Tasks cost nothing while they wait.

Try it

Write a /countdown command that counts down from 5 in the action bar of the player who typed it, one number per second, then shows “Go!”. Reuse start_countdown from above.

Show a solution
static int32_t cmd_countdown(cl_cmd_ctx ctx, void* user) {
    (void)user;
    cl_entity p = cl_cmd_ctx_player(ctx);
    if (CL_IS_NULL(p)) {
        cl_text t = cl_text_literal(CL_STR("Players only."));
        cl_cmd_send_failure(ctx, t);
        cl_text_release(t);
        return 0;
    }
    start_countdown(p);
    return 1;
}

static void on_command_register(void* payload, void* user) {
    (void)payload;
    (void)user;
    cl_cmd_desc d = CL_CMD_DESC_INIT;
    d.name = CL_STR("countdown");
    d.execute = cmd_countdown;
    cl_cmd_register(&d);
}

The command returns right away; the task keeps running for the next five seconds. The player handle is copied into the struct, so it is still there after the command’s context is gone.

Recap: cl_sched_run_server calls a function once after a number of ticks, cl_sched_repeat_server calls it again and again, cl_task_cancel stops it, and values travel along in a malloced struct that the task frees.