Skip to content
Open
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
102 changes: 102 additions & 0 deletions BOXEDWINE_IMPLEMENTATION_GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Boxedwine Implementation Guide

This guide details how to integrate **Boxedwine**, a Linux emulator that runs WINE via WebAssembly, into the Windows 98 Web project. This allows running 16-bit and 32-bit Windows applications directly in the browser.

## 1. Binaries and Assets

Boxedwine requires several core files to run. These should be placed in `public/apps/boxedwine/`.

### Mandatory Files:
* **`public/apps/boxedwine/boxedwine.wasm`**: The core WebAssembly engine.
* **`public/apps/boxedwine/boxedwine.js`**: The Emscripten glue code.
* **`public/apps/boxedwine/boxedwine.zip`**: The root filesystem (approx. 36MB).

You can download the latest versions (e.g., 25R1) from [boxedwine.org](http://boxedwine.org/).

### Demo Game: SkiFree
To test the implementation, you can use **SkiFree** (a 32-bit classic).
* Place `ski32.exe` in `public/games/win32/skifree/ski32.exe`.
* The system will automatically install it to `/C:/Games/SkiFree/SKI32.EXE` on first boot.

## 2. Host Environment (`host.html`)

Create a `public/apps/boxedwine/host.html` file to host the emulator. This file should:
1. Initialize the Emscripten `Module`.
2. Pre-load the `boxedwine.zip` into the Emscripten filesystem.
3. Provide a `startWithArgs(args)` function that the OS can call to launch specific applications.
4. Communicate readiness back to the parent window via `postMessage`.

```html
<!-- Example snippet for host.html -->
<script>
window.Module = {
onRuntimeInitialized: () => {
window.parent.postMessage({ type: "BOXEDWINE_READY" }, "*");
},
// ... other Emscripten configurations
};

window.startWithArgs = (args) => {
Module['arguments'] = args;
Module["removeRunDependency"]("setupBoxedWine");
};
</script>
<script src="boxedwine.js"></script>
```

## 3. Application Class (`boxedwine-app.js`)

Create `src/apps/boxedwine/boxedwine-app.js` extending the `Application` base class.

### Key Responsibilities:
* **Lifecycle Management**: Handle window creation and iframe hosting.
* **Filesystem Syncing**:
* Before launch: Copy files from ZenFS (`/C:/Games/...`) into the Boxedwine internal FS.
* After close: Copy modified files back from Boxedwine to ZenFS.
* **Argument Passing**: Construct the command line for Boxedwine.
* Example: `["-root", "/root", "-zip", "boxedwine.zip", "-mount_drive", "/mnt/c", "c", "-p", "/mnt/c/Games/SkiFree/SKI32.EXE"]`

## 4. System Integration

### Smart EXE Launching
To differentiate between DOS and Windows executables, implement a utility that reads the first few bytes of the file.

* `MZ` header: Standard DOS (use DOSBox).
* `PE` signature (usually at offset 0x3C): Win32 (use Boxedwine).
* `NE` signature: Win16 (use Boxedwine).

Modify the file association logic to use this utility:

```javascript
// Pseudo-code for EXE detection
async function getExeType(path) {
const buffer = await fs.promises.readFile(path, { length: 1024 });
if (buffer[0] === 0x4D && buffer[1] === 0x5A) { // MZ
// Check for PE/NE signatures at offset indicated by 0x3C
const offset = buffer[0x3C] | (buffer[0x3D] << 8);
if (buffer[offset] === 0x50 && buffer[offset+1] === 0x45) return 'WIN32';
if (buffer[offset] === 0x4E && buffer[offset+1] === 0x45) return 'WIN16';
return 'DOS';
}
return 'UNKNOWN';
}
```

### Context Menu ("Open with...")
Add entries to the Explorer context menu for `.exe` files:
* "Open with DOSBox"
* "Open with Boxedwine"

### File Associations
Update `src/config/file-associations.js` or the launcher logic to default to the detected emulator type.

## 5. Persistence
Ensure that `C:` drive changes are persisted. Since Boxedwine runs in an isolated MEMFS inside the iframe, you must manually sync files back to ZenFS (IndexedDB) when the application window is closed.

## 6. Known Issues / Tips
* **Performance**: Boxedwine can be slow for complex 3D games in the browser.
* **Sound**: Ensure the "Enable Sound" toggle is handled if passing `-nosound` to the emulator.
* **Resolution**: Use the `-resolution` argument to match the window size.

## 7. Reference
For a similar integration, refer to `src/apps/dos-box/dos-box-app.js` and `public/games/dos/doswasmx/host.html`, which implement the DOSBox-X integration using a similar iframe-based approach.
166 changes: 166 additions & 0 deletions public/apps/boxedwine/host.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<!DOCTYPE html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<title>Boxedwine Host</title>
<style>
body { margin: 0; padding: 0; background: black; color: white; overflow: hidden; font-family: sans-serif; }
canvas { display: block; width: 100vw; height: 100vh; object-fit: contain; image-rendering: pixelated; }
#status { position: absolute; top: 0; left: 0; background: rgba(0,0,0,0.5); padding: 5px; font-size: 12px; pointer-events: none; opacity: 0; transition: opacity 0.5s; z-index: 10; }
#status.visible { opacity: 1; }
#loading-overlay {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background: #c0c0c0; color: black;
display: flex; flex-direction: column; align-items: center; justify-content: center;
z-index: 20;
}
.spinner {
width: 40px; height: 40px; border: 4px solid #f3f3f3; border-top: 4px solid #3498db;
border-radius: 50%; animation: spin 2s linear infinite; margin-bottom: 20px;
}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
</style>
</head>
<body>
<div id="loading-overlay">
<div class="spinner"></div>
<div id="loading-status">Loading Boxedwine...</div>
<progress id="progress" value="0" max="100" style="width: 200px; margin-top: 10px;"></progress>
</div>
<div id="status">Initializing...</div>
<canvas id="canvas" oncontextmenu="event.preventDefault()"></canvas>

<script>
const statusEl = document.getElementById('status');
const loadingStatusEl = document.getElementById('loading-status');
const progressEl = document.getElementById('progress');
const loadingOverlay = document.getElementById('loading-overlay');
let statusTimeout;

function showStatus(text, duration = 3000) {
statusEl.innerText = text;
statusEl.classList.add('visible');
clearTimeout(statusTimeout);
if (duration > 0) {
statusTimeout = setTimeout(() => {
statusEl.classList.remove('visible');
}, duration);
}
}

function updateLoadingStatus(text, progress) {
loadingStatusEl.innerText = text;
if (progress !== undefined) {
progressEl.value = progress;
}
}

window.Module = {
preRun: [],
postRun: [],
print: (function() {
return function(text) {
if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
console.log(text);
};
})(),
printErr: function(text) {
if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
console.error(text);
},
canvas: (function() {
var canvas = document.getElementById('canvas');
return canvas;
})(),
setStatus: function(text) {
if (!Module.setStatus.last) Module.setStatus.last = { time: Date.now(), text: '' };
if (text === Module.setStatus.last.text) return;
var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/);
var now = Date.now();
if (m && now - Module.setStatus.last.time < 30) return;
Module.setStatus.last.time = now;
Module.setStatus.last.text = text;
if (m) {
text = m[1];
updateLoadingStatus(text, (parseInt(m[2]) / parseInt(m[4])) * 100);
} else {
updateLoadingStatus(text);
}
},
totalDependencies: 0,
monitorRunDependencies: function(left) {
this.totalDependencies = Math.max(this.totalDependencies, left);
Module.setStatus(left ? 'Preparing... (' + (this.totalDependencies-left) + '/' + this.totalDependencies + ')' : 'All downloads complete.');
},
onRuntimeInitialized: function() {
console.log("Boxedwine Runtime Initialized");
}
};

Module["addRunDependency"]("setupBoxedWine");

async function loadBinary(url) {
const response = await fetch(url);
if (!response.ok) throw new Error(`Failed to load ${url}`);
const contentLength = response.headers.get('content-length');
const total = parseInt(contentLength, 10);

if (!total) return await response.arrayBuffer();

let loaded = 0;
const reader = response.body.getReader();
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
loaded += value.length;
updateLoadingStatus(`Downloading ${url.split('/').pop()}...`, (loaded / total) * 100);
}

const result = new Uint8Array(loaded);
let offset = 0;
for (const chunk of chunks) {
result.set(chunk, offset);
offset += chunk.length;
}
return result.buffer;
}

window.startWithArgs = async function(args) {
loadingOverlay.style.display = 'none';
showStatus("Starting Boxedwine...", 2000);

Module['arguments'] = args;
Module["removeRunDependency"]("setupBoxedWine");
};

async function init() {
try {
updateLoadingStatus("Loading core filesystem...", 0);
const rootZipData = await loadBinary('boxedwine.zip');

Module.preRun.push(() => {
Module.FS.createDataFile("/", "boxedwine.zip", new Uint8Array(rootZipData), true, true);
});

updateLoadingStatus("Loading Boxedwine engine...", 100);
const script = document.createElement('script');
script.src = 'boxedwine.js';
script.onerror = () => {
updateLoadingStatus("Error: boxedwine.js not found. Please follow instructions to add binaries.");
};
script.onload = () => {
window.parent.postMessage({ type: "BOXEDWINE_READY" }, "*");
};
document.head.appendChild(script);
} catch (e) {
console.error("Failed to initialize Boxedwine", e);
updateLoadingStatus("Error: boxedwine.zip not found. Please follow instructions to add binaries.");
}
}

init();
</script>
</body>
</html>
Loading