NagyD/SDLPoP

A glitch from original DOS game doesn't work in SDLPoP

mooskagh opened this issue · 7 comments

Sorry for another obscure issue.

DOS version of PoP has these glitches:
image

There are two glitches in this clip:

  1. Going off-screen with the sword and pressing "left" key in correct pattern. [This works both in SDLPoP and DOS version].
  2. Kid falls off edge seemingly earlier than he should.

The glitch 2 is only reproducible in DOS version but not in SDLPoP.

Speed-runners are seemingly able to reproduce this issue 2 quite reliably (once per 5 minutes of trying or so) in DOS version of PoP, but never could do this in SDLPoP.

I have a tool to generate SDLPoP replay files, but I couldn't reproduce it either (tried various starting coordinates, keypress patterns and random seeds. I also tried to analyze the repro video frame by frame to have exactly the same coordinates of guard and kid [while they are visible] and keypresses).

NagyD commented

Here is my method to reliably replicate these kinds of bugs in DOS PoP: https://forum.princed.org/viewtopic.php?p=15536#p15536

Make sure the kid is near the edge, but fully onscreen.
while (the game does not show the other room) {
	Press left, then immediately ESC.
	Press ESC again.
}

(In SDLPoP, Esc opens the pause menu. You can turn it off at Settings -> General -> Enable pause menu.)

That method partly works in SDLPoP: The room change is delayed, but the prince won't fall through the floor, because I have partially fixed the code behind this glitch some years ago.

This is the relevant part of the code:

SDLPoP/src/seg006.c

Lines 716 to 735 in 16d1336

int __pascal far get_tile_div_mod(int xpos) {
// xpos might be negative if the kid is far off left.
// In this case, the array index overflows.
/* if (xpos < 0 || xpos >= 256) {
printf("get_tile_div_mod(): xpos = %d\n", xpos);
}*/
// obj_xl = tile_mod_tbl[xpos];
// return tile_div_tbl[xpos];
int x = xpos - 58;
int xl = x % 14;
int xh = x / 14;
if (xl < 0) {
// Integer division rounds towards zero, but we want to round down.
--xh;
// Modulo returns a negative number if x is negative, but we want 0 <= xl < 14.
xl += 14;
}
obj_xl = xl;
return xh;
}

When xpos < 0, DOS PoP reads an element with an out-of-range index from the tile_div_tbl[] array.
This array is for determining the tile column from the prince's xpos.
(The other array, tile_mod_tbl[], is for determining the position within the tile.)

In SDLPoP I changed this part to use integer division instead of the arrays, because a similar glitch on level 5 didn't work the same way as in DOS PoP.
(That glitch is described here: https://forum.princed.org/viewtopic.php?p=8195#p8195 , under "Backing up off the screen")
That change itself is older than the Git repo, here is the ChangeLog entry.
https://github.com/NagyD/SDLPoP/blob/master/doc/ChangeLog.txt#L144-L147

To reproduce the both glitches correctly, SDLPoP should return the same bogus values as DOS PoP.
Here is my attempt to simulate this: 58f3fdd
Please test it.

By the way, is there any advantage in doing this glitch at the shown place?
It's not possible to grab the ledge below, so the prince will die.

A similar problem might occur when going offscreen to the right, but I didn't change that part yet.
Do you happen to know any place where going offscreen to the right behaves differently in DOS PoP and SDLPoP?

Thanks for the fix, will test it.

In the current form there's indeed no advantage in this glitch, but I hoped to understand what exactly happens to look for other places where it may be useful. And it's much more convenient to do in SDLPoP than by looking at disassembly of DOS version.

As for going offscreen to the right, currently I'm not aware of any differences. At first I heard from one speedrunner that it's not possible at all, but later another speedrunner could do it consistently in DOS PoP too.
There is one glitch in #228 (in the very end; switching room to the left, rather than right, SDLPoP replay is attached to that issue), which I believe noone managed to reproduce in DOS version so far. But I suspect that it's likely that it's just too hard to reproduce, rather than it being SDLPoP only.

Hi @NagyD

To reproduce the both glitches correctly, SDLPoP should return the same bogus values as DOS PoP.

These values are not really bogus, but instead values of what's on the floor on the other side of the screen (x=255, 254, 253...). We've studied what happens when the kid reaches this place and now we know that the fall/clip is triggered by a contact of the kid's heel with the loose tile on the right screen while in combat mode. This triggers the fall animation without a screen transition, having him clip on the next frame (his y position goes from 155 to 154). Here's a detailed explanation I made into a video: https://www.youtube.com/watch?v=MFaVaPX2Vho

By the way, is there any advantage in doing this glitch at the shown place?

Yes, for one we can push guard's x position to overflow as well (the setup is pretty complicated) and teleport to the next screen. This might save a few seconds on a TAS run: https://www.youtube.com/watch?v=fyoq2ZEYDaM

Furthermore, if we could find a way to have him step on exactly x-pos 9, 10, 11, or 12 (kid's heel touches different position to the right, e.g., 253, 252... etc) the clip would land him in the potion layer, skipping the whole level directly. Unfortunately, I could only manually get him to positions 8 (fall to death) and 13 (trigger transition). I believe that a correct emulation of this phenomenon would allow @mooskagh tools to find the right set of actions to achieve [9-12] position.

I hope this is motivation enough and I'll be glad to help in any way I can.

I'm still trying to investigate this together with @SergioMartin86, but

  • It looks that 58f3fdd didn't really fix the issue. (I didn't try very hard to reproduce it, but tried moderately hard)
  • As Sergio wrote, it looks like in DOS version going left "overflows" X coordinate, so that the tile from the room to the right falls when this glitch happens (and similarly with guard, guard "teleports" to the right). While in your fix it seems you expect that values from overflown array access are fixed.
NagyD commented

This post is rather long because I tried to explain everything. Sorry about that.

It looks that 58f3fdd didn't really fix the issue. (I didn't try very hard to reproduce it, but tried moderately hard)

I could reproduce it. Here is a replay file: level_7_fall_through.zip
(I used the Esc trick from my previous comment to record this, but replays don't record when was the game paused.)

As Sergio wrote, it looks like in DOS version going left "overflows" X coordinate, so that the tile from the room to the right falls when this glitch happens

As I understand it, that tile does not fall, only a sound is heard.

I tried to figure out what could make that sound.
My logic was this:
It can't be a loose floor because then we would hear multiple sounds.
It can't be a raise button because then we would hear a gate opening.
So it can only be a drop button. I replaced the two drop buttons in the same row with loose floors. And indeed, one of them fell.

You can try this: If you put a loose floor in place of the drop button in the top row of room 19, then that will fall when this glitch happens.
If you want it to land as well, you need to delete the wall below it, because loose floors stop in walls.
(This works both in DOS PoP, and in SDLPoP with 58f3fdd.)
This means that the sound we hear comes from that drop button.

That room is the 5th room to the right from the room you're looking at (room 2), and the drop button is in column 5 (if you start counting from 0).
Together that is column 55 (= 5*10 + 5) when seen from room 2.
In hexadecimal 55 = 0x37, which appears right here: 58f3fdd#diff-cbb751f05a0622282edb1a0afab3745d1781bdac616a0961342fd087e964fae9R744
It's the 8th element from the back, which means it corresponds to tile_div_tbl[-8].

While in your fix it seems you expect that values from overflown array access are fixed.

In DOS PoP, the following arrays are stored before tile_div_tbl[]:

// data:2284
const word y_loose_land[] = {2, 65, 128, 191, 254};
// data:228E
extern const sbyte dir_front[] INIT(= {-1, 1});
// data:2290
extern const sbyte dir_behind[] INIT(= {1, -1});
// data:2292
extern const short y_clip[] INIT(= {-60, 3, 66, 129, 192});
// data:229C
extern const short y_land[] INIT(= {-8, 55, 118, 181, 244});
// data:22A6
const sbyte tile_div_tbl[256] = {

All of these are constants, the game never writes into them.
The bogus[] array contains the bytes of these arrays.
Except, now that I look at it, the bytes of y_loose_land[] were accidentally clipped after 0xBF (=191). I will fix this.
Update: I fixed this: 2ffc230

These values are not really bogus, but instead values of what's on the floor on the other side of the screen (x=255, 254, 253...).

I think you might have misunderstood the purpose of the array.
The array whose access overflows (tile_div_tbl[]) does not store the tiles found on the current level, if that's what you meant.
Instead, that array is used to convert an xpos to a tile column index.
It looks like this.
(The div and mod in the arrays' names refer to integer division and modulo, respectively.)
Sorry if my previous comment was not clear about that.

Another point I forgot to explain so far: Why is xpos < 0 if the kid's xpos is always between 0 and 255?
The xpos used for indexing is not Kid.xpos (which also appears in the first video) but rather the xpos of his weight. (The "kid's heel", as Sergio wrote.)
The location of the weight is stored for each frame of the kid.
See here for a list of frames: https://forum.princed.org/viewtopic.php?p=15726#p15726
One of the frames used in backing is frame 0x9D. There, XBASE=14 means that the prince's weight is 14 units behind the front edge of his sprite.
If you subtract that from the xposes mentioned in the video (8 to 13), the result is negative.

If you look at the console output from SDLPoP with 58f3fdd, the button sound is played when the following line appears:

get_tile_div_mod(): xpos = -8

Above I mentioned the 8th element from the back in bogus[], which points to column 55. It is accessed at this point.

But this is not what causes the prince to fall through the floor.
I checked the other xpos values displayed on the console, and xpos = -6 seems to be the right one:
You can try this: If you change the 6th element from the back in bogus[] from 0x76 to 0x00, then the glitch will not happen.
So this value causes the glitch.
This -6 could come from 8 - 14, where 8 is the kid's xpos, and 14 is the XBASE I mentioned above.

0x76 = 118 (= 11*10 + 8), which refers to the 8th tile column, in the 11th room to the right from the current room.
There is no such room, so get_tile() will say it's a wall, see here.
Walls are not considered floors, see here.
That's why the prince will start falling.

There is one more thing you can try: Edit the level so that there is a 11th room to the right.
Then fill its top row with loose floors.
When the glitch happens, one of those loose floors should fall.
(Try this both in DOS PoP, and in SDLPoP with 58f3fdd.)

As I understand it, that tile does not fall, only a sound is heard.

It does fall, @SergioMartin86 actually checked. If after the sound you go to the screen to the right, it's actually fallen.

NagyD commented

It does fall, @SergioMartin86 actually checked. If after the sound you go to the screen to the right, it's actually fallen.

I reproduced the glitch in DOSBox (PoP v1.0), then I looked at the room with the loose floor using the cheat keys.
The floor sound can be heard, but the floor did not fall.
Here is a video about it: level_7_fall_through.zip

Am I doing something wrong?

EDIT: I reproduced it in PoP v1.4 as well.
Here is the video: level_7_fall_through_v1.4.zip
The floor sound can be heard, but the loose floor did not fall in this version either.