Hey folks!
Recently I decided to play good old DROD on my Steamdeck to refresh some of my childhood memories.
I came a long way up to level 8 only to realize that I missed a room in the beginning and there is no way back.

I just spent few hours to finish a room with three tar mothers. No undos, no checkpoints, remember? I'm sure you understand me.
Even though I had save at the beginning of the level, I didn't want to replay it again.
So... why not to hack the game and fly over the barrier?
There must be a code that checks if Beethro can go to the tile and I can "
fix"
it.
Also I heard that there are some other bugs that may prevent the game from finishing.
TLDR: Patch byte 0x74 to 0xEB at offset 0x6FD3 to make Beethro fly over walls and other obstacles.
This is 16-bit game for Windows 3.1, sounds like a challenge
So for debugging I used good old Turbo Debugger launched in Windows 3.1 installed inside DOSBox and Ghidra as a disassembler/decompiler.
I also could run the game with wine and debug with gdb but it's too much trouble because 16-bit game is running inside NTVDM. It's better to use native tools.
First, let's find window proc. It should be referenced somewhere before RegisterClass():
LAB_1010_23c6 XREF[1]: 1010:23a6 (j)
1010:23c6 MOV SI ,word ptr [BP + param_3 ]
1010:23c9 MOV word ptr [0x2c4 ],SI
1010:23cd CMP word ptr [BP + param_2 ],0x0
1010:23d1 JNZ LAB_1010_242c
1010:23d3 MOV word ptr [BP + local_2e ],0x3
1010:23d8 MOV word ptr [BP + local_2c ],0x24c0
1010:23dd MOV word ptr [BP + local_2a ],0x1010
1010:23e2 XOR AX ,AX
1010:23e4 MOV word ptr [BP + local_28 ],AX
1010:23e7 MOV word ptr [BP + local_26 ],AX
1010:23ea MOV word ptr [BP + local_24 ],SI
1010:23ed PUSH SI
1010:23ee PUSH DS
1010:23ef PUSH 0x2c8
1010:23f2 CALLF USER::LOADICON undefined LOADICON()
1010:23f7 MOV word ptr [BP + local_22 ],AX
1010:23fa PUSH SI
1010:23fb PUSH DS
1010:23fc PUSH 0xa0c
1010:23ff CALLF USER::LOADCURSOR undefined LOADCURSOR()
1010:2404 MOV word ptr [BP + local_20 ],AX
1010:2407 PUSH 0x1
1010:2409 CALLF GDI::GETSTOCKOBJECT undefined GETSTOCKOBJECT()
1010:240e MOV word ptr [BP + local_1e ],AX
1010:2411 SUB AX ,AX
1010:2413 MOV word ptr [BP + local_1a ],AX
1010:2416 MOV word ptr [BP + local_1c ],AX
1010:2419 MOV AX ,0x2c8
1010:241c MOV word ptr [BP + local_18 ],AX
1010:241f MOV word ptr [BP + local_16 ],DS
1010:2422 LEA AX ,[BP + local_2e ]
1010:2425 PUSH SS
1010:2426 PUSH AX
1010:2427 CALLF USER::REGISTERCLASS undefined REGISTERCLASS()
It's 1010:24C0.
After some analyzis we can find WM_KEYDOWN handler, it's at 1010:2B60:
if (*(int *)&DAT_1020_02c6 != 0) {
return;
}
iVar7 = FUN_1008_4d1a();
if (iVar7 == 2) {
FUN_1010_6c86(param_3);
return;
}
if (iVar7 == 3) {
if (param_3 == (byte *)0x5a) {
FUN_1008_09f4();
}
if (param_3 == (byte *)0x70) {
LAB_1010_2ba4:
FUN_1010_729c();
return;
}
iVar7 = FUN_1010_7404(param_3);
if (iVar7 == 0) {
return;
}
if (iVar7 != 0xd) {
if (iVar7 == 0xe) {
return;
}
if (iVar7 != 0xf) {
iVar18 = FUN_1010_1c60();
if (iVar18 != 0) {
return;
}
FUN_1010_5b54();
FUN_1008_297e(iVar7);
return;
}
iVar7 = FUN_1010_1c60();
if (iVar7 != 0) {
return;
}
FUN_1008_15f6();
return;
}
}
We don't care about DAT_1020_02c6, I don't know what is it, just some check.
Looks like FUN_1008_4d1a returns current game state, 2 is for main menu and 3 is for the game.
Next, the game checks if pressed 'Z' (shows current location) or 'F1' (shows help).
FUN_1010_7404() accepts key and return game action. Here is the list of possible actions:
0 = Empty
1 = Go North
2 = Go North-East
3 = Go West
4 = Empty
5 = Go East
6 = Go South-West
7 = Go South
8 = Go South-East
9 = Swing Clockwise
10 = Swing Counterclockwise
11 = Go North-West
12 = Wait
13 = Cancel, activated on Escape
14 = Unkown, activated on F5
15 = Restart room
After some unimportant checks and call we finally go the main function - FUN_1008_297e.
There is a huge switch in the beginning of the function for every possible action.
Every move action modifies two variables, apparently they define how Beethro moves in X and Y directions, for example:
case 8:
dx = 1;
local_10 = dx;
local_e = dx;
dy = dx;
break;
For South-East, we go in direction [1; 1]. Coordinates start from top-left corner
Next, the game gets tile of the new position. Note that it's calculated differently when Beethro goes to the direction of his sword:
if ((sword_pos % 3 - dx == 1) && (sword_pos / 3 - dy == 1)) {
target_tile = DAT_1020_0268;
if (DAT_1020_0268 == 0xff) {
target_tile = FUN_1008_84d6(DAT_1020_0262,DAT_1020_0264);
}
}
else {
target_tile = FUN_1008_854a(DAT_1020_0254 + dx,dy + DAT_1020_0256);
target_tile = target_tile & 0xff;
}
By the way, sword_pos contains position of Beethro's sword, it's value corresponds to actions form the table above.
Let's say, we a going South-East: sword_pos=8, dx=1, dy=1, 8 % 3 - 1 = 1, 8 / 3 - 1 = 1 - the check works

I didn't understand why this check is required, but anyway...
Checking some tiles, don't care:
if ((((((DAT_1020_025a == 0xdf) || (DAT_1020_025a == 0xd8)) || (DAT_1020_025a == 0xd9)) ||
((DAT_1020_025a == 0xda || (DAT_1020_025a == 0xdb)))) ||
((DAT_1020_025a == 0xdc || ((DAT_1020_025a == 0xdd || (DAT_1020_025a == 0xde)))))) &&
(iVar7 = FUN_1008_5ed8(DAT_1020_025a,dy * 3 + dx + 4), iVar7 != 0)) {
dy = 0;
dx = 0;
bVar1 = true;
}
The most interesting part is right here:
if (!bVar1) {
if ((((target_tile == 0xdf) || (target_tile == 0xd8)) || (target_tile == 0xd9)) ||
(((target_tile == 0xda || (target_tile == 0xdb)) ||
((target_tile == 0xdc || ((target_tile == 0xdd || (target_tile == 0xde)))))))) {
iVar7 = FUN_1008_5ed8(target_tile,dy * 3 + dx + 4);
if (iVar7 == 0) {
local_36 = FUN_1008_84d6(dx + cur_x,dy + cur_y);
iVar7 = FUN_1008_6ea2(local_36);
if (iVar7 == 0) {
cVar2 = (byte)local_36;
goto joined_r0x10083a0b;
}
}
else {
LAB_1008_3ab5:
dy = 0;
dx = 0;
}
}
else if (((dx != 0) || (dy != 0)) && (iVar7 = FUN_1008_6ea2(target_tile), iVar7 == 0)) {
cVar2 = (byte)target_tile;
joined_r0x10083a0b:
if ((((cVar2 != 0x37) && (cVar2 != 0x30)) &&
(((cVar2 != 0x31 && ((cVar2 != 0x32 && (cVar2 != 0x33)))) && (cVar2 != 0x34)))) &&
(((((((cVar2 != 0x35 && (cVar2 != 0x36)) && (cVar2 != 0x17)) &&
((cVar2 != 0x10 && (cVar2 != 0x11)))) && (cVar2 != 0x12)) &&
((cVar2 != 0x13 && (cVar2 != 0x14)))) &&
(((((cVar2 != 0x15 && (((cVar2 != 0x16 && (cVar2 != 0x29)) && (cVar2 != 0xc1)))) &&
(((cVar2 != 0x39 && (cVar2 != 0xc4)) && (cVar2 != 0x78)))) &&
((cVar2 != 0x79 && (cVar2 != 0x88)))) &&
((cVar2 != 0x98 && ((cVar2 != 0xa8 && (cVar2 != 0xc5)))))))))) goto LAB_1008_3ab5;
}
}
Just check this out!
dy = 0;
dx = 0;
This is where the game checks if Beethro can actually go to the new position, exactly what we are looking for!
We just need to change the last check
1008:3ab3 JZ LAB_1008_3abd
to this
1008:3ab3 JMP LAB_1008_3abd
And now Beethro can go fly over the walls
Surprisingly, it doesn't break anything in the game.
It's not very clear patch, I didn't alter the check before, so it might not work for all cases.
But it was enough for me to go back and finish the room.
What can be done next:
0) Fly mode toggled by key press. Currently I have to switch executables.
1) Instant win room
2) Make level finished
3) Invisiblity
4) God mode
5) Something else?
Small bonus, I'm sure you know where this is:
Thanks for reading!
See you later, I have 17 more levels to beat.