mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-17 10:02:23 +00:00
chore: move pi-dosbox to separate repo
Moved to https://github.com/badlogic/pi-dosbox
This commit is contained in:
parent
6289c144bf
commit
866d21c252
26 changed files with 1 additions and 1891 deletions
|
|
@ -5,8 +5,7 @@
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
"packages/web-ui/example",
|
"packages/web-ui/example",
|
||||||
"packages/coding-agent/examples/extensions/with-deps",
|
"packages/coding-agent/examples/extensions/with-deps"
|
||||||
"packages/coding-agent/examples/extensions/pi-dosbox"
|
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "npm run clean --workspaces",
|
"clean": "npm run clean --workspaces",
|
||||||
|
|
|
||||||
|
|
@ -1,183 +0,0 @@
|
||||||
/**
|
|
||||||
* DOSBox extension for pi
|
|
||||||
*
|
|
||||||
* Features:
|
|
||||||
* - Persistent DOSBox instance running in background
|
|
||||||
* - QuickBASIC 4.5 mounted at C:\QB
|
|
||||||
* - /dosbox command to view and interact with DOSBox
|
|
||||||
* - dosbox tool for agent to send keys, read screen, take screenshots
|
|
||||||
*
|
|
||||||
* Usage: pi --extension ./examples/extensions/pi-dosbox
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { StringEnum } from "@mariozechner/pi-ai";
|
|
||||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
||||||
import { Type } from "@sinclair/typebox";
|
|
||||||
import { DosboxComponent } from "./src/dosbox-component.js";
|
|
||||||
import { DosboxInstance } from "./src/dosbox-instance.js";
|
|
||||||
|
|
||||||
export default function (pi: ExtensionAPI) {
|
|
||||||
// Start DOSBox instance at session start
|
|
||||||
pi.on("session_start", async () => {
|
|
||||||
try {
|
|
||||||
await DosboxInstance.getInstance();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to start DOSBox:", error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clean up on session shutdown
|
|
||||||
pi.on("session_shutdown", async () => {
|
|
||||||
await DosboxInstance.destroyInstance();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Register /dosbox command to view DOSBox
|
|
||||||
pi.registerCommand("dosbox", {
|
|
||||||
description: "View and interact with DOSBox (Ctrl+Q to detach)",
|
|
||||||
|
|
||||||
handler: async (_args, ctx) => {
|
|
||||||
if (!ctx.hasUI) {
|
|
||||||
ctx.ui.notify("DOSBox requires interactive mode", "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure instance is running
|
|
||||||
const instance = DosboxInstance.getInstanceSync();
|
|
||||||
if (!instance || !instance.isReady()) {
|
|
||||||
ctx.ui.notify("DOSBox is not running. It should start automatically.", "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
||||||
const fallbackColor = (s: string) => theme.fg("warning", s);
|
|
||||||
return new DosboxComponent(tui, fallbackColor, () => done(undefined));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Register dosbox tool for agent interaction
|
|
||||||
pi.registerTool({
|
|
||||||
name: "dosbox",
|
|
||||||
label: "DOSBox",
|
|
||||||
description: `Interact with DOSBox emulator running QuickBASIC 4.5.
|
|
||||||
Actions:
|
|
||||||
- send_keys: Send keystrokes to DOSBox. Use \\n for Enter, \\t for Tab.
|
|
||||||
- screenshot: Get a PNG screenshot of the current DOSBox screen.
|
|
||||||
- read_text: Read text-mode screen content (returns null in graphics mode).
|
|
||||||
|
|
||||||
QuickBASIC 4.5 is mounted at C:\\QB. Run "C:\\QB\\QB.EXE" to start it.`,
|
|
||||||
parameters: Type.Object({
|
|
||||||
action: StringEnum(["send_keys", "screenshot", "read_text"] as const, {
|
|
||||||
description: "The action to perform",
|
|
||||||
}),
|
|
||||||
keys: Type.Optional(
|
|
||||||
Type.String({
|
|
||||||
description:
|
|
||||||
"For send_keys: the keys to send. Use \\n for Enter, \\t for Tab, or special:<key> for special keys (enter, backspace, tab, escape, up, down, left, right, f5)",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
|
|
||||||
async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
|
|
||||||
const { action, keys } = params;
|
|
||||||
|
|
||||||
const instance = DosboxInstance.getInstanceSync();
|
|
||||||
if (!instance || !instance.isReady()) {
|
|
||||||
return {
|
|
||||||
content: [{ type: "text", text: "Error: DOSBox is not running" }],
|
|
||||||
details: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (action) {
|
|
||||||
case "send_keys": {
|
|
||||||
if (!keys) {
|
|
||||||
return {
|
|
||||||
content: [{ type: "text", text: "Error: keys parameter required for send_keys action" }],
|
|
||||||
details: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle special keys
|
|
||||||
if (keys.startsWith("special:")) {
|
|
||||||
const specialKey = keys.slice(8) as
|
|
||||||
| "enter"
|
|
||||||
| "backspace"
|
|
||||||
| "tab"
|
|
||||||
| "escape"
|
|
||||||
| "up"
|
|
||||||
| "down"
|
|
||||||
| "left"
|
|
||||||
| "right"
|
|
||||||
| "f5";
|
|
||||||
instance.sendSpecialKey(specialKey);
|
|
||||||
return {
|
|
||||||
content: [{ type: "text", text: `Sent special key: ${specialKey}` }],
|
|
||||||
details: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle escape sequences
|
|
||||||
const processedKeys = keys.replace(/\\n/g, "\n").replace(/\\t/g, "\t").replace(/\\r/g, "\r");
|
|
||||||
|
|
||||||
instance.sendKeys(processedKeys);
|
|
||||||
return {
|
|
||||||
content: [{ type: "text", text: `Sent ${processedKeys.length} characters` }],
|
|
||||||
details: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
case "screenshot": {
|
|
||||||
const screenshot = instance.getScreenshot();
|
|
||||||
if (!screenshot) {
|
|
||||||
return {
|
|
||||||
content: [{ type: "text", text: "Error: No frame available yet" }],
|
|
||||||
details: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "image",
|
|
||||||
data: screenshot.base64,
|
|
||||||
mimeType: "image/png",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: `Screenshot: ${screenshot.width}x${screenshot.height} pixels`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
details: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
case "read_text": {
|
|
||||||
const text = instance.readScreenText();
|
|
||||||
if (text === null) {
|
|
||||||
const state = instance.getState();
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: `Screen is in graphics mode (${state.width}x${state.height}). Use screenshot action to see the display.`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
details: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
content: [{ type: "text", text: text || "(empty screen)" }],
|
|
||||||
details: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
return {
|
|
||||||
content: [{ type: "text", text: `Error: Unknown action: ${action}` }],
|
|
||||||
details: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
{
|
|
||||||
"name": "pi-dosbox",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.1",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"clean": "echo 'nothing to clean'",
|
|
||||||
"build": "echo 'nothing to build'",
|
|
||||||
"check": "echo 'nothing to check'"
|
|
||||||
},
|
|
||||||
"pi": {
|
|
||||||
"extensions": [
|
|
||||||
"./index.ts"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"emulators": "^8.3.9"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/node": "^20.11.30"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,71 +0,0 @@
|
||||||
'***
|
|
||||||
' QB.BI - Assembly Support Include File
|
|
||||||
'
|
|
||||||
' Copyright <C> 1987 Microsoft Corporation
|
|
||||||
'
|
|
||||||
' Purpose:
|
|
||||||
' This include file defines the types and gives the DECLARE
|
|
||||||
' statements for the assembly language routines ABSOLUTE,
|
|
||||||
' INTERRUPT, INTERRUPTX, INT86OLD, and INT86XOLD.
|
|
||||||
'
|
|
||||||
'***************************************************************************
|
|
||||||
'
|
|
||||||
' Define the type needed for INTERRUPT
|
|
||||||
'
|
|
||||||
TYPE RegType
|
|
||||||
ax AS INTEGER
|
|
||||||
bx AS INTEGER
|
|
||||||
cx AS INTEGER
|
|
||||||
dx AS INTEGER
|
|
||||||
bp AS INTEGER
|
|
||||||
si AS INTEGER
|
|
||||||
di AS INTEGER
|
|
||||||
flags AS INTEGER
|
|
||||||
END TYPE
|
|
||||||
'
|
|
||||||
' Define the type needed for INTERUPTX
|
|
||||||
'
|
|
||||||
TYPE RegTypeX
|
|
||||||
ax AS INTEGER
|
|
||||||
bx AS INTEGER
|
|
||||||
cx AS INTEGER
|
|
||||||
dx AS INTEGER
|
|
||||||
bp AS INTEGER
|
|
||||||
si AS INTEGER
|
|
||||||
di AS INTEGER
|
|
||||||
flags AS INTEGER
|
|
||||||
ds AS INTEGER
|
|
||||||
es AS INTEGER
|
|
||||||
END TYPE
|
|
||||||
'
|
|
||||||
' DECLARE statements for the 5 routines
|
|
||||||
' -------------------------------------
|
|
||||||
'
|
|
||||||
' Generate a software interrupt, loading all but the segment registers
|
|
||||||
'
|
|
||||||
DECLARE SUB INTERRUPT (intnum AS INTEGER,inreg AS RegType,outreg AS RegType)
|
|
||||||
'
|
|
||||||
' Generate a software interrupt, loading all registers
|
|
||||||
'
|
|
||||||
DECLARE SUB INTERRUPTX (intnum AS INTEGER,inreg AS RegTypeX, outreg AS RegTypeX)
|
|
||||||
'
|
|
||||||
' Call a routine at an absolute address.
|
|
||||||
' NOTE: If the routine called takes parameters, then they will have to
|
|
||||||
' be added to this declare statement before the parameter given.
|
|
||||||
'
|
|
||||||
DECLARE SUB ABSOLUTE (address AS INTEGER)
|
|
||||||
'
|
|
||||||
' Generate a software interrupt, loading all but the segment registers
|
|
||||||
' (old version)
|
|
||||||
'
|
|
||||||
DECLARE SUB INT86OLD (intnum AS INTEGER,_
|
|
||||||
inarray(1) AS INTEGER,_
|
|
||||||
outarray(1) AS INTEGER)
|
|
||||||
'
|
|
||||||
' Gemerate a software interrupt, loading all the registers
|
|
||||||
' (old version)
|
|
||||||
'
|
|
||||||
DECLARE SUB INT86XOLD (intnum AS INTEGER,_
|
|
||||||
inarray(1) AS INTEGER,_
|
|
||||||
outarray(1) AS INTEGER)
|
|
||||||
'
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,198 +0,0 @@
|
||||||
'
|
|
||||||
' QUICKBASIC SUPPORT ROUTINES FOR THEDRAW OBJECT FILES
|
|
||||||
'-----------------------------------------------------------------------------
|
|
||||||
' Compatible with Microsoft QuickBasic v4.0 and v4.5 text modes.
|
|
||||||
'-----------------------------------------------------------------------------
|
|
||||||
'
|
|
||||||
' There are a few routines within the QB4UTIL.LIB file. These are
|
|
||||||
' (along with brief descriptions):
|
|
||||||
'
|
|
||||||
' UNCRUNCH - Flash display routine for crunched image files.
|
|
||||||
' ASCIIDISPLAY - Display routine for ascii only image files.
|
|
||||||
' NORMALDISPLAY - Display routine for normal full binary image files.
|
|
||||||
' INITSCREENARRAY - Maps a dynamic integer array to the physical video
|
|
||||||
' memory.
|
|
||||||
'
|
|
||||||
'=============================================================================
|
|
||||||
' UNCRUNCH (imagedata,video offset)
|
|
||||||
' ASCIIDISPLAY (imagedata,video offset)
|
|
||||||
' NORMALDISPLAY (imagedata,video offset)
|
|
||||||
'=============================================================================
|
|
||||||
'
|
|
||||||
' These three subroutines operate similarly. Each takes a specific data
|
|
||||||
' format (TheDraw crunched data, ascii only, or normal binary) and displays
|
|
||||||
' the image on the screen. Monochrome and color text video displays are
|
|
||||||
' supported. The integer offset parameter is useful with block images,
|
|
||||||
' giving control over where the block appears.
|
|
||||||
'
|
|
||||||
' Example calls:
|
|
||||||
' CALL UNCRUNCH (ImageData&,vidoffset%) <- for crunched data
|
|
||||||
' CALL ASCIIDISPLAY (ImageData&,vidoffset%) <- for ascii-only data
|
|
||||||
' CALL NORMALDISPLAY (ImageData&,vidoffset%) <- for normal binary data
|
|
||||||
'
|
|
||||||
' The parameter IMAGEDATA is the identifier you assign when saving
|
|
||||||
' a QuickBasic object file with TheDraw. ImageData actually becomes a
|
|
||||||
' short function returning information Uncrunch, AsciiDisplay, and
|
|
||||||
' NormalDisplay use to find the screen contents. In addition, three
|
|
||||||
' other related integer functions are created. Assuming the identifier
|
|
||||||
' IMAGEDATA, these are:
|
|
||||||
'
|
|
||||||
' IMAGEDATAWIDTH%
|
|
||||||
' IMAGEDATADEPTH%
|
|
||||||
' IMAGEDATALENGTH%
|
|
||||||
'
|
|
||||||
' The width and depth functions return the size of the block in final
|
|
||||||
' form (ie: a full screen would yield the numbers 80 and 25 respectfully).
|
|
||||||
' The length function returns the size of the stored data. For crunched
|
|
||||||
' files and block saves this might be very small. For a 80x25 full screen
|
|
||||||
' binary image it will be 4000 bytes. The integer functions are useful for
|
|
||||||
' computing screen or window dimensions, etc...
|
|
||||||
'
|
|
||||||
' You must declare all four functions in your Basic source code before
|
|
||||||
' they can be used (naturally). The following code example illustrates.
|
|
||||||
' The identifier used is IMAGEDATA. The data is a 40 character by 10 line
|
|
||||||
' block saved as normal binary.
|
|
||||||
'
|
|
||||||
' ----------------------------------------------------------------------
|
|
||||||
' REM $INCLUDE: 'QB4UTIL.BI'
|
|
||||||
' DECLARE FUNCTION ImageData& ' Important! Do not neglect
|
|
||||||
' DECLARE FUNCTION ImageDataWidth% ' the "&" and "%" symbols
|
|
||||||
' DECLARE FUNCTION ImageDataDepth% ' after the function names.
|
|
||||||
' DECLARE FUNCTION ImageDataLength%
|
|
||||||
'
|
|
||||||
' CALL NORMALDISPLAY (ImageData&, 34 *2+( 5 *160)-162)
|
|
||||||
' ----------------------------------------------------------------------
|
|
||||||
'
|
|
||||||
' That's it! The above displays the 40x10 block at screen coordinates
|
|
||||||
' column 34, line 5 (note these two numbers in above example). If the
|
|
||||||
' data was crunched or ascii use the corresponding routine.
|
|
||||||
'
|
|
||||||
' Note: The ascii-only screen image does not have any color controls.
|
|
||||||
' Whatever the on-screen colors were before, they will be after.
|
|
||||||
' You might want to insert COLOR and CLS statements before calling
|
|
||||||
' the ASCIIDISPLAY routine.
|
|
||||||
'
|
|
||||||
' Regardless of which routine used, each remembers the original horizontal
|
|
||||||
' starting column when it goes to the next line. This permits a block to
|
|
||||||
' be displayed correctly anywhere on the screen. ie:
|
|
||||||
'
|
|
||||||
' +-------------------------------------------------+
|
|
||||||
' | |
|
|
||||||
' | | <- Pretend this
|
|
||||||
' | | is the video
|
|
||||||
' | ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ | display.
|
|
||||||
' | ³ÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ³ |
|
|
||||||
' | ³ÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ³ |
|
|
||||||
' | ³ÛÛ ImageData block ÛÛ³ |
|
|
||||||
' | ³ÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ³ |
|
|
||||||
' | ³ÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ³ |
|
|
||||||
' | ³ÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ³ |
|
|
||||||
' | ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ |
|
|
||||||
' | |
|
|
||||||
' | |
|
|
||||||
' | |
|
|
||||||
' +-------------------------------------------------+
|
|
||||||
'
|
|
||||||
'
|
|
||||||
' The ImageData block could be shown in the upper-left corner of the
|
|
||||||
' screen by changing the call to:
|
|
||||||
'
|
|
||||||
' CALL NORMALDISPLAY (ImageData&,0)
|
|
||||||
'
|
|
||||||
' Notice the video offset has been removed, since we want the upper-left
|
|
||||||
' corner. To display the block in the lower-right corner you would use:
|
|
||||||
'
|
|
||||||
' CALL NORMALDISPLAY (ImageData&, 40 *2+( 15 *160)-162)
|
|
||||||
'
|
|
||||||
' The block is 40 characters wide by 10 lines deep. Therefore to display
|
|
||||||
' such a large block, we must display the block at column 40, line 15.
|
|
||||||
' (column 80 minus 40, line 25 minus 10).
|
|
||||||
'
|
|
||||||
'
|
|
||||||
' NOTES ON THE UNCRUNCH ROUTINE
|
|
||||||
' --------------------------------------------------------------------------
|
|
||||||
'
|
|
||||||
' Many people favor "crunching" screens with TheDraw because the size
|
|
||||||
' of the data generally goes down. When uncrunching an image however,
|
|
||||||
' there is no guarantee what was previously on-screen will be replaced.
|
|
||||||
'
|
|
||||||
' In particular, the uncruncher assumes the screen is previously erased to
|
|
||||||
' black thus permitting better data compression. For instance, assume the
|
|
||||||
' video completely filled with blocks, overwritten by an uncrunched image:
|
|
||||||
'
|
|
||||||
' ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿
|
|
||||||
' ³ÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ³ ³tetetetetetÛÛÛÛÛÛÛÛÛÛ³
|
|
||||||
' ³ÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ³ ³ÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ³
|
|
||||||
' ³ÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ³ ³ eteteteteteteÛÛÛÛ³
|
|
||||||
' ³ÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ³ ³tetetetetÛÛÛÛÛÛÛÛÛÛÛÛ³
|
|
||||||
' ³ÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ³ ³ eteÛÛÛÛÛÛÛÛÛ³
|
|
||||||
' ³ÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ³ ³ etetetetetetÛÛ³
|
|
||||||
' ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ
|
|
||||||
' before uncrunch after uncrunch
|
|
||||||
'
|
|
||||||
' By omitting a CLS statement, the new text appears surrounded by bits of
|
|
||||||
' the previous screen. Proper usage would typically be:
|
|
||||||
'
|
|
||||||
' ----------------------------------------------------------------------
|
|
||||||
' REM $INCLUDE: 'QB4UTIL.BI'
|
|
||||||
' DECLARE FUNCTION ImageData& ' Important! Do not neglect
|
|
||||||
' DECLARE FUNCTION ImageDataWidth% ' the "&" and "%" symbols
|
|
||||||
' DECLARE FUNCTION ImageDataDepth% ' after the function names.
|
|
||||||
' DECLARE FUNCTION ImageDataLength%
|
|
||||||
'
|
|
||||||
' COLOR 15,0 : CLS ' Clear to black screen
|
|
||||||
' CALL UNCRUNCH (ImageData&, 34 *2+( 5 *160)-162)
|
|
||||||
' ----------------------------------------------------------------------
|
|
||||||
'
|
|
||||||
'
|
|
||||||
'=============================================================================
|
|
||||||
' INITSCREENARRAY
|
|
||||||
'=============================================================================
|
|
||||||
'
|
|
||||||
' To directly access the video screen memory requires you to use the
|
|
||||||
' PEEK/POKE statements after setting the DEF SEG value. A cumbersome
|
|
||||||
' and compiler inefficient approach. In addition, you must have some
|
|
||||||
' way of determining if a monochrome or color video is being used before
|
|
||||||
' the DEF SEG can be set properly.
|
|
||||||
'
|
|
||||||
' This subroutine offers a simpler approach, by effectively mapping or
|
|
||||||
' placing an integer array over the video screen. Instead of PEEK/POKE,
|
|
||||||
' you merely reference an array element. ie:
|
|
||||||
'
|
|
||||||
' ----------------------------------------------------------------------
|
|
||||||
' REM $INCLUDE: 'QB4UTIL.BI'
|
|
||||||
'
|
|
||||||
' REM $DYNAMIC <- very important to place this before DIM statement
|
|
||||||
' DIM S%(0)
|
|
||||||
' CALL INITSCREENARRAY (S%())
|
|
||||||
'
|
|
||||||
' S%(0) = ASC("H") + 15 *256 + 1 *4096
|
|
||||||
' S%(1) = ASC("E") + 15 *256 + 1 *4096
|
|
||||||
' S%(2) = ASC("L") + 15 *256 + 1 *4096
|
|
||||||
' S%(3) = ASC("L") + 15 *256 + 1 *4096
|
|
||||||
' S%(4) = ASC("O") + 15 *256 + 1 *4096
|
|
||||||
' ----------------------------------------------------------------------
|
|
||||||
'
|
|
||||||
' The above example directly places the message "HELLO" on the screen
|
|
||||||
' for you, in white lettering (the 15*256) on a blue background (1*4096).
|
|
||||||
' To alter the foreground color, change the 15's to some other number.
|
|
||||||
' Change the 1's for the background color.
|
|
||||||
'
|
|
||||||
' Each array element contains both the character to display plus the
|
|
||||||
' color information. This explains the bit of math following each
|
|
||||||
' ASC statement. You could minimize this using a FOR/NEXT loop.
|
|
||||||
'
|
|
||||||
' The S% array has 2000 elements (0 to 1999) representing the entire
|
|
||||||
' 80 by 25 line video. If in an EGA/VGA screen mode change the 1999 to
|
|
||||||
' 3439 or 3999 respectfully.
|
|
||||||
'
|
|
||||||
' There is no pressing reason to use the array approach, however it
|
|
||||||
' does free up the DEFSEG/PEEK/POKE combination for other uses. In
|
|
||||||
' any case, enjoy!
|
|
||||||
'
|
|
||||||
'
|
|
||||||
DECLARE SUB UNCRUNCH (X&, Z%)
|
|
||||||
DECLARE SUB ASCIIDISPLAY (X&, Z%)
|
|
||||||
DECLARE SUB NORMALDISPLAY (X&, Z%)
|
|
||||||
DECLARE SUB INITSCREENARRAY (A%())
|
|
||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -1,545 +0,0 @@
|
||||||
README.DOC File
|
|
||||||
|
|
||||||
Release Notes for Microsoft (R) QuickBASIC
|
|
||||||
|
|
||||||
Version 4.50
|
|
||||||
|
|
||||||
(C) Copyright Microsoft Corporation, 1990
|
|
||||||
|
|
||||||
Product Serial Number: 00-007-1450-26147102
|
|
||||||
|
|
||||||
|
|
||||||
This document contains release notes for version 4.50 of the Microsoft (R)
|
|
||||||
QuickBASIC for MS-DOS (R). The information in this document is more
|
|
||||||
up-to-date than that in the manuals.
|
|
||||||
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
Contents
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
|
|
||||||
Part Description
|
|
||||||
---- -----------
|
|
||||||
|
|
||||||
1 Using QuickBASIC on a Two-Floppy System
|
|
||||||
|
|
||||||
2 Using Your Mouse with QuickBASIC
|
|
||||||
|
|
||||||
3 Supplementary Information on Mixed-Language Programming
|
|
||||||
|
|
||||||
4 Using Btrieve with QuickBASIC
|
|
||||||
|
|
||||||
5 Using the DOS 3.2 Patch for Math Accuracy
|
|
||||||
|
|
||||||
6 Miscellaneous Information About Using QuickBASIC
|
|
||||||
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
Part 1: Using QuickBASIC on a Two-Floppy System
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
|
|
||||||
Installing QuickBASIC on Floppy Disks
|
|
||||||
-------------------------------------
|
|
||||||
|
|
||||||
The SETUP program can install QuickBASIC on floppy disks for use
|
|
||||||
with a two-floppy system. You must run SETUP to install QuickBASIC
|
|
||||||
on floppy disks. You cannot run QuickBASIC from the disks provided,
|
|
||||||
because the files are stored in a compressed format.
|
|
||||||
|
|
||||||
Before you install QuickBASIC on your two-floppy system, be sure
|
|
||||||
you have enough blank, formatted disks. If you have 360K disk
|
|
||||||
drives, you will need five blank disks. For 720K disk drives, you
|
|
||||||
will need three blank disks.
|
|
||||||
|
|
||||||
To install QuickBASIC, put Disk #1 in drive A. Type A:\SETUP and
|
|
||||||
press Enter.
|
|
||||||
|
|
||||||
When your installation is complete, you should label each disk with
|
|
||||||
the names of the files that are on that disk. QuickBASIC will ask
|
|
||||||
you to swap disks when it cannot find a file that it needs, and
|
|
||||||
you will need to know which disk the file is on.
|
|
||||||
|
|
||||||
If you use 360K disks, label them as follows:
|
|
||||||
|
|
||||||
PROGRAM:
|
|
||||||
QB.EXE QB45QCK.HLP
|
|
||||||
|
|
||||||
UTILITIES:
|
|
||||||
BC.EXE LINK.EXE
|
|
||||||
BQLB45.LIB LIB.EXE
|
|
||||||
BRUN45.EXE QB.QLB
|
|
||||||
BRUN45.LIB QB.LIB
|
|
||||||
|
|
||||||
UTILITIES 2:
|
|
||||||
BCOM45.LIB QB45ENER.HLP
|
|
||||||
|
|
||||||
ADVISOR:
|
|
||||||
QB45ADVR.HLP
|
|
||||||
|
|
||||||
EXAMPLES
|
|
||||||
QB.BI BASIC examples
|
|
||||||
|
|
||||||
If you use 720K disks, label them as follows:
|
|
||||||
|
|
||||||
PROGRAM/EXAMPLES:
|
|
||||||
QB.EXE QB45QCK.HLP
|
|
||||||
QB.BI BASIC examples
|
|
||||||
|
|
||||||
UTILITIES:
|
|
||||||
BC.EXE LINK.EXE
|
|
||||||
BQLB45.LIB LIB.EXE
|
|
||||||
BRUN45.EXE QB.QLB
|
|
||||||
BRUN45.LIB QB.LIB
|
|
||||||
BCOM45.LIB
|
|
||||||
|
|
||||||
ADVISOR:
|
|
||||||
QB45ADVR.HLP QB45ENER.HLP
|
|
||||||
|
|
||||||
|
|
||||||
Running QuickBASIC from Floppy Disks
|
|
||||||
------------------------------------
|
|
||||||
|
|
||||||
During some operations, QuickBASIC asks you to swap disks one
|
|
||||||
or more times. You can minimize disk swapping by following the
|
|
||||||
procedures in this section.
|
|
||||||
|
|
||||||
Since the disks that you installed QuickBASIC on are nearly full, you
|
|
||||||
should keep your BASIC source-code (.BAS) files on a separate disk.
|
|
||||||
Label this disk SOURCE.
|
|
||||||
|
|
||||||
Copy the run-time module BRUN45.EXE from the UTILITIES disk to your
|
|
||||||
SOURCE disk. QuickBASIC needs this file to run executable programs
|
|
||||||
compiled with the run-time support option.
|
|
||||||
|
|
||||||
When you use QuickBASIC, a disk containing source-code (.BAS) files
|
|
||||||
should always be in drive B. If you want to run existing BASIC
|
|
||||||
programs (such as the example programs provided with QuickBASIC),
|
|
||||||
remove the SOURCE disk from drive B and insert the disk containing
|
|
||||||
these files.
|
|
||||||
|
|
||||||
To run QuickBASIC:
|
|
||||||
|
|
||||||
1. Insert the SOURCE disk in drive B.
|
|
||||||
|
|
||||||
2. To make drive B the current drive, type B: and press Enter.
|
|
||||||
|
|
||||||
3. Insert the PROGRAM disk (the disk containing QB.EXE) in drive A.
|
|
||||||
|
|
||||||
4. Type the following command:
|
|
||||||
|
|
||||||
A:QB.EXE
|
|
||||||
|
|
||||||
To insure that QuickBASIC always looks on both disk drives for the
|
|
||||||
files it needs, follow these steps:
|
|
||||||
|
|
||||||
1. From the Options menu, choose Set Paths.
|
|
||||||
|
|
||||||
2. Make sure each of the path settings includes both disk drives. The
|
|
||||||
following line should be in all four text boxes:
|
|
||||||
|
|
||||||
A:\;B:\
|
|
||||||
|
|
||||||
3. Choose OK.
|
|
||||||
|
|
||||||
QuickBASIC saves these path settings in the QB.INI file, so you will
|
|
||||||
not have to enter them again.
|
|
||||||
|
|
||||||
When you exit QuickBASIC or shell to DOS, you will be prompted to
|
|
||||||
insert a disk containing the file COMMAND.COM. Remove the PROGRAM
|
|
||||||
disk from drive A, insert a system disk, and press Enter.
|
|
||||||
|
|
||||||
|
|
||||||
Using Help from Floppy Disks
|
|
||||||
----------------------------
|
|
||||||
|
|
||||||
When you use the QuickBASIC Advisor online help system, you may need
|
|
||||||
to swap disks. For example, if you choose "Details" or "Example" on a
|
|
||||||
help screen, QuickBASIC will inform you that it cannot find the help
|
|
||||||
file QB45ADVR.HLP. Put the disk that contains this file in drive A and
|
|
||||||
choose Retry.
|
|
||||||
|
|
||||||
|
|
||||||
Compiling Your Programs from Floppy Disks
|
|
||||||
-----------------------------------------
|
|
||||||
|
|
||||||
To compile your program from within QuickBASIC:
|
|
||||||
|
|
||||||
1. From the Run menu, choose Make EXE File.
|
|
||||||
|
|
||||||
2. Choose Make EXE. QuickBASIC displays the message "Cannot find file
|
|
||||||
(BC.EXE)."
|
|
||||||
|
|
||||||
3. Insert the UTILITIES disk (the disk containing BC.EXE) in drive A.
|
|
||||||
Type A: and press Enter.
|
|
||||||
|
|
||||||
If the program compiles successfully, QuickBASIC invokes the LINK
|
|
||||||
utility. If LINK cannot find the library, it displays the following
|
|
||||||
message:
|
|
||||||
|
|
||||||
LINK : warning L4051 : BCOM45.LIB : cannot find library
|
|
||||||
Enter new file spec:
|
|
||||||
|
|
||||||
4. Insert the disk containing the requested library (BCOM45.LIB or
|
|
||||||
BRUN45.LIB) in drive A.
|
|
||||||
|
|
||||||
Note: The requested library may be located on the UTILITIES disk
|
|
||||||
already in drive A. If this is the case, leave this disk in drive A.
|
|
||||||
|
|
||||||
5. Type A: and press Enter. After the LINK utility finishes creating
|
|
||||||
your executable program, QuickBASIC displays the message "Cannot
|
|
||||||
find file (QB.EXE)."
|
|
||||||
|
|
||||||
6. Insert the PROGRAM disk in drive A.
|
|
||||||
|
|
||||||
7. Type A: and press Enter.
|
|
||||||
|
|
||||||
If no errors occur during compiling or linking, your compiled program
|
|
||||||
(.EXE) is created on drive B. QuickBASIC also creates an object-module
|
|
||||||
(.OBJ) file. To save space, you can delete object-module files.
|
|
||||||
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
Part 2: Using Your Mouse with QuickBASIC
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
|
|
||||||
New Mouse Driver for Use with QuickBASIC
|
|
||||||
----------------------------------------
|
|
||||||
|
|
||||||
QuickBASIC Version 4.5 can be used with any mouse that is 100%
|
|
||||||
compatible with the Microsoft Mouse. However, you must use a
|
|
||||||
Microsoft Mouse driver Version 6.00 or later. Earlier versions may
|
|
||||||
cause unpredictable behavior when used with QuickBASIC. MOUSE.COM,
|
|
||||||
Version 6.24 is supplied with QuickBASIC Version 4.5.
|
|
||||||
|
|
||||||
Especially if you are writing programs that use the mouse, you
|
|
||||||
should use the supplied version of the mouse driver when working in
|
|
||||||
QuickBASIC. Previous versions have included MOUSE.SYS, which is
|
|
||||||
installed by including the line DEVICE=MOUSE.SYS in your CONFIG.SYS
|
|
||||||
file. This version of QuickBASIC includes MOUSE.COM, which is not
|
|
||||||
installed via CONFIG.SYS. To install MOUSE.COM, just type MOUSE at
|
|
||||||
the DOS prompt. To include MOUSE.COM automatically when your machine
|
|
||||||
boots, make sure MOUSE.COM is in your search path, then put the line
|
|
||||||
|
|
||||||
MOUSE
|
|
||||||
|
|
||||||
in your AUTOEXEC.BAT file. To free up memory, you can remove the
|
|
||||||
mouse driver at any time by typing MOUSE OFF at the DOS prompt.
|
|
||||||
This will restore between 9K and 10.5K of memory with Version 6.11.
|
|
||||||
|
|
||||||
|
|
||||||
Using Mouse Function Calls from QuickBASIC Programs
|
|
||||||
---------------------------------------------------
|
|
||||||
|
|
||||||
If you are programming for the Microsoft Mouse, you should obtain
|
|
||||||
the Microsoft Mouse Programmer's Reference Guide and the library
|
|
||||||
MOUSE.LIB that comes with it. (These are not included in QuickBASIC
|
|
||||||
or Mouse package and must be ordered separately). Most of the
|
|
||||||
information in the Mouse Programmer's Reference Guide applies
|
|
||||||
directly to QuickBASIC Version 4.5. However, the following additional
|
|
||||||
restrictions must be observed:
|
|
||||||
|
|
||||||
Certain Mouse function calls (Functions 9 & 16) require you to set
|
|
||||||
up an integer array and pass the address of the array to the mouse
|
|
||||||
driver. For previous versions, the only restriction on this array
|
|
||||||
was that it had to be $STATIC (the default array type). In QuickBASIC
|
|
||||||
Version 4.5, however, the array also must be in a COMMON block if you
|
|
||||||
will be making the Mouse function call from within the QuickBASIC
|
|
||||||
environment. In addition, it is recommended that the support code
|
|
||||||
for the Mouse call be in a Quick library or linked into the
|
|
||||||
executable file when making Mouse function calls from QuickBASIC.
|
|
||||||
|
|
||||||
To produce a Quick library for using Mouse function calls from
|
|
||||||
within the QuickBASIC environment, use the following command line
|
|
||||||
(produces MOUSE.QLB):
|
|
||||||
|
|
||||||
LINK MOUSE.LIB/QU,MOUSE.QLB,,BQLB40.LIB/NOE;
|
|
||||||
|
|
||||||
An example from PIANO.BAS (included with the Microsoft Mouse
|
|
||||||
Programmer's Reference) for using Mouse function call 9:
|
|
||||||
|
|
||||||
DEFINT A-Z
|
|
||||||
DECLARE SUB MOUSE (M1, M2, M3, M4)
|
|
||||||
DIM Cursor(15, 1)
|
|
||||||
COMMON Cursor() 'Ensures array data is in DGROUP
|
|
||||||
.
|
|
||||||
. (set up Cursor() for mouse cursor shape desired)
|
|
||||||
.
|
|
||||||
M1 = 9: M2 = 6: M3 = 0
|
|
||||||
CALL MOUSE(M1, M2, M3, VARPTR(Cursor(0, 0)))
|
|
||||||
|
|
||||||
In addition to the above, note that Mouse function calls 21-23
|
|
||||||
require dynamically allocated storage out of the home data segment.
|
|
||||||
The recommended way to do this is to allocate space in a dynamic
|
|
||||||
string variable based on the return value from function call 21,
|
|
||||||
using the STRING$ or SPACE$ function. Then use VARPTR on this string
|
|
||||||
variable just prior to calling Mouse function call 22 or 23.
|
|
||||||
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
Part 3: Supplementary Information on Mixed-Language Programming
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
|
|
||||||
Linking from Within QuickC or with QCL
|
|
||||||
--------------------------------------
|
|
||||||
|
|
||||||
Microsoft QuickC and the QCL command both set the /NOI linker
|
|
||||||
by default. Therefore, you should not link from within QuickC, or
|
|
||||||
with QCL, when your program contains modules written in a case-
|
|
||||||
insensitive language such as BASIC. Use LINK to link your program
|
|
||||||
from the command line.
|
|
||||||
|
|
||||||
|
|
||||||
Pascal and FORTRAN Modules in QuickBASIC Programs
|
|
||||||
-------------------------------------------------
|
|
||||||
|
|
||||||
Modules compiled with Microsoft Pascal or FORTRAN can be linked with
|
|
||||||
BASIC programs, as described in the Microsoft Mixed-Language
|
|
||||||
Programming Guide. They can also be incorporated in Quick libraries.
|
|
||||||
However, QuickBASIC programs containing code compiled with Microsoft
|
|
||||||
Pascal must allocate at least 2K near-heap space for Pascal. This can
|
|
||||||
be done by using the DIM statement to allocate a static array of 2K or
|
|
||||||
greater in the NMALLOC named common block, for example, as follows:
|
|
||||||
|
|
||||||
DIM name%(2048)
|
|
||||||
COMMON SHARED /NMALLOC/name%()
|
|
||||||
|
|
||||||
The Pascal run-time assumes it always has at least 2K of near-heap
|
|
||||||
space available. If the Pascal code cannot allocate the required
|
|
||||||
space, QuickBASIC may crash. This applies to Pascal code in Quick
|
|
||||||
libraries as well as Pascal code linked into executable files. The
|
|
||||||
situation is similar for FORTRAN I/O, which also requires near
|
|
||||||
buffer space, and which can be provided by the same means as the
|
|
||||||
Pascal near malloc space.
|
|
||||||
|
|
||||||
|
|
||||||
STATIC Array Allocation
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
If you are writing assembly-language modules for use in QuickBASIC
|
|
||||||
programs, see Section 2.3.3, "Variable Storage Allocation," in the
|
|
||||||
BASIC Language Reference. Assembly-language code should not assume
|
|
||||||
data is in a particular segment. To avoid problems, pass data using
|
|
||||||
the SEG or CALLS keywords, or use FAR pointers. Alternatively, you
|
|
||||||
can declare all arrays dynamic (still using far pointers) since
|
|
||||||
dynamic arrays are handled identically by BC and within QuickBASIC.
|
|
||||||
|
|
||||||
|
|
||||||
Quick Libraries with Leading Zeros in the First Code Segment
|
|
||||||
------------------------------------------------------------
|
|
||||||
|
|
||||||
A Quick library containing leading zeros in the first CODE segment
|
|
||||||
is invalid, causing the message "Error in loading file <name> -
|
|
||||||
Invalid format" when you try to load it in QuickBASIC. For example,
|
|
||||||
this can occur if an assembly-language routine puts data that is
|
|
||||||
initialized to zero in the first CODE segment, and it is subsequently
|
|
||||||
listed first on the LINK command line when you make a Quick library.
|
|
||||||
If you have this problem, do either of the following:
|
|
||||||
(1) link with a BASIC module first on the LINK command line, or
|
|
||||||
(2) make sure that, in whatever module comes first on the LINK
|
|
||||||
command line, the first code segment starts with a non-zero byte.
|
|
||||||
|
|
||||||
|
|
||||||
References to DGROUP in Extended Run-Time Modules
|
|
||||||
-------------------------------------------------
|
|
||||||
|
|
||||||
For mixed-language programs that use the CHAIN command, you should
|
|
||||||
make sure that any code built into an extended run-time module does not
|
|
||||||
contain any references to DGROUP. (The CHAIN command causes DGROUP to
|
|
||||||
move, but does not update references to DGROUP.) This rule applies
|
|
||||||
only to mixed-language programs; because BASIC routines never refer
|
|
||||||
to DGROUP, you can ignore this caution for programs written entirely
|
|
||||||
in BASIC.
|
|
||||||
|
|
||||||
To avoid this problem, you can use the value of SS, since BASIC always
|
|
||||||
assumes that SS coincides with DGROUP.
|
|
||||||
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
Part 4: Using Btrieve
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
|
|
||||||
Using Btrieve in OS/2 Protected Mode
|
|
||||||
------------------------------------
|
|
||||||
|
|
||||||
In OS/2 protected mode, a BASIC program that uses Btrieve must do a
|
|
||||||
Btrieve reset call (function 28) before executing the CHAIN statement.
|
|
||||||
The program must also reopen all Btrieve files when the destination of
|
|
||||||
the CHAIN starts to run.
|
|
||||||
|
|
||||||
|
|
||||||
Using Btrieve with QuickBASIC
|
|
||||||
-----------------------------
|
|
||||||
|
|
||||||
If you use Btrieve with QuickBASIC, you must make a small change to
|
|
||||||
your programs for QuickBASIC Version 4.5. Currently your programs
|
|
||||||
contain a statement that obtains the address of the field buffer for
|
|
||||||
an open file. For example:
|
|
||||||
|
|
||||||
OPEN "NUL" AS #1
|
|
||||||
FIELD #1, 20 AS CITY$, 10 AS STATE$
|
|
||||||
FCB.ADDR% = VARPTR(#1) 'This statement obtains the address
|
|
||||||
|
|
||||||
In QuickBASIC Version 4.5, you should change the indicated statement
|
|
||||||
to return the address of the first variable in your field buffer minus
|
|
||||||
a constant, as follows:
|
|
||||||
|
|
||||||
OPEN "NUL" AS #1
|
|
||||||
FIELD #1, 20 AS CITY$, 10 AS STATE$
|
|
||||||
FCB.ADDR% = SADD(CITY$) - 188 ' CITY$ is the first field
|
|
||||||
' buffer variable
|
|
||||||
|
|
||||||
The following example shows how to obtain the same address for a
|
|
||||||
user-defined type:
|
|
||||||
|
|
||||||
TYPE ADDRESS
|
|
||||||
CITY AS STRING * 20
|
|
||||||
STATE AS STRING * 10
|
|
||||||
END TYPE
|
|
||||||
|
|
||||||
DIM ADD1 AS ADDRESS
|
|
||||||
|
|
||||||
FCB.ADDR% = VARPTR(ADD1) - 188
|
|
||||||
' or, you can use FCB.ADDR% = VARPTR(ADD1.CITY) - 188
|
|
||||||
|
|
||||||
Your programs should function correctly with Btrieve with this change.
|
|
||||||
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
Part 5: DOS 3.20 Patch
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
This information is important only if your system has all of the
|
|
||||||
following characteristics:
|
|
||||||
|
|
||||||
1. Uses MS-DOS version 3.20
|
|
||||||
2. Boots from a hard disk drive
|
|
||||||
3. Has a math coprocessor (for instance, an 8087 chip)
|
|
||||||
4. Runs programs that use floating-point math
|
|
||||||
|
|
||||||
For systems that satisfy all of the preceding conditions, you may be
|
|
||||||
able to eliminate floating-point math problems by installing a small
|
|
||||||
patch in DOS. If you are not sure whether you need the patch, perform
|
|
||||||
the following steps:
|
|
||||||
|
|
||||||
1. Copy the program PATCH87.EXE (included in this release) to the root
|
|
||||||
directory of your hard-disk drive.
|
|
||||||
|
|
||||||
2. Reboot your system from the hard disk, and do not perform any floppy-
|
|
||||||
disk operations after rebooting. It is very important that you avoid
|
|
||||||
floppy-disk I/O after rebooting, since that will affect the
|
|
||||||
reliability of the diagnostic test that you are about to perform.
|
|
||||||
|
|
||||||
3. If necessary, use the CD command to move to the root directory of
|
|
||||||
your hard-disk drive.
|
|
||||||
|
|
||||||
4. Run the PATCH87.EXE program by entering this command at the DOS
|
|
||||||
prompt:
|
|
||||||
|
|
||||||
PATCH87
|
|
||||||
|
|
||||||
5. The program performs a diagnostic test on your system to determine
|
|
||||||
whether it needs the DOS patch, and if the patch is needed,
|
|
||||||
whether the patch can be installed successfully. If the program
|
|
||||||
tells you that you need to install the DOS patch, and that it can be
|
|
||||||
done, follow the procedure described in the next section.
|
|
||||||
|
|
||||||
Note: The floating-point problem has been eliminated in versions of
|
|
||||||
MS-DOS higher than 3.20. This includes MS-DOS versions 3.21 and 3.30.
|
|
||||||
|
|
||||||
If you performed the preceding test and determined that you should
|
|
||||||
install the DOS patch on your system, perform the following steps:
|
|
||||||
|
|
||||||
1. Format a blank floppy disk. (Do NOT use the /s formatting option to
|
|
||||||
transfer system files to the disk.)
|
|
||||||
|
|
||||||
2. Use the SYS command to copy IO.SYS and MSDOS.SYS from the root
|
|
||||||
directory of your hard disk to the new floppy disk. For instance, if
|
|
||||||
you boot from drive C:, you would enter the following commands:
|
|
||||||
|
|
||||||
C:
|
|
||||||
SYS A:
|
|
||||||
|
|
||||||
3. Use the COPY command to copy COMMAND.COM and SYS.COM to the same
|
|
||||||
floppy disk.
|
|
||||||
|
|
||||||
4. Use the COPY command to copy the program PATCH87.EXE (included in
|
|
||||||
this release) to the same floppy disk.
|
|
||||||
|
|
||||||
5. Change the current drive and directory to the floppy disk, by
|
|
||||||
entering the following command:
|
|
||||||
|
|
||||||
A:
|
|
||||||
|
|
||||||
7. Install the DOS patch by entering the following command:
|
|
||||||
|
|
||||||
PATCH87 /F
|
|
||||||
|
|
||||||
WARNING: If you experience any disk errors during steps 2 through 7,
|
|
||||||
do not proceed with step 8. Reboot from your hard disk and repeat the
|
|
||||||
entire process.
|
|
||||||
|
|
||||||
8. If you have not experienced any errors, use the SYS command to
|
|
||||||
transfer the files IO.SYS and MSDOS.SYS from the floppy disk back to
|
|
||||||
your hard disk. For instance, if the boot directory of your system
|
|
||||||
is the root directory of drive C:, you would enter the following
|
|
||||||
command at the DOS prompt:
|
|
||||||
|
|
||||||
A:
|
|
||||||
SYS C:
|
|
||||||
|
|
||||||
9. The DOS patch has been installed. Reboot the system.
|
|
||||||
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
Part 6: Miscellaneous Information About Using QuickBASIC
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
|
|
||||||
Using FIXSHIFT.COM Utility
|
|
||||||
--------------------------
|
|
||||||
|
|
||||||
Some keyboards have an extra set of DIRECTION (i.e. arrow) keys, in
|
|
||||||
addition to those on the numeric keypad. A bug in the ROM BIOS of
|
|
||||||
some machines with these keyboards can interfere with the QuickBASIC
|
|
||||||
editor. The Utilities 2 disk includes a program, FIXSHIFT.COM, that
|
|
||||||
fixes this bug. If you have such a keyboard, run this program by typing
|
|
||||||
FIXSHIFT. If your machine does not have the bug, FIXSHIFT displays a
|
|
||||||
message telling you so. Otherwise FIXSHIFT prompts you for the proper
|
|
||||||
actions. FIXSHIFT takes about 450 bytes of memory. Except for the BIOS
|
|
||||||
bug, it has no effect on other programs you run.
|
|
||||||
|
|
||||||
|
|
||||||
Note on VGA Display Adapters
|
|
||||||
----------------------------
|
|
||||||
|
|
||||||
If you install an IBM (R) Personal System/2 (TM) Video Graphics
|
|
||||||
Array display adapter (VGA) in a non-PS/2 machine, the VGA adapter
|
|
||||||
should be the only adapter in the system, and you should not use
|
|
||||||
monochrome modes (SCREEN 10) if you have a color monitor. Similarly,
|
|
||||||
you should not use color modes (SCREEN 1, 2, 7, 8, 9, 11, 12, 13) if
|
|
||||||
you have a monochrome monitor.
|
|
||||||
|
|
||||||
|
|
||||||
Note on Using QuickBASIC with DOS 2.1
|
|
||||||
-------------------------------------
|
|
||||||
|
|
||||||
To use QuickBASIC with a two-floppy system under DOS 2.1, you must
|
|
||||||
put a copy of COMMAND.COM on each disk containing an executable
|
|
||||||
file ( a file with the .EXE extension).
|
|
||||||
|
|
||||||
|
|
||||||
PTR86, LOF, Naming SUB Procedures and Variables
|
|
||||||
-----------------------------------------------
|
|
||||||
|
|
||||||
PTR86 is no longer supported. Use VARSEG and VARPTR instead.
|
|
||||||
Also, when used with a communications device, LOF now returns the
|
|
||||||
amount of space remaining (in bytes) in the output buffer. In
|
|
||||||
previous versions this was returned in the input buffer. Also, note
|
|
||||||
that a variable and SUB procedure could have the same name in
|
|
||||||
previous versions. In Version 4.5, this causes a "Duplicate
|
|
||||||
definition" error message.
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
Please check "Use folder names." when you extract files (Using Winzip) or use /d option with PKZip because all the path in the INI files are set to C:\QB directory.
|
|
||||||
If you extract it in a different directory, set new paths in
|
|
||||||
Help>Set Paths from the Basic menu.
|
|
||||||
|
|
||||||
<-- Other Basic versions and many other compilers and files are available
|
|
||||||
at http://members.xoom.com/qb_best/ -->
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -1 +0,0 @@
|
||||||
qb/l
|
|
||||||
|
|
@ -1,425 +0,0 @@
|
||||||
/**
|
|
||||||
* DOSBox TUI Component
|
|
||||||
*
|
|
||||||
* Renders DOSBox framebuffer as an image in the terminal.
|
|
||||||
* Connects to the persistent DosboxInstance.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { Component } from "@mariozechner/pi-tui";
|
|
||||||
import {
|
|
||||||
allocateImageId,
|
|
||||||
deleteKittyImage,
|
|
||||||
Image,
|
|
||||||
type ImageTheme,
|
|
||||||
isKeyRelease,
|
|
||||||
Key,
|
|
||||||
matchesKey,
|
|
||||||
truncateToWidth,
|
|
||||||
} from "@mariozechner/pi-tui";
|
|
||||||
import { DosboxInstance } from "./dosbox-instance.js";
|
|
||||||
|
|
||||||
const MAX_WIDTH_CELLS = 120;
|
|
||||||
|
|
||||||
// js-dos key codes
|
|
||||||
const KBD: Record<string, number> = {
|
|
||||||
enter: 257,
|
|
||||||
backspace: 259,
|
|
||||||
tab: 258,
|
|
||||||
esc: 256,
|
|
||||||
space: 32,
|
|
||||||
leftshift: 340,
|
|
||||||
rightshift: 344,
|
|
||||||
leftctrl: 341,
|
|
||||||
rightctrl: 345,
|
|
||||||
leftalt: 342,
|
|
||||||
rightalt: 346,
|
|
||||||
up: 265,
|
|
||||||
down: 264,
|
|
||||||
left: 263,
|
|
||||||
right: 262,
|
|
||||||
home: 268,
|
|
||||||
end: 269,
|
|
||||||
pageup: 266,
|
|
||||||
pagedown: 267,
|
|
||||||
insert: 260,
|
|
||||||
delete: 261,
|
|
||||||
f1: 290,
|
|
||||||
f2: 291,
|
|
||||||
f3: 292,
|
|
||||||
f4: 293,
|
|
||||||
f5: 294,
|
|
||||||
f6: 295,
|
|
||||||
f7: 296,
|
|
||||||
f8: 297,
|
|
||||||
f9: 298,
|
|
||||||
f10: 299,
|
|
||||||
f11: 300,
|
|
||||||
f12: 301,
|
|
||||||
};
|
|
||||||
|
|
||||||
export class DosboxComponent implements Component {
|
|
||||||
private tui: { requestRender: () => void };
|
|
||||||
private onClose: () => void;
|
|
||||||
private instance: DosboxInstance | null = null;
|
|
||||||
private image: Image | null = null;
|
|
||||||
private imageTheme: ImageTheme;
|
|
||||||
private loadingMessage = "Connecting to DOSBox...";
|
|
||||||
private errorMessage: string | null = null;
|
|
||||||
private cachedLines: string[] = [];
|
|
||||||
private cachedWidth = 0;
|
|
||||||
private cachedVersion = -1;
|
|
||||||
private version = 0;
|
|
||||||
private disposed = false;
|
|
||||||
private imageId: number;
|
|
||||||
private kittyPushed = false;
|
|
||||||
private frameListener: ((rgba: Uint8Array, width: number, height: number) => void) | null = null;
|
|
||||||
|
|
||||||
wantsKeyRelease = true;
|
|
||||||
|
|
||||||
constructor(tui: { requestRender: () => void }, fallbackColor: (s: string) => string, onClose: () => void) {
|
|
||||||
this.tui = tui;
|
|
||||||
this.onClose = onClose;
|
|
||||||
this.imageTheme = { fallbackColor };
|
|
||||||
this.imageId = allocateImageId();
|
|
||||||
void this.connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async connect(): Promise<void> {
|
|
||||||
try {
|
|
||||||
this.instance = await DosboxInstance.getInstance();
|
|
||||||
|
|
||||||
// Set up frame listener
|
|
||||||
this.frameListener = (rgba: Uint8Array, width: number, height: number) => {
|
|
||||||
this.updateFrame(rgba, width, height);
|
|
||||||
};
|
|
||||||
this.instance.addFrameListener(this.frameListener);
|
|
||||||
|
|
||||||
// Get initial state
|
|
||||||
const state = this.instance.getState();
|
|
||||||
if (state.lastFrame && state.width && state.height) {
|
|
||||||
this.updateFrame(state.lastFrame, state.width, state.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push Kitty enhanced mode for proper key press/release
|
|
||||||
process.stdout.write("\x1b[>15u");
|
|
||||||
this.kittyPushed = true;
|
|
||||||
|
|
||||||
this.tui.requestRender();
|
|
||||||
} catch (error) {
|
|
||||||
this.errorMessage = error instanceof Error ? error.message : String(error);
|
|
||||||
this.tui.requestRender();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateFrame(rgba: Uint8Array, width: number, height: number): void {
|
|
||||||
const png = this.encodePng(width, height, rgba);
|
|
||||||
const base64 = png.toString("base64");
|
|
||||||
this.image = new Image(
|
|
||||||
base64,
|
|
||||||
"image/png",
|
|
||||||
this.imageTheme,
|
|
||||||
{ maxWidthCells: MAX_WIDTH_CELLS, imageId: this.imageId },
|
|
||||||
{ widthPx: width, heightPx: height },
|
|
||||||
);
|
|
||||||
this.version++;
|
|
||||||
this.tui.requestRender();
|
|
||||||
}
|
|
||||||
|
|
||||||
private encodePng(width: number, height: number, rgba: Uint8Array): Buffer {
|
|
||||||
const { deflateSync } = require("node:zlib");
|
|
||||||
const stride = width * 4;
|
|
||||||
const raw = Buffer.alloc((stride + 1) * height);
|
|
||||||
for (let y = 0; y < height; y++) {
|
|
||||||
const rowOffset = y * (stride + 1);
|
|
||||||
raw[rowOffset] = 0;
|
|
||||||
raw.set(rgba.subarray(y * stride, y * stride + stride), rowOffset + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const compressed = deflateSync(raw);
|
|
||||||
|
|
||||||
const header = Buffer.alloc(13);
|
|
||||||
header.writeUInt32BE(width, 0);
|
|
||||||
header.writeUInt32BE(height, 4);
|
|
||||||
header[8] = 8;
|
|
||||||
header[9] = 6;
|
|
||||||
|
|
||||||
const signature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
||||||
const ihdr = this.createChunk("IHDR", header);
|
|
||||||
const idat = this.createChunk("IDAT", compressed);
|
|
||||||
const iend = this.createChunk("IEND", Buffer.alloc(0));
|
|
||||||
|
|
||||||
return Buffer.concat([signature, ihdr, idat, iend]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private createChunk(type: string, data: Buffer): Buffer {
|
|
||||||
const length = Buffer.alloc(4);
|
|
||||||
length.writeUInt32BE(data.length, 0);
|
|
||||||
const typeBuffer = Buffer.from(type, "ascii");
|
|
||||||
const crcBuffer = Buffer.concat([typeBuffer, data]);
|
|
||||||
const crc = this.crc32(crcBuffer);
|
|
||||||
const crcOut = Buffer.alloc(4);
|
|
||||||
crcOut.writeUInt32BE(crc, 0);
|
|
||||||
return Buffer.concat([length, typeBuffer, data, crcOut]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private crc32(buffer: Buffer): number {
|
|
||||||
let crc = 0xffffffff;
|
|
||||||
for (const byte of buffer) {
|
|
||||||
crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
|
|
||||||
}
|
|
||||||
return (crc ^ 0xffffffff) >>> 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleInput(data: string): void {
|
|
||||||
const released = isKeyRelease(data);
|
|
||||||
|
|
||||||
if (!released && matchesKey(data, Key.ctrl("q"))) {
|
|
||||||
this.dispose();
|
|
||||||
this.onClose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ci = this.instance?.getCommandInterface();
|
|
||||||
if (!ci) return;
|
|
||||||
|
|
||||||
const parsed = parseKeyWithModifiers(data);
|
|
||||||
if (!parsed) return;
|
|
||||||
|
|
||||||
const { keyCode, shift, ctrl, alt } = parsed;
|
|
||||||
|
|
||||||
if (shift) ci.sendKeyEvent(KBD.leftshift, !released);
|
|
||||||
if (ctrl) ci.sendKeyEvent(KBD.leftctrl, !released);
|
|
||||||
if (alt) ci.sendKeyEvent(KBD.leftalt, !released);
|
|
||||||
|
|
||||||
ci.sendKeyEvent(keyCode, !released);
|
|
||||||
}
|
|
||||||
|
|
||||||
invalidate(): void {
|
|
||||||
this.cachedWidth = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
render(width: number): string[] {
|
|
||||||
if (this.errorMessage) {
|
|
||||||
return [truncateToWidth(`DOSBox error: ${this.errorMessage}`, width)];
|
|
||||||
}
|
|
||||||
if (!this.instance?.isReady()) {
|
|
||||||
return [truncateToWidth(this.loadingMessage, width)];
|
|
||||||
}
|
|
||||||
if (!this.image) {
|
|
||||||
return [truncateToWidth("Waiting for DOSBox frame...", width)];
|
|
||||||
}
|
|
||||||
if (width === this.cachedWidth && this.cachedVersion === this.version) {
|
|
||||||
return this.cachedLines;
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageLines = this.image.render(width);
|
|
||||||
const footer = truncateToWidth("\x1b[2mCtrl+Q to detach (DOSBox keeps running)\x1b[22m", width);
|
|
||||||
const lines = [...imageLines, footer];
|
|
||||||
|
|
||||||
this.cachedLines = lines;
|
|
||||||
this.cachedWidth = width;
|
|
||||||
this.cachedVersion = this.version;
|
|
||||||
|
|
||||||
return lines;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispose(): void {
|
|
||||||
if (this.disposed) return;
|
|
||||||
this.disposed = true;
|
|
||||||
|
|
||||||
// Delete the terminal image
|
|
||||||
process.stdout.write(deleteKittyImage(this.imageId));
|
|
||||||
|
|
||||||
if (this.kittyPushed) {
|
|
||||||
process.stdout.write("\x1b[<u");
|
|
||||||
this.kittyPushed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove frame listener but DON'T dispose the instance
|
|
||||||
if (this.instance && this.frameListener) {
|
|
||||||
this.instance.removeFrameListener(this.frameListener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const CRC_TABLE = createCrcTable();
|
|
||||||
|
|
||||||
function createCrcTable(): Uint32Array {
|
|
||||||
const table = new Uint32Array(256);
|
|
||||||
for (let i = 0; i < 256; i++) {
|
|
||||||
let c = i;
|
|
||||||
for (let j = 0; j < 8; j++) {
|
|
||||||
if (c & 1) {
|
|
||||||
c = 0xedb88320 ^ (c >>> 1);
|
|
||||||
} else {
|
|
||||||
c >>>= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
table[i] = c >>> 0;
|
|
||||||
}
|
|
||||||
return table;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ParsedKey {
|
|
||||||
keyCode: number;
|
|
||||||
shift: boolean;
|
|
||||||
ctrl: boolean;
|
|
||||||
alt: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function decodeModifiers(modifierField: number): { shift: boolean; ctrl: boolean; alt: boolean } {
|
|
||||||
const modifiers = modifierField - 1;
|
|
||||||
return {
|
|
||||||
shift: (modifiers & 1) !== 0,
|
|
||||||
alt: (modifiers & 2) !== 0,
|
|
||||||
ctrl: (modifiers & 4) !== 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseKeyWithModifiers(data: string): ParsedKey | null {
|
|
||||||
if (data.startsWith("\x1b[") && data.endsWith("u")) {
|
|
||||||
const body = data.slice(2, -1);
|
|
||||||
const [keyPart, modifierPart] = body.split(";");
|
|
||||||
if (keyPart) {
|
|
||||||
const codepoint = parseInt(keyPart.split(":")[0], 10);
|
|
||||||
if (!Number.isNaN(codepoint)) {
|
|
||||||
const modifierField = modifierPart ? parseInt(modifierPart.split(":")[0], 10) : 1;
|
|
||||||
const { shift, alt, ctrl } = decodeModifiers(Number.isNaN(modifierField) ? 1 : modifierField);
|
|
||||||
const keyCode = codepointToJsDosKey(codepoint);
|
|
||||||
if (keyCode !== null) {
|
|
||||||
return { keyCode, shift, ctrl, alt };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const csiMatch = data.match(/^\x1b\[(\d+);(\d+)(?::\d+)?([~A-Za-z])$/);
|
|
||||||
if (csiMatch) {
|
|
||||||
const code = parseInt(csiMatch[1], 10);
|
|
||||||
const modifierField = parseInt(csiMatch[2], 10);
|
|
||||||
const suffix = csiMatch[3];
|
|
||||||
const { shift, alt, ctrl } = decodeModifiers(modifierField);
|
|
||||||
const keyCode = mapCsiKeyToJsDos(code, suffix);
|
|
||||||
if (keyCode === null) return null;
|
|
||||||
return { keyCode, shift, ctrl, alt };
|
|
||||||
}
|
|
||||||
|
|
||||||
const keyCode = mapKeyToJsDos(data);
|
|
||||||
if (keyCode === null) return null;
|
|
||||||
const shift = data.length === 1 && data >= "A" && data <= "Z";
|
|
||||||
return { keyCode, shift, ctrl: false, alt: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
function codepointToJsDosKey(codepoint: number): number | null {
|
|
||||||
if (codepoint === 13) return KBD.enter;
|
|
||||||
if (codepoint === 9) return KBD.tab;
|
|
||||||
if (codepoint === 27) return KBD.esc;
|
|
||||||
if (codepoint === 8 || codepoint === 127) return KBD.backspace;
|
|
||||||
if (codepoint === 32) return KBD.space;
|
|
||||||
if (codepoint >= 97 && codepoint <= 122) return codepoint - 32;
|
|
||||||
if (codepoint >= 65 && codepoint <= 90) return codepoint;
|
|
||||||
if (codepoint >= 48 && codepoint <= 57) return codepoint;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapCsiKeyToJsDos(code: number, suffix: string): number | null {
|
|
||||||
switch (suffix) {
|
|
||||||
case "A":
|
|
||||||
return KBD.up;
|
|
||||||
case "B":
|
|
||||||
return KBD.down;
|
|
||||||
case "C":
|
|
||||||
return KBD.right;
|
|
||||||
case "D":
|
|
||||||
return KBD.left;
|
|
||||||
case "H":
|
|
||||||
return KBD.home;
|
|
||||||
case "F":
|
|
||||||
return KBD.end;
|
|
||||||
case "P":
|
|
||||||
return KBD.f1;
|
|
||||||
case "Q":
|
|
||||||
return KBD.f2;
|
|
||||||
case "R":
|
|
||||||
return KBD.f3;
|
|
||||||
case "S":
|
|
||||||
return KBD.f4;
|
|
||||||
case "Z":
|
|
||||||
return KBD.tab;
|
|
||||||
case "~":
|
|
||||||
switch (code) {
|
|
||||||
case 1:
|
|
||||||
case 7:
|
|
||||||
return KBD.home;
|
|
||||||
case 2:
|
|
||||||
return KBD.insert;
|
|
||||||
case 3:
|
|
||||||
return KBD.delete;
|
|
||||||
case 4:
|
|
||||||
case 8:
|
|
||||||
return KBD.end;
|
|
||||||
case 5:
|
|
||||||
return KBD.pageup;
|
|
||||||
case 6:
|
|
||||||
return KBD.pagedown;
|
|
||||||
case 15:
|
|
||||||
return KBD.f5;
|
|
||||||
case 17:
|
|
||||||
return KBD.f6;
|
|
||||||
case 18:
|
|
||||||
return KBD.f7;
|
|
||||||
case 19:
|
|
||||||
return KBD.f8;
|
|
||||||
case 20:
|
|
||||||
return KBD.f9;
|
|
||||||
case 21:
|
|
||||||
return KBD.f10;
|
|
||||||
case 23:
|
|
||||||
return KBD.f11;
|
|
||||||
case 24:
|
|
||||||
return KBD.f12;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapKeyToJsDos(data: string): number | null {
|
|
||||||
if (matchesKey(data, Key.enter)) return KBD.enter;
|
|
||||||
if (matchesKey(data, Key.backspace)) return KBD.backspace;
|
|
||||||
if (matchesKey(data, Key.tab)) return KBD.tab;
|
|
||||||
if (matchesKey(data, Key.escape)) return KBD.esc;
|
|
||||||
if (matchesKey(data, Key.space)) return KBD.space;
|
|
||||||
if (matchesKey(data, Key.up)) return KBD.up;
|
|
||||||
if (matchesKey(data, Key.down)) return KBD.down;
|
|
||||||
if (matchesKey(data, Key.left)) return KBD.left;
|
|
||||||
if (matchesKey(data, Key.right)) return KBD.right;
|
|
||||||
if (matchesKey(data, Key.pageUp)) return KBD.pageup;
|
|
||||||
if (matchesKey(data, Key.pageDown)) return KBD.pagedown;
|
|
||||||
if (matchesKey(data, Key.home)) return KBD.home;
|
|
||||||
if (matchesKey(data, Key.end)) return KBD.end;
|
|
||||||
if (matchesKey(data, Key.insert)) return KBD.insert;
|
|
||||||
if (matchesKey(data, Key.delete)) return KBD.delete;
|
|
||||||
if (matchesKey(data, Key.f1)) return KBD.f1;
|
|
||||||
if (matchesKey(data, Key.f2)) return KBD.f2;
|
|
||||||
if (matchesKey(data, Key.f3)) return KBD.f3;
|
|
||||||
if (matchesKey(data, Key.f4)) return KBD.f4;
|
|
||||||
if (matchesKey(data, Key.f5)) return KBD.f5;
|
|
||||||
if (matchesKey(data, Key.f6)) return KBD.f6;
|
|
||||||
if (matchesKey(data, Key.f7)) return KBD.f7;
|
|
||||||
if (matchesKey(data, Key.f8)) return KBD.f8;
|
|
||||||
if (matchesKey(data, Key.f9)) return KBD.f9;
|
|
||||||
if (matchesKey(data, Key.f10)) return KBD.f10;
|
|
||||||
if (matchesKey(data, Key.f11)) return KBD.f11;
|
|
||||||
if (matchesKey(data, Key.f12)) return KBD.f12;
|
|
||||||
|
|
||||||
if (data.length === 1) {
|
|
||||||
const code = data.charCodeAt(0);
|
|
||||||
if (data >= "a" && data <= "z") return code - 32;
|
|
||||||
if (data >= "A" && data <= "Z") return code;
|
|
||||||
if (data >= "0" && data <= "9") return code;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
@ -1,438 +0,0 @@
|
||||||
/**
|
|
||||||
* Persistent DOSBox Instance Manager
|
|
||||||
*
|
|
||||||
* Manages a singleton DOSBox instance that runs in the background.
|
|
||||||
* Provides API for sending keys, reading screen, and taking screenshots.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { createRequire } from "node:module";
|
|
||||||
import { dirname, join } from "node:path";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
import { deflateSync } from "node:zlib";
|
|
||||||
import type { CommandInterface, Emulators } from "emulators";
|
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
||||||
|
|
||||||
let emulatorsInstance: Emulators | undefined;
|
|
||||||
|
|
||||||
async function getEmulators(): Promise<Emulators> {
|
|
||||||
if (!emulatorsInstance) {
|
|
||||||
const require = createRequire(import.meta.url);
|
|
||||||
const distPath = dirname(require.resolve("emulators"));
|
|
||||||
await import("emulators");
|
|
||||||
const g = globalThis as unknown as { emulators: Emulators };
|
|
||||||
const emu = g.emulators;
|
|
||||||
emu.pathPrefix = `${distPath}/`;
|
|
||||||
emu.pathSuffix = "";
|
|
||||||
emulatorsInstance = emu;
|
|
||||||
}
|
|
||||||
return emulatorsInstance;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DosboxState {
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
lastFrame: Uint8Array | null;
|
|
||||||
isGraphicsMode: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EmscriptenModule {
|
|
||||||
FS: EmscriptenFS;
|
|
||||||
_rescanFilesystem?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DosboxInstance {
|
|
||||||
private static instance: DosboxInstance | null = null;
|
|
||||||
|
|
||||||
private ci: CommandInterface | null = null;
|
|
||||||
private state: DosboxState = {
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
lastFrame: null,
|
|
||||||
isGraphicsMode: false,
|
|
||||||
};
|
|
||||||
private frameListeners: Set<(rgba: Uint8Array, width: number, height: number) => void> = new Set();
|
|
||||||
private initPromise: Promise<void> | null = null;
|
|
||||||
private disposed = false;
|
|
||||||
|
|
||||||
private constructor() {}
|
|
||||||
|
|
||||||
static async getInstance(): Promise<DosboxInstance> {
|
|
||||||
if (!DosboxInstance.instance) {
|
|
||||||
DosboxInstance.instance = new DosboxInstance();
|
|
||||||
await DosboxInstance.instance.init();
|
|
||||||
}
|
|
||||||
return DosboxInstance.instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
static getInstanceSync(): DosboxInstance | null {
|
|
||||||
return DosboxInstance.instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async destroyInstance(): Promise<void> {
|
|
||||||
if (DosboxInstance.instance) {
|
|
||||||
await DosboxInstance.instance.dispose();
|
|
||||||
DosboxInstance.instance = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async init(): Promise<void> {
|
|
||||||
if (this.initPromise) return this.initPromise;
|
|
||||||
|
|
||||||
this.initPromise = (async () => {
|
|
||||||
const emu = await getEmulators();
|
|
||||||
const bundle = await this.createBundle(emu);
|
|
||||||
this.ci = await emu.dosboxDirect(bundle);
|
|
||||||
|
|
||||||
// Mount QBasic files after DOSBox starts
|
|
||||||
await this.mountQBasic();
|
|
||||||
|
|
||||||
const events = this.ci.events();
|
|
||||||
|
|
||||||
events.onFrameSize((width: number, height: number) => {
|
|
||||||
this.state.width = width;
|
|
||||||
this.state.height = height;
|
|
||||||
});
|
|
||||||
|
|
||||||
events.onFrame((rgb: Uint8Array | null, rgba: Uint8Array | null) => {
|
|
||||||
if (!this.state.width || !this.state.height) {
|
|
||||||
if (this.ci) {
|
|
||||||
this.state.width = this.ci.width();
|
|
||||||
this.state.height = this.ci.height();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const rgbaFrame = rgba ?? (rgb ? this.expandRgbToRgba(rgb) : null);
|
|
||||||
if (rgbaFrame) {
|
|
||||||
this.state.lastFrame = rgbaFrame;
|
|
||||||
// Detect graphics mode by checking if we're in standard text resolution
|
|
||||||
// Text mode is typically 640x400 or 720x400
|
|
||||||
this.state.isGraphicsMode = this.state.width !== 640 && this.state.width !== 720;
|
|
||||||
|
|
||||||
for (const listener of this.frameListeners) {
|
|
||||||
listener(rgbaFrame, this.state.width, this.state.height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
events.onExit(() => {
|
|
||||||
this.disposed = true;
|
|
||||||
DosboxInstance.instance = null;
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
|
|
||||||
return this.initPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createBundle(emu: Emulators): Promise<Uint8Array> {
|
|
||||||
const bundle = await emu.bundle();
|
|
||||||
// Simple autoexec - we mount files to /home/web_user which maps to C:
|
|
||||||
bundle.autoexec(
|
|
||||||
"@echo off",
|
|
||||||
"c:",
|
|
||||||
"cls",
|
|
||||||
"echo QuickBASIC 4.5 is at C:\\QB",
|
|
||||||
"echo Type: CD QB",
|
|
||||||
"echo Then: QB.EXE",
|
|
||||||
"echo.",
|
|
||||||
"dir",
|
|
||||||
);
|
|
||||||
return bundle.toUint8Array(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async mountQBasic(): Promise<void> {
|
|
||||||
if (!this.ci) return;
|
|
||||||
|
|
||||||
// Access Emscripten module
|
|
||||||
const transport = (this.ci as unknown as { transport: { module: EmscriptenModule } }).transport;
|
|
||||||
const Module = transport.module;
|
|
||||||
const FS = Module.FS;
|
|
||||||
|
|
||||||
// jsdos mounts C: to /home/web_user by default
|
|
||||||
// Let's verify and find the correct path
|
|
||||||
const mountPath = "/home/web_user";
|
|
||||||
|
|
||||||
// Create QB directory
|
|
||||||
const qbPath = `${mountPath}/QB`;
|
|
||||||
try {
|
|
||||||
FS.mkdir(qbPath);
|
|
||||||
} catch {
|
|
||||||
/* exists */
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read QBasic files from the extension directory
|
|
||||||
const qbasicDir = join(__dirname, "..", "qbasic");
|
|
||||||
const { readdirSync, readFileSync } = await import("node:fs");
|
|
||||||
|
|
||||||
const files = readdirSync(qbasicDir);
|
|
||||||
for (const file of files) {
|
|
||||||
if (file.startsWith(".")) continue;
|
|
||||||
try {
|
|
||||||
const data = readFileSync(join(qbasicDir, file));
|
|
||||||
FS.writeFile(`${qbPath}/${file.toUpperCase()}`, data);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`Failed to mount ${file}:`, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rescan so DOS sees the new files
|
|
||||||
if (Module._rescanFilesystem) {
|
|
||||||
Module._rescanFilesystem();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private expandRgbToRgba(rgb: Uint8Array): Uint8Array {
|
|
||||||
const rgba = new Uint8Array((rgb.length / 3) * 4);
|
|
||||||
for (let i = 0, j = 0; i < rgb.length; i += 3, j += 4) {
|
|
||||||
rgba[j] = rgb[i] ?? 0;
|
|
||||||
rgba[j + 1] = rgb[i + 1] ?? 0;
|
|
||||||
rgba[j + 2] = rgb[i + 2] ?? 0;
|
|
||||||
rgba[j + 3] = 255;
|
|
||||||
}
|
|
||||||
return rgba;
|
|
||||||
}
|
|
||||||
|
|
||||||
isReady(): boolean {
|
|
||||||
return this.ci !== null && !this.disposed;
|
|
||||||
}
|
|
||||||
|
|
||||||
getState(): DosboxState {
|
|
||||||
return { ...this.state };
|
|
||||||
}
|
|
||||||
|
|
||||||
getCommandInterface(): CommandInterface | null {
|
|
||||||
return this.ci;
|
|
||||||
}
|
|
||||||
|
|
||||||
addFrameListener(listener: (rgba: Uint8Array, width: number, height: number) => void): void {
|
|
||||||
this.frameListeners.add(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeFrameListener(listener: (rgba: Uint8Array, width: number, height: number) => void): void {
|
|
||||||
this.frameListeners.delete(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send key events to DOSBox
|
|
||||||
*/
|
|
||||||
sendKeys(keys: string): void {
|
|
||||||
if (!this.ci) return;
|
|
||||||
|
|
||||||
for (const key of keys) {
|
|
||||||
const keyCode = this.charToKeyCode(key);
|
|
||||||
if (keyCode !== null) {
|
|
||||||
const needsShift = this.needsShift(key);
|
|
||||||
if (needsShift) {
|
|
||||||
this.ci.sendKeyEvent(KBD.leftshift, true);
|
|
||||||
}
|
|
||||||
this.ci.sendKeyEvent(keyCode, true);
|
|
||||||
this.ci.sendKeyEvent(keyCode, false);
|
|
||||||
if (needsShift) {
|
|
||||||
this.ci.sendKeyEvent(KBD.leftshift, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a special key (enter, backspace, etc.)
|
|
||||||
*/
|
|
||||||
sendSpecialKey(key: "enter" | "backspace" | "tab" | "escape" | "up" | "down" | "left" | "right" | "f5"): void {
|
|
||||||
if (!this.ci) return;
|
|
||||||
|
|
||||||
const keyCode = KBD[key];
|
|
||||||
if (keyCode) {
|
|
||||||
this.ci.sendKeyEvent(keyCode, true);
|
|
||||||
this.ci.sendKeyEvent(keyCode, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read text-mode screen content
|
|
||||||
*/
|
|
||||||
readScreenText(): string | null {
|
|
||||||
if (!this.ci) return null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Try to get screen text from emulators API
|
|
||||||
const text = (this.ci as unknown as { screenText?: () => string }).screenText?.();
|
|
||||||
return text ?? null;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get screenshot as PNG base64
|
|
||||||
*/
|
|
||||||
getScreenshot(): { base64: string; width: number; height: number } | null {
|
|
||||||
if (!this.state.lastFrame || !this.state.width || !this.state.height) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const png = this.encodePng(this.state.width, this.state.height, this.state.lastFrame);
|
|
||||||
return {
|
|
||||||
base64: png.toString("base64"),
|
|
||||||
width: this.state.width,
|
|
||||||
height: this.state.height,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private encodePng(width: number, height: number, rgba: Uint8Array): Buffer {
|
|
||||||
const stride = width * 4;
|
|
||||||
const raw = Buffer.alloc((stride + 1) * height);
|
|
||||||
for (let y = 0; y < height; y++) {
|
|
||||||
const rowOffset = y * (stride + 1);
|
|
||||||
raw[rowOffset] = 0;
|
|
||||||
raw.set(rgba.subarray(y * stride, y * stride + stride), rowOffset + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const compressed = deflateSync(raw);
|
|
||||||
|
|
||||||
const header = Buffer.alloc(13);
|
|
||||||
header.writeUInt32BE(width, 0);
|
|
||||||
header.writeUInt32BE(height, 4);
|
|
||||||
header[8] = 8;
|
|
||||||
header[9] = 6;
|
|
||||||
header[10] = 0;
|
|
||||||
header[11] = 0;
|
|
||||||
header[12] = 0;
|
|
||||||
|
|
||||||
const signature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
||||||
const ihdr = this.createChunk("IHDR", header);
|
|
||||||
const idat = this.createChunk("IDAT", compressed);
|
|
||||||
const iend = this.createChunk("IEND", Buffer.alloc(0));
|
|
||||||
|
|
||||||
return Buffer.concat([signature, ihdr, idat, iend]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private createChunk(type: string, data: Buffer): Buffer {
|
|
||||||
const length = Buffer.alloc(4);
|
|
||||||
length.writeUInt32BE(data.length, 0);
|
|
||||||
const typeBuffer = Buffer.from(type, "ascii");
|
|
||||||
const crcBuffer = Buffer.concat([typeBuffer, data]);
|
|
||||||
const crc = this.crc32(crcBuffer);
|
|
||||||
const crcOut = Buffer.alloc(4);
|
|
||||||
crcOut.writeUInt32BE(crc, 0);
|
|
||||||
return Buffer.concat([length, typeBuffer, data, crcOut]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private crc32(buffer: Buffer): number {
|
|
||||||
let crc = 0xffffffff;
|
|
||||||
for (const byte of buffer) {
|
|
||||||
crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
|
|
||||||
}
|
|
||||||
return (crc ^ 0xffffffff) >>> 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private charToKeyCode(char: string): number | null {
|
|
||||||
const lower = char.toLowerCase();
|
|
||||||
if (lower >= "a" && lower <= "z") {
|
|
||||||
return lower.charCodeAt(0) - 32; // A-Z = 65-90
|
|
||||||
}
|
|
||||||
if (char >= "0" && char <= "9") {
|
|
||||||
return char.charCodeAt(0); // 0-9 = 48-57
|
|
||||||
}
|
|
||||||
if (char === " ") return KBD.space;
|
|
||||||
if (char === "\n" || char === "\r") return KBD.enter;
|
|
||||||
if (char === "\t") return KBD.tab;
|
|
||||||
// Common punctuation
|
|
||||||
const punct: Record<string, number> = {
|
|
||||||
".": 46,
|
|
||||||
",": 44,
|
|
||||||
";": 59,
|
|
||||||
":": 59, // shift
|
|
||||||
"'": 39,
|
|
||||||
'"': 39, // shift
|
|
||||||
"-": 45,
|
|
||||||
_: 45, // shift
|
|
||||||
"=": 61,
|
|
||||||
"+": 61, // shift
|
|
||||||
"[": 91,
|
|
||||||
"]": 93,
|
|
||||||
"\\": 92,
|
|
||||||
"/": 47,
|
|
||||||
"!": 49, // shift+1
|
|
||||||
"@": 50, // shift+2
|
|
||||||
"#": 51, // shift+3
|
|
||||||
$: 52, // shift+4
|
|
||||||
"%": 53, // shift+5
|
|
||||||
"^": 54, // shift+6
|
|
||||||
"&": 55, // shift+7
|
|
||||||
"*": 56, // shift+8
|
|
||||||
"(": 57, // shift+9
|
|
||||||
")": 48, // shift+0
|
|
||||||
};
|
|
||||||
return punct[char] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private needsShift(char: string): boolean {
|
|
||||||
if (char >= "A" && char <= "Z") return true;
|
|
||||||
return '~!@#$%^&*()_+{}|:"<>?'.includes(char);
|
|
||||||
}
|
|
||||||
|
|
||||||
async dispose(): Promise<void> {
|
|
||||||
if (this.disposed) return;
|
|
||||||
this.disposed = true;
|
|
||||||
this.frameListeners.clear();
|
|
||||||
|
|
||||||
if (this.ci) {
|
|
||||||
const origLog = console.log;
|
|
||||||
const origError = console.error;
|
|
||||||
console.log = () => {};
|
|
||||||
console.error = () => {};
|
|
||||||
try {
|
|
||||||
await this.ci.exit();
|
|
||||||
} catch {
|
|
||||||
/* ignore */
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log = origLog;
|
|
||||||
console.error = origError;
|
|
||||||
}, 100);
|
|
||||||
this.ci = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// js-dos key codes
|
|
||||||
const KBD: Record<string, number> = {
|
|
||||||
enter: 257,
|
|
||||||
backspace: 259,
|
|
||||||
tab: 258,
|
|
||||||
escape: 256,
|
|
||||||
space: 32,
|
|
||||||
leftshift: 340,
|
|
||||||
up: 265,
|
|
||||||
down: 264,
|
|
||||||
left: 263,
|
|
||||||
right: 262,
|
|
||||||
f5: 294,
|
|
||||||
};
|
|
||||||
|
|
||||||
const CRC_TABLE = createCrcTable();
|
|
||||||
|
|
||||||
function createCrcTable(): Uint32Array {
|
|
||||||
const table = new Uint32Array(256);
|
|
||||||
for (let i = 0; i < 256; i++) {
|
|
||||||
let c = i;
|
|
||||||
for (let j = 0; j < 8; j++) {
|
|
||||||
if (c & 1) {
|
|
||||||
c = 0xedb88320 ^ (c >>> 1);
|
|
||||||
} else {
|
|
||||||
c >>>= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
table[i] = c >>> 0;
|
|
||||||
}
|
|
||||||
return table;
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue