Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ ScummVM, and embedded web runtimes.

## Highlights

- Authentic Windows XP shell, Explorer, Paint, Notepad, Solitaire, Pinball, themes, and settings
- Authentic Windows XP shell, Explorer, Paint, Notepad, card games, Pinball, themes, and settings
- Draggable game windows, task switching, fullscreen, and volume controls
- Favorites, recently played games, categories, search, and deep links
- Automatic offline support with optional per-game downloads
Expand Down
5 changes: 4 additions & 1 deletion site/apps/core/boxedwine.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const runnerUrl = ({
executable,
resolution,
frameTop,
sound,
}) => {
const url = new URL(`${RUNTIME_ROOT}index.html`, document.baseURI);
url.search = new URLSearchParams({
Expand All @@ -20,7 +21,7 @@ const runnerUrl = ({
executable,
resolution,
frameTop: String(frameTop),
sound: "false",
sound: String(sound),
});
return url.href;
};
Expand All @@ -42,6 +43,7 @@ export const mountBoxedWineApplication = ({
nativeHeight,
resolution,
frameTop = 0,
sound = true,
background = "#000",
}) => {
const host = document.createElement("div");
Expand All @@ -67,6 +69,7 @@ export const mountBoxedWineApplication = ({
resolution:
resolution || `${initialFramebuffer.width}x${initialFramebuffer.height}`,
frameTop,
sound,
});
host.appendChild(frame);

Expand Down
2 changes: 2 additions & 0 deletions site/apps/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { winampApplication } from "./winamp/index.js";
import { minesweeperApplication } from "./minesweeper/index.js";
import { solitaireApplication } from "./solitaire/index.js";
import { pinballApplication } from "./pinball/index.js";
import { xpCardGameApplications } from "./xp-card-games/index.js";

export const applicationRegistry = createApplicationRegistry([
...accessoryApplications,
Expand All @@ -19,6 +20,7 @@ export const applicationRegistry = createApplicationRegistry([
winampApplication,
solitaireApplication,
pinballApplication,
...xpCardGameApplications,
]);

window.AstroShellApplications.install(applicationRegistry);
59 changes: 59 additions & 0 deletions site/apps/xp-card-games/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { defineApplication } from "../core/application.js";
import { mountBoxedWineApplication } from "../core/boxedwine.js";

const defineXpCardGame = ({
id,
title,
icon,
archive,
executable,
width,
height,
}) =>
defineApplication({
id: `__${id}`,
title,
icon,
kind: "native-game",
deepLinkId: id,
offlineGameId: id,
window: {
width,
height,
fitToWorkArea: true,
resizable: false,
maximizable: false,
},
mount: () =>
mountBoxedWineApplication({
title: `Windows XP ${title}`,
packageId: id,
archive,
executable,
nativeWidth: width,
nativeHeight: height - 32,
frameTop: 32,
background: "#27811f",
}),
});

export const xpCardGameApplications = [
defineXpCardGame({
id: "freecell",
title: "FreeCell",
icon: "FreeCell.png",
archive: "xp-freecell",
executable: "freecell.exe",
width: 700,
height: 520,
}),
defineXpCardGame({
id: "spider-solitaire",
title: "Spider Solitaire",
icon: "SpiderSolitaire.png",
archive: "xp-spider-solitaire",
executable: "spider.exe",
width: 800,
height: 600,
}),
];
22 changes: 22 additions & 0 deletions site/iframe/freecell/SOURCES.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"runtime": "boxedwine",
"windowsXp": {
"source": "User-supplied Windows XP Professional SP3 source-media ISO",
"sourceSha256": "fd8c8d42c1581e8767217fe800bfc0d5649c0ad20d754c927d6c763e446d1927",
"package": {
"file": "xp-freecell.zip",
"bytes": 177107,
"sha256": "a450542c141eab60dec0ff0611ef2c4ab6a1e2a03c090a81d3769d5ad92f1348"
},
"files": {
"freecell.exe": {
"member": "I386/FREECELL.EX_",
"sha256": "ba517f623b778f4b842f559ea344cb64d3cae9de172aecfa4e330c733baad601"
},
"cards.dll": {
"member": "I386/CARDS.DL_",
"sha256": "0fb81e48d8b45e9995ae1e05fd28c1631dbbcdc71180904c1356bfa2c1ead506"
}
}
}
}
Binary file added site/iframe/freecell/xp-freecell.zip
Binary file not shown.
18 changes: 18 additions & 0 deletions site/iframe/spider-solitaire/SOURCES.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"runtime": "boxedwine",
"windowsXp": {
"source": "User-supplied Windows XP Professional SP3 source-media ISO",
"sourceSha256": "fd8c8d42c1581e8767217fe800bfc0d5649c0ad20d754c927d6c763e446d1927",
"package": {
"file": "xp-spider-solitaire.zip",
"bytes": 270263,
"sha256": "ba3b5323193d91fcf5d32a2985e5e162881452228f9664b69f597b18de7dd70a"
},
"files": {
"spider.exe": {
"member": "I386/SPIDER.EX_",
"sha256": "d309a839843ccd8b9edef7a271a611eb7794f83a825b69a87cc259c9ede87288"
}
}
}
}
Binary file not shown.
2 changes: 2 additions & 0 deletions site/js/shell/start-menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -987,9 +987,11 @@ const getAllProgramsTree = () => {
label: "Games",
icon: programFolder,
children: [
xpProgramMenuItem("__freecell"),
xpProgramMenuItem("__minesweeper"),
xpProgramMenuItem("__pinball"),
xpProgramMenuItem("__solitaire"),
xpProgramMenuItem("__spider-solitaire"),
{ separator: true },
...gameGroups,
],
Expand Down
6 changes: 5 additions & 1 deletion tests/helpers/shell-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,11 @@ export async function loadShell() {
window.AstroGameLibrary.createManager = () => emptyLibrary;

const offlineSnapshot = {
bundledGames: [{ id: "solitaire" }],
bundledGames: [
{ id: "freecell" },
{ id: "solitaire" },
{ id: "spider-solitaire" },
],
downloadedGameIds: [],
phase: "idle",
updateAvailable: false,
Expand Down
4 changes: 2 additions & 2 deletions tests/shell-behavior.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -624,9 +624,11 @@ test("All Programs exposes system applications and games in the XP hierarchy", a

test("native games open from their public deep links", async () => {
for (const [deepLink, applicationId] of [
["freecell", "__freecell"],
["minesweeper", "__minesweeper"],
["pinball", "__pinball"],
["solitaire", "__solitaire"],
["spider-solitaire", "__spider-solitaire"],
]) {
const shell = await loadShell();
shell.window.location.hash = `#${deepLink}`;
Expand Down Expand Up @@ -678,7 +680,6 @@ test("placeholder-only applications are not installed or exposed by the shell",
"__internet-hearts",
"__internet-reversi",
"__internet-spades",
"__spider-solitaire",
"__windows-catalog",
"__windows-update",
"__backup",
Expand All @@ -702,7 +703,6 @@ test("placeholder-only applications are not installed or exposed by the shell",
"__scheduled-tasks",
"__system-information",
"__address-book",
"__freecell",
];

for (const applicationId of removedApplicationIds) {
Expand Down
2 changes: 1 addition & 1 deletion tests/solitaire.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ describe("Windows XP Solitaire through BoxedWine", () => {
expect(url.searchParams.get("executable")).toBe("resize-host.exe");
expect(url.searchParams.get("resolution")).toBe("586x438");
expect(url.searchParams.get("frameTop")).toBe("32");
expect(url.searchParams.get("sound")).toBe("false");
expect(url.searchParams.get("sound")).toBe("true");
expect(solitaireWindow.querySelectorAll(".resize-handle")).toHaveLength(8);
const maximize = solitaireWindow.querySelector(".maximize-btn");
expect(maximize.disabled).toBeFalse();
Expand Down
101 changes: 101 additions & 0 deletions tests/xp-card-games.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// @ts-nocheck -- Happy DOM's element types intentionally replace lib.dom here.
import { afterEach, describe, expect, test } from "bun:test";
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

import {
cleanupShells,
flushShell,
loadShell,
login,
} from "./helpers/shell-harness";

const require = createRequire(import.meta.url);
const { unzipSync } = require("fflate");
const projectDirectory = join(dirname(fileURLToPath(import.meta.url)), "..");
const sha256 = (content: Uint8Array) =>
createHash("sha256").update(content).digest("hex");

const games = [
{
id: "freecell",
applicationId: "__freecell",
title: "FreeCell",
archive: "xp-freecell",
executable: "freecell.exe",
files: ["cards.dll", "freecell.exe"],
},
{
id: "spider-solitaire",
applicationId: "__spider-solitaire",
title: "Spider Solitaire",
archive: "xp-spider-solitaire",
executable: "spider.exe",
files: ["spider.exe"],
},
];

afterEach(cleanupShells);

const launchGame = (shell, id, applicationId) => {
shell.document.getElementById("start-button").click();
shell.document.getElementById("all-programs-button").click();
const flyouts = shell.document.getElementById("start-menu-flyouts");
flyouts.querySelector('[data-program-id="games"]').click();
flyouts.querySelector(`[data-program-id="${id}"]`).click();
return shell.document.querySelector(
`.xp-window[data-game="${applicationId}"]`,
);
};

describe("Windows XP card games through BoxedWine", () => {
for (const game of games) {
test(`launches ${game.title} with sound and offline support`, async () => {
const shell = await login(await loadShell());
shell.window.ASTRO_GAME_ROOTS = {
[game.id]: `iframe/${game.id}-build-123/`,
};

const gameWindow = launchGame(shell, game.id, game.applicationId);
const frame = gameWindow.querySelector(".boxedwine-app-frame");
const url = new URL(frame.src);

expect(gameWindow.querySelector(".title-text").textContent).toBe(
game.title,
);
expect(new URL(url.searchParams.get("appRoot")).pathname).toBe(
`/iframe/${game.id}-build-123/`,
);
expect(url.searchParams.get("archive")).toBe(game.archive);
expect(url.searchParams.get("executable")).toBe(game.executable);
expect(url.searchParams.get("frameTop")).toBe("32");
expect(url.searchParams.get("sound")).toBe("true");
expect(gameWindow.querySelector(".maximize-btn").disabled).toBeTrue();
expect(gameWindow.querySelector(".resize-handle")).toBeNull();
expect(shell.window.location.hash).toBe(`#${game.id}`);
await flushShell();
expect(shell.offlineDownloads).toEqual([game.id]);
});

test(`${game.title} package matches its provenance manifest`, async () => {
const directory = join(projectDirectory, "site", "iframe", game.id);
const manifest = JSON.parse(
await readFile(join(directory, "SOURCES.json"), "utf8"),
);
const packageContent = new Uint8Array(
await readFile(join(directory, `${game.archive}.zip`)),
);
const files = unzipSync(packageContent);

expect(packageContent.byteLength).toBe(manifest.windowsXp.package.bytes);
expect(sha256(packageContent)).toBe(manifest.windowsXp.package.sha256);
expect(Object.keys(files).sort()).toEqual(game.files);
for (const [name, source] of Object.entries(manifest.windowsXp.files)) {
expect(sha256(files[name])).toBe(source.sha256);
}
});
}
});
2 changes: 2 additions & 0 deletions tools/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -686,7 +686,9 @@ export async function versionGamePackages(
const gameRuntimes: Record<string, string> = {
"pink-panther-hokus-pokus": "scummvm",
"pink-panther-passport-to-peril": "scummvm",
freecell: "boxedwine",
solitaire: "boxedwine",
"spider-solitaire": "boxedwine",
};
const packages: VersionedGamePackages = {};
for (const gameId of [...gameIds].sort()) {
Expand Down