/* Project Solana launcher: a game library for the static recompilations. * * Add disc images (Add disc, Scan folder, or drag and drop). Each one is identified by * the boot executable its SYSTEM.CNF names (BOOT2) and that executable's MD5, then * matched against the games this build knows. A supported disc gets a Play button that * starts that game's wrapper (rac1_wrapper.exe - ) in its own folder with the * settings below as environment variables. The library and settings are kept in * solana.ini next to this program. * * Plain Win32 + GDI, everything custom-drawn (double-buffered), DPI-aware. * Build: tools/build_frontend.py (zig cc, x86-windows-gnu). */ #ifndef UNICODE #define UNICODE #endif #ifndef _UNICODE #define _UNICODE #endif #define WIN32_LEAN_AND_MEAN #include #include #include #include #include #include #include #include #include #include /* ---- the games this build knows ---------------------------------------------------- */ typedef struct { const char *serial; /* BOOT2's file name, as in the disc's root */ const wchar_t *title, *region; const char *md5; /* the executable this recompilation was made from (0: any) */ const wchar_t *dir; /* the game's folder, relative to this program's */ const wchar_t *exe; /* its wrapper in that folder (0: not supported yet) */ const wchar_t *kit; /* its GitHub release tag and folder under games\ (the Build button) */ const wchar_t *const *levels; const wchar_t *rgn; /* build.py --region (0: pal); its output is build\windows_ */ } game_t; static const wchar_t *const rac1_levels[] = { L"Veldin", L"Novalis", L"Aridia", L"Kerwan", L"Eudora", L"Rilgar", L"Blarg Station", L"Umbris", L"Batalia", L"Gaspar", L"Orxon", L"Pokitaru", L"Hoven", L"Gemlik Base", L"Oltanis", L"Quartu", L"Kalebo III", L"Drek's Fleet", L"Veldin (Drek)", 0 }; static const game_t games[] = { { "SCES_509.16", L"Ratchet & Clank", L"PAL", "d86429e2eca4e1da7d7f1cc656af2574", L"games\\rac1\\build\\windows", L"rac1_wrapper.exe", L"rac1", rac1_levels }, { "SCUS_971.99", L"Ratchet & Clank", L"NTSC-U", "0c081d4e0353b419d95e5f95eb0ebacb", L"games\\rac1\\build\\windows_us", L"rac1_wrapper.exe", L"rac1", rac1_levels, L"us" }, { "SCPS_150.37", L"Ratchet & Clank", L"NTSC-J", 0, 0, 0, 0, 0 }, { "SCES_516.07", L"Ratchet & Clank 2", L"PAL", 0, 0, 0, 0, 0 }, { "SCUS_972.68", L"Ratchet & Clank: Going Commando", L"NTSC-U", 0, 0, 0, 0, 0 }, { "SCES_524.56", L"Ratchet & Clank 3", L"PAL", 0, 0, 0, 0, 0 }, { "SCUS_973.53", L"Ratchet & Clank: Up Your Arsenal", L"NTSC-U", 0, 0, 0, 0, 0 }, { "SCES_532.85", L"Ratchet: Gladiator", L"PAL", 0, 0, 0, 0, 0 }, { "SCUS_974.65", L"Ratchet: Deadlocked", L"NTSC-U", 0, 0, 0, 0, 0 }, }; #define NGAMES (int)(sizeof games / sizeof games[0]) /* ---- the library ------------------------------------------------------------------- */ enum { ST_OK, ST_REVISION, ST_NOTYET, ST_UNKNOWN, ST_NOTDISC, ST_MISSING }; typedef struct { wchar_t path[MAX_PATH]; char serial[32], md5[33]; int game; /* index into games[], or -1 */ int status; } disc_t; #define MAXDISCS 64 static disc_t discs[MAXDISCS]; static int ndiscs, sel = -1; /* settings */ static int set_upscale = 1, set_vsync, set_nomtgs, set_noglt, set_fpslog, set_capsec = 3, set_level = -1, set_minimize = 1; static wchar_t base_dir[MAX_PATH], ini_path[MAX_PATH]; static HWND hwnd; static float dpi = 1.0f; #define S(x) ((int)((x) * dpi + 0.5f)) /* ---- MD5 (RFC 1321) ----------------------------------------------------------------- */ typedef struct { unsigned a, b, c, d; unsigned long long n; unsigned char buf[64]; } md5_t; static const unsigned md5_k[64] = { 0xd76aa478,0xe8c7b756,0x242070db,0xc1bdceee,0xf57c0faf,0x4787c62a,0xa8304613,0xfd469501,0x698098d8,0x8b44f7af,0xffff5bb1,0x895cd7be,0x6b901122,0xfd987193,0xa679438e,0x49b40821, 0xf61e2562,0xc040b340,0x265e5a51,0xe9b6c7aa,0xd62f105d,0x02441453,0xd8a1e681,0xe7d3fbc8,0x21e1cde6,0xc33707d6,0xf4d50d87,0x455a14ed,0xa9e3e905,0xfcefa3f8,0x676f02d9,0x8d2a4c8a, 0xfffa3942,0x8771f681,0x6d9d6122,0xfde5380c,0xa4beea44,0x4bdecfa9,0xf6bb4b60,0xbebfbc70,0x289b7ec6,0xeaa127fa,0xd4ef3085,0x04881d05,0xd9d4d039,0xe6db99e5,0x1fa27cf8,0xc4ac5665, 0xf4292244,0x432aff97,0xab9423a7,0xfc93a039,0x655b59c3,0x8f0ccc92,0xffeff47d,0x85845dd1,0x6fa87e4f,0xfe2ce6e0,0xa3014314,0x4e0811a1,0xf7537e82,0xbd3af235,0x2ad7d2bb,0xeb86d391 }; static const unsigned char md5_r[64] = { 7,12,17,22,7,12,17,22,7,12,17,22,7,12,17,22,5,9,14,20,5,9,14,20,5,9,14,20,5,9,14,20, 4,11,16,23,4,11,16,23,4,11,16,23,4,11,16,23,6,10,15,21,6,10,15,21,6,10,15,21,6,10,15,21 }; static void md5_block(md5_t *m, const unsigned char *p) { unsigned w[16], a = m->a, b = m->b, c = m->c, d = m->d; for (int i = 0; i < 16; i++) w[i] = p[i * 4] | p[i * 4 + 1] << 8 | p[i * 4 + 2] << 16 | (unsigned)p[i * 4 + 3] << 24; for (int i = 0; i < 64; i++) { unsigned f, g; if (i < 16) { f = (b & c) | (~b & d); g = i; } else if (i < 32) { f = (d & b) | (~d & c); g = (5 * i + 1) & 15; } else if (i < 48) { f = b ^ c ^ d; g = (3 * i + 5) & 15; } else { f = c ^ (b | ~d); g = (7 * i) & 15; } unsigned t = d; d = c; c = b; unsigned x = a + f + md5_k[i] + w[g]; b = b + ((x << md5_r[i]) | (x >> (32 - md5_r[i]))); a = t; } m->a += a; m->b += b; m->c += c; m->d += d; } static void md5_init(md5_t *m) { m->a = 0x67452301; m->b = 0xefcdab89; m->c = 0x98badcfe; m->d = 0x10325476; m->n = 0; } static void md5_add(md5_t *m, const unsigned char *p, size_t n) { while (n--) { m->buf[m->n++ & 63] = *p++; if (!(m->n & 63)) md5_block(m, m->buf); } } static void md5_hex(md5_t *m, char out[33]) { unsigned long long bits = m->n * 8; unsigned char one = 0x80, zero = 0, len[8]; md5_add(m, &one, 1); while ((m->n & 63) != 56) md5_add(m, &zero, 1); for (int i = 0; i < 8; i++) len[i] = (unsigned char)(bits >> (8 * i)); md5_add(m, len, 8); unsigned v[4] = { m->a, m->b, m->c, m->d }; for (int i = 0; i < 16; i++) sprintf(out + 2 * i, "%02x", (v[i / 4] >> (8 * (i & 3))) & 255); out[32] = 0; } /* ---- disc images (ISO 9660, 2048-byte sectors) -------------------------------------- */ static int read_at(HANDLE f, unsigned long long off, void *buf, DWORD n) { LARGE_INTEGER li; li.QuadPart = (LONGLONG)off; DWORD got = 0; return SetFilePointerEx(f, li, 0, FILE_BEGIN) && ReadFile(f, buf, n, &got, 0) && got == n; } /* a file in the root directory: its first sector and size */ static int iso_find(HANDLE f, const unsigned char *root, unsigned rootn, const char *name, unsigned *lba, unsigned *size) { size_t nl = strlen(name); for (unsigned o = 0; o < rootn;) { unsigned len = root[o]; if (!len) { o = (o / 2048 + 1) * 2048; continue; } unsigned fl = root[o + 32]; const char *fn = (const char *)root + o + 33; if (fl >= nl && !_strnicmp(fn, name, nl) && (fl == nl || fn[nl] == ';')) { memcpy(lba, root + o + 2, 4); memcpy(size, root + o + 10, 4); return 1; } o += len; } (void)f; return 0; } static void identify(disc_t *d) { d->game = -1; d->serial[0] = d->md5[0] = 0; HANDLE f = CreateFileW(d->path, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, 0); if (f == INVALID_HANDLE_VALUE) { d->status = ST_MISSING; return; } unsigned char pvd[2048]; d->status = ST_NOTDISC; if (!read_at(f, 16 * 2048ull, pvd, 2048) || memcmp(pvd + 1, "CD001", 5)) { CloseHandle(f); return; } unsigned rlba, rsize; memcpy(&rlba, pvd + 156 + 2, 4); memcpy(&rsize, pvd + 156 + 10, 4); if (rsize > (1u << 20)) rsize = 1u << 20; unsigned char *root = malloc(rsize); if (!root || !read_at(f, rlba * 2048ull, root, rsize)) { free(root); CloseHandle(f); return; } unsigned lba, size; if (iso_find(f, root, rsize, "SYSTEM.CNF", &lba, &size) && size < 4096) { char cnf[4096] = { 0 }; if (read_at(f, lba * 2048ull, cnf, size)) { char *b = strstr(cnf, "BOOT2"); if (b && (b = strchr(b, '\\'))) { int k = 0; for (b++; *b && *b != ';' && *b != '\r' && *b != '\n' && k < 31; b++) d->serial[k++] = *b; d->serial[k] = 0; } } } d->status = ST_UNKNOWN; for (int i = 0; i < NGAMES; i++) if (!_stricmp(games[i].serial, d->serial)) d->game = i; if (d->game >= 0 && iso_find(f, root, rsize, d->serial, &lba, &size) && size < (64u << 20)) { unsigned char *exe = malloc(size); if (exe && read_at(f, lba * 2048ull, exe, size)) { md5_t m; md5_init(&m); md5_add(&m, exe, size); md5_hex(&m, d->md5); } free(exe); } free(root); CloseHandle(f); if (d->game < 0) return; const game_t *g = &games[d->game]; if (!g->exe) d->status = ST_NOTYET; else if (g->md5 && strcmp(g->md5, d->md5)) d->status = ST_REVISION; else d->status = ST_OK; } /* ---- game folders --------------------------------------------------------------------- */ static void game_dir(const game_t *g, wchar_t *out) { /* next to this program, in its games\\ folder (built by the Build button), or in * build\\windows (where development builds put it) */ const wchar_t *tries[3] = { L"", g->dir, L"build\\windows" }; for (int i = 0; i < 3; i++) { wchar_t d[MAX_PATH], p[MAX_PATH]; if (tries[i][0]) swprintf(d, MAX_PATH, L"%ls\\%ls", base_dir, tries[i]); else wcscpy(d, base_dir); swprintf(p, MAX_PATH, L"%ls\\%ls", d, g->exe); if (GetFileAttributesW(p) != INVALID_FILE_ATTRIBUTES) { wcscpy(out, d); return; } } swprintf(out, MAX_PATH, L"%ls\\%ls", base_dir, g->dir); } static int game_installed(const game_t *g) { wchar_t d[MAX_PATH], p[MAX_PATH]; if (!g->exe) return 0; game_dir(g, d); swprintf(p, MAX_PATH, L"%ls\\%ls", d, g->exe); if (GetFileAttributesW(p) == INVALID_FILE_ATTRIBUTES) return 0; swprintf(p, MAX_PATH, L"%ls\\rac1_game.dll", d); if (GetFileAttributesW(p) != INVALID_FILE_ATTRIBUTES) return 1; swprintf(p, MAX_PATH, L"%ls\\rac1_game.dll.manifest", d); /* still in parts: the wrapper joins them */ return GetFileAttributesW(p) != INVALID_FILE_ATTRIBUTES; } /* ---- settings and library on disk ------------------------------------------------------ */ static int ini_int(const wchar_t *sec, const wchar_t *key, int def) { return (int)GetPrivateProfileIntW(sec, key, def, ini_path); } static void ini_set_int(const wchar_t *sec, const wchar_t *key, int v) { wchar_t b[16]; swprintf(b, 16, L"%d", v); WritePrivateProfileStringW(sec, key, b, ini_path); } static void save_all(void) { WritePrivateProfileStringW(L"library", 0, 0, ini_path); ini_set_int(L"library", L"count", ndiscs); for (int i = 0; i < ndiscs; i++) { wchar_t k[16]; swprintf(k, 16, L"disc%d", i); WritePrivateProfileStringW(L"library", k, discs[i].path, ini_path); } ini_set_int(L"library", L"selected", sel); ini_set_int(L"settings", L"upscaled", set_upscale); ini_set_int(L"settings", L"vsync", set_vsync); ini_set_int(L"settings", L"no_gs_thread", set_nomtgs); ini_set_int(L"settings", L"no_gl_thread", set_noglt); ini_set_int(L"settings", L"fps_log", set_fpslog); ini_set_int(L"settings", L"capture_seconds", set_capsec); ini_set_int(L"settings", L"start_level", set_level); ini_set_int(L"settings", L"minimize", set_minimize); } static int add_disc(const wchar_t *path, int select) { wchar_t full[MAX_PATH]; if (!GetFullPathNameW(path, MAX_PATH, full, 0)) wcscpy(full, path); for (int i = 0; i < ndiscs; i++) if (!_wcsicmp(discs[i].path, full)) { if (select) sel = i; return i; } if (ndiscs >= MAXDISCS) return -1; disc_t *d = &discs[ndiscs]; memset(d, 0, sizeof *d); wcsncpy(d->path, full, MAX_PATH - 1); identify(d); if (select) sel = ndiscs; return ndiscs++; } static void load_all(void) { int n = ini_int(L"library", L"count", 0); for (int i = 0; i < n && i < MAXDISCS; i++) { wchar_t k[16], p[MAX_PATH]; swprintf(k, 16, L"disc%d", i); GetPrivateProfileStringW(L"library", k, L"", p, MAX_PATH, ini_path); if (p[0]) add_disc(p, 0); } sel = ini_int(L"library", L"selected", ndiscs ? 0 : -1); if (sel >= ndiscs) sel = ndiscs - 1; set_upscale = ini_int(L"settings", L"upscaled", 1); set_vsync = ini_int(L"settings", L"vsync", 0); set_nomtgs = ini_int(L"settings", L"no_gs_thread", 0); set_noglt = ini_int(L"settings", L"no_gl_thread", 0); set_fpslog = ini_int(L"settings", L"fps_log", 0); set_capsec = ini_int(L"settings", L"capture_seconds", 3); set_level = ini_int(L"settings", L"start_level", -1); set_minimize = ini_int(L"settings", L"minimize", 1); if (set_capsec < 1 || set_capsec > 10) set_capsec = 3; } /* ---- running a game ---------------------------------------------------------------------- */ static HANDLE game_proc; static int running = -1; static wchar_t message[256]; /* a line under the Play button */ static int message_bad; #define WM_GAME_EXIT (WM_APP + 1) static DWORD WINAPI watch(LPVOID p) { HANDLE h = (HANDLE)p; DWORD code = 0; WaitForSingleObject(h, INFINITE); GetExitCodeProcess(h, &code); PostMessageW(hwnd, WM_GAME_EXIT, code, 0); return 0; } static void env(const wchar_t *k, const wchar_t *v) { SetEnvironmentVariableW(k, v); } /* Upscaled: the smallest whole multiple of the PS2's 448 lines that covers the height of * the monitor this window is on (1080p: 3x, 1440p and 4K: 4x), at least 2x */ static int screen_h(void) { MONITORINFO mi; mi.cbSize = sizeof mi; if (GetMonitorInfoW(MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST), &mi)) return mi.rcMonitor.bottom - mi.rcMonitor.top; return GetSystemMetrics(SM_CYSCREEN); } static int upscale_factor(void) { int f = (screen_h() + 447) / 448; return f < 2 ? 2 : f > 4 ? 4 : f; } static void play(int i) { if (i < 0 || running >= 0) return; disc_t *d = &discs[i]; identify(d); if (d->status != ST_OK && d->status != ST_REVISION) return; const game_t *g = &games[d->game]; wchar_t dir[MAX_PATH], exe[MAX_PATH], cmd[2 * MAX_PATH + 64]; game_dir(g, dir); swprintf(exe, MAX_PATH, L"%ls\\%ls", dir, g->exe); if (!game_installed(g)) { swprintf(message, 256, L"The game files are not in %ls", dir); message_bad = 1; InvalidateRect(hwnd, 0, 0); return; } wchar_t b[32]; env(L"UYA_RENDERER", L"gl"); swprintf(b, 32, L"%d", set_upscale ? upscale_factor() : 1); env(L"UYA_SCALE", b); env(L"UYA_VSYNC", set_vsync ? L"1" : 0); env(L"UYA_MTGS", set_nomtgs ? L"0" : 0); env(L"UYA_GLTHREAD", set_noglt ? L"0" : 0); env(L"UYA_FPSLOG", set_fpslog ? L"1" : 0); swprintf(b, 32, L"%d", set_capsec * 50); env(L"UYA_GSREC_N", b); if (set_level >= 0) { swprintf(b, 32, L"%d@1", set_level); env(L"UYA_LEVEL", b); } else env(L"UYA_LEVEL", 0); swprintf(cmd, sizeof cmd / sizeof cmd[0], L"\"%ls\" - \"%ls\"", exe, d->path); STARTUPINFOW si; PROCESS_INFORMATION pi; memset(&si, 0, sizeof si); si.cb = sizeof si; if (!CreateProcessW(exe, cmd, 0, 0, FALSE, 0, 0, dir, &si, &pi)) { swprintf(message, 256, L"Could not start %ls (error %lu)", g->exe, GetLastError()); message_bad = 1; InvalidateRect(hwnd, 0, 0); return; } CloseHandle(pi.hThread); game_proc = pi.hProcess; running = i; message[0] = 0; message_bad = 0; CloseHandle(CreateThread(0, 0, watch, pi.hProcess, 0, 0)); if (set_minimize) ShowWindow(hwnd, SW_MINIMIZE); InvalidateRect(hwnd, 0, 0); } #include "solana_build.h" static void open_log(int i) { if (i < 0 || discs[i].game < 0 || !games[discs[i].game].exe) return; wchar_t dir[MAX_PATH], p[MAX_PATH]; const game_t *g = &games[discs[i].game]; game_dir(g, dir); swprintf(p, MAX_PATH, L"%ls\\rac1_log.txt", dir); if (!game_installed(g) || (bld.failed && bld.disc == i)) swprintf(p, MAX_PATH, L"%ls\\games\\%ls\\build_log.txt", base_dir, g->kit); /* (the build's) */ if (GetFileAttributesW(p) == INVALID_FILE_ATTRIBUTES) { wcscpy(message, L"No log yet: it is written when the game runs."); message_bad = 0; InvalidateRect(hwnd, 0, 0); return; } ShellExecuteW(hwnd, L"open", p, 0, 0, SW_SHOWNORMAL); } static void open_folder(int i) { if (i < 0 || discs[i].game < 0 || !games[discs[i].game].exe) return; wchar_t dir[MAX_PATH]; game_dir(&games[discs[i].game], dir); ShellExecuteW(hwnd, L"open", dir, 0, 0, SW_SHOWNORMAL); } /* ---- adding discs ---------------------------------------------------------------------------- */ static void pick_files(void) { static wchar_t buf[32768]; OPENFILENAMEW of; memset(&of, 0, sizeof of); buf[0] = 0; of.lStructSize = sizeof of; of.hwndOwner = hwnd; of.lpstrFilter = L"PS2 disc images (*.iso)\0*.iso\0All files\0*.*\0"; of.lpstrFile = buf; of.nMaxFile = 32768; of.lpstrTitle = L"Add disc images"; of.Flags = OFN_FILEMUSTEXIST | OFN_EXPLORER | OFN_ALLOWMULTISELECT | OFN_HIDEREADONLY; if (!GetOpenFileNameW(&of)) return; wchar_t *p = buf + wcslen(buf) + 1; if (!*p) add_disc(buf, 1); else for (; *p; p += wcslen(p) + 1) { wchar_t f[MAX_PATH]; swprintf(f, MAX_PATH, L"%ls\\%ls", buf, p); add_disc(f, 1); } save_all(); } static int scan_dir(const wchar_t *dir, int depth) { wchar_t pat[MAX_PATH]; WIN32_FIND_DATAW fd; int n = 0; swprintf(pat, MAX_PATH, L"%ls\\*", dir); HANDLE h = FindFirstFileW(pat, &fd); if (h == INVALID_HANDLE_VALUE) return 0; do { if (fd.cFileName[0] == L'.') continue; wchar_t p[MAX_PATH]; swprintf(p, MAX_PATH, L"%ls\\%ls", dir, fd.cFileName); if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { if (depth < 4) n += scan_dir(p, depth + 1); } else { size_t l = wcslen(fd.cFileName); if (l > 4 && !_wcsicmp(fd.cFileName + l - 4, L".iso")) { int before = ndiscs; add_disc(p, 0); n += ndiscs > before; } } } while (FindNextFileW(h, &fd)); FindClose(h); return n; } static void pick_folder(void) { BROWSEINFOW bi; memset(&bi, 0, sizeof bi); bi.hwndOwner = hwnd; bi.lpszTitle = L"Find disc images (.iso) in this folder and its subfolders"; bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_NEWDIALOGSTYLE; LPITEMIDLIST id = SHBrowseForFolderW(&bi); if (!id) return; wchar_t dir[MAX_PATH]; if (SHGetPathFromIDListW(id, dir)) { HCURSOR c = SetCursor(LoadCursor(0, IDC_WAIT)); int n = scan_dir(dir, 0); SetCursor(c); swprintf(message, 256, n == 1 ? L"Found 1 new disc image." : L"Found %d new disc images.", n); message_bad = 0; if (n && sel < 0) sel = 0; save_all(); } CoTaskMemFree(id); InvalidateRect(hwnd, 0, 0); } /* ---- drawing ----------------------------------------------------------------------------------- */ /* the palette (assets/palette.json, "Horizon") */ #define C_BG RGB(0x0A, 0x0E, 0x1A) #define C_SIDE RGB(0x0F, 0x14, 0x24) #define C_CARD RGB(0x15, 0x1C, 0x30) #define C_CARDHI RGB(0x1C, 0x25, 0x40) #define C_SELECT RGB(0x1C, 0x25, 0x40) #define C_LINE RGB(0x26, 0x30, 0x4D) #define C_TEXT RGB(0xF4, 0xF6, 0xFB) #define C_MUTED RGB(0x9A, 0xA6, 0xC6) #define C_DIM RGB(0x5E, 0x68, 0x88) #define C_ACCENT RGB(0xFF, 0x7A, 0x3D) #define C_ACCENTHI RGB(0xFF, 0x94, 0x61) #define C_ONACCENT RGB(0x0A, 0x0E, 0x1A) #define C_CYAN RGB(0x7F, 0xD1, 0xFF) #define C_OK RGB(0x3D, 0xD6, 0x8C) #define C_WARN RGB(0xFF, 0xB2, 0x38) #define C_BAD RGB(0xFF, 0x5A, 0x5F) #include "solana_img.h" enum { W_NONE, W_ADD, W_SCAN, W_ITEM, W_PLAY, W_SETTINGS, W_LOG, W_FOLDER, W_REMOVE, W_BACK, W_RENDER, W_TOGGLE, W_CAPDOWN, W_CAPUP, W_LVLDOWN, W_LVLUP, W_CANCEL, W_REBUILD }; typedef struct { RECT r; int id, arg; } widget_t; static widget_t wid[96]; static int nwid, hover_id = W_NONE, hover_arg, press_id = W_NONE, press_arg; static int view_settings, scroll; static HFONT f_title, f_head, f_body, f_bold, f_small, f_logo, f_btn; static void add_w(RECT r, int id, int arg) { if (nwid < 96) { wid[nwid].r = r; wid[nwid].id = id; wid[nwid].arg = arg; nwid++; } } static int is_hover(int id, int arg) { return hover_id == id && hover_arg == arg; } static HFONT mkfont(int px, int weight) { return CreateFontW(-S(px), 0, 0, 0, weight, 0, 0, 0, DEFAULT_CHARSET, 0, 0, CLEARTYPE_QUALITY, 0, L"Segoe UI"); } static void make_fonts(void) { HFONT *all[] = { &f_title, &f_head, &f_body, &f_bold, &f_small, &f_logo, &f_btn }; for (unsigned i = 0; i < sizeof all / sizeof all[0]; i++) if (*all[i]) DeleteObject(*all[i]); f_title = mkfont(30, FW_SEMIBOLD); f_head = mkfont(15, FW_SEMIBOLD); f_body = mkfont(14, FW_NORMAL); f_bold = mkfont(15, FW_SEMIBOLD); f_small = mkfont(12, FW_NORMAL); f_logo = mkfont(17, FW_BOLD); f_btn = mkfont(14, FW_SEMIBOLD); } static void fill(HDC dc, RECT r, COLORREF c) { SetDCBrushColor(dc, c); FillRect(dc, &r, GetStockObject(DC_BRUSH)); } static void rrect(HDC dc, RECT r, int rad, COLORREF fillc, COLORREF edge) { SelectObject(dc, GetStockObject(DC_BRUSH)); SelectObject(dc, GetStockObject(DC_PEN)); SetDCBrushColor(dc, fillc); SetDCPenColor(dc, edge); RoundRect(dc, r.left, r.top, r.right, r.bottom, S(rad), S(rad)); } static void text(HDC dc, const wchar_t *s, RECT r, HFONT f, COLORREF c, UINT fmt) { SelectObject(dc, f); SetTextColor(dc, c); SetBkMode(dc, TRANSPARENT); DrawTextW(dc, s, -1, &r, fmt | DT_NOPREFIX); } static int text_w(HDC dc, const wchar_t *s, HFONT f) { SIZE z; SelectObject(dc, f); GetTextExtentPoint32W(dc, s, (int)wcslen(s), &z); return z.cx; } static RECT rc(int l, int t, int r, int b) { RECT x = { l, t, r, b }; return x; } static void status_text(const disc_t *d, wchar_t *out, COLORREF *c) { switch (d->status) { case ST_OK: if (bld.active && bld.disc == (int)(d - discs)) { wcscpy(out, L"Building\u2026"); *c = C_ACCENT; } else if (games[d->game].exe && !game_installed(&games[d->game])) { wcscpy(out, games[d->game].kit ? L"Ready to build" : L"Game files missing"); *c = games[d->game].kit ? C_ACCENT : C_BAD; } else { wcscpy(out, L"Ready to play"); *c = C_OK; } break; case ST_REVISION: wcscpy(out, L"Different disc revision"); *c = C_WARN; break; case ST_NOTYET: wcscpy(out, L"Not supported yet"); *c = C_DIM; break; case ST_UNKNOWN: wcscpy(out, L"Unknown PS2 disc"); *c = C_DIM; break; case ST_MISSING: wcscpy(out, L"Disc image not found"); *c = C_BAD; break; default: wcscpy(out, L"Not a PS2 disc image"); *c = C_BAD; break; } } static void disc_title(const disc_t *d, wchar_t *out) { if (d->game >= 0) { wcscpy(out, games[d->game].title); return; } const wchar_t *n = wcsrchr(d->path, L'\\'); n = n ? n + 1 : d->path; wcsncpy(out, n, 127); out[127] = 0; } /* a button: label, and an icon (assets/png/icons, white, tinted here) left of it */ static void button_i(HDC dc, RECT r, const wchar_t *label, int id, int arg, int primary, int enabled, int icon) { COLORREF bg = primary ? C_ACCENT : C_CARD, fg = primary ? C_ONACCENT : C_TEXT, edge = primary ? C_ACCENT : C_LINE; if (!enabled) { bg = primary ? RGB(0x5A, 0x3A, 0x2C) : C_CARD; fg = primary ? RGB(0x9A, 0x7E, 0x70) : C_DIM; edge = primary ? bg : C_LINE; } else if (is_hover(id, arg)) { bg = primary ? C_ACCENTHI : C_CARDHI; edge = primary ? bg : RGB(0x34, 0x40, 0x66); } if (enabled && press_id == id && press_arg == arg && is_hover(id, arg)) OffsetRect(&r, 0, S(1)); rrect(dc, r, 6, bg, edge); HFONT f = primary ? f_bold : f_btn; int isz = S(18), gap = S(8), tw = text_w(dc, label, f); int total = tw + (icon ? isz + gap : 0), x = (r.left + r.right - total) / 2; if (icon) { img_icon(dc, icon, x, (r.top + r.bottom - isz) / 2, isz, fg); x += isz + gap; } text(dc, label, rc(x, r.top, x + tw + S(2), r.bottom), f, fg, DT_LEFT | DT_VCENTER | DT_SINGLELINE); if (enabled) add_w(r, id, arg); } static void button(HDC dc, RECT r, const wchar_t *label, int id, int arg, int primary, int enabled) { button_i(dc, r, label, id, arg, primary, enabled, 0); } static void toggle(HDC dc, int x, int y, int on, int arg) { RECT r = rc(x, y, x + S(40), y + S(22)); rrect(dc, r, 22, on ? C_ACCENT : C_LINE, on ? C_ACCENT : C_LINE); int kx = on ? r.right - S(19) : r.left + S(3); RECT k = rc(kx, y + S(3), kx + S(16), y + S(19)); rrect(dc, k, 16, on ? C_ONACCENT : C_TEXT, on ? C_ONACCENT : C_TEXT); } /* one row of the settings list: title, explanation, and the control area on the right */ static int setting_row(HDC dc, RECT area, int y, const wchar_t *title, const wchar_t *help) { RECT t = rc(area.left, y, area.right - S(220), y + S(22)); text(dc, title, t, f_bold, C_TEXT, DT_LEFT | DT_SINGLELINE | DT_VCENTER); RECT h = rc(area.left, y + S(22), area.right - S(220), y + S(58)); text(dc, help, h, f_small, C_MUTED, DT_LEFT | DT_WORDBREAK); return y + S(66); } static void draw_settings(HDC dc, RECT area) { int y = area.top; text(dc, L"Settings", rc(area.left, y, area.right, y + S(40)), f_title, C_TEXT, DT_LEFT | DT_SINGLELINE); button(dc, rc(area.right - S(90), y + S(4), area.right, y + S(36)), L"Done", W_BACK, 0, 0, 1); y += S(52); text(dc, L"Apply the next time a game starts.", rc(area.left, y, area.right, y + S(20)), f_body, C_MUTED, DT_LEFT | DT_SINGLELINE); y += S(34); int cx = area.right - S(200); int y0 = y; { wchar_t help[200]; swprintf(help, 200, L"Native draws at the PS2's own resolution, as the game was. Upscaled draws at %dx for this %d-line screen: sharper, more GPU work.", upscale_factor(), screen_h()); y = setting_row(dc, area, y, L"Resolution", help); } { const wchar_t *opt[2] = { L"Native", L"Upscaled" }; RECT box = rc(cx, y0 + S(4), area.right, y0 + S(36)); rrect(dc, box, 6, C_CARD, C_LINE); for (int k = 0; k < 2; k++) { RECT s = rc(box.left + S(3) + k * (S(197) / 2), box.top + S(3), box.left + S(3) + (k + 1) * (S(197) / 2) - S(3), box.bottom - S(3)); int on = (k == 1) == (set_upscale != 0); if (on) rrect(dc, s, 6, C_ACCENT, C_ACCENT); else if (is_hover(W_RENDER, k)) rrect(dc, s, 6, C_CARDHI, C_CARDHI); text(dc, opt[k], s, f_btn, on ? C_ONACCENT : C_TEXT, DT_CENTER | DT_VCENTER | DT_SINGLELINE); add_w(s, W_RENDER, k); } } struct { const wchar_t *t, *h; int *v; int invert; } rows[] = { { L"VSync", L"Waits for the monitor's refresh. Off lets the game pace itself at 50 fps.", &set_vsync, 0 }, { L"Graphics on its own thread", L"Runs the PS2 graphics chip beside the game. Turn off only to test a problem.", &set_nomtgs, 1 }, { L"OpenGL calls on their own thread", L"Sends drawing to the driver from a separate thread. Turn off only to test a problem.", &set_noglt, 1 }, { L"Performance statistics", L"Writes frame timings to rac1_log.txt every 2 seconds, for bug reports.", &set_fpslog, 0 }, { L"Minimize while playing", L"Tucks this window away when a game starts.", &set_minimize, 0 }, }; for (unsigned i = 0; i < sizeof rows / sizeof rows[0]; i++) { int ry = y; y = setting_row(dc, area, y, rows[i].t, rows[i].h); int on = rows[i].invert ? !*rows[i].v : *rows[i].v; RECT hit = rc(area.right - S(48), ry + S(4), area.right, ry + S(30)); toggle(dc, area.right - S(44), ry + S(4), on, (int)i); add_w(hit, W_TOGGLE, (int)i); } /* steppers */ { int ry = y; wchar_t b[64]; y = setting_row(dc, area, y, L"F9 capture length", L"F9 in the game records this much graphics input to gsrec_NN.bin, for glitch reports (about 260 MB a second)."); swprintf(b, 64, L"%d second%ls", set_capsec, set_capsec == 1 ? L"" : L"s"); RECT box = rc(cx, ry + S(4), area.right, ry + S(36)); rrect(dc, box, 6, C_CARD, C_LINE); text(dc, b, box, f_btn, C_TEXT, DT_CENTER | DT_VCENTER | DT_SINGLELINE); RECT l = rc(box.left + S(3), box.top + S(3), box.left + S(35), box.bottom - S(3)), r = rc(box.right - S(35), box.top + S(3), box.right - S(3), box.bottom - S(3)); if (is_hover(W_CAPDOWN, 0)) rrect(dc, l, 6, C_CARDHI, C_CARDHI); if (is_hover(W_CAPUP, 0)) rrect(dc, r, 6, C_CARDHI, C_CARDHI); text(dc, L"−", l, f_bold, C_TEXT, DT_CENTER | DT_VCENTER | DT_SINGLELINE); text(dc, L"+", r, f_bold, C_TEXT, DT_CENTER | DT_VCENTER | DT_SINGLELINE); add_w(l, W_CAPDOWN, 0); add_w(r, W_CAPUP, 0); } { int ry = y; wchar_t b[64]; y = setting_row(dc, area, y, L"Start in a level (testing)", L"Skips the menus and loads this level with a fresh save. Off for normal play."); if (set_level < 0) wcscpy(b, L"Off"); else swprintf(b, 64, L"%02d %ls", set_level, rac1_levels[set_level]); RECT box = rc(cx, ry + S(4), area.right, ry + S(36)); rrect(dc, box, 6, set_level >= 0 ? C_SELECT : C_CARD, set_level >= 0 ? C_ACCENT : C_LINE); RECT mid = rc(box.left + S(36), box.top, box.right - S(36), box.bottom); text(dc, b, mid, f_btn, C_TEXT, DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS); RECT l = rc(box.left + S(3), box.top + S(3), box.left + S(35), box.bottom - S(3)), r = rc(box.right - S(35), box.top + S(3), box.right - S(3), box.bottom - S(3)); if (is_hover(W_LVLDOWN, 0)) rrect(dc, l, 6, C_CARDHI, C_CARDHI); if (is_hover(W_LVLUP, 0)) rrect(dc, r, 6, C_CARDHI, C_CARDHI); text(dc, L"‹", l, f_bold, C_TEXT, DT_CENTER | DT_VCENTER | DT_SINGLELINE); text(dc, L"›", r, f_bold, C_TEXT, DT_CENTER | DT_VCENTER | DT_SINGLELINE); add_w(l, W_LVLDOWN, 0); add_w(r, W_LVLUP, 0); } { /* building the selected game again (a newer build kit, or after a problem) */ int ry = y; int ok = sel >= 0 && discs[sel].game >= 0 && games[discs[sel].game].kit && (discs[sel].status == ST_OK || discs[sel].status == ST_REVISION) && !bld.active && running < 0; y = setting_row(dc, area, y, L"Rebuild the game", L"Downloads the latest build kit and builds the selected game from your disc again. Saves are kept."); button_i(dc, rc(cx, ry + S(4), area.right, ry + S(36)), L"Rebuild", W_REBUILD, 0, 0, ok, IMG_I_REBUILD); } } static void info_row(HDC dc, int x, int *y, int w, const wchar_t *k, const wchar_t *v, COLORREF vc) { text(dc, k, rc(x, *y, x + S(130), *y + S(22)), f_body, C_MUTED, DT_LEFT | DT_SINGLELINE | DT_VCENTER); text(dc, v, rc(x + S(130), *y, x + w, *y + S(22)), f_body, vc, DT_LEFT | DT_SINGLELINE | DT_VCENTER | DT_PATH_ELLIPSIS); *y += S(30); } static void draw_details(HDC dc, RECT area) { if (sel < 0 || sel >= ndiscs) { int cy = (area.top + area.bottom) / 2 + S(70); img_draw(dc, IMG_EMPTY, (area.left + area.right) / 2 - S(160), cy - S(290), S(320), S(200)); text(dc, L"Your library is empty", rc(area.left, cy - S(60), area.right, cy - S(24)), f_title, C_TEXT, DT_CENTER | DT_SINGLELINE); text(dc, L"Add your own copy of the game as a disc image (.iso), or drop one on this window.", rc(area.left, cy - S(14), area.right, cy + S(10)), f_body, C_MUTED, DT_CENTER | DT_SINGLELINE); int bw = S(176); button_i(dc, rc((area.left + area.right) / 2 - bw - S(6), cy + S(30), (area.left + area.right) / 2 - S(6), cy + S(70)), L"Add disc image", W_ADD, 1, 1, 1, IMG_I_ADD); button_i(dc, rc((area.left + area.right) / 2 + S(6), cy + S(30), (area.left + area.right) / 2 + bw + S(6), cy + S(70)), L"Scan a folder", W_SCAN, 1, 0, 1, IMG_I_SCAN); return; } disc_t *d = &discs[sel]; const game_t *g = d->game >= 0 ? &games[d->game] : 0; wchar_t t[160], st[64], b[MAX_PATH + 64]; COLORREF sc; disc_title(d, t); status_text(d, st, &sc); int y = area.top, w = area.right - area.left; /* banner */ RECT ban = rc(area.left, y, area.right, y + S(150)); { /* the Horizon banner (assets/png/default_banner.png): filling the area, anchored right, under rounded corners */ HRGN rg = CreateRoundRectRgn(ban.left, ban.top, ban.right + 1, ban.bottom + 1, S(10), S(10)); SelectClipRgn(dc, rg); fill(dc, ban, C_BG); img_cover(dc, IMG_BANNER, ban, 1.0); SelectClipRgn(dc, 0); DeleteObject(rg); } text(dc, t, rc(ban.left + S(24), ban.top + S(30), ban.right - S(200), ban.top + S(74)), f_title, C_TEXT, DT_LEFT | DT_SINGLELINE | DT_END_ELLIPSIS); { int x = ban.left + S(24), by = ban.top + S(84); if (g) { int tw = text_w(dc, g->region, f_small) + S(16); RECT chip = rc(x, by, x + tw, by + S(22)); rrect(dc, chip, 22, C_CARD, C_LINE); text(dc, g->region, chip, f_small, C_TEXT, DT_CENTER | DT_VCENTER | DT_SINGLELINE); x += tw + S(10); } SelectObject(dc, GetStockObject(DC_BRUSH)); SelectObject(dc, GetStockObject(NULL_PEN)); SetDCBrushColor(dc, sc); Ellipse(dc, x, by + S(7), x + S(9), by + S(16)); text(dc, st, rc(x + S(15), by, ban.right - S(200), by + S(22)), f_body, C_TEXT, DT_LEFT | DT_VCENTER | DT_SINGLELINE); } y = ban.bottom + S(24); /* actions */ int supported = (d->status == ST_OK || d->status == ST_REVISION) && g && g->exe; int installed = supported && game_installed(g); int building = bld.active && bld.disc == sel; if (building) { /* the build's progress in place of the buttons */ RECT pc = rc(area.left, y, area.right, y + S(48)); int bw = (area.right - S(110)) - area.left; RECT track = rc(area.left, y + S(30), area.left + bw - S(12), y + S(38)); rrect(dc, track, 8, C_CARD, C_LINE); int fw = (int)((track.right - track.left) * (bld.frac < 0 ? 0 : bld.frac > 1 ? 1 : bld.frac)); if (fw > S(8)) rrect(dc, rc(track.left, track.top, track.left + fw, track.bottom), 8, C_ACCENT, C_ACCENT); wchar_t pcnt[16]; swprintf(pcnt, 16, L"%d%%", (int)(bld.frac * 100 + 0.5)); text(dc, bld.status, rc(area.left, y, area.left + bw - S(70), y + S(24)), f_bold, C_TEXT, DT_LEFT | DT_SINGLELINE | DT_VCENTER | DT_END_ELLIPSIS); text(dc, pcnt, rc(area.left + bw - S(70), y, area.left + bw - S(12), y + S(24)), f_bold, C_MUTED, DT_RIGHT | DT_SINGLELINE | DT_VCENTER); button_i(dc, rc(area.right - S(110), y + S(4), area.right, y + S(44)), bld.cancel ? L"Stopping\u2026" : L"Cancel", W_CANCEL, 0, 0, !bld.cancel, IMG_I_CANCEL); (void)pc; } else { int can = supported && running < 0 && (installed || (g->kit && !bld.active)); const wchar_t *pl = running == sel ? L"Running\u2026" : (supported && !installed && g->kit) ? L"Build" : L"Play"; button_i(dc, rc(area.left, y, area.left + S(170), y + S(48)), pl, W_PLAY, 0, 1, can, (supported && !installed && g->kit) ? IMG_I_BUILD : IMG_I_PLAY); int bx = area.left + S(182); int has_files = g && g->exe; button_i(dc, rc(bx, y + S(4), bx + S(128), y + S(44)), L"Settings", W_SETTINGS, 0, 0, 1, IMG_I_SETTINGS); bx += S(140); button_i(dc, rc(bx, y + S(4), bx + S(128), y + S(44)), L"Open log", W_LOG, 0, 0, has_files, IMG_I_LOG); bx += S(140); button_i(dc, rc(bx, y + S(4), bx + S(150), y + S(44)), L"Game folder", W_FOLDER, 0, 0, has_files, IMG_I_FOLDER); button_i(dc, rc(area.right - S(120), y + S(4), area.right, y + S(44)), L"Remove", W_REMOVE, 0, 0, running != sel && !(bld.active && bld.disc == sel), IMG_I_REMOVE); } y += S(60); if (bld.failed && bld.disc == sel && !building) text(dc, bld.err, rc(area.left, y, area.right, y + S(22)), f_body, C_BAD, DT_LEFT | DT_SINGLELINE | DT_END_ELLIPSIS); else if (message[0]) { text(dc, message, rc(area.left, y, area.right, y + S(22)), f_body, message_bad ? C_BAD : C_MUTED, DT_LEFT | DT_SINGLELINE | DT_END_ELLIPSIS); } else if (building) text(dc, L"Building from your disc. You can keep using your PC; closing this window stops the build.", rc(area.left, y, area.right, y + S(22)), f_body, C_MUTED, DT_LEFT | DT_SINGLELINE | DT_END_ELLIPSIS); else if (supported && !installed && g->kit) text(dc, L"First time: downloads about 95 MB of tools and source, then builds the game from your disc (10 to 30 minutes).", rc(area.left, y, area.right, y + S(22)), f_body, C_MUTED, DT_LEFT | DT_SINGLELINE | DT_END_ELLIPSIS); else if (d->status == ST_REVISION) text(dc, L"This disc's executable differs from the one this build was made from. It may not run correctly.", rc(area.left, y, area.right, y + S(22)), f_body, C_WARN, DT_LEFT | DT_SINGLELINE | DT_END_ELLIPSIS); else if (d->status == ST_NOTYET) text(dc, L"This game is recognised, but its recompilation is not built yet.", rc(area.left, y, area.right, y + S(22)), f_body, C_MUTED, DT_LEFT | DT_SINGLELINE); y += S(36); /* the disc */ RECT card = rc(area.left, y, area.right, y + S(190)); rrect(dc, card, 10, C_CARD, C_LINE); int x = card.left + S(20), iy = card.top + S(16), iw = w - S(40); text(dc, L"Disc", rc(x, iy, x + iw, iy + S(22)), f_head, C_TEXT, DT_LEFT | DT_SINGLELINE); iy += S(32); info_row(dc, x, &iy, iw, L"Image", d->path, C_TEXT); MultiByteToWideChar(CP_ACP, 0, d->serial[0] ? d->serial : "none", -1, b, 64); info_row(dc, x, &iy, iw, L"Boot executable", b, C_TEXT); if (d->md5[0]) { wchar_t m[64]; MultiByteToWideChar(CP_ACP, 0, d->md5, -1, m, 64); swprintf(b, MAX_PATH + 64, L"%ls %ls", m, d->status == ST_OK ? L"(matches)" : d->status == ST_REVISION ? L"(different revision)" : L""); info_row(dc, x, &iy, iw, L"MD5", b, d->status == ST_REVISION ? C_WARN : C_TEXT); } else info_row(dc, x, &iy, iw, L"MD5", L"–", C_DIM); if (g && g->exe) { wchar_t gd[MAX_PATH]; game_dir(g, gd); if (game_installed(g)) info_row(dc, x, &iy, iw, L"Game files", gd, C_TEXT); else info_row(dc, x, &iy, iw, L"Game files", g->kit ? L"Not built yet" : L"Missing", g->kit ? C_MUTED : C_BAD); } else info_row(dc, x, &iy, iw, L"Game files", L"–", C_DIM); y = card.bottom + S(12); /* the controller: the PS2's buttons and the same buttons on an Xbox pad (assets/png/controller_colour) */ if (g && g->exe && y + S(232) < area.bottom) { RECT c2 = rc(area.left, y, area.right, y + S(232)); rrect(dc, c2, 10, C_CARD, C_LINE); x = c2.left + S(20); iy = c2.top + S(16); text(dc, L"Controller", rc(x, iy, x + S(200), iy + S(22)), f_head, C_TEXT, DT_LEFT | DT_SINGLELINE); text(dc, L"Xbox, DualShock 4 and DualSense, over USB or Bluetooth", rc(x + S(110), iy, x + iw, iy + S(22)), f_small, C_MUTED, DT_RIGHT | DT_SINGLELINE | DT_VCENTER); iy += S(36); static const struct { int ps, xb; const wchar_t *pn, *xn; } rows[10] = { { IMG_PS_CROSS, IMG_XB_A, L"Cross", L"A" }, { IMG_PS_CIRCLE, IMG_XB_B, L"Circle", L"B" }, { IMG_PS_SQUARE, IMG_XB_X, L"Square", L"X" }, { IMG_PS_TRIANGLE, IMG_XB_Y, L"Triangle", L"Y" }, { IMG_PS_START, IMG_XB_MENU, L"Start", L"Menu" }, { IMG_PS_L1, IMG_XB_LB, L"L1", L"LB" }, { IMG_PS_R1, IMG_XB_RB, L"R1", L"RB" }, { IMG_PS_L2, IMG_XB_LT, L"L2", L"LT" }, { IMG_PS_R2, IMG_XB_RT, L"R2", L"RT" }, { IMG_PS_SELECT, IMG_XB_VIEW, L"Select", L"View" } }; int col = iw / 2, gs = S(24), rh = S(30); for (int i = 0; i < 10; i++) { int cx0 = x + (i / 5) * col, ry = iy + (i % 5) * rh; img_draw(dc, rows[i].ps, cx0, ry, gs, gs); text(dc, rows[i].pn, rc(cx0 + gs + S(10), ry, cx0 + gs + S(100), ry + gs), f_body, C_MUTED, DT_LEFT | DT_SINGLELINE | DT_VCENTER); text(dc, L"\u2192", rc(cx0 + S(120), ry, cx0 + S(144), ry + gs), f_body, C_DIM, DT_CENTER | DT_SINGLELINE | DT_VCENTER); img_draw(dc, rows[i].xb, cx0 + S(154), ry, gs, gs); text(dc, rows[i].xn, rc(cx0 + S(154) + gs + S(10), ry, cx0 + col - S(8), ry + gs), f_bold, C_TEXT, DT_LEFT | DT_SINGLELINE | DT_VCENTER); } } } static void paint(HDC dc, RECT cr) { nwid = 0; { /* the starfield tile (512 x 512 at 3x: 171 px at 100%) behind the page */ HBRUSH tb = img_tile_brush(IMG_BGTILE, S(171)); if (tb) { SetBrushOrgEx(dc, 0, 0, 0); FillRect(dc, &cr, tb); } else fill(dc, cr, C_BG); } int sw = S(300), hh = S(64); /* sidebar */ RECT side = rc(0, 0, sw, cr.bottom); fill(dc, side, C_SIDE); fill(dc, rc(sw - 1, 0, sw, cr.bottom), C_LINE); /* the wordmark (assets/png/wordmark.png, 260 x 40 at 100%) */ if (!img_draw(dc, IMG_WORDMARK, S(20), S(14), S(260), S(40))) text(dc, L"PROJECT SOLANA", rc(S(20), S(18), sw - S(10), S(46)), f_logo, C_TEXT, DT_LEFT | DT_VCENTER | DT_SINGLELINE); text(dc, L"LIBRARY", rc(S(20), hh + S(6), sw - S(20), hh + S(26)), f_small, C_DIM, DT_LEFT | DT_SINGLELINE); int ly = hh + S(32) - scroll, ih = S(68); HRGN clip = CreateRectRgn(0, hh + S(30), sw - 1, cr.bottom - S(116)); SelectClipRgn(dc, clip); for (int i = 0; i < ndiscs; i++, ly += ih + S(6)) { RECT it = rc(S(12), ly, sw - S(12), ly + ih); if (it.bottom < hh || it.top > cr.bottom) continue; disc_t *d = &discs[i]; COLORREF bg = i == sel ? C_SELECT : is_hover(W_ITEM, i) ? C_CARD : C_SIDE; rrect(dc, it, 10, bg, i == sel ? C_LINE : bg); if (i == sel) fill(dc, rc(it.left, it.top + S(14), it.left + S(3), it.bottom - S(14)), C_ACCENT); wchar_t t[160], st[64], sub[128]; COLORREF sc; disc_title(d, t); status_text(d, st, &sc); text(dc, t, rc(it.left + S(16), it.top + S(10), it.right - S(12), it.top + S(32)), f_bold, C_TEXT, DT_LEFT | DT_SINGLELINE | DT_END_ELLIPSIS); if (d->game >= 0) swprintf(sub, 128, L"%ls", games[d->game].region); else sub[0] = 0; int x = it.left + S(16); if (sub[0]) { text(dc, sub, rc(x, it.top + S(36), it.right, it.top + S(56)), f_small, C_MUTED, DT_LEFT | DT_SINGLELINE); x += text_w(dc, sub, f_small) + S(10); } SelectObject(dc, GetStockObject(DC_BRUSH)); SelectObject(dc, GetStockObject(NULL_PEN)); SetDCBrushColor(dc, sc); Ellipse(dc, x, it.top + S(42), x + S(7), it.top + S(49)); text(dc, st, rc(x + S(12), it.top + S(36), it.right - S(12), it.top + S(56)), f_small, C_MUTED, DT_LEFT | DT_SINGLELINE | DT_END_ELLIPSIS); add_w(it, W_ITEM, i); } SelectClipRgn(dc, 0); DeleteObject(clip); if (!ndiscs) text(dc, L"No games yet", rc(S(20), hh + S(36), sw - S(20), hh + S(60)), f_body, C_DIM, DT_LEFT | DT_SINGLELINE); fill(dc, rc(0, cr.bottom - S(116), sw - 1, cr.bottom - S(115)), C_LINE); button_i(dc, rc(S(16), cr.bottom - S(100), sw - S(16), cr.bottom - S(60)), L"Add disc image", W_ADD, 0, 0, 1, IMG_I_ADD); button_i(dc, rc(S(16), cr.bottom - S(52), sw - S(16), cr.bottom - S(16)), L"Scan a folder", W_SCAN, 0, 0, 1, IMG_I_SCAN); RECT area = rc(sw + S(24), S(24), cr.right - S(24), cr.bottom - S(24)); if (view_settings) draw_settings(dc, area); else draw_details(dc, area); } /* ---- input ---------------------------------------------------------------------------------- */ static widget_t *hit(int x, int y) { POINT p = { x, y }; for (int i = nwid - 1; i >= 0; i--) if (PtInRect(&wid[i].r, p)) return &wid[i]; return 0; } static void click(int id, int arg) { switch (id) { case W_ADD: pick_files(); view_settings = 0; break; case W_SCAN: pick_folder(); view_settings = 0; break; case W_ITEM: sel = arg; view_settings = 0; message[0] = 0; identify(&discs[sel]); save_all(); break; case W_PLAY: if (sel >= 0 && discs[sel].game >= 0 && games[discs[sel].game].exe && !game_installed(&games[discs[sel].game])) build_start(sel); else play(sel); break; case W_CANCEL: bld.cancel = 1; if (bld.job) TerminateJobObject(bld.job, 1); break; case W_REBUILD: if (sel >= 0 && running < 0) { view_settings = 0; build_start(sel); } break; case W_SETTINGS: view_settings = 1; break; case W_BACK: view_settings = 0; save_all(); break; case W_LOG: open_log(sel); break; case W_FOLDER: open_folder(sel); break; case W_REMOVE: if (sel >= 0 && running != sel) { memmove(&discs[sel], &discs[sel + 1], (size_t)(ndiscs - sel - 1) * sizeof discs[0]); ndiscs--; if (running > sel) running--; if (sel >= ndiscs) sel = ndiscs - 1; message[0] = 0; save_all(); } break; case W_RENDER: set_upscale = arg; save_all(); break; case W_TOGGLE: { int *v[] = { &set_vsync, &set_nomtgs, &set_noglt, &set_fpslog, &set_minimize }; *v[arg] = !*v[arg]; save_all(); break; } case W_CAPDOWN: if (set_capsec > 1) set_capsec--; save_all(); break; case W_CAPUP: if (set_capsec < 10) set_capsec++; save_all(); break; case W_LVLDOWN: set_level = set_level < 0 ? 18 : set_level - 1; save_all(); break; case W_LVLUP: set_level = set_level >= 18 ? -1 : set_level + 1; save_all(); break; } InvalidateRect(hwnd, 0, 0); } static LRESULT CALLBACK wndproc(HWND h, UINT m, WPARAM w, LPARAM l) { switch (m) { case WM_PAINT: { PAINTSTRUCT ps; HDC dc = BeginPaint(h, &ps); RECT cr; GetClientRect(h, &cr); HDC mem = CreateCompatibleDC(dc); HBITMAP bm = CreateCompatibleBitmap(dc, cr.right, cr.bottom); HGDIOBJ old = SelectObject(mem, bm); paint(mem, cr); BitBlt(dc, 0, 0, cr.right, cr.bottom, mem, 0, 0, SRCCOPY); SelectObject(mem, old); DeleteObject(bm); DeleteDC(mem); EndPaint(h, &ps); return 0; } case WM_ERASEBKGND: return 1; case WM_MOUSEMOVE: { widget_t *x = hit(GET_X_LPARAM(l), GET_Y_LPARAM(l)); int id = x ? x->id : W_NONE, arg = x ? x->arg : 0; if (id != hover_id || arg != hover_arg) { hover_id = id; hover_arg = arg; InvalidateRect(h, 0, 0); } SetCursor(LoadCursor(0, id != W_NONE ? IDC_HAND : IDC_ARROW)); TRACKMOUSEEVENT t = { sizeof t, TME_LEAVE, h, 0 }; TrackMouseEvent(&t); return 0; } case WM_MOUSELEAVE: hover_id = W_NONE; InvalidateRect(h, 0, 0); return 0; case WM_SETCURSOR: if (LOWORD(l) == HTCLIENT) return 1; break; case WM_LBUTTONDOWN: { widget_t *x = hit(GET_X_LPARAM(l), GET_Y_LPARAM(l)); press_id = x ? x->id : W_NONE; press_arg = x ? x->arg : 0; SetCapture(h); InvalidateRect(h, 0, 0); return 0; } case WM_LBUTTONUP: { widget_t *x = hit(GET_X_LPARAM(l), GET_Y_LPARAM(l)); int id = press_id, arg = press_arg; press_id = W_NONE; ReleaseCapture(); if (x && x->id == id && x->arg == arg) click(id, arg); else InvalidateRect(h, 0, 0); return 0; } case WM_LBUTTONDBLCLK: { widget_t *x = hit(GET_X_LPARAM(l), GET_Y_LPARAM(l)); if (x && x->id == W_ITEM) { sel = x->arg; if (discs[sel].game >= 0 && games[discs[sel].game].exe && game_installed(&games[discs[sel].game])) play(sel); else InvalidateRect(h, 0, 0); } return 0; } case WM_MOUSEWHEEL: { RECT cr; GetClientRect(h, &cr); int total = ndiscs * (S(68) + S(6)), view = cr.bottom - S(116) - S(96); scroll -= GET_WHEEL_DELTA_WPARAM(w) / 120 * S(60); if (scroll > total - view) scroll = total - view; if (scroll < 0) scroll = 0; InvalidateRect(h, 0, 0); return 0; } case WM_KEYDOWN: if (w == VK_RETURN && sel >= 0 && discs[sel].game >= 0 && games[discs[sel].game].exe && game_installed(&games[discs[sel].game])) play(sel); else if ((w == VK_DOWN || w == VK_UP) && ndiscs) { sel += w == VK_DOWN ? 1 : -1; if (sel < 0) sel = 0; if (sel >= ndiscs) sel = ndiscs - 1; view_settings = 0; message[0] = 0; save_all(); InvalidateRect(h, 0, 0); } else if (w == VK_ESCAPE && view_settings) { view_settings = 0; InvalidateRect(h, 0, 0); } return 0; case WM_DROPFILES: { HDROP d = (HDROP)w; wchar_t p[MAX_PATH]; UINT n = DragQueryFileW(d, 0xFFFFFFFF, 0, 0); for (UINT i = 0; i < n; i++) if (DragQueryFileW(d, i, p, MAX_PATH)) { if (GetFileAttributesW(p) & FILE_ATTRIBUTE_DIRECTORY) scan_dir(p, 0); else add_disc(p, 1); } DragFinish(d); view_settings = 0; save_all(); InvalidateRect(h, 0, 0); return 0; } case WM_BUILD_UPDATE: if (w == 1) { /* finished */ if (!bld.failed) { wcscpy(message, L"Built. Ready to play."); message_bad = 0; FlashWindow(h, TRUE); } if (IsIconic(h)) ShowWindow(h, SW_RESTORE); } InvalidateRect(h, 0, 0); return 0; case WM_CLOSE: if (bld.active && MessageBoxW(h, L"A game is being built. Stop the build and close?", L"Project Solana", MB_YESNO | MB_ICONQUESTION) != IDYES) return 0; DestroyWindow(h); return 0; case WM_GAME_EXIT: if (game_proc) { CloseHandle(game_proc); game_proc = 0; } if (w) { swprintf(message, 256, L"The game closed with an error (code 0x%lX). Open log shows what happened.", (unsigned long)w); message_bad = 1; } running = -1; if (IsIconic(h)) ShowWindow(h, SW_RESTORE); InvalidateRect(h, 0, 0); return 0; case WM_GETMINMAXINFO: { MINMAXINFO *mi = (MINMAXINFO *)l; mi->ptMinTrackSize.x = S(940); mi->ptMinTrackSize.y = S(800); return 0; } case WM_DPICHANGED: { dpi = HIWORD(w) / 96.0f; make_fonts(); img_flush(); RECT *r = (RECT *)l; SetWindowPos(h, 0, r->left, r->top, r->right - r->left, r->bottom - r->top, SWP_NOZORDER | SWP_NOACTIVATE); return 0; } case WM_SIZE: InvalidateRect(h, 0, 0); return 0; case WM_DESTROY: save_all(); PostQuitMessage(0); return 0; } return DefWindowProcW(h, m, w, l); } int WINAPI wWinMain(HINSTANCE inst, HINSTANCE prev, PWSTR cmdline, int show) { (void)prev; (void)cmdline; { /* per-monitor DPI when Windows has it (10 1703+), else system DPI */ typedef BOOL (WINAPI *ctx_t)(HANDLE); ctx_t f = (ctx_t)(void *)GetProcAddress(GetModuleHandleW(L"user32.dll"), "SetProcessDpiAwarenessContext"); if (!f || !f((HANDLE)-4)) SetProcessDPIAware(); } CoInitializeEx(0, COINIT_APARTMENTTHREADED); GetModuleFileNameW(0, base_dir, MAX_PATH); wchar_t *s = wcsrchr(base_dir, L'\\'); if (s) *s = 0; swprintf(ini_path, MAX_PATH, L"%ls\\solana.ini", base_dir); { HDC dc = GetDC(0); dpi = GetDeviceCaps(dc, LOGPIXELSX) / 96.0f; ReleaseDC(0, dc); } make_fonts(); load_all(); WNDCLASSEXW wc; memset(&wc, 0, sizeof wc); wc.cbSize = sizeof wc; wc.style = CS_DBLCLKS; wc.lpfnWndProc = wndproc; wc.hInstance = inst; wc.hIcon = LoadIconW(inst, MAKEINTRESOURCEW(1)); wc.hIconSm = wc.hIcon; wc.hCursor = LoadCursor(0, IDC_ARROW); wc.lpszClassName = L"ProjectSolanaLauncher"; RegisterClassExW(&wc); hwnd = CreateWindowExW(WS_EX_ACCEPTFILES, wc.lpszClassName, L"Project Solana", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, S(1100), S(860), 0, 0, inst, 0); { /* the window's DPI may differ from the primary monitor's */ typedef UINT (WINAPI *gdw_t)(HWND); gdw_t f = (gdw_t)(void *)GetProcAddress(GetModuleHandleW(L"user32.dll"), "GetDpiForWindow"); if (f) { float d2 = f(hwnd) / 96.0f; if (d2 != dpi && d2 > 0) { dpi = d2; make_fonts(); SetWindowPos(hwnd, 0, 0, 0, S(1100), S(860), SWP_NOMOVE | SWP_NOZORDER); } } } { /* dark title bar (Windows 10 2004+ / 11) */ HMODULE dw = LoadLibraryW(L"dwmapi.dll"); if (dw) { typedef HRESULT (WINAPI *set_t)(HWND, DWORD, LPCVOID, DWORD); set_t f = (set_t)(void *)GetProcAddress(dw, "DwmSetWindowAttribute"); BOOL on = TRUE; if (f) { f(hwnd, 20, &on, sizeof on); COLORREF cap = C_SIDE; f(hwnd, 35, &cap, sizeof cap); } } } ShowWindow(hwnd, show); UpdateWindow(hwnd); MSG msg; while (GetMessageW(&msg, 0, 0, 0) > 0) { TranslateMessage(&msg); DispatchMessageW(&msg); } return 0; }