#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 #include #include #include #include #include #include /* ---------- 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; }