mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
NX2navi-Setup.exe installs the launcher into %LOCALAPPDATA%\Programs\NX2navi without administrator rights, with shortcuts and an Apps entry. The launcher finds the player's .nup, downloads the build tools itself (the MSYS2 UCRT64 packages toolchain.lock.json pins, checked by sha256 and unpacked without pacman, and zbic from PyPI) and runs tools/make_game.py, whose steps it shows with their progress. Its window follows the game's options menu: colours sampled from it, the menu's filigree, the white glowing cursor, the Triforce in the title band and as the programs' icon, and all text in the game's Cetra Display, decoded from the player's own dump. Mouse, keyboard and an XInput pad drive it. Updates come from GitHub releases (NX2navi-update.json): the new version is downloaded, checked and staged beside the running one, the stub restarts into it and falls back to the previous version when it fails to start, and the game is rebuilt without the dump. make_game.py now keys its exefs and romfs steps on the game files alone, has --rebuild and --events, compiles only the files whose source, headers, flags or compiler changed, and replaces ocarina.exe only once the new one links. tools/make_launcher.py builds the release on Windows or Linux. Tested: tests/launcher_test.py (19 tests), the real toolchain download and install, and under Wine 11 the setup, the stub, the window on the bundled Windows Python, an update from a local feed by clicking Updates, the rollback of a version that fails to start, and the downloaded clang compiling and linking a program against the game's libraries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Merged onto the current port: make_game.py keeps the launcher's step keys, cached link and in-place swap, and its own runtime manifest (windres), AMD's FidelityFX library and the shader preparation, which becomes a step of its own ("shaders") that the launcher shows in its install, rebuild and update jobs. make_launcher.py ships the shader tools and, with --ffx, AMD's library. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
383 lines
15 KiB
C
383 lines
15 KiB
C
/* NX2navi-Setup.exe: the one-click setup. This program with the launcher's folder appended as a zip.
|
|
|
|
It unpacks the zip into %LOCALAPPDATA%\Programs\NX2navi (no administrator rights), adds the Start menu and Desktop
|
|
shortcuts and the entry in Settings > Apps, and starts NX2navi.exe with --from <the setup's folder>, where the
|
|
launcher looks for the player's dump first. A small window in the launcher's colours shows while it unpacks.
|
|
When the folder already holds a newer version than the setup carries, the setup leaves the files alone (the
|
|
launcher updates itself) and only repairs the shortcuts.
|
|
|
|
The end of the file is a trailer: "NX2NSET1", the zip's offset and size (u64 little endian) and the version (24
|
|
bytes, NUL padded), written by tools/make_launcher.py. The zip is read here with zlib's inflate, so the setup
|
|
needs nothing from Windows beyond its own DLLs.
|
|
|
|
Options: --dest DIR, --no-shortcuts, --no-launch, --quiet (no window; for tests). */
|
|
#include "common.h"
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
#include <zlib.h>
|
|
|
|
#define TRAILER 48
|
|
#define WM_DONE (WM_APP + 1)
|
|
|
|
typedef struct {
|
|
wchar_t exe[PATHN], dest[PATHN], src_dir[PATHN];
|
|
uint64_t off, size;
|
|
wchar_t version[32];
|
|
int shortcuts, launch, quiet;
|
|
int result; /* 0 ok, else an error message id */
|
|
wchar_t error[512];
|
|
uint64_t written;
|
|
} Setup;
|
|
|
|
static Setup S;
|
|
static HWND g_wnd;
|
|
|
|
static void fail_msg(const wchar_t *fmt, const wchar_t *arg)
|
|
{
|
|
swprintf(S.error, 512, fmt, arg ? arg : L"");
|
|
S.result = 1;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- reading our own file */
|
|
|
|
typedef struct { HANDLE f; uint64_t base, size; } Src;
|
|
|
|
static int src_read(Src *s, uint64_t at, void *buf, DWORD n)
|
|
{
|
|
LARGE_INTEGER li;
|
|
DWORD got = 0;
|
|
if (at + n > s->size) return 0;
|
|
li.QuadPart = (LONGLONG)(s->base + at);
|
|
if (!SetFilePointerEx(s->f, li, NULL, FILE_BEGIN)) return 0;
|
|
return ReadFile(s->f, buf, n, &got, NULL) && got == n;
|
|
}
|
|
|
|
static uint16_t le16(const unsigned char *p) { return (uint16_t)(p[0] | p[1] << 8); }
|
|
static uint32_t le32(const unsigned char *p) { return (uint32_t)p[0] | (uint32_t)p[1] << 8 | (uint32_t)p[2] << 16 | (uint32_t)p[3] << 24; }
|
|
static uint64_t le64(const unsigned char *p) { return (uint64_t)le32(p) | (uint64_t)le32(p + 4) << 32; }
|
|
|
|
static int read_trailer(void)
|
|
{
|
|
unsigned char t[TRAILER];
|
|
HANDLE f = CreateFileW(S.exe, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
|
|
if (f == INVALID_HANDLE_VALUE) return 0;
|
|
LARGE_INTEGER size, li;
|
|
DWORD got = 0;
|
|
int ok = GetFileSizeEx(f, &size) && size.QuadPart > TRAILER;
|
|
if (ok) {
|
|
li.QuadPart = size.QuadPart - TRAILER;
|
|
ok = SetFilePointerEx(f, li, NULL, FILE_BEGIN) && ReadFile(f, t, TRAILER, &got, NULL) && got == TRAILER &&
|
|
!memcmp(t, "NX2NSET1", 8);
|
|
}
|
|
CloseHandle(f);
|
|
if (!ok) return 0;
|
|
S.off = le64(t + 8);
|
|
S.size = le64(t + 16);
|
|
char v[25];
|
|
memcpy(v, t + 24, 24);
|
|
v[24] = 0;
|
|
MultiByteToWideChar(CP_UTF8, 0, v, -1, S.version, 32);
|
|
return S.off + S.size <= (uint64_t)size.QuadPart;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- unzip */
|
|
|
|
static int make_dirs(wchar_t *path) /* every folder above path (path is a file) */
|
|
{
|
|
for (wchar_t *p = path + 3; *p; p++) {
|
|
if (*p == L'\\') {
|
|
*p = 0;
|
|
if (!is_dir(path) && !CreateDirectoryW(path, NULL) && GetLastError() != ERROR_ALREADY_EXISTS) {
|
|
*p = L'\\';
|
|
return 0;
|
|
}
|
|
*p = L'\\';
|
|
}
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static int safe_name(const char *name)
|
|
{
|
|
if (!name[0] || name[0] == '/' || name[0] == '\\' || strchr(name, ':')) return 0;
|
|
for (const char *p = name; *p; p++)
|
|
if ((p == name || p[-1] == '/' || p[-1] == '\\') && p[0] == '.' && p[1] == '.' &&
|
|
(p[2] == '/' || p[2] == '\\' || !p[2]))
|
|
return 0;
|
|
return 1;
|
|
}
|
|
|
|
static int extract_entry(Src *z, uint64_t local, uint32_t csize, uint32_t usize, uint16_t method, uint32_t crc,
|
|
const wchar_t *target)
|
|
{
|
|
unsigned char lh[30];
|
|
if (!src_read(z, local, lh, 30) || le32(lh) != 0x04034b50) return 0;
|
|
uint64_t data = local + 30 + le16(lh + 26) + le16(lh + 28);
|
|
HANDLE out = CreateFileW(target, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
|
|
if (out == INVALID_HANDLE_VALUE) return 0;
|
|
static unsigned char in[1 << 16], buf[1 << 17];
|
|
uLong check = crc32(0L, Z_NULL, 0);
|
|
int ok = 1;
|
|
uint64_t left = csize, pos = data, total = 0;
|
|
z_stream st;
|
|
memset(&st, 0, sizeof st);
|
|
if (method == 8 && inflateInit2(&st, -MAX_WBITS) != Z_OK) ok = 0;
|
|
while (ok && (left || (method == 8 && st.avail_in))) {
|
|
DWORD n = (DWORD)(left < sizeof in ? left : sizeof in);
|
|
if (n && !src_read(z, pos, in, n)) { ok = 0; break; }
|
|
pos += n;
|
|
left -= n;
|
|
if (method == 0) {
|
|
DWORD put;
|
|
check = crc32(check, in, n);
|
|
ok = WriteFile(out, in, n, &put, NULL) && put == n;
|
|
total += n;
|
|
continue;
|
|
}
|
|
st.next_in = in;
|
|
st.avail_in = n;
|
|
do {
|
|
st.next_out = buf;
|
|
st.avail_out = sizeof buf;
|
|
int r = inflate(&st, Z_NO_FLUSH);
|
|
if (r != Z_OK && r != Z_STREAM_END && !(r == Z_BUF_ERROR && st.avail_in == 0)) { ok = 0; break; }
|
|
DWORD have = (DWORD)(sizeof buf - st.avail_out), put;
|
|
if (have) {
|
|
check = crc32(check, buf, have);
|
|
if (!WriteFile(out, buf, have, &put, NULL) || put != have) { ok = 0; break; }
|
|
total += have;
|
|
}
|
|
if (r == Z_STREAM_END) { left = 0; st.avail_in = 0; break; }
|
|
} while (st.avail_out == 0);
|
|
}
|
|
if (method == 8) inflateEnd(&st);
|
|
CloseHandle(out);
|
|
S.written += total;
|
|
return ok && total == usize && check == crc;
|
|
}
|
|
|
|
static int unzip(void)
|
|
{
|
|
Src z;
|
|
unsigned char eocd[22];
|
|
z.f = CreateFileW(S.exe, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
|
|
if (z.f == INVALID_HANDLE_VALUE) { fail_msg(L"The setup cannot read itself.", NULL); return 0; }
|
|
z.base = S.off;
|
|
z.size = S.size;
|
|
/* the end-of-central-directory record: the zip has no comment, so it is the last 22 bytes */
|
|
if (!src_read(&z, z.size - 22, eocd, 22) || le32(eocd) != 0x06054b50) {
|
|
CloseHandle(z.f);
|
|
fail_msg(L"The setup is damaged. Download it again.", NULL);
|
|
return 0;
|
|
}
|
|
uint32_t count = le16(eocd + 10), cd_size = le32(eocd + 12), cd_off = le32(eocd + 16);
|
|
unsigned char *cd = malloc(cd_size);
|
|
if (!cd || !src_read(&z, cd_off, cd, cd_size)) {
|
|
free(cd);
|
|
CloseHandle(z.f);
|
|
fail_msg(L"The setup is damaged. Download it again.", NULL);
|
|
return 0;
|
|
}
|
|
wchar_t target[PATHN], wname[PATHN];
|
|
char name[PATHN];
|
|
uint32_t at = 0;
|
|
int ok = 1;
|
|
for (uint32_t i = 0; ok && i < count; i++) {
|
|
if (at + 46 > cd_size || le32(cd + at) != 0x02014b50) { ok = 0; break; }
|
|
uint16_t method = le16(cd + at + 10), nlen = le16(cd + at + 28), xlen = le16(cd + at + 30),
|
|
clen = le16(cd + at + 32);
|
|
uint32_t crc = le32(cd + at + 16), csize = le32(cd + at + 20), usize = le32(cd + at + 24),
|
|
local = le32(cd + at + 42);
|
|
if (nlen >= PATHN || at + 46 + nlen > cd_size || (method != 0 && method != 8)) { ok = 0; break; }
|
|
memcpy(name, cd + at + 46, nlen);
|
|
name[nlen] = 0;
|
|
at += 46 + nlen + xlen + clen;
|
|
if (!safe_name(name)) { ok = 0; break; }
|
|
MultiByteToWideChar(CP_UTF8, 0, name, -1, wname, PATHN);
|
|
for (wchar_t *p = wname; *p; p++)
|
|
if (*p == L'/') *p = L'\\';
|
|
swprintf(target, PATHN, L"%ls\\%ls", S.dest, wname);
|
|
size_t tl = wcslen(target);
|
|
if (tl && target[tl - 1] == L'\\') {
|
|
target[tl - 1] = 0;
|
|
ok = make_dirs(wcscat(target, L"\\")) && (is_dir(target) || CreateDirectoryW(target, NULL));
|
|
continue;
|
|
}
|
|
if (!make_dirs(target)) { ok = 0; break; }
|
|
if (!extract_entry(&z, local, csize, usize, method, crc, target)) {
|
|
fail_msg(L"%ls could not be written. Close NX2navi if it is open, then run the setup again.", target);
|
|
free(cd);
|
|
CloseHandle(z.f);
|
|
return 0;
|
|
}
|
|
if (g_wnd && (i & 31) == 0) InvalidateRect(g_wnd, NULL, FALSE);
|
|
}
|
|
free(cd);
|
|
CloseHandle(z.f);
|
|
if (!ok) fail_msg(L"The setup is damaged. Download it again.", NULL);
|
|
return ok;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- the work */
|
|
|
|
static DWORD WINAPI work(void *arg)
|
|
{
|
|
wchar_t cur[PATHN], have[64];
|
|
(void)arg;
|
|
swprintf(cur, PATHN, L"%ls\\app\\current.txt", S.dest);
|
|
int newer_installed = read_line(cur, have, 64) && version_cmp(have, S.version) > 0;
|
|
wchar_t probe[PATHN];
|
|
swprintf(probe, PATHN, L"%ls\\x", S.dest);
|
|
if (!make_dirs(probe)) {
|
|
fail_msg(L"The folder %ls cannot be created.", S.dest);
|
|
} else if (!newer_installed) {
|
|
unzip();
|
|
}
|
|
if (!S.result) {
|
|
if (S.shortcuts) {
|
|
set_shortcut(S.dest, 0, 1);
|
|
set_shortcut(S.dest, 1, 1);
|
|
}
|
|
write_uninstall_entry(S.dest, newer_installed ? have : S.version, (DWORD)(S.written / 1024 + 1));
|
|
}
|
|
if (g_wnd) PostMessageW(g_wnd, WM_DONE, 0, 0);
|
|
return 0;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- the window */
|
|
|
|
static LRESULT CALLBACK wndproc(HWND w, UINT m, WPARAM wp, LPARAM lp)
|
|
{
|
|
switch (m) {
|
|
case WM_PAINT: {
|
|
PAINTSTRUCT ps;
|
|
HDC dc = BeginPaint(w, &ps);
|
|
RECT r;
|
|
GetClientRect(w, &r);
|
|
HBRUSH bg = CreateSolidBrush(RGB(0x0c, 0x2e, 0x1b)), edge = CreateSolidBrush(RGB(0xa9, 0x87, 0x4f));
|
|
FillRect(dc, &r, edge);
|
|
RECT in = {r.left + 2, r.top + 2, r.right - 2, r.bottom - 2};
|
|
FillRect(dc, &in, bg);
|
|
/* the Triforce: three gold triangles */
|
|
HBRUSH gold = CreateSolidBrush(RGB(0xe8, 0xaf, 0x3d));
|
|
HPEN none = CreatePen(PS_NULL, 0, 0);
|
|
HGDIOBJ ob = SelectObject(dc, gold), op = SelectObject(dc, none);
|
|
int cx = 52, top = 28, s = 44;
|
|
POINT t1[3] = {{cx, top}, {cx + s / 4, top + s * 433 / 1000}, {cx - s / 4, top + s * 433 / 1000}};
|
|
POINT t2[3] = {{cx - s / 4, top + s * 433 / 1000 + 1}, {cx, top + s * 866 / 1000}, {cx - s / 2, top + s * 866 / 1000}};
|
|
POINT t3[3] = {{cx + s / 4, top + s * 433 / 1000 + 1}, {cx + s / 2, top + s * 866 / 1000}, {cx, top + s * 866 / 1000}};
|
|
Polygon(dc, t1, 3);
|
|
Polygon(dc, t2, 3);
|
|
Polygon(dc, t3, 3);
|
|
SelectObject(dc, ob);
|
|
SelectObject(dc, op);
|
|
HFONT big = CreateFontW(-26, 0, 0, 0, FW_SEMIBOLD, 0, 0, 0, DEFAULT_CHARSET, 0, 0, CLEARTYPE_QUALITY, 0, L"Segoe UI");
|
|
HFONT small = CreateFontW(-16, 0, 0, 0, FW_NORMAL, 0, 0, 0, DEFAULT_CHARSET, 0, 0, CLEARTYPE_QUALITY, 0, L"Segoe UI");
|
|
SetBkMode(dc, TRANSPARENT);
|
|
SetTextColor(dc, RGB(0xf3, 0xec, 0xdc));
|
|
HGDIOBJ of = SelectObject(dc, big);
|
|
TextOutW(dc, 96, 30, APP_NAME, (int)wcslen(APP_NAME));
|
|
SelectObject(dc, small);
|
|
SetTextColor(dc, RGB(0xe9, 0xdc, 0xc0));
|
|
wchar_t line[96];
|
|
swprintf(line, 96, L"Installing version %ls\x2026 %llu MB", S.version, (unsigned long long)(S.written >> 20));
|
|
TextOutW(dc, 96, 66, line, (int)wcslen(line));
|
|
SelectObject(dc, of);
|
|
DeleteObject(big);
|
|
DeleteObject(small);
|
|
DeleteObject(gold);
|
|
DeleteObject(none);
|
|
DeleteObject(bg);
|
|
DeleteObject(edge);
|
|
EndPaint(w, &ps);
|
|
return 0;
|
|
}
|
|
case WM_TIMER:
|
|
InvalidateRect(w, NULL, FALSE);
|
|
return 0;
|
|
case WM_DONE:
|
|
DestroyWindow(w);
|
|
return 0;
|
|
case WM_DESTROY:
|
|
PostQuitMessage(0);
|
|
return 0;
|
|
}
|
|
return DefWindowProcW(w, m, wp, lp);
|
|
}
|
|
|
|
static void run_with_window(HINSTANCE inst)
|
|
{
|
|
WNDCLASSW wc = {0};
|
|
wc.lpfnWndProc = wndproc;
|
|
wc.hInstance = inst;
|
|
wc.hCursor = LoadCursor(NULL, IDC_WAIT);
|
|
wc.hIcon = LoadIconW(inst, MAKEINTRESOURCEW(1));
|
|
wc.lpszClassName = L"NX2naviSetup";
|
|
RegisterClassW(&wc);
|
|
int w = 440, h = 120;
|
|
int x = (GetSystemMetrics(SM_CXSCREEN) - w) / 2, y = (GetSystemMetrics(SM_CYSCREEN) - h) / 3;
|
|
g_wnd = CreateWindowExW(WS_EX_APPWINDOW, wc.lpszClassName, L"NX2navi Setup", WS_POPUP | WS_VISIBLE, x, y, w, h,
|
|
NULL, NULL, inst, NULL);
|
|
SetTimer(g_wnd, 1, 200, NULL);
|
|
HANDLE t = CreateThread(NULL, 0, work, NULL, 0, NULL);
|
|
MSG msg;
|
|
while (GetMessageW(&msg, NULL, 0, 0) > 0) {
|
|
TranslateMessage(&msg);
|
|
DispatchMessageW(&msg);
|
|
}
|
|
WaitForSingleObject(t, INFINITE);
|
|
CloseHandle(t);
|
|
}
|
|
|
|
int WINAPI wWinMain(HINSTANCE inst, HINSTANCE prev, PWSTR cmdline, int show)
|
|
{
|
|
(void)prev; (void)cmdline; (void)show;
|
|
GetModuleFileNameW(NULL, S.exe, PATHN);
|
|
exe_dir(S.src_dir, PATHN);
|
|
S.shortcuts = S.launch = 1;
|
|
S.dest[0] = 0;
|
|
int argc = 0;
|
|
wchar_t **argv = CommandLineToArgvW(GetCommandLineW(), &argc);
|
|
for (int i = 1; argv && i < argc; i++) {
|
|
if (!wcscmp(argv[i], L"--dest") && i + 1 < argc) {
|
|
GetFullPathNameW(argv[++i], PATHN, S.dest, NULL);
|
|
} else if (!wcscmp(argv[i], L"--no-shortcuts")) S.shortcuts = 0;
|
|
else if (!wcscmp(argv[i], L"--no-launch")) S.launch = 0;
|
|
else if (!wcscmp(argv[i], L"--quiet")) S.quiet = 1;
|
|
}
|
|
if (argv) LocalFree(argv);
|
|
if (!S.dest[0]) {
|
|
wchar_t base[PATHN];
|
|
if (!known_folder(&FOLDERID_UserProgramFiles, base, PATHN)) {
|
|
if (!GetEnvironmentVariableW(L"LOCALAPPDATA", base, PATHN)) return 1;
|
|
wcscat(base, L"\\Programs");
|
|
}
|
|
swprintf(S.dest, PATHN, L"%ls\\%ls", base, APP_NAME);
|
|
}
|
|
size_t dl = wcslen(S.dest);
|
|
while (dl > 3 && (S.dest[dl - 1] == L'\\' || S.dest[dl - 1] == L'/')) S.dest[--dl] = 0;
|
|
if (!read_trailer()) {
|
|
MessageBoxW(NULL, L"This setup is incomplete. Download NX2navi-Setup.exe again.", APP_NAME, MB_ICONERROR);
|
|
return 1;
|
|
}
|
|
if (S.quiet) work(NULL);
|
|
else run_with_window(inst);
|
|
if (S.result) {
|
|
if (!S.quiet) MessageBoxW(NULL, S.error, APP_NAME, MB_OK | MB_ICONERROR);
|
|
return 2;
|
|
}
|
|
if (S.launch) {
|
|
static wchar_t cmd[PATHN * 3];
|
|
wchar_t exe[PATHN];
|
|
swprintf(exe, PATHN, L"%ls\\NX2navi.exe", S.dest);
|
|
swprintf(cmd, PATHN * 3, L"\"%ls\" --from \"%ls\"", exe, S.src_dir);
|
|
STARTUPINFOW si = {.cb = sizeof si};
|
|
PROCESS_INFORMATION pi;
|
|
if (CreateProcessW(exe, cmd, NULL, NULL, FALSE, 0, NULL, S.dest, &si, &pi)) {
|
|
CloseHandle(pi.hThread);
|
|
CloseHandle(pi.hProcess);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|