Joystick, Mouse and Keyboard
QBASIC offers two functions to access gamepad data. According to the online help, STRIG() and STICK() will return information for two joysticks based on the passed argument. STRIG() returns the button currently pressed (2 per controller) while STICK() returns the value of the X and Y axis of the analog stick (1 per controller.) As a side note, STICK(0) must be called before using any other STICK().
Check out the following code to see these functions in action. Make sure the gamepad is plugged in before launching DOSBox (or turning on your computer,) because they are not plug-and-play as far as I can tell. If no controller is found, installed or connected, the X and Y axis should return 0 (they would return a value close to 128 otherwise.)
The "button status change" functions are only really useful if you plan to use STRIG() as an event-trapping statement. In this mode, a process running in the background picks up on gamepad presses and jumps to a label. This method requires the use of GOSUB and seem to cause some delay. I would recommend you staying away from this method like the haunted house on the hill your friends keep saying it's going to be fun to visit, but deep down you know some kind of poltergist will get you one by one and you, lone survivor of the blood-thirsty demonic ghoul, will end up with halloween-induced PTSD for the rest of your life. Regardless, here's an example for your edification. Now that you know how to do it, don't.
Also, maybe I'm misunderstanding the documentation, but in practice (at least under DOSBox,) when only one controller is plugged in, these functions "extend" the number of analog sticks and buttons available for that one controller. In other words, you have two analog sticks and four buttons "slots" available and they are shared amongst multiple controllers. Here's how the values should be interpreted for an XBox 360 controller:
Using DOS interrupts
A while back, Ralf Brown dedicated a shitload of his time writing an extensive list of DOS interrupts, so why not use that instead? In his document, we can see interrupt &h15 is used to get joystick information. To do so, we set the AX register to &h8000 and select a function with register DX. There are two functions we really want to look at: 0 (which returns the X and Y axis of the analog stick) and 1 (which returns the button status in an easy-to-use 16-bit integer) as shown in this code (requires QuickBASIC 4.5 or a "backported" CALL INTERRUPTX instruction.) Note that if you're using DOS interrupts, the value returned for buttons is inverted (-1 if the button is released, 0 if it is pressed - hence the XORing in the example snippet.)
Deadzone and joystick arthrosis
In theory, when the analog stick of your joystick is at rest the value returned for the X and Y axis should be 128. In practice that is rarely the case. To better understand what's going on, we should remove 128 out of the X and Y values; a properly functionning gamepad will return values in range -127 to 126. When the value is negative, the stick is pushed left and up. When it is positive, the stick is pushed right and down. Since we're not touching the stick, the values should be 0,0.
However, analog sticks tend to deteriorate after a while and they can no longer access the whole range of values they should, or tend no to get back to their central position. If you're not taking these parameters into account when coding your program, you may end up with unintended movements. This program will show you how far away the stick is lingering from the center, give you an idea of how much range it can cover, and what would happen if you tried to control a character using unfiltered values (something such as "IF x < 0 THEN moveLeft" would force the character to move left on its own.)
If you can find the time to write some kind of program to calibrate the gamepad, you'd probably end up with something like this, which forces X,Y coordinates to be in range -127 to 126 and makes sure values within the deadzone are ignored. The filtering function also provides normalized X,Y coordinates which are extremely helpful for precise control.
Essentially, you need to gauge how large the deadzone is (user input,) convert X,Y coordinates into a vector, substract the deadzone length from the movement vector, rescale the vector to have the maximum length the thumbstick can reach.
Via DOS Interrupts
I like to start these articles by providing an exemple of QBASIC's native instructions at work but in this case there's none. QBASIC has built-in instructions for digital pens and joysticks, but not for your run-of-the-mill pointing device. Go figure.
QBASIC 1.1 programmers usually relied on assembly code to enable mouse support (note: I have no idea who wrote this specific code, many copied and pasted it, often claiming it was theirs despite older programs showing the exact same listing.) Internally, the assembly code (launched with CALL ABSOLUTE) feeds ax, bx, cx and dx registers to DOS Interrupt &h33. Alternatively, the code can be rewritten to use CALL INTERRUPTX instead to the exact same result. Ralf Brown documented the various effects the ax value has in his interrupt list (INT 33,) here are the most common:
Handler direct call
Then, there's another method, which is... peculiar. I've only encountered it once in some demo code. Instead of relying on DOS Interrupt 0x33, it searches for the sub program (handler) invoked by Interrupt 0x33 and runs it via CALL ABSOLUTE. That sounds like a lot of fun, so let's look into it! Interrupts are wrappers linking services to handlers. Each handler has its address (segment and offset) stored in a table (each entry is thus 4 bytes long.) So, if we know where this table is located in memory (spoiler: it starts at 0000:0000,) we can obtain the address to any handler (called "interrupt vector") with the standard PEEK function.
First, PEEK two bytes at 0000:00CE (&h33 * 4 + 2) and 0000:00CC (&h33 * 4) to obtain the segment and offset to the mouse handler for Interrupt &h33. Then, use CALL ABSOLUTE to pass the AX, BX, CX and DX registers and execute it. Alternatively, the interrupt vector could be obtained by invoking DOS Interrupt 0x21 with ah = &h35 and al = &h33, which will return the segment and offset of the routine in registers ES and BX respectively. Note: ah is the High Byte of AX, while al is the Low Byte of AX (in other words, AX = AH * 256 + AL.)
Needless to say, this is a ballsy approach... but if you're truly hellbent on rejecting assembly wrapper and CALL INTERRUPTX, it works. Now, just don't take it too far.
That one function everyone uses at some point
If you have no other choice, INKEY$ is a semi-reliable solution to keyboard handling. This function reads a character from the keyboard and returns it as a string. The string may be NULL (no length) if no key is pressed, one byte long if the key is a standard (printable) ASCII code, or two bytes long for extended characters (the first byte is always 0, while the second byte is a keyboard scancode.) It's easy to use but clumsy: for starter INKEY$ will not return special keys like Alt, Control and Shift. Second, when a key is held, INKEY$ will fire NULL at seemingly random. Finally, INKEY$ doesn't allow multiple key presses at once which can be a problem for action video games. Thanksfuly, there IS a better way to handle key presses.
Putting keys in line
Before going further, let's talk a bit (just a tiny little bit) about keyboards. First, you should know that there's a circular 32 bytes long buffer stored at 0040:001A, and every key hit is stored there as a word (2 bytes.) Because it is circular, two words (stored at 0040:001A and 0040:001C) are used to keep track of the head and tail respectively. Head points to the next word in the buffer to be read, and Tail points to the word the next key press must be stored. This buffer is used by INKEY$ to obtain its return values.
This buffer is used so that programs can process keys at their pace, should they fall behind. On VERY old computers (you can try that all day in DOSBox but it won't work,) when too many keys are pressed at once, it would freeze and beep as it waits for some free cycles to process the buffer and clear some room for more keys. In order to avoid this freeze, you may either call INKEY$ as often as possible to clear the buffer, or you may empty the buffer yourself by simply POKEing the head pointer to the tail pointer as so.
Second, every time a key is pressed or released, the keyboard will send the key status to port 0x60 along with its scan code (that would be the extended character number returned as a two byte string by INKEY$.) And we can use that.
Reading port 0x60
Like the title states, we can obtain keyboard input via port 0x60 (with INP().) If the byte read there has bit 7 set, the key has been released. If bit 7 is unset, then the key has been pressed. I need to stress out the past tense because port 0x60 will not update until next key status change and doesn't reflect the current status of any key specifically.
So here's the problem: reading port 0x60 in a loop won't be sufficient because it will only catch the last key status change "whenever the program can," while it should be reading port 0x60 "whenever there's a change happening." Quickly releasing multiple keys may rapid-fire changes on port 0x60, some of which may go unnoticed because the program is busy doing something else.
So the question is "how do I know when port 0x60 should be read?" Well, there's a built-in Interrupt Service Register (ISR for short) dedicated to keyboard reads. It's part of register 0x21 and is located at index 9 (Keyboard BIOS Interrupt.) The interrupt vector (basically a routine that reads a scan code and stores it in the keyboard buffer) is automatically called each time port 0x60 changes. So what we need to do is create our own routine, get its memory address, and use it to hook our own code to ISR 9. To keep things sane, we'll also have to get the memory address of the default routine so we can restore it when our program terminates.
This is where things get tricky, because QuickBASIC doesn't have a way to provide the memory address of a routine, which we desperately need. What we could do however, is write the routine in assembly, store it in a buffer, get the memory address of that buffer and hook it to ISR 9. More or less what Milo Sedlacek did with his MultiKey function. His code, divided in 4 parts, contains a header starting at 0x0000 (which would jump to the relevant part of the code and store two pointers,) an installer at 0x0020 (which sets the new interrupt vector,) an uninstaller at 0x0042 (which sets the old interrupt vector,) and the actual interrupt handler at 0x0056.
Ideally, we want to do as much as possible in QuickBASIC; the installer and uninstaller sections can easily be replaced with CALL INTERRUPTX. To set an interrupt vector, we call register 0x21 and use AH = 0x25, AL = ISR number, DS = segment where handler code resides, and DX = offset where handler code resides. To get an interrupt vector (so we can revert back to the default handler after we're done,) we also call register 0x21 but use AH = 0x35 and AL = ISR number (we obtain the segment and offset where the handler resides in the returned ES and BX registers.)
Now, modifying the keyboard handler at that level means that the default handler is no longer available and thus, INKEY$ and INPUT won't work until the original ISR is restored. In fact, not even the DOS prompt will work until the ISR is restored, so don't forget to reset it before your program exits... (or crashes.) Here's the final result.
Some links you might find useful