Entropie git · main
Entropie festival game; first steps
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.gzentropie/src/game/save.c 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;
}