Nimbin[12]?Games / Entropie / entropie/src/game/save.c

Entropie git · main

Entropie festival game; first steps

sdl c game entropie cmake · first commit 2026-10-10 · last commit 2026-11-02 (18 hours ago) · synced 10 hours ago

C 88.5% Markdown 9.1% GLSL 2.3%
git clone https://git.christianimmanuel.de/games/Entropie.gitwget https://git.christianimmanuel.de/games/Entropie/archive/Entropie.tar.gz
entropie/src/game/save.c 40.3 KB · 804 lines raw
#define _POSIX_C_SOURCE 200809L
#include "game/save.h"
#include "game/game_internal.h"
#include "world/car.h"
#include "world/cast.h"
#include "world/guests.h"
#include "world/clothes.h"
#include "world/ladder.h"
#include "world/tent.h"
#include "world/terrain.h"
#include "world/weather.h"
#include "world/wetness.h"
#include "people/hands.h"
#include "world/hose.h"
#include "build_info.h"

#include <dirent.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>

/* ---------- What is saved: field tables ---------- */

typedef enum { F_FLOAT, F_INT, F_BOOL, F_VEC3, F_REF, F_BYTE } FieldKind;
typedef struct {
    const char *key;
    FieldKind   kind;
    size_t      off;
} Field;

#define FL(k, m) { k, F_FLOAT, offsetof(Object, m) }
#define IN(k, m) { k, F_INT,   offsetof(Object, m) }
#define BO(k, m) { k, F_BOOL,  offsetof(Object, m) }
#define VE(k, m) { k, F_VEC3,  offsetof(Object, m) }
#define RE(k, m) { k, F_REF,   offsetof(Object, m) }
#define BY(k, m) { k, F_BYTE,  offsetof(Object, m) }

/* Every thing (not people) */
static const Field THING[] = {
    VE("pos", position), VE("rot", rotation), VE("size", size), FL("weight", weight), BO("static", is_static), BO("broken", broken),
    IN("variant", variant), IN("sips", sips), FL("alcohol", alcohol), FL("quench", quench), FL("nourish", nourish), FL("refresh", refresh),
    FL("wake", wake), IN("stored", stored), FL("work", work), FL("cut_at", cut_at), FL("mark", mark), VE("mark_at", mark_at),
    VE("mark_out", mark_out), IN("branches", branches), BO("trunk_cut", trunk_cut), BO("toppled", toppled), FL("topple", topple),
    VE("fall_dir", fall_dir), FL("fall_base_y", fall_base_y), VE("fall_origin", fall_origin), FL("fall_ground_y", fall_ground_y),
    FL("fall_slide", fall_slide), VE("fell_plan", fell_plan), BO("fell_blocked", fell_blocked), BO("keep", keep), FL("charred", charred), BO("umbrella_ruined", umbrella_ruined), IN("build_stage", build_stage), FL("build_work", build_work), IN("stock", stock), VE("hat_tint", look.hat_color), IN("umbrella_flips", umbrella_flips),
    FL("glow", glow), FL("fuel", fuel), BO("burning", burning), FL("water", water), FL("grill", grill), BO("grilled", grilled),
    BO("burnt", burnt), FL("extend", extend), FL("raised", raised), BO("leaning", leaning), VE("lean_normal", lean_normal),
    FL("lean_top", lean_top), FL("stability", stability), IN("crate_empties", crate_empties), IN("tent_stage", tent_stage),
    FL("tent_work", tent_work), IN("tent_beds", tent_beds), BO("tent_down", tent_down), RE("owner", owner), RE("owner2", owner2),
    RE("stump", stump), RE("stink", stink), RE("on_grill", on_grill), RE("cloth_of", cloth_of), RE("cloth_host", cloth_host),
    FL("cloth_gap", cloth_gap), BO("gate_open", gate_open), FL("gate_t", gate_t), FL("chair_out", chair_out),
    FL("chair_goal", chair_goal), VE("tucked", tucked), BO("tucked_set", tucked_set), BO("has_home", has_home),
    VE("home_spot", home_spot), VE("home_rot", home_rot), BO("spot_checked", spot_checked), BO("boot_open", boot_open),
};

/* People (you, everybody, the dog: what's theirs) */
static const Field PERSON[] = {
    VE("pos", position), VE("rot", rotation), FL("weight", weight), FL("strength", strength),
    FL("stamina", needs.stamina), FL("hunger", needs.hunger), FL("thirst", needs.thirst), FL("bladder", needs.bladder),
    FL("sleepiness", needs.sleepiness), FL("drunk", drunk), BO("passed_out", passed_out), FL("tolerance", tolerance),
    FL("bladder_size", bladder_size), FL("last_drunk", last_drunk), FL("dirt", dirt), IN("scrapes", scrapes), FL("chill", chill),
    FL("pride", temperament.pride), FL("energy", temperament.energy), FL("nerves", temperament.nerves),
    VE("skin", look.skin), VE("hair", look.hair), VE("shirt", look.shirt), VE("pants", look.pants), VE("shoes", look.shoes),
    FL("height", look.height), IN("hair_style", look.hair_style), BO("long_sleeves", look.long_sleeves), IN("garment", look.garment),
    IN("top", look.top), VE("jacket", look.jacket), BO("jacket_tied", look.jacket_tied), FL("belly", look.belly),
    BO("keeps_shirt", look.keeps_shirt), FL("wet", look.wet), BO("hoodie", look.hoodie), IN("hat", look.hat), IN("cast", cast_id), FL("commitment", commitment), IN("leads", leads), IN("helps", helps), IN("hats_lost", hats_lost), VE("hat_color", look.hat_color),
    FL("ladder_skill", ladder_skill), FL("fitness", fitness), FL("wits", wits), FL("worn", worn), FL("height_fear", height_fear),
    FL("top_speed", top_speed), FL("jump_speed", jump_speed), FL("pace", pace), FL("tempo", tempo), IN("main_hand", main_hand),
    BO("female", female), BO("urinella", urinella), IN("urinella_tried", urinella_tried), IN("urinellas", urinellas),
    BO("lighter", lighter), RE("lighter_of", lighter_of), IN("lighter_uses", lighter_uses), IN("cigarettes", cigarettes),
    IN("tobacco", tobacco), IN("papers", papers), IN("filters", filters), BO("filter_habit", filter_habit),
    BO("pocket_tray", pocket_tray), IN("pocket_butts", pocket_butts), BO("work_trousers", work_trousers), FL("smoker", smoker),
    FL("craving", craving), RE("pullover", pullover), BO("pullover_down", pullover_down), RE("in_tent", in_tent),
    BO("rain_nap", rain_nap), RE("riding", riding), IN("ride_seat", ride_seat), FL("indoors", indoors), BO("lives_here", lives_here),
    IN("ai_mode", ai.mode), IN("ai_order", ai.order), VE("ai_area", ai.work_area), VE("ai_home", ai.home),
    RE("ai_woodpile", ai.woodpile), BY("ai_banned", ai.banned), BY("ai_defies", ai.defies), BO("ai_unload", ai.unload),
    BO("ai_greet", ai.greet), FL("willing", ai.character.willing), IN("snails", snails),
    IN("injured", injured), IN("injury_side", injury_side), FL("hospital_until", hospital_until), IN("cast_until", cast_until),
    IN("cast_limb", look.cast), FL("sprain", sprain), FL("like", like), FL("love", love), FL("harass_t", harass_t), VE("last_ground", last_ground), IN("guest", guest), FL("fun", fun), IN("camp_spot", camp_spot), BO("gone_home", gone_home), BO("supporter", supporter), BO("heading_home", heading_home), BO("storm_told", storm_told),
    VE("inside_door", inside_door),
};

#define COUNT(a) ((int)(sizeof(a) / sizeof((a)[0])))

/* Things that never change while you play (buildings, walls, roads, bushes): only found again, nothing written. */
static bool fixedThing(const Object *o) {
    switch (o->type) {
    case OBJ_HOUSE: case OBJ_BARN: case OBJ_WALL: case OBJ_STRUCTURE: case OBJ_DECAL: case OBJ_BUSH: case OBJ_BIN:
    case OBJ_BOX_STATIC: case OBJ_CYLINDER_STATIC: case OBJ_SUPPLY:
        return o->scene_id > 0;
    default:
        return false;
    }
}

static bool person(const Object *o) { return o->is_character || o->type == OBJ_DOG; }

/* Used up and gone (not a thing anybody points at any more): not written. */
static bool gone(const Object *o) {
    return !person(o) && o->scene_id == 0 && o->type != OBJ_CLOTHES && o->broken && !o->mesh.vao;
}

/* ---------- Writing ---------- */

typedef struct {
    Object *const *objects;
    int count;
    int *id; /* save id per world index (-1: not written) */
} Ids;

static int idOf(const Ids *ids, const Object *o) {
    if (!o) return -1;
    for (int i = 0; i < ids->count; i++)
        if (ids->objects[i] == o) return ids->id[i];
    return -1;
}

static void writeFields(FILE *f, const Object *o, const Field *t, int n, const Ids *ids) {
    const char *base = (const char *)o;
    for (int k = 0; k < n; k++) {
        const void *p = base + t[k].off;
        switch (t[k].kind) {
        case F_FLOAT: fprintf(f, "%s %.9g\n", t[k].key, (double)*(const float *)p); break;
        case F_INT:   fprintf(f, "%s %d\n", t[k].key, *(const int *)p); break;
        case F_BOOL:  fprintf(f, "%s %d\n", t[k].key, *(const bool *)p ? 1 : 0); break;
        case F_BYTE:  fprintf(f, "%s %d\n", t[k].key, (int)*(const unsigned char *)p); break;
        case F_VEC3: { const Vec3 *v = p; fprintf(f, "%s %.9g %.9g %.9g\n", t[k].key, (double)v->x, (double)v->y, (double)v->z); break; }
        case F_REF:   fprintf(f, "%s %d\n", t[k].key, idOf(ids, *(Object *const *)p)); break;
        }
    }
}

static void path(char *out, size_t n, const char *dir, const char *name, const char *ext) {
    snprintf(out, n, "%s/%s%s", dir, name, ext);
}

static bool makeDir(const char *dir) {
    char tmp[512];
    snprintf(tmp, sizeof tmp, "%s", dir);
    for (char *p = tmp + 1; *p; p++)
        if (*p == '/') { *p = '\0'; mkdir(tmp, 0755); *p = '/'; }
    return mkdir(tmp, 0755) == 0 || errno == EEXIST;
}

const char *Save_blocked(const Game *game) {
    if (game->arrival.stage == ARRIVAL_DRIVING) return "Not while you're driving in";
    if (game->meeting.kind || game->meeting.fading) return "Not during a meeting in Berlin";
    if (game->player && game->player->riding) return "Not while you're in the car";
    if (game->ambulance.stage != AMB_NONE || (game->player && game->player->injured)) return "Not now - somebody's hurt, the ambulance is coming";
    return NULL;
}

bool Save_write(const Game *game, const char *dir, const char *name, char *err, size_t err_size) {
    const char *why = Save_blocked(game);
    if (why) { snprintf(err, err_size, "%s", why); return false; }
    if (!makeDir(dir)) { snprintf(err, err_size, "Can't make the folder %s", dir); return false; }
    char file[600], tmp[620], ground[600];
    path(file, sizeof file, dir, name, ".sav");
    path(ground, sizeof ground, dir, name, ".ground");
    snprintf(tmp, sizeof tmp, "%s.tmp", file);
    FILE *f = fopen(tmp, "w");
    if (!f) { snprintf(err, err_size, "Can't write %s", tmp); return false; }

    const World *w = &game->world;
    Ids ids = { w->objects, w->count, calloc((size_t)(w->count > 0 ? w->count : 1), sizeof(int)) };
    if (!ids.id) { fclose(f); snprintf(err, err_size, "Out of memory"); return false; }
    int next = 0;
    for (int i = 0; i < w->count; i++) ids.id[i] = gone(w->objects[i]) ? -1 : next++;

    char clock[16];
    DayCycle_format(&game->daycycle, clock, sizeof clock);
    clock[5] = '\0'; /* (HH:MM) */
    time_t now = time(NULL);
    char date[32];
    strftime(date, sizeof date, "%b %d, %H:%M", localtime(&now));
    fprintf(f, "entropie-save %d\n", SAVE_FORMAT);
    fprintf(f, "version %s\n", APP_VERSION);
    fprintf(f, "label %s - saved %s\n", clock, date);

    fprintf(f, "[game]\n");
    fprintf(f, "time %.9g\nscale %.9g\nthird %d\n", (double)game->daycycle.seconds, (double)game->daycycle.time_scale, game->third_person);
    fprintf(f, "cam %.9g %.9g\n", (double)game->camera.yaw, (double)game->camera.pitch);
    fprintf(f, "rain %.9g %.9g %.9g %d %.9g\n", (double)w->rain, (double)w->rain_goal, (double)w->rain_left, w->weather, (double)w->wet);
    fprintf(f, "wind %.9g %.9g %.9g %.9g %.9g %d\n", (double)w->wind_goal, (double)w->wind_level, (double)w->wind_angle,
            (double)w->wind_left, (double)w->gust_t, w->wind_kind);
    const Arrival *a = &game->arrival;
    fprintf(f, "arrival %d %d %d %.9g %d %.9g %d %.9g %d %d\n", a->stage, a->task, a->step, (double)a->time, a->welcome,
            (double)a->welcome_t, a->chat, (double)a->chat_wait, a->greeted, idOf(&ids, a->car));
    fprintf(f, "guide %d\n", a->guide);
    fprintf(f, "why %d %d %d %d\n", w->why[0], w->why[1], w->why[2], w->why_heard);
    fprintf(f, "weekend %d %.9g %d %d %d\n", game->weekend, (double)game->weekend_from, game->weekend_warned, game->fest_name, game->focus);
    const BigTree *bt = &w->bigtree;
    fprintf(f, "bigtree %d %.9g %.9g %.9g %d %.9g %d %d\n", bt->step, (double)bt->work, (double)bt->rope_h, (double)bt->rope_wait, bt->pullers,
            (double)bt->off, bt->lines_down, bt->told);
    const Festival *fe = &game->festival;
    fprintf(f, "festival %d %d %d %d %d %u %d %.9g %d %d %d %d\n", fe->stage, fe->came, fe->caught, fe->inside, fe->counted, fe->played,
            fe->review, (double)fe->review_t, fe->result, fe->helped, fe->harassed, fe->stepped);
    fprintf(f, "aware %.9g\n", (double)w->aware);
    fprintf(f, "cleanup %d %d %d %d %d %d %d %d\n", fe->clean, fe->litter_start, fe->litter_left, fe->helpers, fe->left_early, fe->supported,
            fe->supported2, fe->verdict);
    fprintf(f, "told %d %d\n", game->rain_told, game->wet_told);
    fprintf(f, "story %.9g\n", (double)game->story_hours);
    fprintf(f, "cash %.9g\n", (double)w->cash);
    fprintf(f, "field %d %d %d %d ", w->inspected, w->snails_rescued, w->snails_killed, game->snail_warned);
    for (int c = 0; c < WORLD_FIELD_CELLS; c++) fprintf(f, "%02x", w->mown[c]);
    fprintf(f, "\n");
    const Errand *e = &game->errand;
    if (e->stage != ERRAND_NONE)
        fprintf(f, "errand %d %d %d %d %d %d %.9g %.9g\n", e->stage, idOf(&ids, e->driver), e->list[0], e->list[1], e->list[2], e->list[3],
                (double)e->time, (double)e->s);

    for (int i = 0; i < w->count; i++) {
        const Object *o = w->objects[i];
        if (ids.id[i] < 0) continue;
        fprintf(f, "[o %d]\n", ids.id[i]);
        if (o->scene_id > 0)
            fprintf(f, "scene %d %d %.9g %.9g %.9g\n", o->scene_id, o->spawn_type, (double)o->spawn.x, (double)o->spawn.y, (double)o->spawn.z);
        fprintf(f, "type %d\npalette %d\nmake %.9g\n", o->type, o->palette, (double)o->make_arg);
        if (fixedThing(o)) continue;
        if (o->name[0]) fprintf(f, "name %s\n", o->name);
        if (!person(o) && o->broken && !o->mesh.vao) fprintf(f, "dead 1\n");
        if (person(o)) {
            writeFields(f, o, PERSON, COUNT(PERSON), &ids);
            fprintf(f, "skill");
            for (int k = 0; k < ORDER_COUNT; k++) fprintf(f, " %.9g", (double)o->skill[k]);
            fprintf(f, "\nlikes");
            for (int k = 0; k < ORDER_COUNT; k++) fprintf(f, " %.9g", (double)o->ai.character.likes[k]);
            fprintf(f, "\n");
            if (o->keys) fprintf(f, "keys %s\n", o->keys);
            if (o->body.asleep) fprintf(f, "asleep 1\n");
            if (o->on_ladder) /* (you up a ladder: back on it, as high; the others come back at its foot) */
                fprintf(f, "ladder %d %.9g\n", idOf(&ids, o->on_ladder), (double)o->ladder_at);
            if (o->hands.grabbed) { /* holding on to something (lifted together, maybe): what, where the hands are on it */
                const Object *g = o->hands.grabbed;
                fprintf(f, "grab %d %d %d %.9g %.9g %.9g %.9g %.9g %.9g\n", idOf(&ids, g), g->lifted, g->end_up,
                        (double)o->hands.grab_point[0].x, (double)o->hands.grab_point[0].y, (double)o->hands.grab_point[0].z,
                        (double)o->hands.grab_point[1].x, (double)o->hands.grab_point[1].y, (double)o->hands.grab_point[1].z);
            }
            if (o->type != OBJ_PLAYER && o->ai.mode == AI_HELP && o->ai.leader) fprintf(f, "help %d\n", idOf(&ids, o->ai.leader));
            if (o->sitting && !o->riding) /* (where they sit: on what, facing where) */
                fprintf(f, "sit %d %.9g %.9g %.9g\n", idOf(&ids, o->seat), (double)o->sit_dir.x, (double)o->sit_dir.y, (double)o->sit_dir.z);
            for (int h = 0; h < 2; h++)
                if (o->hands.item[h] && o->hands.state[h] == HAND_HOLDING) fprintf(f, "hand %d %d\n", h, idOf(&ids, o->hands.item[h]));
        } else {
            writeFields(f, o, THING, COUNT(THING), &ids);
            if (o->cloth_where) fprintf(f, "where %s\n", o->cloth_where);
            if (o->type == OBJ_CRATE) { /* what's in each slot (kind and sips) */
                fprintf(f, "slots");
                for (int q = 0; q < CRATE_SLOTS; q++) fprintf(f, " %d", o->crate_slot[q]);
                fprintf(f, "\n");
            }
            if (o->type == OBJ_HOSE) { /* its rope: where every bit lies, melted, taped */
                const HoseRope *rope = Hose_of((World *)w, o);
                if (rope && rope->ready) {
                    fprintf(f, "rope %.9g %d", (double)rope->length, HOSE_NODES);
                    for (int k = 0; k < HOSE_NODES; k++)
                        fprintf(f, " %.6g %.6g %.6g %.4g %d", (double)rope->p[k].x, (double)rope->p[k].y, (double)rope->p[k].z, (double)rope->melt[k], rope->taped[k]);
                    fprintf(f, "\n");
                }
            }
            if (o->type == OBJ_CAR && o->load) {
                fprintf(f, "load %d", o->load->n);
                for (int k = 0; k < o->load->n; k++) fprintf(f, " %d %d", o->load->kind[k], idOf(&ids, o->load->owner[k]));
                fprintf(f, "\n");
            }
        }
    }
    fprintf(f, "[end]\n");
    free(ids.id);
    bool ok = fflush(f) == 0 && !ferror(f);
    fclose(f);
    if (!ok) { remove(tmp); snprintf(err, err_size, "Writing %s failed", tmp); return false; }

    FILE *g = fopen(ground, "wb");
    ok = g && Terrain_writeWear(&w->terrain, g) && Wetness_write(&w->terrain.wet, g);
    if (g) fclose(g);
    if (!ok) { remove(tmp); snprintf(err, err_size, "Writing %s failed", ground); return false; }
    if (rename(tmp, file) != 0) { snprintf(err, err_size, "Can't write %s", file); return false; }
    return true;
}

/* ---------- Reading ---------- */

typedef struct {
    int        id, scene_id;
    ObjectType spawn_type, type;
    Vec3       spawn;
    Palette    palette;
    float      make;
    int        first, lines;  /* its key lines */
    Object    *obj;           /* found again or made anew */
    bool       remade;
} Rec;

typedef struct {
    char **line;
    int    n;
    Rec   *rec;
    int    recs;
    int    game_first, game_lines;
    int    format;
} Parsed;

static char *word(char **s) {
    while (**s == ' ') (*s)++;
    char *start = *s;
    while (**s && **s != ' ') (*s)++;
    if (**s) *(*s)++ = '\0';
    return start;
}

static float numF(char **s) { return strtof(word(s), NULL); }
static int numI(char **s) { return (int)strtol(word(s), NULL, 10); }
static Vec3 numV(char **s) { float x = numF(s), y = numF(s), z = numF(s); return vec3(x, y, z); }

/* Old saves to the current format, step by step (none yet: format 1 is the first).
   A step rewrites keys / values in p->line before anything is read from them. */
static bool migrate(Parsed *p, char *err, size_t err_size) {
    (void)err; (void)err_size;
    /* e.g.: if (p->format < 2) { rename "foo" to "bar" in every object's lines }; p->format = 2; */
    p->format = SAVE_FORMAT;
    return true;
}

static bool parse(char *text, Parsed *p, char *err, size_t err_size) {
    int cap = 1024;
    p->line = malloc(sizeof(char *) * (size_t)cap);
    if (!p->line) return false;
    for (char *s = text; *s;) {
        char *e = strchr(s, '\n');
        if (e) *e = '\0';
        if (*s) {
            if (p->n == cap) {
                cap *= 2;
                char **more = realloc(p->line, sizeof(char *) * (size_t)cap);
                if (!more) return false;
                p->line = more;
            }
            p->line[p->n++] = s;
        }
        if (!e) break;
        s = e + 1;
    }
    if (p->n < 1 || sscanf(p->line[0], "entropie-save %d", &p->format) != 1) { snprintf(err, err_size, "Not a save file"); return false; }
    if (p->format > SAVE_FORMAT) { snprintf(err, err_size, "This save is from a newer version (format %d) - update the game", p->format); return false; }
    int objs = 0;
    for (int i = 0; i < p->n; i++) objs += !strncmp(p->line[i], "[o ", 3);
    p->rec = calloc((size_t)(objs > 0 ? objs : 1), sizeof(Rec));
    if (!p->rec) return false;
    Rec *r = NULL;
    bool in_game = false;
    for (int i = 1; i < p->n; i++) {
        char *l = p->line[i];
        if (l[0] == '[') {
            r = NULL;
            in_game = !strcmp(l, "[game]");
            if (in_game) { p->game_first = i + 1; p->game_lines = 0; }
            if (!strncmp(l, "[o ", 3)) {
                r = &p->rec[p->recs++];
                r->id = atoi(l + 3);
                r->first = i + 1;
            }
            continue;
        }
        if (r) r->lines++;
        else if (in_game) p->game_lines++;
    }
    return migrate(p, err, err_size);
}

/* Reads the identity lines (scene, type, palette, make) of each record. */
static void identities(Parsed *p) {
    for (int k = 0; k < p->recs; k++) {
        Rec *r = &p->rec[k];
        r->type = OBJ_BOX;
        for (int i = r->first; i < r->first + r->lines; i++) {
            char buf[256], *s = buf;
            snprintf(buf, sizeof buf, "%s", p->line[i]);
            char *key = word(&s);
            if (!strcmp(key, "scene")) { r->scene_id = numI(&s); r->spawn_type = (ObjectType)numI(&s); r->spawn = numV(&s); }
            else if (!strcmp(key, "type")) r->type = (ObjectType)numI(&s);
            else if (!strcmp(key, "palette")) r->palette = (Palette)numI(&s);
            else if (!strcmp(key, "make")) r->make = numF(&s);
        }
    }
}

static Object *byId(const Parsed *p, int id) {
    if (id < 0) return NULL;
    if (id < p->recs && p->rec[id].id == id) return p->rec[id].obj;
    for (int k = 0; k < p->recs; k++) if (p->rec[k].id == id) return p->rec[k].obj;
    return NULL;
}

static bool applyField(Object *o, const Field *t, int n, const char *key, char *s, const Parsed *p) {
    for (int k = 0; k < n; k++) {
        if (strcmp(t[k].key, key)) continue;
        void *at = (char *)o + t[k].off;
        switch (t[k].kind) {
        case F_FLOAT: *(float *)at = numF(&s); break;
        case F_INT:   *(int *)at = numI(&s); break;
        case F_BOOL:  *(bool *)at = numI(&s) != 0; break;
        case F_BYTE:  *(unsigned char *)at = (unsigned char)numI(&s); break;
        case F_VEC3:  *(Vec3 *)at = numV(&s); break;
        case F_REF:   *(Object **)at = byId(p, numI(&s)); break;
        }
        return true;
    }
    return false;
}

/* A thing that turned into something else (a tree into a log) or changed
   shape: its mesh and colliders made again for what it is now. */
static void remake(Object *o, ObjectType type, Palette palette, Vec3 size, float make) {
    Object *fresh = Object_create(type, palette, o->position, size, make);
    if (!fresh) return;
    Mesh_destroy(&o->mesh);
    o->mesh = fresh->mesh;
    fresh->mesh = (Mesh){ 0 };
    o->type = type;
    o->pivot = fresh->pivot;
    o->size = fresh->size;
    o->is_static = fresh->is_static;
    memcpy(o->colliders, fresh->colliders, sizeof o->colliders);
    o->collider_count = fresh->collider_count;
    Object_destroy(fresh);
}

static const char *keysText(const char *saved) {
    static const char *const ALL[] = { "Car keys", "House keys" };
    for (size_t i = 0; i < sizeof ALL / sizeof ALL[0]; i++) if (!strcmp(saved, ALL[i])) return ALL[i];
    return NULL;
}

/* Finds the scene's thing again: by its number, else by what and where it was made. */
static Object *findScene(World *w, const Rec *r, bool *taken) {
    int i = r->scene_id - 1;
    if (i >= 0 && i < w->count && !taken[i]) {
        Object *o = w->objects[i];
        if (o->scene_id == r->scene_id && o->spawn_type == r->spawn_type && vec3_length(vec3_sub(o->spawn, r->spawn)) < 0.05f) { taken[i] = true; return o; }
    }
    for (int k = 0; k < w->count; k++) {
        Object *o = w->objects[k];
        if (taken[k] || o->scene_id <= 0 || o->spawn_type != r->spawn_type || vec3_length(vec3_sub(o->spawn, r->spawn)) > 0.05f) continue;
        taken[k] = true;
        return o;
    }
    return NULL;
}

static void applyGame(Game *game, const Parsed *p) {
    World *w = &game->world;
    for (int i = p->game_first; i < p->game_first + p->game_lines; i++) {
        char buf[2048], *s = buf; /* (the field line is long) */
        snprintf(buf, sizeof buf, "%s", p->line[i]);
        char *key = word(&s);
        if (!strcmp(key, "time")) game->daycycle.seconds = numF(&s);
        else if (!strcmp(key, "scale")) game->daycycle.time_scale = numF(&s);
        else if (!strcmp(key, "third")) game->third_person = numI(&s) != 0;
        else if (!strcmp(key, "cam")) { game->camera.yaw = numF(&s); game->camera.pitch = numF(&s); Camera_rotate(&game->camera, 0.0f, 0.0f); }
        else if (!strcmp(key, "rain")) { w->rain = numF(&s); w->rain_goal = numF(&s); w->rain_left = numF(&s); w->weather = numI(&s); w->wet = numF(&s); }
        else if (!strcmp(key, "wind")) {
            w->wind_goal = numF(&s); w->wind_level = numF(&s); w->wind_angle = numF(&s); w->wind_left = numF(&s); w->gust_t = numF(&s); w->wind_kind = numI(&s);
        } else if (!strcmp(key, "arrival")) {
            Arrival *a = &game->arrival;
            a->stage = (ArrivalStage)numI(&s); a->task = (ArrivalTask)numI(&s); a->step = numI(&s); a->time = numF(&s);
            a->welcome = numI(&s); a->welcome_t = numF(&s); a->chat = numI(&s); a->chat_wait = numF(&s); a->greeted = numI(&s) != 0;
            a->car = byId(p, numI(&s));
        } else if (!strcmp(key, "told")) { game->rain_told = numI(&s) != 0; game->wet_told = numI(&s) != 0; }
        else if (!strcmp(key, "aware")) w->aware = numF(&s);
        else if (!strcmp(key, "cleanup")) {
            Festival *fe = &game->festival;
            fe->clean = numI(&s); fe->litter_start = numI(&s); fe->litter_left = numI(&s); fe->helpers = numI(&s); fe->left_early = numI(&s);
            fe->supported = numI(&s); fe->supported2 = numI(&s); fe->verdict = numI(&s);
            if (fe->clean >= CLEAN_DRIVING) fe->clean = CLEAN_TO_CAR; /* (saved before: never in the car) */
        }
        else if (!strcmp(key, "bigtree")) {
            BigTree *bt = &w->bigtree;
            bt->step = numI(&s); bt->work = numF(&s); bt->rope_h = numF(&s); bt->rope_wait = numF(&s); bt->pullers = numI(&s);
            bt->off = numF(&s); bt->lines_down = numI(&s) != 0; bt->told = numI(&s) != 0;
            if (bt->step == BT_CUT || bt->step == BT_FALLING) bt->step = BT_PULL; /* (mid-cut: from the pulling again) */
        }
        else if (!strcmp(key, "festival")) {
            Festival *fe = &game->festival;
            fe->stage = numI(&s); fe->came = numI(&s); fe->caught = numI(&s); fe->inside = numI(&s); fe->counted = numI(&s) != 0;
            fe->played = (unsigned)numI(&s); fe->review = numI(&s); fe->review_t = numF(&s); fe->result = numI(&s); fe->helped = numI(&s);
            fe->harassed = numI(&s); fe->stepped = numI(&s);
            w->fest.helped = fe->helped;
        }
        else if (!strcmp(key, "story")) game->story_hours = numF(&s);
        else if (!strcmp(key, "cash")) game->world.cash = numF(&s);
        else if (!strcmp(key, "guide")) game->arrival.guide = numI(&s);
        else if (!strcmp(key, "why")) { for (int k = 0; k < WHY_COUNT; k++) w->why[k] = numI(&s); w->why_heard = numI(&s); }
        else if (!strcmp(key, "weekend")) {
            game->weekend = numI(&s); game->weekend_from = numF(&s); game->weekend_warned = numI(&s) != 0; game->fest_name = numI(&s); game->focus = numI(&s);
        }
        else if (!strcmp(key, "field")) {
            w->inspected = numI(&s) != 0; w->snails_rescued = numI(&s); w->snails_killed = numI(&s); game->snail_warned = numI(&s);
            while (*s == ' ') s++;
            for (int c = 0; c < WORLD_FIELD_CELLS && s[0] && s[1]; c++, s += 2) {
                char hex[3] = { s[0], s[1], 0 };
                w->mown[c] = (unsigned char)strtol(hex, NULL, 16);
            }
            w->mow_rev++;
        }
        else if (!strcmp(key, "errand")) {
            Errand *e = &game->errand;
            e->stage = (ErrandStage)numI(&s); e->driver = byId(p, numI(&s));
            for (int k = 0; k < SHOP_ITEMS; k++) e->list[k] = numI(&s);
            e->time = numF(&s); e->s = numF(&s);
        }
    }
}

bool Save_read(Game *game, const char *dir, const char *name, char *err, size_t err_size) {
    char file[600], ground[600];
    path(file, sizeof file, dir, name, ".sav");
    path(ground, sizeof ground, dir, name, ".ground");
    FILE *f = fopen(file, "rb");
    if (!f) { snprintf(err, err_size, "Can't open %s", file); return false; }
    fseek(f, 0, SEEK_END);
    long bytes = ftell(f);
    fseek(f, 0, SEEK_SET);
    char *text = malloc((size_t)bytes + 1);
    if (!text) { fclose(f); snprintf(err, err_size, "Out of memory"); return false; }
    size_t got = fread(text, 1, (size_t)bytes, f);
    text[got] = '\0';
    fclose(f);

    Parsed p = { 0 };
    if (!parse(text, &p, err, err_size)) { free(p.line); free(p.rec); free(text); return false; }
    identities(&p);

    /* The world as at the start, without the drive in */
    Game_shutdown(game);
    memset(game, 0, sizeof(*game));
    if (!Game_initWorld(game, false)) { snprintf(err, err_size, "Building the world failed"); free(p.line); free(p.rec); free(text); return false; }
    World *w = &game->world;
    bool *taken = calloc((size_t)w->count + 1, sizeof(bool));

    /* Each thing: found again (the scene's) or made anew */
    for (int k = 0; k < p.recs; k++) {
        Rec *r = &p.rec[k];
        Vec3 pos = vec3_zero(), size = vec3_zero(), rot = vec3_zero();
        int cast = -1, guest = 0;
        for (int i = r->first; i < r->first + r->lines; i++) {
            char buf[256], *s = buf;
            snprintf(buf, sizeof buf, "%s", p.line[i]);
            char *key = word(&s);
            if (!strcmp(key, "pos")) pos = numV(&s);
            else if (!strcmp(key, "size")) size = numV(&s);
            else if (!strcmp(key, "rot")) rot = numV(&s);
            else if (!strcmp(key, "cast")) cast = numI(&s);
            else if (!strcmp(key, "guest")) guest = numI(&s);
        }
        if (r->scene_id > 0) {
            r->obj = taken ? findScene(w, r, taken) : NULL;
            if (!r->obj) continue; /* (the scene changed since: that thing isn't there any more) */
            bool shape = (r->type == OBJ_LOG || r->type == OBJ_FIREWOOD || r->type == OBJ_STUMP || r->type == OBJ_BRANCH) &&
                         vec3_length(vec3_sub(size, r->obj->size)) > 1e-4f;
            if (r->obj->type != r->type || shape) { r->obj->position = pos; remake(r->obj, r->type, r->palette, size, r->make); r->remade = true; }
            continue;
        }
        Object *o = NULL;
        switch (r->type) {
        case OBJ_CAR:      o = Car_create(pos, rot.y); break;
        case OBJ_CLOTHES:  o = Object_createDecal((Mesh){ 0 }, pos); if (o) o->type = OBJ_CLOTHES; break;
        case OBJ_HUMAN:    if (cast > 0) o = Cast_create(cast, pos); else if (guest > 0) o = Guest_create(guest, pos); break; /* (the crew turned up after the start, the guests; the rest: the scene's) */
        case OBJ_DECAL: case OBJ_STRUCTURE: case OBJ_WALL: case OBJ_PLAYER: case OBJ_DOG: break; /* (made by the scene only) */
        default:           o = Object_create(r->type, r->palette, pos, size, r->make); break;
        }
        if (!o) continue;
        o->palette = r->palette;
        o->make_arg = r->make;
        if (!World_add(w, o)) { Object_destroy(o); continue; }
        r->obj = o;
    }
    free(taken);

    /* Everything as it was (references now that everything is there) */
    for (int k = 0; k < p.recs; k++) {
        Rec *r = &p.rec[k];
        Object *o = r->obj;
        if (!o) continue;
        bool is_person = person(o);
        for (int i = r->first; i < r->first + r->lines; i++) {
            char buf[1024], *s = buf;
            snprintf(buf, sizeof buf, "%s", p.line[i]);
            char *key = word(&s);
            if (applyField(o, is_person ? PERSON : THING, is_person ? COUNT(PERSON) : COUNT(THING), key, s, &p)) continue;
            if (!strcmp(key, "skill")) for (int q = 0; q < ORDER_COUNT; q++) o->skill[q] = numF(&s);
            else if (!strcmp(key, "likes")) for (int q = 0; q < ORDER_COUNT; q++) o->ai.character.likes[q] = numF(&s);
            else if (!strcmp(key, "keys")) o->keys = keysText(s);
            else if (!strcmp(key, "where")) o->cloth_where = Clothes_whereText(s);
            else if (!strcmp(key, "name")) snprintf(o->name, sizeof o->name, "%s", s);
            else if (!strcmp(key, "rope") && o->type == OBJ_HOSE) {
                HoseRope *rope = Hose_of(w, o);
                float len = numF(&s);
                int n = numI(&s);
                if (rope && n == HOSE_NODES) {
                    rope->length = len;
                    for (int q = 0; q < HOSE_NODES; q++) {
                        rope->p[q] = rope->prev[q] = numV(&s);
                        rope->melt[q] = numF(&s);
                        rope->taped[q] = numI(&s) != 0;
                        rope->heat[q] = 0.0f;
                    }
                    rope->ready = true;
                }
            }
            else if (!strcmp(key, "slots") && o->type == OBJ_CRATE) {
                for (int q = 0; q < CRATE_SLOTS; q++) o->crate_slot[q] = (unsigned char)numI(&s);
                Crate_recount(o);
            }
            else if (!strcmp(key, "load") && o->type == OBJ_CAR && o->load) {
                int n = numI(&s);
                o->load->n = 0;
                for (int q = 0; q < n && q < CAR_LOAD_MAX; q++) {
                    o->load->kind[q] = (CargoKind)numI(&s);
                    o->load->owner[q] = byId(&p, numI(&s));
                    o->load->n++;
                }
            }
        }
    }

    /* How it looks again, and people back in their skins */
    for (int k = 0; k < p.recs; k++) {
        Rec *r = &p.rec[k];
        Object *o = r->obj;
        if (!o || person(o)) continue;
        bool dead = false;
        for (int i = r->first; i < r->first + r->lines; i++) if (!strcmp(p.line[i], "dead 1")) dead = true;
        if (dead) { Object_useUp(o); continue; }
        switch (o->type) {
        case OBJ_TREE:
            if (o->trunk_cut || o->toppled || r->remade) Object_rebuildTree(o);
            break;
        case OBJ_WATER:
            if (!o->broken) { int sips = o->sips; Object_setDrink(o, o->variant); o->sips = sips; }
            break;
        case OBJ_BAG:      Object_fillBag(o, 0.0f); break;
        case OBJ_TENT:     Tent_refresh(o); break;
        case OBJ_GEAR:     Gear_refresh(o); break;
        case OBJ_CRATE: {
            bool slots = false;
            for (int i = r->first; i < r->first + r->lines; i++) if (!strncmp(p.line[i], "slots ", 6)) slots = true;
            if (!slots) { /* (an older save: so many full, so many empties - all its own) */
                int full = o->stored - o->crate_empties, empties = o->crate_empties;
                Crate_fill(o);
                for (int q = 0; q < CRATE_SLOTS; q++) {
                    if (q >= full + empties) o->crate_slot[q] = 0;
                    else if (q >= full) o->crate_slot[q] = CRATE_SLOT(CRATE_KIND(o->crate_slot[q]), 0);
                }
                Crate_recount(o);
            }
            o->shown = -1;
            break;
        }
        case OBJ_LADDER:   Ladder_setExtend(o, o->extend); break;
        case OBJ_CAR:
            if (o->boot_open) { o->boot_open = false; Car_openBoot(o, true); }
            break;
        default: break;
        }
        if (Object_isBottle(o) && o->broken && o->mesh.vao) { o->broken = false; Object_shatter(o, o->position.y); }
        o->velocity = o->angular_velocity = vec3_zero();
        Object_syncColliders(o);
    }
    for (int k = 0; k < p.recs; k++) {
        Rec *r = &p.rec[k];
        Object *o = r->obj;
        if (!o || !person(o)) continue;
        o->velocity = o->movement = vec3_zero();
        o->sitting = false;
        o->seat = NULL;
        o->body.replant = true;
        o->ai.timer = 0.0f;
        o->ai.target = o->ai.leader = NULL;
        for (int q = 0; q < AI_SKIPS; q++) o->ai.skip[q] = NULL;
        if (o->ai.mode != AI_IDLE && o->ai.mode != AI_WANDER && o->ai.mode != AI_WORK) o->ai.mode = AI_IDLE;
        Object_syncColliders(o);
        bool asleep = false, sits = false;
        Object *seat = NULL, *ladder = NULL;
        float ladder_at = 0.0f;
        Vec3 sit_dir = vec3(0.0f, 0.0f, 1.0f);
        for (int i = r->first; i < r->first + r->lines; i++) {
            char buf[256], *s = buf;
            snprintf(buf, sizeof buf, "%s", p.line[i]);
            char *key = word(&s);
            if (!strcmp(key, "asleep")) asleep = true;
            else if (!strcmp(key, "sit")) { sits = true; seat = byId(&p, numI(&s)); sit_dir = numV(&s); }
            else if (!strcmp(key, "ladder")) { ladder = byId(&p, numI(&s)); ladder_at = numF(&s); }
            else if (!strcmp(key, "hand")) {
                int h = numI(&s);
                Object *item = byId(&p, numI(&s));
                if (h >= 0 && h < 2 && item && !item->is_held && !item->broken && !Hands_receive(o, h, item)) {
                    Vec3 ahead = Object_facing(o); /* (too big for one hand: at their feet) */
                    item->position = vec3_add(o->position, vec3(ahead.x * 0.6f, 0.05f, ahead.z * 0.6f));
                    Object_syncColliders(item);
                }
            }
        }
        if (o->riding) {
            Object *car = o->riding;
            o->riding = NULL;
            Car_getIn(car, o, o->ride_seat);
        } else if (o->in_tent) {
            Object *tent = o->in_tent;
            o->in_tent = NULL;
            Tent_crawlIn(tent, o);
        } else if (ladder && ladder->type == OBJ_LADDER && ladder->leaning && !ladder->is_held) {
            if (o->type == OBJ_PLAYER) Ladder_mountAt(o, ladder, ladder_at);
            else { /* (their climb is AI work, and that starts fresh: down at its foot, not dropped from up there) */
                Vec3 foot = vec3_add(ladder->position, vec3_scale(ladder->lean_normal, 0.45f));
                o->position = vec3(foot.x, World_groundHeight(w, foot.x, foot.z, foot.y + 0.5f), foot.z);
                Object_syncColliders(o);
                o->body.replant = true;
            }
        }
        else if (asleep) Body_lieDown(&o->body);
        else if (sits && Object_sit(o, seat, vec3_add(o->position, vec3_scale(sit_dir, 3.0f)))) /* (sat down again where they sat ... */
            o->ai.timer = 20.0f + 0.37f * (float)(k % 41);                                    /* ... and stay a while) */
    }
    for (int k = 0; k < p.recs; k++) { /* helping somebody, holding on to something together (once everybody's back) */
        Rec *r = &p.rec[k];
        Object *o = r->obj;
        if (!o || !o->is_character) continue;
        for (int pass = 0; pass < 2; pass++) /* (helping first: it lets go of things; then the grip) */
        for (int i = r->first; i < r->first + r->lines; i++) {
            char buf[256], *s = buf;
            snprintf(buf, sizeof buf, "%s", p.line[i]);
            char *key = word(&s);
            if (pass == 0 && !strcmp(key, "help")) {
                Object *leader = byId(&p, numI(&s));
                if (leader && leader->is_character && o->type != OBJ_PLAYER) Ai_help(o, leader);
            } else if (pass == 1 && !strcmp(key, "grab")) {
                Object *g = byId(&p, numI(&s));
                bool lifted = numI(&s) != 0, end_up = numI(&s) != 0;
                Vec3 g0 = numV(&s), g1 = numV(&s);
                if (!g || g->broken || !Hands_grabObject(o, g)) continue;
                o->hands.grab_point[0] = g0;
                o->hands.grab_point[1] = g1;
                if (lifted) { g->lifted = true; g->end_up = end_up; g->velocity = vec3_zero(); }
            }
        }
    }
    game->holding = game->player && game->player->hands.grabbed; /* (you hold on as before, no F needed) */
    for (int k = 0; k < p.recs; k++) /* (pullovers last: they need their owners' look) */
        if (p.rec[k].obj && p.rec[k].obj->type == OBJ_CLOTHES) Clothes_rebuild(p.rec[k].obj);
    if (game->arrival.car) Car_place(w, game->arrival.car, game->arrival.car->position, game->arrival.car->rotation.y);
    applyGame(game, &p);
    game_shopLoaded(game);
    game_meetingLoaded(game);

    /* The ground: worn paths, then the wet (it lies on the paths) */
    FILE *g = fopen(ground, "rb");
    if (g) {
        if (Terrain_readWear(&w->terrain, g)) Wetness_read(&w->terrain, g);
        fclose(g);
    }
    free(p.line);
    free(p.rec);
    free(text);
    return true;
}

/* ---------- The list ---------- */

int Save_list(const char *dir, SaveInfo *out, int max) {
    DIR *d = opendir(dir);
    if (!d) return 0;
    int n = 0;
    struct dirent *e;
    while ((e = readdir(d)) && n < max) {
        size_t len = strlen(e->d_name);
        if (len < 5 || strcmp(e->d_name + len - 4, ".sav") || len - 4 >= SAVE_NAME_MAX) continue;
        SaveInfo *s = &out[n];
        memset(s, 0, sizeof(*s));
        memcpy(s->name, e->d_name, len - 4);
        char file[600];
        path(file, sizeof file, dir, s->name, ".sav");
        struct stat st;
        if (stat(file, &st) == 0) s->when = (long long)st.st_mtime;
        FILE *f = fopen(file, "r");
        if (f) {
            char line[160];
            for (int k = 0; k < 4 && fgets(line, sizeof line, f); k++)
                if (!strncmp(line, "label ", 6)) {
                    line[strcspn(line, "\n")] = '\0';
                    snprintf(s->label, sizeof s->label, "%.95s", line + 6);
                }
            fclose(f);
        }
        n++;
    }
    closedir(d);
    for (int i = 1; i < n; i++) /* (newest first) */
        for (int j = i; j > 0 && out[j].when > out[j - 1].when; j--) { SaveInfo t = out[j]; out[j] = out[j - 1]; out[j - 1] = t; }
    return n;
}