There are by my estimation THREE different ways of reading keyboard input in DOS, using C.
The first is getch().
From what I can tell, most compilers interpret this function as the DOS BIOS' 0h INT16 call (which is get key from buffer). The problem with this is the usual - you must deal with the operating system's manipulation of the keyboard buffer to give you keys read; which is simply one interpretation of the keyboard's state. You can modify the terminal / TTY settings of stdin/stdout, but at the end it is not appropriate for games. getch() does not return any keyup events, as these are handled on the OS layer, so you will never get a value from getch() with bit 7 set (presumably).
The second is reading the keyboard's I/O ports directly:
The 8255 PPI is legacy-emulated on pretty much every keyboard nowadays, by the way! USB converts its own signal to PS/2 which is by definition compatible with IBM/PC.
Anyway, this assembly works great! And it's super fast!
Unfortunately, as you can see by the first line comment, it is possible (and happens frequently) that if you are not constantly monitoring the keyboard, you will miss a keyup event. This is fine for probably 80% of purposes, but for action games in particular, a stuck key means death.
I tried a large variety of things to get around this limitation, but unfortunately, the only thing that works is clearing out every other "pressed" key state you have in memory when a new key is pressed. The code may be simple, but no two keys can be pressed at a time! Argh!!
The third method is using the DOS INT16 0h and 1h calls to check for and retrieve keys from the system's keyboard buffer. This isn't very difficult to implement, so I won't waste time on it here, but the effect is nearly identical to default getch() and stdin. You don't get super fast access to the PPI and you can still miss a keyup event.
After a bunch of hemming and hawing, it was time to do more research.
So, operating systems don't "scan" a keyboard the way older micro-computers did. On virtually every 8-bit system I've programmed for, you can get the state of any key on the keyboard at any time - with exceptions, depending on how things are wired.
What is done nowadays is the keyboard is hooked into a hardware interrupt. And unfortunately, I seemed to be in a situation where that was the only option of getting rid of keyboard input bugs.
So I did my research, but wouldn't you know it, that kind of thing doesn't have a lot of information. There are some StackOverflow posts here and there, but nothing particularly clear and definitive. There wasn't even a clear list of where the interrupt vector address was!
After some frustration and tweaking of my search keywords, I discovered this wonderful post, which had some information I couldn't find elsewhere:
1. The DOS keyboard interrupt vector is 09h
2. Watcom C/++ has a getvect() and setvect() macro, which took me to being able to find out that the vector table for MS-DOS is 255 entries of two words, one containing the segment address and one the instruction pointer (code offset). This means 0024h holds the vector address!
Well, hold on. Trying to write the address of my replacer function to 0024h directly isn't working, even with interrupts off. So, I did what every sane person does: Disassembled a Watcom produced .exe that uses the setvect function to see what it does!
FUN_1000_0008:1000:0082(c)
1000:0174 52 PUSH DX
1000:0175 89 da MOV DX,BX
1000:0177 1e PUSH DS
1000:0178 8e d9 MOV DS,CX
1000:017a b4 25 MOV AH,0x25
1000:017c cd 21 INT 0x21
1000:017e 1f POP DS
1000:017f 5a POP DX
1000:0180 c3 RET
Oh - they don't do anything at all. They use the DOS service routine 25h to do it for them! No wonder.
So, copying this method, we can PROPERLY replace their IRQ service routine with our own, based on the post above, which will read every key's scancode into a 0 or 1 buffer.
Marking it as __interrupt will (we hope!) cause it to save all registers properly. We only need it to flip a bit in the char keys array, so I simplified it down a bit and removed almost all of the compiler-dependant code and wrote ASM macros for Watcom.
In the end, the final, interrupt-based, flawless method looks like this:
Three full days of work! Phew! (And I didn't even do the hard part myself!)
Astute viewers may have noticed the static buffer char inside the static kb_init() function. This is explicitly to save the previous interrupt's E0, E1 or E2 character in the case of an EXTENDED character code. All we need to do is check this buffer char *first* to see if we are an extended key scancode or not. If the buffer HAS one of those Ex keys, we clear for the next key.
This looks longer than it should be because of the assembly macros, but it is quite simple if you take the time to look at it.
Done! This will get the immediate state of every key on the keyboard, through a properly handled interrupt - perfect for game development!
If this helped you, or you have anything in reply, please leave a comment!