diff --git a/ball/game_server.c b/ball/game_server.c index 903fa048..4a8b07ff 100644 --- a/ball/game_server.c +++ b/ball/game_server.c @@ -14,6 +14,7 @@ #include #include +#include #include #include "vec3.h" @@ -90,6 +91,33 @@ struct input static struct input input_current; +/* + * wasmga.me: every step's input since the level began, and the step the goal + * opened at, so a Resume snapshot can play the level up to where it stood + * (snapshot.diff). + */ + +static struct input *wg_steps; +static int wg_count; +static int wg_cap; +static int wg_goal_step = -1; + +static void wg_record(void) +{ + if (wg_count == wg_cap) + { + int cap = wg_cap ? wg_cap * 2 : 4096; + struct input *steps = realloc(wg_steps, cap * sizeof (struct input)); + + if (!steps) + return; + + wg_steps = steps; + wg_cap = cap; + } + wg_steps[wg_count++] = input_current; +} + static void input_init(void) { input_current.s = RESPONSE; @@ -388,6 +416,9 @@ int game_server_init(const char *file_name, int t, int e) time_limit = (float) t / 100.0f; time_elapsed = 0.0f; + wg_count = 0; + wg_goal_step = -1; + timer = 0.0f; status = GAME_NONE; coins = 0; @@ -874,6 +905,8 @@ static int game_step(const float g[3], float dt, int bt) static void game_server_iter(float dt) { + wg_record(); + switch (status) { case GAME_GOAL: game_step(GRAVITY_UP, dt, 0); break; @@ -904,6 +937,9 @@ float game_server_blend(void) void game_set_goal(void) { + if (wg_goal_step < 0) + wg_goal_step = wg_count; + audio_play(AUD_SWITCH, 1.0f); goal_e = 1; @@ -954,6 +990,40 @@ void game_set_rot(float r) /*---------------------------------------------------------------------------*/ +int wg_server_steps(void) +{ + return wg_count; +} + +int wg_server_goal_step(void) +{ + return wg_goal_step; +} + +void wg_server_input(int i, float *s, float *x, float *z, float *r, int *c) +{ + *s = wg_steps[i].s; + *x = wg_steps[i].x; + *z = wg_steps[i].z; + *r = wg_steps[i].r; + *c = wg_steps[i].c; +} + +/* Plays the steps of a level just started, with the goal opening where it did. */ +void wg_server_replay(int n, int goal_step, void (*input)(int, float *, float *, float *, float *, int *)) +{ + int i; + + for (i = 0; i < n; i++) + { + if (i == goal_step) + game_set_goal(); + + input(i, &input_current.s, &input_current.x, &input_current.z, &input_current.r, &input_current.c); + game_server_iter(DT); + } +} + float curr_time_elapsed(void) { return time_elapsed; diff --git a/ball/game_server.h b/ball/game_server.h index e6d049c7..3650426b 100644 --- a/ball/game_server.h +++ b/ball/game_server.h @@ -26,6 +26,11 @@ void game_set_rot(float); float curr_time_elapsed(void); +int wg_server_steps(void); +int wg_server_goal_step(void); +void wg_server_input(int, float *, float *, float *, float *, int *); +void wg_server_replay(int, int, void (*)(int, float *, float *, float *, float *, int *)); + /*---------------------------------------------------------------------------*/ #endif diff --git a/ball/main.c b/ball/main.c index 261e3d9b..2be4199e 100644 --- a/ball/main.c +++ b/ball/main.c @@ -780,7 +780,11 @@ static void step(void *data) { struct main_loop *mainloop = (struct main_loop *) data; - int running = loop(); + int running; + + wg_tick(); + + running = loop(); if (running) { diff --git a/ball/progress.c b/ball/progress.c index b4d09e74..019054bf 100644 --- a/ball/progress.c +++ b/ball/progress.c @@ -23,6 +23,27 @@ #include "game_common.h" #include "game_client.h" +#include "state.h" +#include "st_common.h" +#include "hud.h" +#include "st_play.h" +#include "st_pause.h" +#include "st_level.h" +#include "st_start.h" +#include "st_set.h" +#include "st_goal.h" +#include "st_fail.h" +#include "st_over.h" +#include "st_done.h" + +#include +#include +#include +#include + +#ifdef __EMSCRIPTEN__ +#include +#endif #include "game_server.h" #include @@ -414,3 +435,251 @@ const char *mode_to_str(int m, int l) } /*---------------------------------------------------------------------------*/ + +/*---------------------------------------------------------------------------*/ + +/* + * wasmga.me: the play page's Resume snapshots (snapshot.diff). + * + * wg_request_snapshot() writes WG_SNAPSHOT at the top of the next frame and + * wg_snapshot_count() goes up. In a level it holds the set, the level, the + * mode, balls and score, and every step's input so far; loading it starts the + * level and plays those steps at once, then pauses. Elsewhere in a set it + * holds the set, and loading it opens its level list. wg_restore() loads + * WG_RESTORE. + */ + +#define WG_SNAPSHOT "/wg/snapshot.txt" +#define WG_RESTORE "/wg/restore.txt" +#define WG_HEAD "neverball-snapshot 1" + +static int wg_snapshot_requested; +static int wg_snapshots; +static int wg_restore_requested; + +#ifdef __EMSCRIPTEN__ +EMSCRIPTEN_KEEPALIVE void wg_request_snapshot(void) { wg_snapshot_requested = 1; } +EMSCRIPTEN_KEEPALIVE int wg_snapshot_count(void) { return wg_snapshots; } +EMSCRIPTEN_KEEPALIVE void wg_restore(void) { wg_restore_requested = 1; } +#endif + +static int wg_in_level(struct state *st) +{ + return st == &st_level || st == &st_play_ready || st == &st_play_set || + st == &st_play_loop || st == &st_look || st == &st_pause || + st == &st_fail; +} + +static int wg_in_set(struct state *st) +{ + return st == &st_start || st == &st_goal || st == &st_over || st == &st_done; +} + +static int wg_level_index(void) +{ + int i; + + for (i = 0; get_level(i); i++) + if (get_level(i) == level) + return i; + + return -1; +} + +static void wg_write_snapshot(void) +{ + struct state *st = curr_state(); + FILE *fp; + + mkdir("/wg", 0777); + + if (!(fp = fopen(WG_SNAPSHOT, "w"))) + return; + + fprintf(fp, WG_HEAD "\n"); + + if (level && wg_level_index() >= 0 && game_server_state() && + (wg_in_level(st) || wg_in_set(st)) && mode != MODE_STANDALONE) + { + fprintf(fp, "set %s\n", set_file(curr_set())); + + if (wg_in_level(st)) + { + /* Still rolling: carry on. Fallen or out of time: from the start. */ + const int playing = curr_status() == GAME_NONE && st != &st_fail; + const int n = playing ? wg_server_steps() : 0; + int i, j; + + fprintf(fp, "kind level\n"); + fprintf(fp, "level %d\n", wg_level_index()); + fprintf(fp, "mode %d\n", mode); + fprintf(fp, "balls %d\nscore %d\ntimes %d\n", curr.balls, curr.score, curr.times); + fprintf(fp, "goal %d\n", playing ? wg_server_goal_step() : -1); + fprintf(fp, "steps %d\n", n); + + /* One line for each run of steps with the same input. */ + + for (i = 0; i < n; i = j) + { + float s, x, z, r, s2, x2, z2, r2; + int c, c2; + + wg_server_input(i, &s, &x, &z, &r, &c); + + for (j = i + 1; j < n; j++) + { + wg_server_input(j, &s2, &x2, &z2, &r2, &c2); + + if (s2 != s || x2 != x || z2 != z || r2 != r || c2 != c) + break; + } + fprintf(fp, "%d %a %a %a %a %d\n", j - i, s, x, z, r, c); + } + } + else + fprintf(fp, "kind set\n"); + } + else + fprintf(fp, "kind none\n"); + + fclose(fp); + wg_snapshots++; +} + +static struct wg_run { int n; float s, x, z, r; int c; } *wg_runs; +static int wg_run_count; + +static void wg_input(int i, float *s, float *x, float *z, float *r, int *c) +{ + /* Steps come in order, so the run is found from where the last one was. */ + + static int k, first; + + if (i < first) + k = first = 0; + + while (k < wg_run_count - 1 && i >= first + wg_runs[k].n) + first += wg_runs[k++].n; + + *s = wg_runs[k].s; + *x = wg_runs[k].x; + *z = wg_runs[k].z; + *r = wg_runs[k].r; + *c = wg_runs[k].c; +} + +static void wg_load_snapshot(void) +{ + char line[512], kind[32] = "", file[256] = ""; + int li = -1, m = MODE_NORMAL, balls = 2, score = 0, times = 0; + int goal_step = -1, steps = 0, idx; + FILE *fp; + + if (!(fp = fopen(WG_RESTORE, "r"))) + return; + + if (!fgets(line, sizeof (line), fp) || strncmp(line, WG_HEAD "\n", sizeof (WG_HEAD)) != 0) + { + fclose(fp); + return; + } + + free(wg_runs); + wg_runs = NULL; + wg_run_count = 0; + + while (fgets(line, sizeof (line), fp)) + { + if (sscanf(line, "set %255s", file) == 1) ; + else if (sscanf(line, "kind %31s", kind) == 1) ; + else if (sscanf(line, "level %d", &li) == 1) ; + else if (sscanf(line, "mode %d", &m) == 1) ; + else if (sscanf(line, "balls %d", &balls) == 1) ; + else if (sscanf(line, "score %d", &score) == 1) ; + else if (sscanf(line, "times %d", ×) == 1) ; + else if (sscanf(line, "goal %d", &goal_step) == 1) ; + else if (sscanf(line, "steps %d", &steps) == 1) + { + int total = 0; + + wg_runs = calloc(steps > 0 ? steps : 1, sizeof (*wg_runs)); + + while (wg_runs && total < steps && fgets(line, sizeof (line), fp)) + { + struct wg_run *run = &wg_runs[wg_run_count]; + + if (sscanf(line, "%d %a %a %a %a %d", &run->n, &run->s, &run->x, &run->z, &run->r, &run->c) != 6 || run->n <= 0) + break; + + total += run->n; + wg_run_count++; + } + + if (total != steps) + steps = 0; + } + } + fclose(fp); + + if (!file[0] || (strcmp(kind, "set") != 0 && strcmp(kind, "level") != 0)) + return; + + set_init(); + + if ((idx = set_find(file)) < 0) + return; + + set_goto(idx); + + if (strcmp(kind, "level") == 0 && get_level(li)) + { + progress_init(m); + + curr.balls = balls; + curr.score = score; + curr.times = times; + + if (progress_play(get_level(li))) + { + if (steps == 0) + { + goto_state(&st_level); + return; + } + + wg_server_replay(steps, goal_step, wg_input); + game_client_sync(demo_fp); + game_client_blend(game_server_blend()); + game_kill_fade(); + + if (goal > 0 && (goal = goal_i - curr_coins()) < 0) + goal = 0; + + /* Paused where it stood: Continue goes back to the level. */ + + goto_state(&st_pause); + wg_pause_continue_to(&st_play_loop); + hud_update(0); + return; + } + } + + goto_start(idx, &st_set); +} + +void wg_tick(void) +{ + /* Not while the game is still loading: it opens the title screen after. */ + + if (wg_restore_requested && curr_state() && curr_state() != &st_loading) + { + wg_restore_requested = 0; + wg_load_snapshot(); + } + + if (wg_snapshot_requested) + { + wg_snapshot_requested = 0; + wg_write_snapshot(); + } +} diff --git a/ball/progress.h b/ball/progress.h index e0e9b993..f3504780 100644 --- a/ball/progress.h +++ b/ball/progress.h @@ -62,4 +62,6 @@ const char *mode_to_str(int, int); /*---------------------------------------------------------------------------*/ +void wg_tick(void); + #endif diff --git a/ball/st_pause.c b/ball/st_pause.c index e31719d3..2ad7c349 100644 --- a/ball/st_pause.c +++ b/ball/st_pause.c @@ -40,6 +40,12 @@ enum static struct state *st_continue; +/* wasmga.me: where Continue goes when the game opens paused (snapshot.diff). */ +void wg_pause_continue_to(struct state *st) +{ + st_continue = st; +} + /*---------------------------------------------------------------------------*/ static int pause_action(int tok, int val) diff --git a/ball/st_pause.h b/ball/st_pause.h index 4e13f38b..16e4e5ea 100644 --- a/ball/st_pause.h +++ b/ball/st_pause.h @@ -5,4 +5,6 @@ extern struct state st_pause; +void wg_pause_continue_to(struct state *); + #endif