Skip to content

AI and saved data

Lesson 7.9

This lesson moves mobs along paths, gives them goals written in C, and saves data on entities, chunks and the world.

Paths

cl_path_opts o = CL_PATH_OPTS_INIT;
o.doors = 1;                         /* walk through (and open) wooden doors */
o.climb = true;                      /* ladders, vines, scaffolding */
cl_vec3 home = {x + 0.5, y, z + 0.5};
if (cl_entity_path_to(mob, home, 1.0, &o) == CL_E_NOT_FOUND) { /* no path at all */ }
if (cl_entity_path_state(mob) == CL_PATH_DONE) { /* arrived */ }

Planning without moving gives an owned path:

o.entity = mob;                      /* the mob's size and navigation */
cl_path p = cl_level_find_path(level, from, home, &o);
if (p.h) {
    cl_path_info info;
    cl_path_get_info(p, &info);      /* reaches_target, nodes, length, ... */
    cl_path_release(p);
}

A path cost changes where paths go (negative: never step there):

static float avoid_water(cl_level level, cl_block_pos pos, void* user) {
    cl_block_state s = 0;
    cl_level_get_block(level, pos, &s);
    return s == water_state ? -1.0f : 0.0f;
}
cl_path_cost_desc c = CL_PATH_COST_DESC_INIT;
c.id = CL_STR("my_mod:no_water");
c.cost = avoid_water;
cl_ai_path_cost_register(&c);
o.cost = CL_STR("my_mod:no_water");

Goals

A goal type is a set of callbacks; register it once, then add it to mobs:

static bool guard_can_use(cl_entity e, cl_goal g, void* user) {
    cl_vec3 pos;
    cl_entity_position(e, &pos);
    cl_entity near;
    return cl_level_nearest_player(cl_entity_level(e), pos, 8.0, &near) == CL_OK;
}
static void guard_tick(cl_entity e, cl_goal g, void* user) {
    cl_vec3 pos;
    cl_entity near;
    cl_entity_position(e, &pos);
    if (cl_level_nearest_player(cl_entity_level(e), pos, 8.0, &near) == CL_OK) cl_entity_look_at_entity(e, near, 0, 0);
}

cl_goal_type_desc d = CL_GOAL_TYPE_DESC_INIT;
d.id = CL_STR("my_mod:guard");
d.flags = CL_GOAL_LOOK;
d.can_use = guard_can_use;
d.tick = guard_tick;
cl_ai_goal_type_register(&d, NULL);

cl_goal g;
cl_ai_add_goal(mob, 2, CL_STR("my_mod:guard"), &g);                   /* one mob */
cl_ai_type_goal(CL_STR("minecraft:iron_golem"), 2, CL_STR("my_mod:guard"));   /* every golem, also after loading */

Goals are not saved; cl_ai_type_goal adds them again when mobs load. cl_ai_set_target(mob, player) chooses who a hostile mob attacks.

Saved data

Values are SNBT text (what /data get shows) or binary NBT in a cl_buf:

cl_entity_data_set(player, CL_STR("my_mod:home"), CL_STR("{x:12,y:64,z:-3}"), 0);
char text[256];
size_t len = 0;
if (cl_entity_data_get(player, CL_STR("my_mod:home"), text, sizeof text, &len) == CL_OK) { /* parse text */ }

cl_level_chunk_data_set(level, x >> 4, z >> 4, CL_STR("my_mod:claim"), CL_STR("{owner:\"Steve\"}"));
cl_level_saved_data_set(level, CL_STR("my_mod:stats"), CL_STR("{visits:1}"));

CL_DATA_SYNC in cl_entity_data_set also sends the value to Celestial clients with CLight. Everything is saved with the world, and vanilla ignores it.

Data with a default

A typed attachment gives a key a default and decides whether a player keeps it through death (API 1.20):

cl_attachment_desc d = CL_ATTACHMENT_DESC_INIT;
d.default_snbt = CL_STR("{value:20}");
d.flags = CL_ATTACH_SYNC;                  /* without CL_ATTACH_COPY_ON_DEATH: back to the default after a death */
cl_attachment_register(CL_STR("mana"), &d);

cl_entity_data_get then returns {value:20} for a player who has none yet. More in the guide Data Attachments.

Exercise

Count how many times each chunk was entered by a player and keep the total for the whole world.

Show a solution
static void count_chunk(cl_level level, cl_vec3 pos) {
    const int cx = (int)floor(pos.x) >> 4, cz = (int)floor(pos.z) >> 4;
    char text[64];
    size_t len = 0;
    int n = 0;
    if (cl_level_chunk_data_get(level, cx, cz, CL_STR("my_mod:visits"), text, sizeof text, &len) == CL_OK) n = atoi(text);
    snprintf(text, sizeof text, "%d", n + 1);
    cl_level_chunk_data_set(level, cx, cz, CL_STR("my_mod:visits"), (cl_str){text, strlen(text)});
    int total = 0;
    if (cl_level_saved_data_get(level, CL_STR("my_mod:visits"), text, sizeof text, &len) == CL_OK) total = atoi(text);
    snprintf(text, sizeof text, "%d", total + 1);
    cl_level_saved_data_set(level, CL_STR("my_mod:visits"), (cl_str){text, strlen(text)});
}

Call it from a tick.server_post handler when a player’s chunk changed.

Recap: cl_entity_path_to and cl_level_find_path move and plan, goal types run your C code inside the mob’s goal list, and the *_data_* functions keep values on entities, chunks and levels. Guides: AI and Pathfinding, Saved Data.