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 2.18.0: saving and loading, start menu
- Start menu: continue (the autosave), new game, load a save, quit - Esc menu: save as (type a name), quick save, load a save - F5 quick save, F9 quick load; autosave every 5 minutes and on quit - Saves: a versioned text file plus the ground (worn paths, wet ground); the world is rebuilt and everything put back (people, pockets, clothes, tents, crates, felled trees, the car, the weather, the time) - Format version and migration steps so old saves keep working - Fix: a freed people list could be used after rebuilding the world
commit_message.txt | 19 +- entropie/ARCHITECTURE.md | 14 +- entropie/Makefile | 2 +- entropie/README.md | 12 +- entropie/STATE.md | 11 +- entropie/src/core/input.c | 10 +- entropie/src/core/input.h | 4 + entropie/src/core/platform.c | 13 + entropie/src/core/platform.h | 5 + entropie/src/game/game.c | 12 +- entropie/src/game/game.h | 2 + entropie/src/game/save.c | 629 +++++++++++++++++++++++++++++++++++++++++++ entropie/src/game/save.h | 44 +++ entropie/src/main.c | 264 +++++++++++++++--- entropie/src/render/ui.h | 2 +- entropie/src/world/car.c | 8 + entropie/src/world/car.h | 2 + entropie/src/world/clothes.c | 18 ++ entropie/src/world/clothes.h | 4 + entropie/src/world/object.c | 6 + entropie/src/world/object.h | 8 + entropie/src/world/tent.c | 5 + entropie/src/world/tent.h | 2 + entropie/src/world/terrain.c | 13 + entropie/src/world/terrain.h | 4 + entropie/src/world/wetness.c | 40 +++ entropie/src/world/wetness.h | 5 + entropie/src/world/world.c | 2 + 28 files changed, 1101 insertions(+), 59 deletions(-) diff --git a/commit_message.txt b/commit_message.txt index ff953ba..bd1ec9d 100644 --- a/commit_message.txt +++ b/commit_message.txt @@ -1,11 +1,10 @@ -entropie 2.17.0: arrival, car, seats, crates, tents +entropie 2.18.0: saving and loading, start menu -- Arriving: a stretch after the drive, then over to the fire together at a - stroll; Kurti walks slowly; "everybody" shouts leave out Kurti and sons -- Volkram drives, you sit in the back; a new old estate car that fits the - farm; heads no longer through the roof, seats lean back -- Nobody sits on the same spot as someone else (benches, logs, chairs) -- Kurti: one rounded heavy build (chest into belly, not a ball) -- Empty bottles go back into crates (you and the tidy NPCs) -- NPCs walk as fast as you and slow down carrying crates like you -- Tents sit on the ground (no hovering); move a tent without pegs with F +- Start menu: continue (the autosave), new game, load a save, quit +- Esc menu: save as (type a name), quick save, load a save +- F5 quick save, F9 quick load; autosave every 5 minutes and on quit +- Saves: a versioned text file plus the ground (worn paths, wet ground); + the world is rebuilt and everything put back (people, pockets, clothes, + tents, crates, felled trees, the car, the weather, the time) +- Format version and migration steps so old saves keep working +- Fix: a freed people list could be used after rebuilding the world diff --git a/entropie/ARCHITECTURE.md b/entropie/ARCHITECTURE.md index f196f39..ff35b49 100644 --- a/entropie/ARCHITECTURE.md +++ b/entropie/ARCHITECTURE.md @@ -12,7 +12,7 @@ same version when the structure changes (rules for working on it: [RULES.md](RUL | `src/render/` | renderer and its passes, shaders' uniforms, meshes, procedural shapes (`shapes*.c`), figures, camera, sky, UI | core, math, render | | `src/world/` | `Object`, `World`, physics, terrain, the map (`site.c`, `scene.c`, `manor.c`, `course.c`), tools, forestry, ladder, hose, camp, clothes, car, water, weather, grass, grid, day | everything but game | | `src/people/` | bodies (animation), hands, AI, talk, condition, dog, climbing | everything but game | -| `src/game/` | the frame: input to actions, player, camera follow, talk and dev UI, the arrival by car at the start | all | +| `src/game/` | the frame: input to actions, player, camera follow, talk and dev UI, the arrival by car at the start, saves (`save.c`) | all | - `render` knows nothing of the world: it gets meshes, matrices and plain numbers. - `world` and `people` are one layer: an `Object` owns its `Body`, `Ai` and `Hands`. @@ -78,6 +78,18 @@ of view (objects, 8×8 terrain tiles). pass draws the outlines over it all. - New GL calls: add them to the list in `core/gl.h`. +## Saves + +- `src/game/save.c`: `<name>.sav` (text: `key value` lines, `[game]`, then `[o id]` per object) and `<name>.ground` (binary: the wear map, the wet chunks). In the user's data folder (`Platform_saveDir`). +- Loading builds the world as at the start (`Game_initWorld(game, false)`: no drive in), finds the scene's things by `scene_id` / `spawn` / `spawn_type` (set when the scene is built), makes the rest anew (`Object_create` with the saved type, palette, size and `make_arg`), then sets the fields and rebuilds what looks different (trees, tents, gear, crates, bags, bottles, ladders, pullovers, the car). +- What's saved: the tables `THING` and `PERSON` in save.c (plus skills, likes, keys, hands, the car's boot, asleep). Buildings, walls, roads, bushes: only found again. +- Keeping old saves working: + - A new field: add it to a table. Old saves don't have it: it keeps what the fresh world gives it. + - A key renamed or its meaning changed: raise `SAVE_FORMAT` (save.h) and add a step to `migrate()` that rewrites the old lines. + - A save from a newer format is refused, never guessed at. + - A thing the scene no longer makes is dropped; one it newly makes keeps its fresh state. +- Not kept (comes back fresh): what anybody was in the middle of (AI plans, paths, poses; sitting: they stand), the hose (lies at its tap), sparks and smoke. + ## Versions `Makefile` `VERSION`; the build id is an md5 of sources and assets diff --git a/entropie/Makefile b/entropie/Makefile index ce1d7d2..49a6ec8 100644 --- a/entropie/Makefile +++ b/entropie/Makefile @@ -1,5 +1,5 @@ APP := entropie -VERSION := 2.17.0 +VERSION := 2.18.0 PREFIX ?= /usr BINDIR ?= $(PREFIX)/bin diff --git a/entropie/README.md b/entropie/README.md index 1c8e93c..3fa2578 100644 --- a/entropie/README.md +++ b/entropie/README.md @@ -677,6 +677,15 @@ say no more often and don't come to you for a bit. your skills. Only pressing needs show otherwise - The others live the same day: they eat, drink, pee and sleep at night +## Saving + +- At the start: continue (the autosave), a new game, or load a save +- Esc: save as (type a name), quick save, load a save +- F5 quick save, F9 load it +- Autosave every 5 minutes and on quit +- Saves live in your data folder (on Linux: `~/.local/share/entropie/entropie/saves`) +- Old saves keep working with newer versions + ## Controls | Input | Action | @@ -717,7 +726,8 @@ say no more often and don't come to you for a bit. | I | Show all keys in game | | Tab | Your stats (Left / Right: other page) | | F1 | Dev menu: pick anyone, change body, needs, temperament, likes and skills (Up / Down, Left / Right); teleport you to the parkour or the yard; time of day, game speed, weather, wind, skip the start | -| Esc | Menu: resume, keys, quit (the game waits meanwhile) | +| Esc | Menu: resume, save, load, keys, quit (the game waits meanwhile) | +| F5 / F9 | Quick save / quick load | The game starts in third person. Close to a wall the view moves in, never through it. With a tool and nothing to work on, a click drinks from the other hand. diff --git a/entropie/STATE.md b/entropie/STATE.md index 02a6946..f940ffe 100644 --- a/entropie/STATE.md +++ b/entropie/STATE.md @@ -266,6 +266,12 @@ Round 2.17: - Bottles back into crates (`Crate_putBottle`, `crate_empties`): left click at a crate with an empty in hand (or both hands full) puts it in; taking one out gives a full one ("Only empties left in this one"). The tidy carry empties to the nearest crate with room within 50 m instead of the bin (`ai_help.c`). - Tents stood on the sleeping bags lying where they were spread (`World_groundHeight` counted them): on the terrain now (also when blown about). Moving one (`game_actions.c` moveTent, `Tent_movable` / `Tent_startMove`, `tent_dragged`): no pegs in it, hold F: it comes along in front of you, turning with you, set down where you let go; then it goes up again from there (hold left click: the pegs). Nobody works on it while it's moved. +Round 2.18: +- Saves (`game/save.c`, `save.h`, SAVE_FORMAT 1; see ARCHITECTURE.md "Saves"): `Save_write` / `Save_read` / `Save_list` / `Save_blocked` (not while driving in, or in the car). Text `.sav` (field tables THING / PERSON by offsetof, references as save ids) + binary `.ground` (`Terrain_writeWear` / `readWear`, `Wetness_write` / `read`). Objects carry `palette`, `make_arg` (Object_create), `scene_id`, `spawn`, `spawn_type` (Game_initWorld), `cloth_gap`. Rebuild helpers: `Object_rebuildTree`, `Tent_refresh`, `Gear_refresh`, `Clothes_rebuild`, `Clothes_whereText`, `Ladder_setExtend`, `Object_fillBag`, `Object_setDrink`, `Object_shatter`. Held things come back in the hand (`Hands_receive`; too big for one hand: at their feet), sleepers asleep, people in tents / the car back in. +- `Game_initWorld(game, arrive)`; Game_init = with the drive in. `World_init` / `World_destroy` forget the people list (Object_seePeople) - loading used the old world's freed array. +- Menus (`main.c`): a start menu over the waiting world (Continue: the autosave, New game, Load a save, Quit); Esc: Resume, Save as (`Platform_typing`, `Input.text` / `erase`: letters, digits, - _), Quick save, Load a save, Keys, Quit (autosaves). F5 quick save, F9 quick load. Autosave every AUTOSAVE_EVERY 300 s of play and on quit / closing the window. Folder: `Platform_saveDir` (SDL_GetPrefPath + "saves"). +- Tested in the harness: a save written and read back (the same process, and another one): people, pockets, a tent half taken down, crates, rain, the wet ground, a pullover put down, a felled tree with fewer branches; under AddressSanitizer. The menus themselves aren't run here (no SDL library to link): compiled with all warnings only. + Unfinished building (Shell_build, at SHELL_AT in the frame's frame): brick shell 6.4 x 4.7, walls 2.9, open front (2 m) to the frame (now only the front half: pillars at z -13.8 / -9.2), window holes, bare floor, tiled roof with slabs. Barn: brick courses on BUILDING_BRICK walls (`addBrickCourses`: mortar a shade lighter, so the ink doesn't trace it), ivy (`Shapes_ivy`, OBJ_STRUCTUREs) on both long sides; the woodpile (OBJ_WOODPILE "Woodpile", WOODPILE_A/B) left of the toilet, everybody's woodpile from the start (`scene.c` Scene_finish). Fireplace tripod: wooden poles, feet TRIPOD_OUT 0.9 m outside the stones, TRIPOD_PAST 0.4 m past the top, a lashing; each pole an OBJ_WALL along it ("Tripod pole"). Gates (FURN_GATE, green boards): in your garages' doorways (GATES: hinge, end), `gate_open` / `gate_t`, `hinge`, `gate_shut` / `gate_out`; Object_swingGate (1.2 s), driven by showProps; a click within 1.2 m opens / shuts both leaves. Props that show (1.89, `world.c` showProps every frame): the tin's mesh is built again when its butts change (`Shapes_ashtray`: up to 12 show, the ash rises, a heap from 14; `shown` = what's built). @@ -415,6 +421,9 @@ Most important first. - Tents, still to do: NPCs taking theirs down on their own (leaving) - Weather, still to do: a sound for thunder, wind and rain (no sound in the game yet); a hat or anything held blown out of the hands +**Saves, still to do** +- The hose (its rope) and things being done (sitting, a job half way, a ladder being climbed) come back fresh, not as they were + **Tools for us** - Optional local AI (llama.cpp, small model) for talking and tasks; the menu stays as fallback @@ -422,4 +431,4 @@ Most important first. - Drawing many figures on a real graphics card: not measured (software GL: 300 people cost 3x the rest of the scene) --- -State of `entropie 2.17.0 (build 603c526)` +State of `entropie 2.18.0 (build a8a2692)` diff --git a/entropie/src/core/input.c b/entropie/src/core/input.c index b5c5bc0..36fff03 100644 --- a/entropie/src/core/input.c +++ b/entropie/src/core/input.c @@ -1,6 +1,8 @@ #include "core/input.h" #include <SDL3/SDL.h> +#include <stdio.h> +#include <string.h> #define MOUSE_SENSITIVITY 0.1f /* degrees per pixel */ @@ -12,6 +14,10 @@ void Input_poll(Input *input) { if (event.type == SDL_EVENT_QUIT) input->quit = true; if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && event.button.button == SDL_BUTTON_LEFT) input->click = true; if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && event.button.button == SDL_BUTTON_RIGHT) input->right_click = true; + if (event.type == SDL_EVENT_TEXT_INPUT) { + size_t have = strlen(input->text); + snprintf(input->text + have, sizeof(input->text) - have, "%s", event.text.text); + } if (event.type != SDL_EVENT_KEY_DOWN || event.key.repeat) continue; switch (event.key.scancode) { case SDL_SCANCODE_ESCAPE: input->escape = true; break; @@ -26,7 +32,7 @@ void Input_poll(Input *input) { case SDL_SCANCODE_C: input->sit = true; break; case SDL_SCANCODE_R: input->call = true; break; case SDL_SCANCODE_H: input->dismiss_help = true; break; - case SDL_SCANCODE_BACKSPACE: input->cancel = true; break; + case SDL_SCANCODE_BACKSPACE: input->cancel = input->erase = true; break; case SDL_SCANCODE_I: input->show_keys = true; break; case SDL_SCANCODE_TAB: input->show_stats = true; break; case SDL_SCANCODE_P: input->pee = true; break; @@ -44,6 +50,8 @@ void Input_poll(Input *input) { case SDL_SCANCODE_DOWN: input->menu_move = 1; break; case SDL_SCANCODE_UP: input->menu_move = -1; break; case SDL_SCANCODE_F1: input->dev_menu = true; break; + case SDL_SCANCODE_F5: input->quick_save = true; break; + case SDL_SCANCODE_F9: input->quick_load = true; break; default: break; } } diff --git a/entropie/src/core/input.h b/entropie/src/core/input.h index 9bcd5cd..4c56220 100644 --- a/entropie/src/core/input.h +++ b/entropie/src/core/input.h @@ -43,6 +43,10 @@ typedef struct { int menu_move; /* Down / Up arrow pressed: next / previous line (dev menu) */ bool dev_menu; /* F1 pressed: dev menu on / off */ bool pockets; /* B pressed: your pockets open / shut */ + char text[32]; /* typed this frame (only while typing a name: see Platform_typing) */ + bool erase; /* Backspace pressed (typing: one letter back) */ + bool quick_save; /* F5 pressed */ + bool quick_load; /* F9 pressed */ } Input; /* Pumps SDL events and fills in the current frame's input. */ diff --git a/entropie/src/core/platform.c b/entropie/src/core/platform.c index 74ebb77..4b3a095 100644 --- a/entropie/src/core/platform.c +++ b/entropie/src/core/platform.c @@ -100,3 +100,16 @@ void Platform_windowSize(const Platform *p, int *width, int *height) { void Platform_captureMouse(const Platform *p, bool captured) { SDL_SetWindowRelativeMouseMode(p->window, captured); } + +bool Platform_saveDir(char *out, size_t out_size) { + char *base = SDL_GetPrefPath("entropie", "entropie"); + if (!base) return false; + snprintf(out, out_size, "%ssaves", base); + SDL_free(base); + return true; +} + +void Platform_typing(const Platform *p, bool on) { + if (on) SDL_StartTextInput(p->window); + else SDL_StopTextInput(p->window); +} diff --git a/entropie/src/core/platform.h b/entropie/src/core/platform.h index dd64e8d..126aa51 100644 --- a/entropie/src/core/platform.h +++ b/entropie/src/core/platform.h @@ -35,4 +35,9 @@ void Platform_captureMouse(const Platform *p, bool captured); */ bool Platform_assetPath(const char *relative, char *out, size_t out_size); +/* Where the saves go (the user's data folder: e.g. ~/.local/share/entropie/entropie/saves). */ +bool Platform_saveDir(char *out, size_t out_size); +/* Typing text on / off (a save's name): letters come as Input.text, not as keys. */ +void Platform_typing(const Platform *p, bool on); + #endif /* CORE_PLATFORM_H */ diff --git a/entropie/src/game/game.c b/entropie/src/game/game.c index 9882dd9..a6dc4b9 100644 --- a/entropie/src/game/game.c +++ b/entropie/src/game/game.c @@ -212,7 +212,9 @@ int Game_stats(const Game *game, StatBar *bars, int max, const char **title, cha return count; } -bool Game_init(Game *game) { +bool Game_init(Game *game) { return Game_initWorld(game, true); } + +bool Game_initWorld(Game *game, bool arrive) { game->dark = 0.0f; game->show_keys = false; game->fast_sleep = false; @@ -244,8 +246,14 @@ bool Game_init(Game *game) { } Camera_init(&game->camera, game->camera.position, look * 180.0f / PI_F, 0.0f, CAMERA_FOV); game->marker = Shapes_marker(1.8f, 0.8f); + for (int i = 0; i < game->world.count; i++) { /* (what the scene made: a save finds it again by this) */ + Object *o = game->world.objects[i]; + o->scene_id = i + 1; + o->spawn = o->position; + o->spawn_type = o->type; + } memset(&game->arrival, 0, sizeof(game->arrival)); - if (ARRIVE_BY_CAR && !START_AT_COURSE) game_arrivalStart(game); + if (arrive && ARRIVE_BY_CAR && !START_AT_COURSE) game_arrivalStart(game); followPlayer(game, 0.0f); return true; } diff --git a/entropie/src/game/game.h b/entropie/src/game/game.h index 3d40071..d8fcabb 100644 --- a/entropie/src/game/game.h +++ b/entropie/src/game/game.h @@ -99,6 +99,8 @@ int Game_choices(const Game *game, const char *out[TALK_CHOICES]); const char *Game_hint(Game *game); bool Game_init(Game *game); +/* The world as at the start; arrive: and the drive in (a fresh game). A save is loaded onto it without. */ +bool Game_initWorld(Game *game, bool arrive); void Game_shutdown(Game *game); void Game_update(Game *game, const Input *input, float dt, float aspect); diff --git a/entropie/src/game/save.c b/entropie/src/game/save.c new file mode 100644 index 0000000..3975626 --- /dev/null +++ b/entropie/src/game/save.c @@ -0,0 +1,629 @@ +#define _POSIX_C_SOURCE 200809L +#include "game/save.h" +#include "game/game_internal.h" +#include "world/car.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 "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), + 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), + 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), +}; + +#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: case OBJ_HOSE: + 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->player && game->player->riding) return "Not while you're in the car"; + 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, "%Y-%m-%d %H:%M", localtime(&now)); + fprintf(f, "entropie-save %d\n", SAVE_FORMAT); + fprintf(f, "version %s\n", APP_VERSION); + fprintf(f, "label %s in the game - saved %s - %s\n", clock, date, APP_VERSION); + + 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, "told %d %d\n", game->rain_told, game->wet_told); + + 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"); + 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_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[512], *s = buf; + 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; } + } +} + +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(); + 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); + } + 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_DECAL: case OBJ_STRUCTURE: case OBJ_WALL: case OBJ_HUMAN: 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, "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: 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; + 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, "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 (asleep) Body_lieDown(&o->body); + } + 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); + + /* 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; +} diff --git a/entropie/src/game/save.h b/entropie/src/game/save.h new file mode 100644 index 0000000..ddaa2c7 --- /dev/null +++ b/entropie/src/game/save.h @@ -0,0 +1,44 @@ +#ifndef GAME_SAVE_H +#define GAME_SAVE_H + +/* + * Saving and loading a game: a text file (<name>.sav, "key value" lines: the + * game, then every object) and a binary one (<name>.ground: worn paths and + * the wet ground). Loading builds the world as at the start (no drive in), + * finds the scene's things again by where they were made, makes the rest + * anew, and puts everything back as it was. + * + * Keeping old saves working (see ARCHITECTURE.md "Saves"): + * - A new field: just add it to a table below. Old saves don't have it: it + * keeps what the fresh world (or Object_create) gives it. + * - A field renamed, or its meaning changed: raise SAVE_FORMAT and add a step + * to migrate() in save.c that rewrites the old keys / values. + * - Saves from a newer format are refused (not guessed at). + */ + +#include <stdbool.h> +#include <stddef.h> + +#include "game/game.h" + +#define SAVE_FORMAT 1 +#define SAVE_NAME_MAX 48 +#define SAVE_LIST_MAX 32 +#define SAVE_AUTO "autosave" + +typedef struct { + char name[SAVE_NAME_MAX]; /* file name without .sav */ + char label[96]; /* "Day 1, 14:05 - 2.18.0" */ + long long when; /* written (seconds since 1970): newest first */ +} SaveInfo; + +/* Writes the game as <dir>/<name>.sav (and .ground). False: why, in err. */ +bool Save_write(const Game *game, const char *dir, const char *name, char *err, size_t err_size); +/* Loads <dir>/<name>.sav into game: it is shut down and built again first. */ +bool Save_read(Game *game, const char *dir, const char *name, char *err, size_t err_size); +/* The saves in dir, newest first; how many. */ +int Save_list(const char *dir, SaveInfo *out, int max); +/* Saving isn't possible right now (driving in): why, or NULL. */ +const char *Save_blocked(const Game *game); + +#endif /* GAME_SAVE_H */ diff --git a/entropie/src/main.c b/entropie/src/main.c index baf8c78..3dcb529 100644 --- a/entropie/src/main.c +++ b/entropie/src/main.c @@ -5,7 +5,9 @@ * Layers: core (platform, gl, input) -> render -> game -> main */ +#include <math.h> #include <stdio.h> +#include <string.h> #include <SDL3/SDL.h> #include "build_info.h" /* generated by the Makefile */ @@ -15,6 +17,7 @@ #include "render/renderer.h" #include "render/skybox.h" #include "render/ui.h" +#include "game/save.h" #define APP_NAME "Entropie" #define WINDOW_WIDTH 800 @@ -31,7 +34,7 @@ #define STATS_LABEL 48 /* the stats panel */ #define STATS_MAX 12 #define DEV_LABEL 64 /* the dev menu (F1) */ -#define MENU_LABEL 80 /* the menu (Esc) */ +#define MENU_LABEL 104 /* the menus (start, Esc, load, save as): up to 16 slots */ #define POCKET_LABEL 88 /* your pockets (B) */ static const char *const KEYS[] = { /* proportional font: no column padding */ @@ -64,7 +67,7 @@ static const char *const KEYS[] = { /* proportional font: no column padding */ "F1: dev menu (change anybody's stats)", "Tab: your stats (arrow keys: next page)", "K: smoke; B: your pockets (Up / Down, Enter: take out or give)", - "Esc: menu (resume, keys, quit)", + "Esc: menu (resume, save, load, keys, quit); F5 quick save, F9 quick load", }; #define KEY_COUNT ((int)(sizeof(KEYS) / sizeof(KEYS[0]))) #define MAX_FRAME_DT 0.1f /* avoids tunneling after stalls (window drag etc.) */ @@ -128,46 +131,134 @@ static void drawPockets(Ui *ui, Game *game, int width, int height) { } } -/* ---------- The menu (Esc): the game waits meanwhile ---------- */ +/* ---------- Menus: the start, Esc (the game waits meanwhile), loading, naming a save ---------- */ -enum { MENU_RESUME, MENU_KEYS, MENU_QUIT, MENU_COUNT }; -static const char *const MENU_TEXT[MENU_COUNT] = { "Resume", "Keys (show / hide)", "Quit" }; -#define MENU_W 380.0f -#define MENU_BH 52.0f /* button height */ -#define MENU_GAP 14.0f +typedef enum { SCREEN_NONE, SCREEN_START, SCREEN_PAUSE, SCREEN_LOAD, SCREEN_NAME } Screen; +typedef enum { ACT_NONE, ACT_NEW, ACT_CONTINUE, ACT_LOAD_LIST, ACT_QUIT, ACT_RESUME, ACT_SAVE_AS, ACT_QUICK_SAVE, + ACT_KEYS, ACT_LOAD, ACT_BACK, ACT_NAME, ACT_SAVE_NAMED } Action; +#define MENU_ITEMS 14 +#define MENU_W 520.0f +#define MENU_BH 44.0f /* button height */ +#define MENU_GAP 10.0f +#define AUTOSAVE_EVERY 300.0f /* s of play between autosaves */ +#define QUICK_SAVE "quicksave" + +typedef struct { + Screen screen, back; /* back: where "Back" / Esc goes from the load list or the name */ + int chosen, count; + char item[MENU_ITEMS][UI_TEXT_MAX]; + Action act[MENU_ITEMS]; + int arg[MENU_ITEMS]; /* ACT_LOAD: which save */ + const char *title; + SaveInfo saves[SAVE_LIST_MAX]; + int save_count; + char name[SAVE_NAME_MAX]; /* a save's name being typed */ + char dir[512]; /* where the saves go */ +} Menu; + +static void menuAdd(Menu *m, Action act, int arg, const char *text) { + if (m->count >= MENU_ITEMS) return; + snprintf(m->item[m->count], UI_TEXT_MAX, "%s", text); + m->act[m->count] = act; + m->arg[m->count] = arg; + m->count++; +} + +/* Fills the menu for a screen. */ +static void menuOpen(Menu *m, Screen screen) { + m->screen = screen; + m->count = 0; + m->chosen = 0; + char line[UI_TEXT_MAX]; + switch (screen) { + case SCREEN_START: { + m->title = APP_NAME " " APP_VERSION; + m->save_count = Save_list(m->dir, m->saves, SAVE_LIST_MAX); + int autosave = -1; + for (int i = 0; i < m->save_count; i++) if (!strcmp(m->saves[i].name, SAVE_AUTO)) autosave = i; + if (autosave >= 0) { + snprintf(line, sizeof line, "Continue (%.60s)", m->saves[autosave].label); + menuAdd(m, ACT_CONTINUE, autosave, line); + } + menuAdd(m, ACT_NEW, 0, "New game"); + if (m->save_count > 0) menuAdd(m, ACT_LOAD_LIST, 0, "Load a save"); + menuAdd(m, ACT_QUIT, 0, "Quit"); + break; + } + case SCREEN_PAUSE: + m->title = "Paused"; + menuAdd(m, ACT_RESUME, 0, "Resume"); + menuAdd(m, ACT_SAVE_AS, 0, "Save as ..."); + menuAdd(m, ACT_QUICK_SAVE, 0, "Quick save (F5; F9 loads it)"); + menuAdd(m, ACT_LOAD_LIST, 0, "Load a save"); + menuAdd(m, ACT_KEYS, 0, "Keys (show / hide)"); + menuAdd(m, ACT_QUIT, 0, "Quit (autosaves)"); + break; + case SCREEN_LOAD: + m->title = "Load a save"; + m->save_count = Save_list(m->dir, m->saves, SAVE_LIST_MAX); + for (int i = 0; i < m->save_count && m->count < MENU_ITEMS - 1; i++) { + snprintf(line, sizeof line, "%.24s - %.60s", m->saves[i].name, m->saves[i].label); + menuAdd(m, ACT_LOAD, i, line); + } + menuAdd(m, ACT_BACK, 0, "Back"); + break; + case SCREEN_NAME: + m->title = "Save as (type a name, Enter)"; + snprintf(line, sizeof line, "%s_", m->name); + menuAdd(m, ACT_NAME, 0, line); + menuAdd(m, ACT_SAVE_NAMED, 0, "Save"); + menuAdd(m, ACT_BACK, 0, "Back"); + m->chosen = 1; + break; + case SCREEN_NONE: + break; + } +} /* Button k's box (pixels, y from the bottom). */ -static void menuButton(int k, int width, int height, float *x, float *y) { - float total = (float)MENU_COUNT * (MENU_BH + MENU_GAP) - MENU_GAP; +static void menuButton(const Menu *m, int k, int width, int height, float *x, float *y) { + float total = (float)m->count * (MENU_BH + MENU_GAP) - MENU_GAP; *x = ((float)width - MENU_W) * 0.5f + 20.0f; *y = (float)height * 0.5f + total * 0.5f - (float)(k + 1) * MENU_BH - (float)k * MENU_GAP - 20.0f; } /* The button under the pointer, or -1. */ -static int menuHit(const Platform *platform, const Input *in, int width, int height) { +static int menuHit(const Menu *m, const Platform *platform, const Input *in, int width, int height) { int ww, wh; Platform_windowSize(platform, &ww, &wh); if (ww <= 0 || wh <= 0) return -1; float mx = in->mouse_x * (float)width / (float)ww, my = (float)height - in->mouse_y * (float)height / (float)wh; - for (int k = 0; k < MENU_COUNT; k++) { + for (int k = 0; k < m->count; k++) { float x, y; - menuButton(k, width, height, &x, &y); + menuButton(m, k, width, height, &x, &y); if (mx >= x && mx <= x + MENU_W - 40.0f && my >= y && my <= y + MENU_BH) return k; } return -1; } -static void drawMenu(Ui *ui, int chosen, int width, int height) { - float total = (float)MENU_COUNT * (MENU_BH + MENU_GAP) - MENU_GAP + 40.0f + 50.0f; - float px = ((float)width - MENU_W) * 0.5f, py = (float)height * 0.5f - total * 0.5f + 25.0f; +static void drawMenu(Ui *ui, const Menu *m, const char *note, int width, int height) { + float total = (float)m->count * (MENU_BH + MENU_GAP) - MENU_GAP + 40.0f + 50.0f + (note ? 36.0f : 0.0f); + float px = ((float)width - MENU_W) * 0.5f, py = (float)height * 0.5f - total * 0.5f + 25.0f - (note ? 36.0f : 0.0f); Ui_drawPatch(ui, px, py - 20.0f, MENU_W, total, 0.93f, 0.85f, 0.67f, 0.95f, 1.0f, width, height); - Ui_drawTextLeft(ui, MENU_LABEL, "Paused", px + 24.0f, py + total - 70.0f, width, height); - for (int k = 0; k < MENU_COUNT; k++) { + Ui_drawTextLeft(ui, MENU_LABEL, m->title, px + 24.0f, py + total - 70.0f, width, height); + for (int k = 0; k < m->count; k++) { float x, y; - menuButton(k, width, height, &x, &y); - bool on = k == chosen; + menuButton(m, k, width, height, &x, &y); + bool on = k == m->chosen; Ui_drawPatch(ui, x, y, MENU_W - 40.0f, MENU_BH, on ? 0.95f : 0.86f, on ? 0.72f : 0.80f, on ? 0.40f : 0.66f, 1.0f, 0.6f, width, height); - Ui_drawTextLeft(ui, MENU_LABEL + 1 + k, MENU_TEXT[k], x + 22.0f, y + 12.0f, width, height); + Ui_drawTextLeft(ui, MENU_LABEL + 1 + k, m->item[k], x + 18.0f, y + 9.0f, width, height); + } + if (note) Ui_drawTextLeft(ui, MENU_LABEL + 1 + MENU_ITEMS, note, px + 24.0f, py - 6.0f, width, height); +} + +/* A name typed in: letters, digits, - and _ (spaces become -). */ +static void typeName(char *name, const Input *in) { + size_t n = strlen(name); + if (in->erase && n > 0) name[--n] = '\0'; + for (const char *c = in->text; *c && n + 1 < SAVE_NAME_MAX - 1; c++) { + char ch = *c == ' ' ? '-' : *c; + if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '-' || ch == '_') { name[n++] = ch; name[n] = '\0'; } } } @@ -194,48 +285,137 @@ int main(int argc, char **argv) { Platform_frameDelta(&platform); /* reset timer after loading */ float anim_time = 0.0f; /* real seconds, drives the painted animation */ - bool menu_open = false, look_settle = false; - int menu_choice = 0; - float menu_mouse_x = -1.0f, menu_mouse_y = -1.0f; + bool look_settle = false; + float menu_mouse_x = -1.0f, menu_mouse_y = -1.0f, autosave_t = 0.0f, note_time = 0.0f; + char note[UI_TEXT_MAX] = ""; + static Menu menu; + if (!Platform_saveDir(menu.dir, sizeof menu.dir)) snprintf(menu.dir, sizeof menu.dir, "saves"); + menuOpen(&menu, SCREEN_START); /* (the world waits behind it: a new game starts from here) */ + Platform_captureMouse(&platform, false); for (;;) { float dt = Platform_frameDelta(&platform); if (dt > MAX_FRAME_DT) dt = MAX_FRAME_DT; anim_time += dt; + note_time = fmaxf(0.0f, note_time - dt); Input input; Input_poll(&input); - if (input.quit) break; + bool playing = menu.screen == SCREEN_NONE || menu.screen == SCREEN_PAUSE || menu.back == SCREEN_PAUSE; + if (input.quit) { + char err[160]; + if (playing && menu.screen != SCREEN_START) Save_write(&game, menu.dir, SAVE_AUTO, err, sizeof err); /* (closing the window: autosaved) */ + break; + } int width, height; Platform_drawableSize(&platform, &width, &height); - if (input.escape) { /* the menu, or back to the game */ - menu_open = !menu_open; - menu_choice = MENU_RESUME; - Platform_captureMouse(&platform, !menu_open); + Action act = ACT_NONE; + int arg = 0; + if (input.escape) { /* the menu, or back to the game (from a list: back a step) */ + if (menu.screen == SCREEN_NONE) menuOpen(&menu, SCREEN_PAUSE); + else if (menu.screen == SCREEN_PAUSE) act = ACT_RESUME; + else if (menu.screen == SCREEN_LOAD || menu.screen == SCREEN_NAME) act = ACT_BACK; + Platform_captureMouse(&platform, menu.screen == SCREEN_NONE); look_settle = true; } - if (menu_open) { - int hit = menuHit(&platform, &input, width, height); - if (hit >= 0 && (input.mouse_x != menu_mouse_x || input.mouse_y != menu_mouse_y)) menu_choice = hit; + if (menu.screen == SCREEN_NONE && (input.quick_save || input.quick_load)) { + char err[160]; + if (input.quick_save) { + bool ok = Save_write(&game, menu.dir, QUICK_SAVE, err, sizeof err); + snprintf(note, sizeof note, ok ? "Quick saved" : "Not saved: %s", err); + } else { + bool ok = Save_read(&game, menu.dir, QUICK_SAVE, err, sizeof err); + snprintf(note, sizeof note, ok ? "Quick save loaded" : "Not loaded: %s", err); + look_settle = true; + } + note_time = 3.0f; + } + if (menu.screen != SCREEN_NONE && act == ACT_NONE) { + int hit = menuHit(&menu, &platform, &input, width, height); + if (hit >= 0 && (input.mouse_x != menu_mouse_x || input.mouse_y != menu_mouse_y)) menu.chosen = hit; menu_mouse_x = input.mouse_x; menu_mouse_y = input.mouse_y; - if (input.menu_move) menu_choice = (menu_choice + input.menu_move + MENU_COUNT) % MENU_COUNT; - int pick = input.enter ? menu_choice : (input.click && hit >= 0 ? hit : -1); - if (pick == MENU_QUIT) break; - if (pick == MENU_KEYS) game.show_keys = !game.show_keys; - if (pick == MENU_RESUME) { - menu_open = false; + if (input.menu_move) menu.chosen = (menu.chosen + input.menu_move + menu.count) % menu.count; + if (menu.screen == SCREEN_NAME) { /* typing the name */ + typeName(menu.name, &input); + snprintf(menu.item[0], UI_TEXT_MAX, "%s_", menu.name); + } + int pick = input.enter ? menu.chosen : (input.click && hit >= 0 ? hit : -1); + if (menu.screen == SCREEN_NAME && input.enter) pick = 1; /* (Enter: save it) */ + if (pick >= 0 && pick < menu.count) { act = menu.act[pick]; arg = menu.arg[pick]; } + } + + char err[160] = ""; + switch (act) { + case ACT_NEW: case ACT_RESUME: + menu.screen = SCREEN_NONE; + menu.back = SCREEN_NONE; + Platform_captureMouse(&platform, true); + look_settle = true; + break; + case ACT_CONTINUE: case ACT_LOAD: + if (Save_read(&game, menu.dir, menu.saves[arg].name, err, sizeof err)) { + snprintf(note, sizeof note, "Loaded %s", menu.saves[arg].name); + menu.screen = SCREEN_NONE; + menu.back = SCREEN_NONE; Platform_captureMouse(&platform, true); look_settle = true; - } - } else { + autosave_t = 0.0f; + } else snprintf(note, sizeof note, "Not loaded: %.80s", err); + note_time = 3.0f; + break; + case ACT_LOAD_LIST: + menu.back = menu.screen; + menuOpen(&menu, SCREEN_LOAD); + break; + case ACT_SAVE_AS: + if (Save_blocked(&game)) { snprintf(note, sizeof note, "%s", Save_blocked(&game)); note_time = 3.0f; break; } + menu.back = SCREEN_PAUSE; + menu.name[0] = '\0'; + menuOpen(&menu, SCREEN_NAME); + Platform_typing(&platform, true); + break; + case ACT_SAVE_NAMED: + if (!menu.name[0]) break; + if (Save_write(&game, menu.dir, menu.name, err, sizeof err)) snprintf(note, sizeof note, "Saved as %s", menu.name); + else snprintf(note, sizeof note, "Not saved: %.80s", err); + note_time = 3.0f; + Platform_typing(&platform, false); + menuOpen(&menu, SCREEN_PAUSE); + menu.back = SCREEN_NONE; + break; + case ACT_QUICK_SAVE: + if (Save_write(&game, menu.dir, QUICK_SAVE, err, sizeof err)) snprintf(note, sizeof note, "Quick saved"); + else snprintf(note, sizeof note, "Not saved: %.80s", err); + note_time = 3.0f; + break; + case ACT_KEYS: + game.show_keys = !game.show_keys; + break; + case ACT_BACK: + if (menu.screen == SCREEN_NAME) Platform_typing(&platform, false); + menuOpen(&menu, menu.back == SCREEN_NONE ? SCREEN_PAUSE : menu.back); + menu.back = SCREEN_NONE; + break; + case ACT_QUIT: + if (menu.screen == SCREEN_PAUSE) Save_write(&game, menu.dir, SAVE_AUTO, err, sizeof err); + goto done; + case ACT_NONE: case ACT_NAME: + break; + } + + if (menu.screen == SCREEN_NONE) { if (look_settle) { /* (the first mouse move after the menu is a jump: ignored) */ input.look_yaw = input.look_pitch = 0.0f; look_settle = false; } Game_update(&game, &input, dt, (float)width / (float)height); + if ((autosave_t += dt) >= AUTOSAVE_EVERY && !Save_blocked(&game)) { /* (every few minutes, quietly) */ + autosave_t = 0.0f; + if (Save_write(&game, menu.dir, SAVE_AUTO, err, sizeof err)) { snprintf(note, sizeof note, "Autosaved"); note_time = 2.0f; } + } } Renderer_beginFrame(&renderer, width, height, anim_time); @@ -274,10 +454,12 @@ int main(int argc, char **argv) { } const char *hint = Game_hint(&game); if (hint) Ui_drawTextLine(&ui, HINT_LABEL, hint, answers, UI_BOTTOM_LEFT, width, height); - if (menu_open) drawMenu(&ui, menu_choice, width, height); + if (menu.screen != SCREEN_NONE) drawMenu(&ui, &menu, note_time > 0.0f ? note : NULL, width, height); + else if (note_time > 0.0f) Ui_drawText(&ui, MENU_LABEL + 1 + MENU_ITEMS, note, UI_CENTER, width, height); Platform_swap(&platform); } +done: exit_code = 0; cleanup: diff --git a/entropie/src/render/ui.h b/entropie/src/render/ui.h index 86aa0d5..395f77b 100644 --- a/entropie/src/render/ui.h +++ b/entropie/src/render/ui.h @@ -9,7 +9,7 @@ #include "core/gl.h" #define UI_TEXT_MAX 96 -#define UI_LABELS 100 +#define UI_LABELS 140 typedef enum { UI_TOP_LEFT, diff --git a/entropie/src/world/car.c b/entropie/src/world/car.c index 0772dca..4c65cbf 100644 --- a/entropie/src/world/car.c +++ b/entropie/src/world/car.c @@ -97,6 +97,14 @@ Object *Gear_create(GearKind kind, Object *owner, Vec3 at) { return item; } +void Gear_refresh(Object *gear) { + if (gear->type != OBJ_GEAR) return; + Mesh mesh = Shapes_gear(gear->variant, gearColor((GearKind)gear->variant, gear->owner)); + if (!mesh.vao) return; + Mesh_destroy(&gear->mesh); + gear->mesh = mesh; +} + Object *Car_takeOut(World *world, Object *car) { CarLoad *l = car->load; if (!l || l->n == 0 || !car->boot_open) return NULL; diff --git a/entropie/src/world/car.h b/entropie/src/world/car.h index 6157cad..893f733 100644 --- a/entropie/src/world/car.h +++ b/entropie/src/world/car.h @@ -66,5 +66,7 @@ void Car_serve(World *world); /* Somebody's camping thing (OBJ_GEAR) lying at `at` (not added to the world yet), or NULL. */ Object *Gear_create(GearKind kind, Object *owner, Vec3 at); +/* Its look again in its owner's colours (after loading a save). */ +void Gear_refresh(Object *gear); #endif /* WORLD_CAR_H */ diff --git a/entropie/src/world/clothes.c b/entropie/src/world/clothes.c index 75cf4ee..02109b6 100644 --- a/entropie/src/world/clothes.c +++ b/entropie/src/world/clothes.c @@ -143,6 +143,7 @@ bool Clothes_putDown(World *world, Object *who, const ClothSpot *s) { c->position = s->at; } c->variant = (int)s->lay; + c->cloth_gap = fmaxf(s->gap, 0.04f); c->rotation = vec3(0.0f, s->yaw, 0.0f); c->cloth_of = who; c->cloth_host = s->host; @@ -168,6 +169,23 @@ void Clothes_takeBack(Object *who, bool wear) { who->look.jacket_tied = who->look.top != TOP_JACKET; } +const char *Clothes_whereText(const char *saved) { + static const char *const ALL[] = { "over the chair's back", "on the ground", "over the railing", "over the wall", "over the bench", + "over the chair", "over it", "on the table", "on the bench", "on the chair", "on top of it" }; + for (size_t i = 0; saved && i < sizeof ALL / sizeof ALL[0]; i++) + if (!strcmp(saved, ALL[i])) return ALL[i]; + return "somewhere"; +} + +void Clothes_rebuild(Object *c) { + const Object *who = c->cloth_of; + if (c->type != OBJ_CLOTHES || !who || !who->pullover_down || who->pullover != c) return; + Mesh mesh = Shapes_pullover(c->variant, who->look.jacket, fmaxf(c->cloth_gap, 0.04f)); + if (!mesh.vao) return; + Mesh_destroy(&c->mesh); + c->mesh = mesh; +} + const char *Clothes_where(const Object *who) { return who->pullover_down && who->pullover ? who->pullover->cloth_where : NULL; } diff --git a/entropie/src/world/clothes.h b/entropie/src/world/clothes.h index d5d162f..4995c80 100644 --- a/entropie/src/world/clothes.h +++ b/entropie/src/world/clothes.h @@ -32,6 +32,10 @@ bool Clothes_putDown(World *world, Object *who, const ClothSpot *spot); void Clothes_takeBack(Object *who, bool wear); /* Where theirs is ("over the chair's back"), or NULL: with them. */ const char *Clothes_where(const Object *who); +/* The same words as said when it was put down (loading a save: the text back to one of ours). */ +const char *Clothes_whereText(const char *saved); +/* A pullover put down: its look again (after loading a save). */ +void Clothes_rebuild(Object *cloth); /* Somebody's pullover lying near this ray (from the eye, dir unit) within reach, or NULL. */ Object *Clothes_alongRay(const World *world, Vec3 eye, Vec3 dir, float reach); /* Every frame: pullovers go along with the chair they hang on; people sitting diff --git a/entropie/src/world/object.c b/entropie/src/world/object.c index 61d6043..6b8eaae 100644 --- a/entropie/src/world/object.c +++ b/entropie/src/world/object.c @@ -97,6 +97,8 @@ Object *Object_create(ObjectType type, Palette palette, Vec3 position, Vec3 size if (!obj) return NULL; obj->type = type; + obj->palette = palette; + obj->make_arg = weight; obj->hands.drink_hand = -1; obj->work_state.since = 1e3f; /* hasn't worked yet */ obj->main_hand = HAND_RIGHT; @@ -1005,6 +1007,10 @@ static void rebuildTree(Object *obj) { Object_syncColliders(obj); } +void Object_rebuildTree(Object *obj) { + if (obj->type == OBJ_TREE) rebuildTree(obj); +} + void Object_cutTrunk(Object *obj, float height) { if (obj->type != OBJ_TREE) return; height = clampf(height, 0.1f, obj->size.y - 0.5f); diff --git a/entropie/src/world/object.h b/entropie/src/world/object.h index 9e04483..38c66eb 100644 --- a/entropie/src/world/object.h +++ b/entropie/src/world/object.h @@ -127,6 +127,12 @@ typedef struct { typedef struct Object { ObjectType type; + Palette palette; /* as made (Object_create) ... */ + float make_arg; /* ... and the weight it was made with (crates, tents, gear: which one) */ + int scene_id; /* made by the scene (1..): saves find it again by this ... */ + Vec3 spawn; /* ... and where it was made ... */ + ObjectType spawn_type; /* ... as what */ + float cloth_gap; /* clothes: hung over an edge this thick */ Vec3 position; /* base point (bottom center) */ Vec3 rotation; /* Euler radians, applied Y, X, Z */ Vec3 pivot; /* mesh origin relative to position */ @@ -472,6 +478,8 @@ void Object_cutTrunk(Object *obj, float height); void Object_setBranches(Object *obj, int branches); /* Bottles and trees: starts falling over so the top lands in direction dir (horizontal). */ +/* A tree's mesh and colliders again from its trunk, crown and branches (after loading a save). */ +void Object_rebuildTree(Object *obj); void Object_topple(Object *obj, Vec3 dir); /* Turns a fallen tree (or a log) into a log of this length lying along axis, centered at center. Weight follows from the wood volume. */ diff --git a/entropie/src/world/tent.c b/entropie/src/world/tent.c index fd013df..f7dd374 100644 --- a/entropie/src/world/tent.c +++ b/entropie/src/world/tent.c @@ -96,6 +96,11 @@ void Tent_takeDown(Object *const *objects, int count, Object *tent) { reshape(tent); } +void Tent_refresh(Object *tent) { + tent->shown = -1; + reshape(tent); +} + bool Tent_movable(const Object *tent) { return tent && tent->type == OBJ_TENT && !tent->broken && (tent->tent_stage < TENT_PEGS || (tent->tent_stage == TENT_PEGS && tent->stored == 0) || (tent->tent_down && tent->tent_stage <= TENT_COVER)); diff --git a/entropie/src/world/tent.h b/entropie/src/world/tent.h index aa91650..232cefb 100644 --- a/entropie/src/world/tent.h +++ b/entropie/src/world/tent.h @@ -26,6 +26,8 @@ int Tent_work(Object *tent, Object *who, float dt); void Tent_takeDown(Object *const *objects, int count, Object *tent); /* No pegs in it (spread out, on its poles, the cover on, the pegs out): it can be moved. */ bool Tent_movable(const Object *tent); +/* Its look and box again for how far it's up (after loading a save). */ +void Tent_refresh(Object *tent); /* Picked up to move it: from now on it goes up again (a tent being taken down with its pegs out stays as it is, waiting for its pegs). */ void Tent_startMove(Object *tent); diff --git a/entropie/src/world/terrain.c b/entropie/src/world/terrain.c index b607a04..c73fbea 100644 --- a/entropie/src/world/terrain.c +++ b/entropie/src/world/terrain.c @@ -334,6 +334,19 @@ void Terrain_destroy(Terrain *t) { memset(t, 0, sizeof(*t)); } +bool Terrain_writeWear(const Terrain *t, FILE *f) { + unsigned res = TERRAIN_WEAR_RES; + return fwrite(&res, 4, 1, f) == 1 && fwrite(t->wear, sizeof(float), (size_t)res * res, f) == (size_t)res * res; +} + +bool Terrain_readWear(Terrain *t, FILE *f) { + unsigned res; + if (fread(&res, 4, 1, f) != 1 || res != TERRAIN_WEAR_RES) return false; + if (fread(t->wear, sizeof(float), (size_t)res * res, f) != (size_t)res * res) return false; + t->wear_dirty = true; + return true; +} + /* Bilinear sample of the wear map, matching GPU linear filtering. */ static float wearAt(const Terrain *t, float x, float z) { float gx = clampf((x + HALF) / WEAR_CELL - 0.5f, 0.0f, (float)(TERRAIN_WEAR_RES - 1)); diff --git a/entropie/src/world/terrain.h b/entropie/src/world/terrain.h index 6421b53..137af15 100644 --- a/entropie/src/world/terrain.h +++ b/entropie/src/world/terrain.h @@ -15,6 +15,7 @@ */ #include <stdbool.h> +#include <stdio.h> #include "core/gl.h" #include "world/object.h" @@ -73,6 +74,9 @@ Vec3 Terrain_grassColor(float x, float z); /* How trampled (0 meadow .. 1 bare path). */ float Terrain_wear(const Terrain *t, float x, float z); +/* Saves: the worn paths out to / in from a file. */ +bool Terrain_writeWear(const Terrain *t, FILE *f); +bool Terrain_readWear(Terrain *t, FILE *f); /* How hard the surface is (0 meadow .. 1 tar or concrete; gravel in between). */ float Terrain_hardness(const Terrain *t, float x, float z); diff --git a/entropie/src/world/wetness.c b/entropie/src/world/wetness.c index 427f30d..60f68ff 100644 --- a/entropie/src/world/wetness.c +++ b/entropie/src/world/wetness.c @@ -90,6 +90,46 @@ static WetChunk *newChunk(Terrain *t, int cx, int cz) { return c; } +#define WET_SAVE_TAG 0x57455431u /* "WET1" */ + +bool Wetness_write(const Wetness *w, FILE *f) { + unsigned tag = WET_SAVE_TAG, n = 0, res = WET_RES, chunk = WET_CHUNK; + for (int i = 0; i < w->live_count; i++) { const WetChunk *c = w->chunks[w->live[i]]; if (c && !c->dead) n++; } + if (fwrite(&tag, 4, 1, f) != 1 || fwrite(&res, 4, 1, f) != 1 || fwrite(&chunk, 4, 1, f) != 1 || fwrite(&n, 4, 1, f) != 1) return false; + for (int i = 0; i < w->live_count; i++) { + const WetChunk *c = w->chunks[w->live[i]]; + if (!c || c->dead) continue; + int head[2] = { c->cx, c->cz }; + if (fwrite(head, sizeof head, 1, f) != 1 || fwrite(c->water, sizeof c->water, 1, f) != 1 || fwrite(c->soil, sizeof c->soil, 1, f) != 1 || + fwrite(c->pee, sizeof c->pee, 1, f) != 1 || fwrite(c->mud, sizeof c->mud, 1, f) != 1) return false; + } + return true; +} + +bool Wetness_read(Terrain *t, FILE *f) { + unsigned tag, n, res, chunk; + if (fread(&tag, 4, 1, f) != 1 || tag != WET_SAVE_TAG || fread(&res, 4, 1, f) != 1 || fread(&chunk, 4, 1, f) != 1 || fread(&n, 4, 1, f) != 1) return false; + if (res != WET_RES || chunk != WET_CHUNK) return false; /* (another grid: the ground starts dry) */ + for (unsigned k = 0; k < n; k++) { + int head[2]; + WetChunk tmp; + if (fread(head, sizeof head, 1, f) != 1 || fread(tmp.water, sizeof tmp.water, 1, f) != 1 || fread(tmp.soil, sizeof tmp.soil, 1, f) != 1 || + fread(tmp.pee, sizeof tmp.pee, 1, f) != 1 || fread(tmp.mud, sizeof tmp.mud, 1, f) != 1) return false; + if (head[0] < 0 || head[1] < 0 || head[0] >= WET_CHUNKS || head[1] >= WET_CHUNKS) continue; + WetChunk *c = chunkAt(&t->wet, head[0], head[1]); + if (!c) c = newChunk(t, head[0], head[1]); + if (!c) return false; + memcpy(c->water, tmp.water, sizeof c->water); + memcpy(c->soil, tmp.soil, sizeof c->soil); + memcpy(c->pee, tmp.pee, sizeof c->pee); + memcpy(c->mud, tmp.mud, sizeof c->mud); + c->running = true; + c->dirty = true; + c->dead = false; + } + return true; +} + /* The chunk and index of cell (gx, gz), made if missing and make is set. */ static WetChunk *cellAt(Terrain *t, int gx, int gz, bool make, int *index) { if (gx < 0 || gz < 0 || gx >= WET_RES || gz >= WET_RES) return NULL; diff --git a/entropie/src/world/wetness.h b/entropie/src/world/wetness.h index c396061..42a0da5 100644 --- a/entropie/src/world/wetness.h +++ b/entropie/src/world/wetness.h @@ -16,6 +16,7 @@ */ #include <stdbool.h> +#include <stdio.h> #include "core/gl.h" @@ -45,6 +46,10 @@ typedef struct { struct Terrain; bool Wetness_init(Wetness *w); +/* Saves: the wet ground (each chunk in use: water, soil, pee, mud) out to / + in from a file. Reading needs the terrain (and its wear) in place first. */ +bool Wetness_write(const Wetness *w, FILE *f); +bool Wetness_read(struct Terrain *t, FILE *f); void Wetness_destroy(Wetness *w); /* Liters landing on the ground around (x, z); pee: how much of it is pee (0..1). */ diff --git a/entropie/src/world/world.c b/entropie/src/world/world.c index 6ce2142..f462d95 100644 --- a/entropie/src/world/world.c +++ b/entropie/src/world/world.c @@ -1157,12 +1157,14 @@ static void pushOffCarried(World *world, Object *who) { void World_init(World *world) { memset(world, 0, sizeof(*world)); + Object_seePeople(NULL, 0); /* (not the people of a world gone: loading a save builds a new one) */ world->rng = 0x2545F491u; world->hour = 12.0f; Weather_init(world); } void World_destroy(World *world) { + Object_seePeople(NULL, 0); for (int i = 0; i < world->count; i++) Object_destroy(world->objects[i]); free(world->objects);