Files
NX2navi/runtime/folder_pick_sdl.c
chrg6 b2074499de Let players export their save into one zip for a bug report and import a save someone shared, in the launcher and in the game's menu.
The launcher's home has a Saves screen: Export Save writes NX2navi-save-<date>_<time>.zip into Downloads (or a chosen folder) with a manifest, the save the game would load next and, for a bug report, the game's latest log and nx2navi.ini, never a key file; Import a Save checks a zip's manifest, game, paths and CRCs, backs the current save up into the saves folder and replaces it while the game is not running. The game's Game tab has the same exports and an import that waits in profile/save-import and becomes the save at the next start, since the running game would write its progress back over a save replaced under it.

The zip code (runtime/save_zip.c) and the import at start are checked without game files by tests/save_zip_check.py and tests/save_share_check.py; the launcher side by its test suite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 13:13:43 -05:00

68 lines
2.4 KiB
C

// The SDL front end of folder_pick.h: SDL_ShowOpenFolderDialog and SDL_ShowOpenFileDialog (the desktop portal, else
// zenity), started on the window thread, which runs SDL's event loop that delivers the portal's answer, and SDL's user
// folders.
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <SDL3/SDL.h>
#include "folder_pick.h"
#include "host.h"
#include "input_sdl.h"
typedef struct {
char start[PATH_MAX]; // absolute, or empty: the dialog's own default
char ext[16]; // a file of this extension; empty: a folder
SDL_DialogFileFilter filters[2]; // read by SDL until the answer comes
FolderPickDone done;
void *ctx;
} PickReq;
static void SDLCALL on_pick(void *userdata, const char *const *list, int filter) {
(void)filter;
PickReq *r = userdata;
if (!list) rt_log("folder_pick: the folder dialog failed (%s)", SDL_GetError());
r->done(list && list[0] ? list[0] : NULL, r->ctx);
free(r);
}
static void show(void *arg) {
PickReq *r = arg;
if (!r->ext[0]) {
SDL_ShowOpenFolderDialog(on_pick, r, gfx_window_sdl(), r->start[0] ? r->start : NULL, false);
return;
}
r->filters[0] = (SDL_DialogFileFilter){r->ext, r->ext}; // the extension is its own name: no text to translate
r->filters[1] = (SDL_DialogFileFilter){"*", "*"};
SDL_ShowOpenFileDialog(on_pick, r, gfx_window_sdl(), r->filters, 2, r->start[0] ? r->start : NULL, false);
}
static void pick(const char *start, const char *ext, FolderPickDone done, void *ctx) {
PickReq *r = calloc(1, sizeof *r);
if (!r) {
done(NULL, ctx);
return;
}
r->done = done;
r->ctx = ctx;
if (ext) snprintf(r->ext, sizeof r->ext, "%s", *ext ? ext : "*");
if (start && *start && !realpath(start, r->start)) r->start[0] = 0;
gfx_window_sdl_run(show, r);
}
void folder_pick(const char *start, FolderPickDone done, void *ctx) { pick(start, NULL, done, ctx); }
void file_pick(const char *start, const char *ext, FolderPickDone done, void *ctx) { pick(start, ext, done, ctx); }
int folder_downloads(char *out, size_t cap) {
const char *p = SDL_GetUserFolder(SDL_FOLDER_DOWNLOADS);
size_t n = p ? strlen(p) : 0;
while (n > 1 && p[n - 1] == '/') n--; // SDL ends it with a separator
if (!n || n >= cap) return 0;
memcpy(out, p, n);
out[n] = 0;
return 1;
}