diff --git a/native/xp-card-games/resize-host.c b/native/boxedwine/resize-host.c similarity index 53% rename from native/xp-card-games/resize-host.c rename to native/boxedwine/resize-host.c index fc5db9ff..bc9d321f 100644 --- a/native/xp-card-games/resize-host.c +++ b/native/boxedwine/resize-host.c @@ -1,21 +1,13 @@ #define WIN32_LEAN_AND_MEAN #include -#define SIZE_FILE L"D:\\solitaire-size.txt" +#define SIZE_FILE L"D:\\boxedwine-size.txt" -#if defined(RESIZE_FREECELL) -#define TARGET_EXECUTABLE L"freecell.exe" -#define TARGET_TITLE L"FreeCell" -#define NATIVE_SCREEN_WIDTH 640 -#define NATIVE_SCREEN_HEIGHT 480 -#elif defined(RESIZE_SPIDER_SOLITAIRE) -#define TARGET_EXECUTABLE L"spider.exe" -#define TARGET_TITLE L"Spider Solitaire" -#define NATIVE_SCREEN_WIDTH 794 -#define NATIVE_SCREEN_HEIGHT 601 -#else -#error A supported card game must be selected. -#endif +typedef struct { + DWORD process_id; + HWND window; + LONG area; +} WindowSearch; static BOOL parse_dimension(char **cursor, char *end, unsigned int *value) { unsigned int result = 0; @@ -31,7 +23,9 @@ static BOOL parse_dimension(char **cursor, char *end, unsigned int *value) { return TRUE; } -static BOOL read_window_size(unsigned int *width, unsigned int *height) { +static BOOL read_window_size(unsigned int *width, unsigned int *height, + unsigned int *native_width, + unsigned int *native_height) { char buffer[64]; DWORD bytes_read = 0; HANDLE file = CreateFileW(SIZE_FILE, GENERIC_READ, @@ -43,8 +37,37 @@ static BOOL read_window_size(unsigned int *width, unsigned int *height) { char *cursor = buffer; char *end = buffer + bytes_read; return read && parse_dimension(&cursor, end, width) && - parse_dimension(&cursor, end, height) && *width >= 100 && - *height >= 100; + parse_dimension(&cursor, end, height) && + parse_dimension(&cursor, end, native_width) && + parse_dimension(&cursor, end, native_height) && *width >= 100 && + *height >= 100 && *native_width >= 100 && *native_height >= 100; +} + +static BOOL CALLBACK find_process_window(HWND window, LPARAM parameter) { + WindowSearch *search = (WindowSearch *)parameter; + DWORD process_id = 0; + RECT bounds; + GetWindowThreadProcessId(window, &process_id); + if (process_id != search->process_id || !IsWindowVisible(window) || + GetWindow(window, GW_OWNER) || !GetWindowRect(window, &bounds)) + return TRUE; + LONG area = (bounds.right - bounds.left) * (bounds.bottom - bounds.top); + if (area > search->area) { + search->window = window; + search->area = area; + } + return TRUE; +} + +static BOOL find_target(WCHAR *application_path, WCHAR *filename) { + static const WCHAR *targets[] = {L"sol.exe", L"freecell.exe", L"spider.exe"}; + for (unsigned int index = 0; index < sizeof(targets) / sizeof(targets[0]); + ++index) { + lstrcpyW(filename, targets[index]); + if (GetFileAttributesW(application_path) != INVALID_FILE_ATTRIBUTES) + return TRUE; + } + return FALSE; } static DWORD run(void) { @@ -62,8 +85,8 @@ static DWORD run(void) { if (!path_length || path_length == MAX_PATH) return 1; WCHAR *filename = application_path + path_length; while (filename > application_path && filename[-1] != L'\\') --filename; - if (filename == application_path) return 1; - lstrcpyW(filename, TARGET_EXECUTABLE); + if (filename == application_path || !find_target(application_path, filename)) + return 1; if (!CreateProcessW(application_path, NULL, NULL, NULL, FALSE, 0, NULL, NULL, &startup, &process)) return 1; @@ -72,17 +95,22 @@ static DWORD run(void) { while (WaitForSingleObject(process.hProcess, 50) == WAIT_TIMEOUT) { unsigned int width; unsigned int height; - HWND application = FindWindowW(NULL, TARGET_TITLE); - if (!application || !read_window_size(&width, &height)) continue; + unsigned int native_width; + unsigned int native_height; + WindowSearch search = {process.dwProcessId, NULL, 0}; + EnumWindows(find_process_window, (LPARAM)&search); + if (!search.window || + !read_window_size(&width, &height, &native_width, &native_height)) + continue; if (width == applied_width && height == applied_height) continue; if (!calibrated) { RECT bounds; - if (!GetWindowRect(application, &bounds)) continue; - width_adjustment = bounds.right - bounds.left - NATIVE_SCREEN_WIDTH; - height_adjustment = bounds.bottom - bounds.top - NATIVE_SCREEN_HEIGHT; + if (!GetWindowRect(search.window, &bounds)) continue; + width_adjustment = bounds.right - bounds.left - (int)native_width; + height_adjustment = bounds.bottom - bounds.top - (int)native_height; calibrated = TRUE; } - if (SetWindowPos(application, NULL, 0, 0, (int)width + width_adjustment, + if (SetWindowPos(search.window, NULL, 0, 0, (int)width + width_adjustment, (int)height + height_adjustment, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE)) { applied_width = width; diff --git a/native/solitaire/resize-host.c b/native/solitaire/resize-host.c deleted file mode 100644 index c126390f..00000000 --- a/native/solitaire/resize-host.c +++ /dev/null @@ -1,84 +0,0 @@ -#define WIN32_LEAN_AND_MEAN -#include - -#define SIZE_FILE L"D:\\solitaire-size.txt" -#define NATIVE_SCREEN_WIDTH 586 -#define NATIVE_SCREEN_HEIGHT 438 - -static BOOL parse_dimension(char **cursor, char *end, unsigned int *value) { - unsigned int result = 0; - while (*cursor < end && (**cursor == ' ' || **cursor == '\r' || - **cursor == '\n' || **cursor == '\t')) - ++*cursor; - if (*cursor == end || **cursor < '0' || **cursor > '9') return FALSE; - while (*cursor < end && **cursor >= '0' && **cursor <= '9') { - result = result * 10 + (unsigned int)(**cursor - '0'); - ++*cursor; - } - *value = result; - return TRUE; -} - -static BOOL read_window_size(unsigned int *width, unsigned int *height) { - char buffer[64]; - DWORD bytes_read = 0; - HANDLE file = CreateFileW(SIZE_FILE, GENERIC_READ, - FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, - OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); - if (file == INVALID_HANDLE_VALUE) return FALSE; - BOOL read = ReadFile(file, buffer, sizeof(buffer), &bytes_read, NULL); - CloseHandle(file); - char *cursor = buffer; - char *end = buffer + bytes_read; - return read && parse_dimension(&cursor, end, width) && - parse_dimension(&cursor, end, height) && *width >= 100 && - *height >= 100; -} - -static DWORD run(void) { - STARTUPINFOW startup = {0}; - PROCESS_INFORMATION process = {0}; - WCHAR solitaire_path[MAX_PATH]; - unsigned int applied_width = 0; - unsigned int applied_height = 0; - int width_adjustment = 0; - int height_adjustment = 0; - BOOL calibrated = FALSE; - - startup.cb = sizeof(startup); - DWORD path_length = GetModuleFileNameW(NULL, solitaire_path, MAX_PATH); - if (!path_length || path_length == MAX_PATH) return 1; - WCHAR *filename = solitaire_path + path_length; - while (filename > solitaire_path && filename[-1] != L'\\') --filename; - if (filename == solitaire_path) return 1; - lstrcpyW(filename, L"sol.exe"); - if (!CreateProcessW(solitaire_path, NULL, NULL, NULL, FALSE, 0, NULL, NULL, - &startup, &process)) - return 1; - - CloseHandle(process.hThread); - while (WaitForSingleObject(process.hProcess, 50) == WAIT_TIMEOUT) { - unsigned int width; - unsigned int height; - HWND solitaire = FindWindowW(NULL, L"Solitaire"); - if (!solitaire || !read_window_size(&width, &height)) continue; - if (width == applied_width && height == applied_height) continue; - if (!calibrated) { - RECT bounds; - if (!GetWindowRect(solitaire, &bounds)) continue; - width_adjustment = bounds.right - bounds.left - NATIVE_SCREEN_WIDTH; - height_adjustment = bounds.bottom - bounds.top - NATIVE_SCREEN_HEIGHT; - calibrated = TRUE; - } - if (SetWindowPos(solitaire, NULL, 0, 0, (int)width + width_adjustment, - (int)height + height_adjustment, - SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE)) { - applied_width = width; - applied_height = height; - } - } - CloseHandle(process.hProcess); - return 0; -} - -void WinMainCRTStartup(void) { ExitProcess(run()); } diff --git a/site/apps/core/boxedwine-card-game.js b/site/apps/core/boxedwine-card-game.js new file mode 100644 index 00000000..79a12529 --- /dev/null +++ b/site/apps/core/boxedwine-card-game.js @@ -0,0 +1,39 @@ +import { defineApplication } from "./application.js"; +import { mountBoxedWineApplication } from "./boxedwine.js"; + +export const defineBoxedWineCardGame = ({ + id, + title, + icon, + archive, + width, + height, + nativeWidth = width - 6, + nativeHeight = height - 32, +}) => + defineApplication({ + id: `__${id}`, + title, + icon, + kind: "native-game", + deepLinkId: id, + offlineGameId: id, + window: { + width, + height, + fitToWorkArea: true, + resizable: true, + maximizable: true, + }, + mount: () => + mountBoxedWineApplication({ + title: `Windows XP ${title}`, + packageId: id, + archive, + executable: "resize-host.exe", + nativeWidth, + nativeHeight, + frameTop: 32, + background: "#27811f", + }), + }); diff --git a/site/apps/core/boxedwine-resize.js b/site/apps/core/boxedwine-resize.js index f0c214f4..9802aafc 100644 --- a/site/apps/core/boxedwine-resize.js +++ b/site/apps/core/boxedwine-resize.js @@ -1,12 +1,16 @@ const isResizeMessage = (event, origin) => { - const { type, width, height } = event.data || {}; + const { type, width, height, baseWidth, baseHeight } = event.data || {}; return ( event.origin === origin && type === "boxedwine-framebuffer-resize" && Number.isInteger(width) && Number.isInteger(height) && + Number.isInteger(baseWidth) && + Number.isInteger(baseHeight) && width > 0 && - height > 0 + height > 0 && + baseWidth > 0 && + baseHeight > 0 ); }; @@ -22,12 +26,15 @@ export const installBoxedWineResizeBridge = (hostWindow, module) => { typeof module.FS?.writeFile !== "function" ) return; - const { width, height } = pendingResize; - const sizePath = "/d_drive/solitaire-size.txt"; + const { width, height, baseWidth, baseHeight } = pendingResize; + const sizePath = "/d_drive/boxedwine-size.txt"; const temporaryPath = `${sizePath}.tmp`; module._boxedwine_resize_screen(width, height); - module.FS.writeFile(temporaryPath, `${width} ${height}`); + module.FS.writeFile( + temporaryPath, + `${width} ${height} ${baseWidth} ${baseHeight}`, + ); try { module.FS.unlink(sizePath); } catch { diff --git a/site/apps/core/boxedwine.js b/site/apps/core/boxedwine.js index 4b9837bc..5ceec910 100644 --- a/site/apps/core/boxedwine.js +++ b/site/apps/core/boxedwine.js @@ -110,7 +110,12 @@ export const mountBoxedWineApplication = ({ frame.style.transformOrigin = ""; } frame.contentWindow?.postMessage( - { type: "boxedwine-framebuffer-resize", ...framebuffer }, + { + type: "boxedwine-framebuffer-resize", + ...framebuffer, + baseWidth: nativeWidth, + baseHeight: nativeHeight + frameTop, + }, new URL(frame.src).origin, ); }; diff --git a/site/apps/solitaire/index.js b/site/apps/solitaire/index.js index 57e7b507..1d6b303d 100644 --- a/site/apps/solitaire/index.js +++ b/site/apps/solitaire/index.js @@ -1,32 +1,12 @@ -import { mountBoxedWineApplication } from "../core/boxedwine.js"; -import { defineApplication } from "../core/application.js"; +import { defineBoxedWineCardGame } from "../core/boxedwine-card-game.js"; -const mountSolitaire = () => - mountBoxedWineApplication({ - title: "Windows XP Solitaire", - packageId: "solitaire", - archive: "xp-solitaire", - executable: "resize-host.exe", - nativeWidth: 586, - nativeHeight: 406, - frameTop: 32, - background: "#27811f", - }); - -export const solitaireApplication = defineApplication({ - id: "__solitaire", +export const solitaireApplication = defineBoxedWineCardGame({ + id: "solitaire", title: "Solitaire", icon: "Solitaire.png", - kind: "native-game", - deepLinkId: "solitaire", - offlineGameId: "solitaire", - window: { - width: 592, - height: 438, - className: "xp-native-solitaire-window", - fitToWorkArea: true, - resizable: true, - maximizable: true, - }, - mount: mountSolitaire, + archive: "xp-solitaire", + width: 592, + height: 438, + nativeWidth: 586, + nativeHeight: 406, }); diff --git a/site/apps/xp-card-games/index.js b/site/apps/xp-card-games/index.js index 6f812f78..c778f2b2 100644 --- a/site/apps/xp-card-games/index.js +++ b/site/apps/xp-card-games/index.js @@ -1,45 +1,7 @@ -import { defineApplication } from "../core/application.js"; -import { mountBoxedWineApplication } from "../core/boxedwine.js"; - -const defineXpCardGame = ({ - id, - title, - icon, - archive, - width, - height, - nativeWidth = width - 6, - nativeHeight = height - 31, -}) => - defineApplication({ - id: `__${id}`, - title, - icon, - kind: "native-game", - deepLinkId: id, - offlineGameId: id, - window: { - width, - height, - fitToWorkArea: true, - resizable: true, - maximizable: true, - }, - mount: () => - mountBoxedWineApplication({ - title: `Windows XP ${title}`, - packageId: id, - archive, - executable: "resize-host.exe", - nativeWidth, - nativeHeight, - frameTop: 32, - background: "#27811f", - }), - }); +import { defineBoxedWineCardGame } from "../core/boxedwine-card-game.js"; export const xpCardGameApplications = [ - defineXpCardGame({ + defineBoxedWineCardGame({ id: "freecell", title: "FreeCell", icon: "FreeCell.png", @@ -49,12 +11,13 @@ export const xpCardGameApplications = [ nativeWidth: 640, nativeHeight: 448, }), - defineXpCardGame({ + defineBoxedWineCardGame({ id: "spider-solitaire", title: "Spider Solitaire", icon: "SpiderSolitaire.png", archive: "xp-spider-solitaire", width: 800, height: 600, + nativeHeight: 569, }), ]; diff --git a/site/iframe/freecell/SOURCES.json b/site/iframe/freecell/SOURCES.json index 677e55bf..9c3f055c 100644 --- a/site/iframe/freecell/SOURCES.json +++ b/site/iframe/freecell/SOURCES.json @@ -5,8 +5,8 @@ "sourceSha256": "fd8c8d42c1581e8767217fe800bfc0d5649c0ad20d754c927d6c763e446d1927", "package": { "file": "xp-freecell.zip", - "bytes": 178462, - "sha256": "048b03721fd9205387274e24c095a21344b1cf97cb3b03d08dd496372a9ee6a7" + "bytes": 178720, + "sha256": "11262b2bd42a0da786a0ef8facf61c9395bea97768285da2529a27bef14c3252" }, "files": { "freecell.exe": { @@ -18,9 +18,9 @@ "sha256": "0fb81e48d8b45e9995ae1e05fd28c1631dbbcdc71180904c1356bfa2c1ead506" }, "resize-host.exe": { - "source": "native/xp-card-games/resize-host.c", - "build": "i686-w64-mingw32-gcc -Os -s -nostdlib -mwindows -DRESIZE_FREECELL native/xp-card-games/resize-host.c -lkernel32 -luser32 -o resize-host.exe", - "sha256": "ad94b439cc8bc9e18e00ee9332f558c347773f66e97dcefb2548346b9d6f4426" + "source": "native/boxedwine/resize-host.c", + "build": "i686-w64-mingw32-gcc -Os -s -nostdlib -mwindows native/boxedwine/resize-host.c -lkernel32 -luser32 -o resize-host.exe", + "sha256": "adf6e0075ed512a40eaa06ec9f0389f1777e0b885153a4fc1b9eedf070426f5c" } } } diff --git a/site/iframe/freecell/xp-freecell.zip b/site/iframe/freecell/xp-freecell.zip index 541ea160..6bddd386 100644 Binary files a/site/iframe/freecell/xp-freecell.zip and b/site/iframe/freecell/xp-freecell.zip differ diff --git a/site/iframe/solitaire/SOURCES.json b/site/iframe/solitaire/SOURCES.json index 424dbc4c..ce0b2564 100644 --- a/site/iframe/solitaire/SOURCES.json +++ b/site/iframe/solitaire/SOURCES.json @@ -5,8 +5,8 @@ "sourceSha256": "fd8c8d42c1581e8767217fe800bfc0d5649c0ad20d754c927d6c763e446d1927", "package": { "file": "xp-solitaire.zip", - "bytes": 177971, - "sha256": "074945b324b42c105146f309f2fe55744ea719acfdb42cdddc32b0857b28b802" + "bytes": 178239, + "sha256": "d4ba31c630b08b478a60bee49630e6b4fc449ed0a168ca3d4a2bda1c89c4216c" }, "files": { "sol.exe": { @@ -18,9 +18,9 @@ "sha256": "0fb81e48d8b45e9995ae1e05fd28c1631dbbcdc71180904c1356bfa2c1ead506" }, "resize-host.exe": { - "source": "native/solitaire/resize-host.c", - "build": "i686-w64-mingw32-gcc -Os -s -nostdlib -mwindows native/solitaire/resize-host.c -lkernel32 -luser32 -o resize-host.exe", - "sha256": "1d2bee7b669f10d2af97e2670fcdabedb3c060f45a68e2eed4285adea23f3727" + "source": "native/boxedwine/resize-host.c", + "build": "i686-w64-mingw32-gcc -Os -s -nostdlib -mwindows native/boxedwine/resize-host.c -lkernel32 -luser32 -o resize-host.exe", + "sha256": "adf6e0075ed512a40eaa06ec9f0389f1777e0b885153a4fc1b9eedf070426f5c" } } } diff --git a/site/iframe/solitaire/xp-solitaire.zip b/site/iframe/solitaire/xp-solitaire.zip index e23918ff..2ba1aaa2 100644 Binary files a/site/iframe/solitaire/xp-solitaire.zip and b/site/iframe/solitaire/xp-solitaire.zip differ diff --git a/site/iframe/spider-solitaire/SOURCES.json b/site/iframe/spider-solitaire/SOURCES.json index b6fd36e5..62a1f84a 100644 --- a/site/iframe/spider-solitaire/SOURCES.json +++ b/site/iframe/spider-solitaire/SOURCES.json @@ -5,8 +5,8 @@ "sourceSha256": "fd8c8d42c1581e8767217fe800bfc0d5649c0ad20d754c927d6c763e446d1927", "package": { "file": "xp-spider-solitaire.zip", - "bytes": 271617, - "sha256": "950bc7a733a96cbc4cdc222e394f4aea8fe710c343a2857bf8b5460422ce5112" + "bytes": 271876, + "sha256": "a3a05d06ac7d6abd0680ba1ccdc28a2e7a0d27ca0e461fd80ae47fad7b37d847" }, "files": { "spider.exe": { @@ -14,9 +14,9 @@ "sha256": "d309a839843ccd8b9edef7a271a611eb7794f83a825b69a87cc259c9ede87288" }, "resize-host.exe": { - "source": "native/xp-card-games/resize-host.c", - "build": "i686-w64-mingw32-gcc -Os -s -nostdlib -mwindows -DRESIZE_SPIDER_SOLITAIRE native/xp-card-games/resize-host.c -lkernel32 -luser32 -o resize-host.exe", - "sha256": "a54053947fc3cfda0ded2832b307d3ae075921b19efd4afefefd54006d84f3a5" + "source": "native/boxedwine/resize-host.c", + "build": "i686-w64-mingw32-gcc -Os -s -nostdlib -mwindows native/boxedwine/resize-host.c -lkernel32 -luser32 -o resize-host.exe", + "sha256": "adf6e0075ed512a40eaa06ec9f0389f1777e0b885153a4fc1b9eedf070426f5c" } } } diff --git a/site/iframe/spider-solitaire/xp-spider-solitaire.zip b/site/iframe/spider-solitaire/xp-spider-solitaire.zip index a760ee39..78a24bc1 100644 Binary files a/site/iframe/spider-solitaire/xp-spider-solitaire.zip and b/site/iframe/spider-solitaire/xp-spider-solitaire.zip differ diff --git a/tests/solitaire.test.ts b/tests/solitaire.test.ts index 2324de6e..bc4d0aed 100644 --- a/tests/solitaire.test.ts +++ b/tests/solitaire.test.ts @@ -192,6 +192,8 @@ describe("Windows XP Solitaire through BoxedWine", () => { type: "boxedwine-framebuffer-resize", width: 1372, height: 844, + baseWidth: 586, + baseHeight: 438, }, origin: new URL(frame.src).origin, }); @@ -206,6 +208,8 @@ describe("Windows XP Solitaire through BoxedWine", () => { type: "boxedwine-framebuffer-resize", width: 586, height: 438, + baseWidth: 586, + baseHeight: 438, }, origin: new URL(frame.src).origin, }); @@ -225,6 +229,8 @@ describe("Windows XP Solitaire through BoxedWine", () => { type: "boxedwine-framebuffer-resize", width: 586, height: 438, + baseWidth: 586, + baseHeight: 438, }, origin: new URL(frame.src).origin, }); @@ -256,6 +262,8 @@ describe("Windows XP Solitaire through BoxedWine", () => { type: "boxedwine-framebuffer-resize", width: 1372, height: 844, + baseWidth: 586, + baseHeight: 438, }, }); module._boxedwine_resize_screen = (width, height) => @@ -274,7 +282,7 @@ describe("Windows XP Solitaire through BoxedWine", () => { }; module.onRuntimeInitialized(); expect(resizeCalls).toEqual([{ width: 1372, height: 844 }]); - expect(files.get("/d_drive/solitaire-size.txt")).toBe("1372 844"); + expect(files.get("/d_drive/boxedwine-size.txt")).toBe("1372 844 586 438"); messageListener({ origin: "https://flash.test", @@ -282,10 +290,12 @@ describe("Windows XP Solitaire through BoxedWine", () => { type: "boxedwine-framebuffer-resize", width: 586, height: 438, + baseWidth: 586, + baseHeight: 438, }, }); expect(resizeCalls.at(-1)).toEqual({ width: 586, height: 438 }); - expect(files.get("/d_drive/solitaire-size.txt")).toBe("586 438"); + expect(files.get("/d_drive/boxedwine-size.txt")).toBe("586 438 586 438"); messageListener({ origin: "https://other.test", diff --git a/tests/xp-card-games.test.ts b/tests/xp-card-games.test.ts index 6893d87d..f991db8f 100644 --- a/tests/xp-card-games.test.ts +++ b/tests/xp-card-games.test.ts @@ -122,6 +122,8 @@ describe("Windows XP card games through BoxedWine", () => { type: "boxedwine-framebuffer-resize", width: nativeWidth, height: nativeHeight + 32, + baseWidth: nativeWidth, + baseHeight: nativeHeight + 32, }); Object.defineProperties(desktop, { @@ -146,6 +148,8 @@ describe("Windows XP card games through BoxedWine", () => { type: "boxedwine-framebuffer-resize", width: 1018, height: 739, + baseWidth: nativeWidth, + baseHeight: nativeHeight + 32, }); }); @@ -165,6 +169,15 @@ describe("Windows XP card games through BoxedWine", () => { for (const [name, source] of Object.entries(manifest.windowsXp.files)) { expect(sha256(files[name])).toBe(source.sha256); } + const solitaireManifest = JSON.parse( + await readFile( + join(projectDirectory, "site", "iframe", "solitaire", "SOURCES.json"), + "utf8", + ), + ); + expect(manifest.windowsXp.files["resize-host.exe"].sha256).toBe( + solitaireManifest.windowsXp.files["resize-host.exe"].sha256, + ); }); } });