I made a DOS "TSR" for the game "Quest for Glory 1", to let me have unlimited "Points" when creating my character - and learnt a whole lot in the process :D
Why and How:
I had never written a "TSR" ("terminate-and-stay-resident") program. I remember hearing about them in the computer lab at school during lunch breaks, where older students would compare notes and show off their DOS TSRs... but I never learnt about DOS interrupts, or much of the Turbo Pascal programming language... somehow I went from BASIC and DOS 6 into Windows 95 and onto Visual Studio and Delphi (with a brief stop in Windows 3.11 on a Hercules 286).
There was a TSR I knew of, and used often, to cheat in games - called Game Wizard 32. For years I felt like I'd missed a "rite of passage" or "geek badge" and longed to one day write a game-cheating TSR of my own. That day/year finally came - and my target? Quest for Glory 1's character creation screen.
With some leave from work I did some googling, dreading having to learn about obscure + old "DOS interrupts" and fighting to get old compilers to work in a modern age, and came across an incredibly useful video by "LateBit" titled "How to write a TSR in DOS" which is a fantastic crash-course into the process and some core information needed (eg: using DosBox to run a game and inspect/dump its memory, and how interrupts work).
It turns out, as I understand them anyway, DOS Interrupts are pointers in specific memory locations which are called as functions to do stuff, and these memory locations can be written to to point to your code instead... effectively overwriting or "hooking"(?) the interrupts for your own purposes. I used the modern "CheatEngine" tool, attached to the DOSBox process, to scan for the DOS game's memory and find the values I wanted to change and and explorer the memory regions before using the DOSBox debugger to dump memory regions and compare them (as shown in LateBit's video) to find the addresses as DOS sees them (as opposed to what CheatEngine was reporting which was the DOSBox, windows application, memory space's offsets).
Interrupts trigger for rather specific things - such as when a key is pressed (interrupt 9), or on a timer (interrupt 8). LateBit's video shows a game overwriting an interrupt he wanted to use (keypress), so moves on to another game which doesn't. Because you'd have to run your TSR first, then the app or game, which would effectively block your TSR from triggering - rendering it useless :/ I figured if you set up your TSR on the timer interrupt it wouldn't matter if the "key press" interrupt was overwritten as you could detect this and re-write your code's address to it (remembering to call the game function after yours).
Problem 1 - interrupts being overwritten:
Comparing the DOSBox memory dump at 0000:0000
before and after running "Quest for Glory 1" it was clear the game overwrote both the keypress and timer interrupts I'd hoped to use :/
After a bit more reading I noticed 1Ch = Timer tick handler - called by INT 08
- not a timer interrupt itself, but called by the timer interrupt, so almost as good :D This suggested I might just be able to pull off my Quest for Glory 1 cheat TSR after all. I wouldn't have it trigger on keypress, but just keep firing in the background deciding what to do and when... time to get to coding.
Problem 2 - learning to write TSRs in Pascal:
I didn't actually "known" Turbo Pascal, there's surprisingly little information (I could find) online about making TSRs in Turbo Pascal, and while ChatGPT was convinced it could help me the code it wrote did not compile and often didn't make much sense. I came across the "SWAG TSR Utilities and Routines" which was an incredibly useful collection of Turbo Pascal TSR code and managed to make a basic timer-based TSR which just "beeped" every second to prove it was alive and running... and it continued beeping when the game was launched!
Problem 4 - memory segments and 16bits:
Reading (and writing) memory in Turbo Pascal is surprisingly easy... Mem[$segment:$offset]
is all it takes to access it! Armed with the "DOS offsets" of the game's value I wanted to change (eventually) I added an if
condition to my beeper TSR to check for the memory value in the game to check I could actually read it... and it never beeped.
The memory address I was after was 0x252bE - which Turbo Pascal wouldn't let me use (in hex or decimal), as it's too big for a 16bit integer. Turbo Pascal has a LongInt
I could set to this value, but using it with Mem
resulted in it reading or writing the wrong location. I thought the game was in (32bit?) "protected mode" and I wouldn't be able to access its memory, and I also read that I could divide my offset by 64 (bytes? kilobytes? maybe 640 kilobytes?) to get the "segment" to pass to Mem
along with the remainder as the offset, but for some reason I always ended up in memory segment 0.
As it turns out I needed to divide by 16 to get the correct offset, which finally let me read+write where I intended. Except...
Problem 5 - memory addresses changing:
Although obvious in hindsight, it took some memory searching and head scratching to realise that by loading my TSR into memory before running the game it meant the game's memory ended up in a different location and my offset was no longer valid. But every time I changed my TSR's code (and compiled and re-ran it) it would cause the game's memory locations to change and I'd have to find the new offset. The TSR affects the game's memory and I need to know the game's memory to write my TSR.
I figured I could code my TSR with the wrong memory addresses in place and run it like that (at the risk of corrupting some memory and crashing DOS or the game), then find the correct memory address, put those back into my source code and recompile it without its size or memory usage changing, so the game's memory offset would remain the same - which worked!
The Result:
If I'm honest all I really have right now is a simple, dangerous, prototype which blindly spams the value 50
to a fixed memory address, so that when you run this one specific game and create a new character, you have unlimited "Points"... but it works, and is more than I expected to achieve. It looks like this:
{$m $800,0,0 }
program hypntsr;
uses crt, dos;
Const
Seconds = 1;
Counter: Word = 0;
CurrentValue: Byte = 0;
{$f+}
procedure new_int; interrupt;
begin
Inc(Counter);
If Counter>(Trunc(18.2*Seconds)) Then
Begin
CurrentValue := Mem[10020:14];
if CurrentValue = 50 then
begin
sound(CurrentValue);
delay(100);
nosound;
end;
Mem[10020:14] := 55;
Counter := 0;
End;
end;
{$f-}
begin
setintvec($1C, addr(new_int));
writeln('Hypn TSR v0.1 Loaded!');
keep(0)
end.
I intend to add checks that the correct game is running, and on the character creation screen, before writing to memory... and remove that beeping... and move magic hardcoded numbers to variables... but for now I am happy! :D