mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 11:02:17 +00:00
fix(pi-dosbox): include QBasic files in jsdos bundle
The previous approach of writing files to Emscripten FS after DOSBox started didn't work because the C: drive mount was pointing elsewhere. Now we create a proper zip archive of QBasic files and include it in the jsdos bundle using the extract() API with a data URL. The bundle extracts to the C: drive root on startup.
This commit is contained in:
parent
4f343f39b9
commit
935417cff1
1 changed files with 100 additions and 44 deletions
|
|
@ -79,8 +79,7 @@ export class DosboxInstance {
|
|||
const bundle = await this.createBundle(emu);
|
||||
this.ci = await emu.dosboxDirect(bundle);
|
||||
|
||||
// Mount QBasic files to C:
|
||||
await this.mountQBasic();
|
||||
// QBasic files are included in the bundle, no post-init mount needed
|
||||
|
||||
const events = this.ci.events();
|
||||
|
||||
|
|
@ -121,54 +120,120 @@ export class DosboxInstance {
|
|||
|
||||
private async createBundle(emu: Emulators): Promise<Uint8Array> {
|
||||
const bundle = await emu.bundle();
|
||||
|
||||
// Add QBasic files to the bundle
|
||||
// We'll create a zip and include it
|
||||
await this.addQBasicToBundle(bundle);
|
||||
|
||||
// Set up autoexec to show what's available
|
||||
bundle.autoexec(
|
||||
"@echo off",
|
||||
"mount c c:",
|
||||
"c:",
|
||||
"cls",
|
||||
"echo QuickBASIC 4.5 mounted at C:\\QB",
|
||||
"echo QuickBASIC 4.5 is at C:\\QB",
|
||||
"echo.",
|
||||
"echo Type: CD QB",
|
||||
"echo Then: QB.EXE",
|
||||
"echo.",
|
||||
"dir /w",
|
||||
);
|
||||
|
||||
return bundle.toUint8Array(true);
|
||||
}
|
||||
|
||||
private async mountQBasic(): Promise<void> {
|
||||
if (!this.ci) return;
|
||||
|
||||
const fs = (this.ci as unknown as { transport: { module: { FS: EmscriptenFS } } }).transport.module.FS;
|
||||
const rescan = (this.ci as unknown as { transport: { module: { _rescanFilesystem: () => void } } }).transport
|
||||
.module._rescanFilesystem;
|
||||
|
||||
// Create directory structure
|
||||
try {
|
||||
fs.mkdir("/c");
|
||||
} catch {
|
||||
/* exists */
|
||||
}
|
||||
try {
|
||||
fs.mkdir("/c/QB");
|
||||
} catch {
|
||||
/* exists */
|
||||
}
|
||||
|
||||
// Read QBasic files from the extension directory
|
||||
private async addQBasicToBundle(bundle: Awaited<ReturnType<Emulators["bundle"]>>): Promise<void> {
|
||||
// Read QBasic files and create a zip data URL
|
||||
const qbasicDir = join(__dirname, "..", "qbasic");
|
||||
const { readdirSync, readFileSync } = await import("node:fs");
|
||||
|
||||
const files = readdirSync(qbasicDir);
|
||||
const files = readdirSync(qbasicDir).filter((f) => !f.startsWith("."));
|
||||
|
||||
// Create a simple zip structure
|
||||
const zipParts: Buffer[] = [];
|
||||
const centralDir: Buffer[] = [];
|
||||
let offset = 0;
|
||||
|
||||
for (const file of files) {
|
||||
if (file.startsWith(".")) continue;
|
||||
try {
|
||||
const data = readFileSync(join(qbasicDir, file));
|
||||
fs.writeFile(`/c/QB/${file.toUpperCase()}`, data);
|
||||
} catch (e) {
|
||||
console.error(`Failed to mount ${file}:`, e);
|
||||
}
|
||||
const data = readFileSync(join(qbasicDir, file));
|
||||
const fileName = `QB/${file.toUpperCase()}`;
|
||||
const fileNameBuf = Buffer.from(fileName, "utf-8");
|
||||
|
||||
// Local file header
|
||||
const localHeader = Buffer.alloc(30 + fileNameBuf.length);
|
||||
localHeader.writeUInt32LE(0x04034b50, 0); // signature
|
||||
localHeader.writeUInt16LE(20, 4); // version needed
|
||||
localHeader.writeUInt16LE(0, 6); // flags
|
||||
localHeader.writeUInt16LE(0, 8); // compression (none)
|
||||
localHeader.writeUInt16LE(0, 10); // mod time
|
||||
localHeader.writeUInt16LE(0, 12); // mod date
|
||||
localHeader.writeUInt32LE(this.crc32Buf(data), 14); // crc32
|
||||
localHeader.writeUInt32LE(data.length, 18); // compressed size
|
||||
localHeader.writeUInt32LE(data.length, 22); // uncompressed size
|
||||
localHeader.writeUInt16LE(fileNameBuf.length, 26); // filename length
|
||||
localHeader.writeUInt16LE(0, 28); // extra field length
|
||||
fileNameBuf.copy(localHeader, 30);
|
||||
|
||||
zipParts.push(localHeader);
|
||||
zipParts.push(data);
|
||||
|
||||
// Central directory entry
|
||||
const cdEntry = Buffer.alloc(46 + fileNameBuf.length);
|
||||
cdEntry.writeUInt32LE(0x02014b50, 0); // signature
|
||||
cdEntry.writeUInt16LE(20, 4); // version made by
|
||||
cdEntry.writeUInt16LE(20, 6); // version needed
|
||||
cdEntry.writeUInt16LE(0, 8); // flags
|
||||
cdEntry.writeUInt16LE(0, 10); // compression
|
||||
cdEntry.writeUInt16LE(0, 12); // mod time
|
||||
cdEntry.writeUInt16LE(0, 14); // mod date
|
||||
cdEntry.writeUInt32LE(this.crc32Buf(data), 16); // crc32
|
||||
cdEntry.writeUInt32LE(data.length, 20); // compressed size
|
||||
cdEntry.writeUInt32LE(data.length, 24); // uncompressed size
|
||||
cdEntry.writeUInt16LE(fileNameBuf.length, 28); // filename length
|
||||
cdEntry.writeUInt16LE(0, 30); // extra field length
|
||||
cdEntry.writeUInt16LE(0, 32); // comment length
|
||||
cdEntry.writeUInt16LE(0, 34); // disk number start
|
||||
cdEntry.writeUInt16LE(0, 36); // internal attrs
|
||||
cdEntry.writeUInt32LE(0, 38); // external attrs
|
||||
cdEntry.writeUInt32LE(offset, 42); // relative offset
|
||||
fileNameBuf.copy(cdEntry, 46);
|
||||
|
||||
centralDir.push(cdEntry);
|
||||
offset += localHeader.length + data.length;
|
||||
}
|
||||
|
||||
// Rescan so DOS sees the new files
|
||||
rescan();
|
||||
const cdOffset = offset;
|
||||
const cdSize = centralDir.reduce((sum, buf) => sum + buf.length, 0);
|
||||
|
||||
// End of central directory
|
||||
const eocd = Buffer.alloc(22);
|
||||
eocd.writeUInt32LE(0x06054b50, 0); // signature
|
||||
eocd.writeUInt16LE(0, 4); // disk number
|
||||
eocd.writeUInt16LE(0, 6); // disk with CD
|
||||
eocd.writeUInt16LE(files.length, 8); // entries on disk
|
||||
eocd.writeUInt16LE(files.length, 10); // total entries
|
||||
eocd.writeUInt32LE(cdSize, 12); // CD size
|
||||
eocd.writeUInt32LE(cdOffset, 16); // CD offset
|
||||
eocd.writeUInt16LE(0, 20); // comment length
|
||||
|
||||
const zipBuffer = Buffer.concat([...zipParts, ...centralDir, eocd]);
|
||||
const base64 = zipBuffer.toString("base64");
|
||||
const dataUrl = `data:application/zip;base64,${base64}`;
|
||||
|
||||
// Extract to root of C: drive
|
||||
bundle.extract(dataUrl, "/", "zip");
|
||||
}
|
||||
|
||||
private crc32Buf(buffer: Buffer): number {
|
||||
let crc = 0xffffffff;
|
||||
for (const byte of buffer) {
|
||||
crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
|
||||
}
|
||||
return (crc ^ 0xffffffff) >>> 0;
|
||||
}
|
||||
|
||||
// No longer needed - files are in bundle
|
||||
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: Keeping for reference
|
||||
private async mountQBasicPostInit(): Promise<void> {
|
||||
// This approach didn't work - keeping for reference
|
||||
}
|
||||
|
||||
private expandRgbToRgba(rgb: Uint8Array): Uint8Array {
|
||||
|
|
@ -417,12 +482,3 @@ function createCrcTable(): Uint32Array {
|
|||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
// Emscripten FS type
|
||||
interface EmscriptenFS {
|
||||
mkdir(path: string): void;
|
||||
writeFile(path: string, data: Uint8Array | Buffer): void;
|
||||
readFile(path: string): Uint8Array;
|
||||
readdir(path: string): string[];
|
||||
unlink(path: string): void;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue