mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
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>
116 lines
4.4 KiB
C++
116 lines
4.4 KiB
C++
// The Win32 front end of folder_pick.h: the shell's open dialog (IFileOpenDialog; FOS_PICKFOLDERS for a folder), owned
|
|
// by the game window, on a thread of its own so the present thread that draws the menu keeps running, and the shell's
|
|
// Downloads folder.
|
|
#include <windows.h>
|
|
#include <shlobj.h>
|
|
#include <shobjidl.h>
|
|
#include <stdlib.h>
|
|
#include <string>
|
|
|
|
extern "C" {
|
|
#include "folder_pick.h"
|
|
#include "host.h"
|
|
#include "input_win.h"
|
|
#include "platform/platform.h"
|
|
}
|
|
|
|
struct PickReq {
|
|
std::string start;
|
|
std::string ext; // a file of this extension; empty: a folder
|
|
FolderPickDone done;
|
|
void *ctx;
|
|
};
|
|
|
|
static std::wstring wide(const std::string &utf8) {
|
|
int n = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, NULL, 0);
|
|
std::wstring w(n > 0 ? n : 1, L'\0');
|
|
if (n > 0) MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, &w[0], n);
|
|
w.resize(wcslen(w.c_str()));
|
|
return w;
|
|
}
|
|
|
|
static std::string utf8(const wchar_t *w) {
|
|
int n = WideCharToMultiByte(CP_UTF8, 0, w, -1, NULL, 0, NULL, NULL);
|
|
std::string s(n > 0 ? n : 1, '\0');
|
|
if (n > 0) WideCharToMultiByte(CP_UTF8, 0, w, -1, &s[0], n, NULL, NULL);
|
|
s.resize(strlen(s.c_str()));
|
|
return s;
|
|
}
|
|
|
|
// The dialog wants an absolute folder that exists; anything else leaves it at the shell's last folder.
|
|
static void set_start(IFileOpenDialog *d, const std::string &start) {
|
|
wchar_t full[MAX_PATH * 2];
|
|
if (start.empty() || !GetFullPathNameW(wide(start).c_str(), MAX_PATH * 2, full, NULL)) return;
|
|
IShellItem *item = NULL;
|
|
if (SUCCEEDED(SHCreateItemFromParsingName(full, NULL, IID_PPV_ARGS(&item)))) {
|
|
d->SetFolder(item);
|
|
item->Release();
|
|
}
|
|
}
|
|
|
|
static void pick_main(void *arg) {
|
|
PickReq *r = (PickReq *)arg;
|
|
std::string chosen;
|
|
HRESULT co = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
|
|
IFileOpenDialog *d = NULL;
|
|
HRESULT hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&d));
|
|
if (SUCCEEDED(hr)) {
|
|
FILEOPENDIALOGOPTIONS opt = 0;
|
|
d->GetOptions(&opt);
|
|
if (r->ext.empty()) {
|
|
d->SetOptions(opt | FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST);
|
|
} else {
|
|
d->SetOptions(opt | FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST | FOS_FILEMUSTEXIST);
|
|
std::wstring pattern = L"*." + wide(r->ext); // the pattern is its own name: no text to translate
|
|
COMDLG_FILTERSPEC types[] = {{pattern.c_str(), pattern.c_str()}, {L"*.*", L"*.*"}};
|
|
d->SetFileTypes(2, types);
|
|
}
|
|
set_start(d, r->start);
|
|
hr = d->Show(gfx_window_handle());
|
|
IShellItem *item = NULL;
|
|
if (SUCCEEDED(hr) && SUCCEEDED(hr = d->GetResult(&item))) {
|
|
PWSTR path = NULL;
|
|
if (SUCCEEDED(hr = item->GetDisplayName(SIGDN_FILESYSPATH, &path))) {
|
|
chosen = utf8(path);
|
|
CoTaskMemFree(path);
|
|
}
|
|
item->Release();
|
|
}
|
|
d->Release();
|
|
}
|
|
if (FAILED(hr) && hr != HRESULT_FROM_WIN32(ERROR_CANCELLED))
|
|
rt_log("folder_pick: the %s dialog failed (0x%08lx)", r->ext.empty() ? "folder" : "file", (unsigned long)hr);
|
|
r->done(chosen.empty() ? NULL : chosen.c_str(), r->ctx);
|
|
if (SUCCEEDED(co)) CoUninitialize();
|
|
delete r;
|
|
}
|
|
|
|
static void pick(const char *start, const char *ext, FolderPickDone done, void *ctx) {
|
|
PickReq *r = new PickReq{start ? start : "", ext ? ext : "", done, ctx};
|
|
if (!plat_thread_spawn(pick_main, r, "folder dialog", 0)) {
|
|
rt_log("folder_pick: cannot start the dialog thread");
|
|
done(NULL, ctx);
|
|
delete r;
|
|
}
|
|
}
|
|
|
|
extern "C" void folder_pick(const char *start, FolderPickDone done, void *ctx) { pick(start, NULL, done, ctx); }
|
|
|
|
extern "C" void file_pick(const char *start, const char *ext, FolderPickDone done, void *ctx) {
|
|
pick(start, ext && *ext ? ext : "*", done, ctx);
|
|
}
|
|
|
|
extern "C" int folder_downloads(char *out, size_t cap) {
|
|
// FOLDERID_Downloads, spelled out so no import library has to carry it
|
|
static const GUID downloads = {0x374de290, 0x123f, 0x4565, {0x91, 0x64, 0x39, 0xc4, 0x92, 0x5e, 0x46, 0x7b}};
|
|
PWSTR path = NULL;
|
|
int ok = 0;
|
|
if (SUCCEEDED(SHGetKnownFolderPath(downloads, 0, NULL, &path)) && path) {
|
|
std::string s = utf8(path);
|
|
ok = !s.empty() && s.size() < cap;
|
|
if (ok) memcpy(out, s.c_str(), s.size() + 1);
|
|
}
|
|
CoTaskMemFree(path);
|
|
return ok;
|
|
}
|