View Page Source

Revision (current)
Last Updated by feos on 2/4/2024 9:47 AM
Back to Page

!!! Introduction

This page is dedicated to document aspects of the DOS port of Prince of Persia (PoP) that pertain to TAS and speedrunning. 

!! External Resources

Abundant information exists regarding all other aspects of this game, its history, and trivia. Here I document a the most important:

* Prince of Persia  [https://popuw.com/|Unofficial Website]: It's the most complete resource regarding the PoP series, their official and unofficial releases, modding community, and trivia.
* [https://www.princed.org/|Princed Project]: A site dedicated to PoP modding community and applications. Their [https://forum.princed.org/|forum] is the ultimate community interaction townhall.
* [https://github.com/NagyD/SDLPoP|SDLPoP]: Is an open-source port of Prince of Persia, based on the disassembly of the DOS version, extended with new features.' This project was and is the ultimate resource for deciphering the game's internal mechanics and subsequent botting efforts.
* The [https://www.speedrun.com/pop1|PoP speedrunning community]. This is where the best players gather and run the game as fast as possible. A lot of information regarding how to speedrun the game and tips and tricks can be found here. 
* [https://github.com/SergioMartin86/jaffarPlus|JaffarPlus]. A routing used to discover the fastest way to solve PoP levels.

!!! Table of Contents

%%TOC%%


!!! Emulation

!! Tools

Casual gamers are advised to use [https://github.com/NagyD/SDLPoP|SDLPoP] to conveniently and quickly run the game over Windows or Linux. Speedrunners can use DosBox to accurately [https://www.speedrun.com/pop1/guide/r6nek|emulate] and [https://www.speedrun.com/pop1/guide/edoyd|time] their runs competitively.

! TAS Emulation

For TASing, however, the game requires a precise, reproducible environment. Here I provide the currently better tested setup, based on the instructions given in this  [EmulatorResources/PCem|article].

* Game Release: Prince of Persia v1.4, from the [http://redump.org/disc/9664/|Prince of Persia Collection: Limited Edition] CD (MD5: 7f185559f694140312bc84fba0558d9a)
* Emulator: [https://github.com/TASEmulators/pcem/releases/tag/17%2Bst-1|PCem 17+st-1] + [EmulatorResources/PCem/DOS/Configurations#Late90s_2|Late 90's] PC configuration.
* TAS Engine: [https://github.com/clementgallet/libTAS/releases|LibTAS 1.4.4]
* Recommended OS: [https://ubuntu.com/download/desktop|Ubuntu Desktop 22.04]

! Additional tools

* Memory Analysis: [https://github.com/korcankaraokcu/PINCE|PINCE (Pince Is Not Cheat Engine)]
* Routing: [https://github.com/SergioMartin86/jaffarPlus|JaffarPlus]


!! Running the Game

! PCem Configuration

The [EmulatorResources/PCem/DOS#PremadeConfigurations|late '90s package] can be used, except in {{late90s.cfg}} you need to change {{mem_size}} from 32768 to 8192, as the game does not need that much, and it makes the savestates and boot (memcheck) time smaller.

Also don't forget to set the {{cdrom_path}} to point to the game image and {{hdc_fn}} to point to {{late90s.img}} (full path without quotes, spaces are fine).

! LibTAS Configuration

Don't be like me and make sure the following configurations are activated in libTAS

* Frames per second: 100/1 [https://i.ibb.co/vmGHBd6/image2.png]

* Runtime configuration: Should look exactly like this [https://i.ibb.co/HttKdhH/image5.png]

! Launching libTAS

To run libTAS, you can use the following command:
 
 libTAS /usr/bin/pcem --config ${PWD}/late90s.cfg

! Launching the game

Once you go through booting the PCem machine, you will be greeted by the DOS command line. The quickest way to launch the game is by running it straight from the CD-ROM, by running the following commands:


 D:
 cd prince
 cd prince1
 prince

!!! Memory Analysis

If you are going to TAS this game, you absolutely need to keep track of important variables. To achieve this, you need to use memory analysis tools, like Cheat Engine. Unfortunately, CE does not work natively on Linux, so you need any of the following alternatives:

* 1) Running PCem inside a Virtual Machine (e.g., using [https://www.vmware.com/products/workstation-player.html|VMware Workstation Player]) and running CE on the host. The disadvantage of this approach is that CE will need to scan the entire VM memory to find the correct values.
* 2) Running a Linux-native tool like [https://github.com/korcankaraokcu/PINCE|PINCE]. This is the recommended way as it allows attaching directly to the PCem process. 
* 3) Using libTAS' LUA engine to do the scanning and logging. I haven't tried this one, but it could be an easier way to grab data from the game.

!! Attaching PINCE to PCem

To attach PINCE to the game, first install PINCE and then run it using the following command:

 bash PINCE.sh

Then, attach it to PCem by following these steps: [https://i.ibb.co/j8sP83P/image4.png]

! Looking for the Hook

After launching the game, press the "Pause" button during the game's loading phase. At this point, use PINCE to look for the following array of bytes:

 00 00 00 b0 1d 2b 0f 00 00 00

[https://i.ibb.co/GWNfvd0/image19.png]

A single memory address should be found. 

[https://i.ibb.co/pwdFmsx/image13.png]

This address will be the hook address. This value has no intrinsic meaning, other than being the address from which we will obtain other relevant addresses. 

! Relevant Addresses

Use a programmer calculator to calculate the addresses of the following relevant values, based on their offset with regards to the hook address.

* RNG State (-0x0233): A 64-bit integer (0x00000000-0xFFFFFFFF) containing the current state of the game's only pseudo-random number generator. 
* Timer Minutes Left (+0xCBD): The number of minutes left before the game's timeout. Used to calculate the minute aspect of the hall of fame scoreboard.
* Timer Ticks Left (+0xCC3): The number of 1/12s of a second (ticks) left before the current timer minute runs out.

!!! Game Mechanics

This section discusses game mechanics that are relevant to effective TASing.

!! Game Phases

! Game Loading

As soon as the game is launched, it starts loading all the relevant files from the CDROM (or floppy disk or hard disk drive) into memory. At this stage the game displays the following text:

[https://i.ibb.co/pn1M4Mp/image1.png]

As soon as the game finishes loading, it transitions towards the title screen.

! Title Screen & Demo

The title screen is the first image that appears after the loading phase. As long as no keys are pressed, the credits and backstory will show. Later, a randomized demo sequence will be shown. After the demo sequence finishes, the title screen is displayed again. 

[https://i.ibb.co/5RVJTBm/image11.png]

You come back to the title screen after the game restarts. This can happen in three situations:

* You manually press Ctrl+R. In this case, the current game is terminated and title screen reappears immediately.
* You lose the game on time. This happens when the time runs out (more on this later).
* You lose the game on death. If you die and do not press any key within the allotted time, the game restarts.
* You beat the game. In this case, the title screen reappears after the hall of fame.

! Copy Protection Level

If you play an original copy of the game, you will need to pass a playable copy protection level between levels 1 and 2. This level appears only once in the session.) after you pass the level, you won't be prompted to pass it again later if you restart the game.

[https://i.ibb.co/d4fwPnP/image10.png]

In this level, a total of 14 potions are presented to you. Your task is to drink the correct potion, i.e., the one whose letter corresponds to the first letter of the word within the original user manual indicated in the prompt at the bottom of the screen. After you drink the correct potion, the exit door will open and you can progress to level 2. If you drink the wrong potion, you'll lose half your life. If you drink two wrong potions you'll die and the game will restart.

Funnily enough, you can beat the protection scheme very quickly by restarting and advancing (shift+L) repeatedly until the closest potion is the correct one, as shown in this [https://www.youtube.com/watch?v=Bs4NFx3Etoc|video].


! Cutscenes

As soon as the current level finishes (screen goes to black) and the next level starts, the clock will remain stopped. This includes the cutscenes that play during some of the levels. 

[https://i.ibb.co/LR7wn5j/image8.png]

The following table shows what are the game cutscenes and their duration

||Transition||Duration||Description||
|Level 1->2|44 Frames|Princess stands anxiously while the sand clock runs|
|Level 3->4|44 Frames|Princess lays anxiously while the sand clock runs|
|Level 5->6|44 Frames|Princess stands anxiously while the sand clock runs|
|Level 7->8|79 Frames|Princess releases mouse to aid our hero|
|Level 8->9|90 Frames|Princess receives a successful mouse|
|Level 11->12|44 Frames|Princess stands anxiously while the sand clock runs|

! Levels

The game consists of 15 playable levels. 

* Levels 1-13 are the ones needed to beat the game.
* Level 14 is the last level where you run to meet the princess. You cannot die, hurt yourself, or lose on time in this level and the only possible outcome is the transition to the game win sequence. 
* Level 15 is the copy protection level. Plays between levels 1 and 2, only if the test hasn't been passed before.
* Level 0 (non-playable) corresponds to the demonstration mode.

! Hall of Fame

The hall of fame (HoF) appears once you beat the game, after the last 'hug cutscene' and the closing credits. 

[https://i.ibb.co/pLxLGvY/image18.png]

The HoF, like many arcade machines of its era, is dedicated as a persistent record of the best players. After beating the game, the player will be given the opportunity of inputting their names. The entry will be stored in the order of score (the best on top). The store reflects how much time remained at the moment of defeating Jaffar, as explained in later.

!! Game Timer

Both the human and tool-assisted speedruns of the game use the In-Game Timer (IGT), as opposed to wall-clock time, for measuring which run is faster. There are multiple reasons for the game's speedrunning community to have reached this decision:

* The IGT timer provides an accurate and consistent measurement of how many game frames have passed between the start and the end of the run. 
* Taking the wall-clock time (i.e., using a real-life chronometer) cannot provide consistent measurements since different PCs and emulators take different amounts of time to run each frame, and to load cutscenes and level transitions.
* The IGT timer is accessible directly from RAM, via the provided two addresses above.

! Timer Logic

At the very beginning of the game, the timer is initialized with the following values:

* Minutes Left: 60
* Ticks Left: 719

Each game frame, the number of ticks is reduced by one. When the number of ticks left reaches 001, the number of minutes left is reduced by one, and the tick count comes back to 711. The following progression illustrates the Minute/Tick progression from start to game over:

%%SRC_EMBED
59:711
59:710
...
59:002
59:001
58:711
...
01:002
01:001
00:711 <- GAME OVER
%%END_EMBED

When the minutes left counter reaches zero, the game is lost and the bad ending scene is displayed.

[https://i.ibb.co/1ZPrVBB/image3.png]

! Timer Bug

Those keen eyed individuals might have noted that the timer logic is flawed. In total, 711 ticks pass for each reduction in the minute timer. This bug has the following interpretations:

* Each game second equals 711/712 = 0.9986 real life seconds.
* Every minute, the game subtracts one tick from the remaining time.
* The game time runs out in 42660, instead of 42720 frames

Anybody looking to TAS this game and squeeze every single frame from IGT needs to keep this bug in mind, as it might affect their final movie length calculations.

! Exceptions

The game timer will run as described, except for the following situations:

* Title screen. During the loading phase, title screen, and demo playing, the game has not started yet and the timer is not running. If the player restarts the game (Ctrl+R), the timer will stop and will reset back to 59:711 as soon as the game starts again.

* Copy Protection Level. To avoid handicapping the player during this level, the game stops the clock until the player exits and level 2 starts.

* Cutscenes. The clock remains stopped during cutscenes.

* Level Restart. If the player restarts the level (Ctrl+A), the first frame after the restart is not counter towards the timer. This affects the timing in levels 1 and 3, where the restart is used to perform small skips by restarting.

* Jaffar Dies. The very frame that Jaffar dies in level 13, the clock stops running and no more ticks, including the current one, are reduced. You can still die after this point, after which both the level and the clock will restart.

* Level 14. The final level after Jaffar is defeated, the timer no longer runs.

! HoF Score Calculation

The score is calculated as the remaining minutes/seconds remaining at the time of beating Jaffar in level 13. If the timer was 49:01 at the time of Jaffar's death, the HoF will display 48:00 as the player's score. If the timer was 48:711, time HoF will display 47:59. The time displayed by the HoF is crucial as proof of score for a TAS, where the improvement can be registered at a second level. Should a TAS improve on another at the tick level (where the tick does not pass the second barrier), the TASer / judge would have to provide proof of improvement by accessing the internal timer as shown before.

!!! Random Number Generator (RNG)

The game contains a single RNG from which all pseudo-randomized events are determined. Although most of the game elements that use the RNG are not relevant to the outcome of a play (i.e., they are mere decoration), one crucial gameplay element is affected by it: the guard's fighting actions. Therefore, any changes in RNG will make a TAS movie desync whereas a different one might work, exactly and only at the points where guard fights are involved.

!! Internal State

The game's pseudo random events are determined by a single RNG whose internal state is a 64-bit integer. Just like any other RNG, this state is updated every time the RNG is used to generate a new number, such that it produces a (mostly) different number every time it is queried.

! Initialization Logic

Upon launching the game, the 4 bytes of memory space corresponding to the internal state start [https://en.wikipedia.org/wiki/Uninitialized_variable|uninitialized]. Before displaying the Title screen for the first time, the game calls the RNG update function. Inside this function, there is a check whereas, if the current value of the internal state:

* If it is zero, use the system's timer to determine the initial value.
* Otherwise, use the update logic explained below.

At this point in the game initialization phase, the game banks on the fact that a zero value for the timer is the most likely scenario (especially on a fresh system boot) or the operating system clears previous memory pages before assigning them to a new process, we can assume that querying the system clock will be the most likely course of action.

The DOS function used to query the clock will return the number of microseconds passed since the start of the day. The system date does not count towards setting the initial RNG state.


! Update Logic

The way in which the internal state is updated is given by the formula:

 state = state * 0x000343FD + 0x000269EC3

Nevertheless, given that the state is a 64-bit integer, both the multiplication and addition are subject to overflow. Effectively they are passed through a modulo filter after each calculation, such that, purely mathematically, the calculation corresponds to:
 
 state = ( ( (state * 0x000343FD) % 0x100000000) + 0x000269EC3 ) % 0x100000000

The combination of these operations make it so that each state number maps to another number within the 64-bit range that appears to be entirely random. The relation is bijective, such that it is possible to calculate an undoing of the random state by the following operation:

 state = (state + 0xFFd9613D) * 0xB9B33155

Similarly, to account for overflow:

 state = ( ( (state + 0xFFd9613D) % 0x100000000) * 0xB9B33155 ) % 0x100000000

All events in the game update the RNG state using the above formula. However, some events trigger it more than once in between frames.

!! Active RNG Elements

! Guard RNG Behavior

Guards are the only element in the game that can change the outcome of a TAS movie if the RNG diverges. That is because the guards' fighting actions are determined by a set of [https://github.com/NagyD/SDLPoP/blob/7aeacd83167ab407272ab539ed5190fffc1d80f7/src/data.h#L840|action probabilities]. These actions (attack, wait, respond, advance) contain a trigger probability that goes from 0 to 100, where 0 means that it won't ever be triggered, or; 100, meaning it will always be triggered. Every guard in the game contains a different set of probabilities based on their skill, where the skills are determined by the following [https://www.speedrun.com/pop1/guide/kxbch|table].

[https://i.ibb.co/nMN9STj/image15.png]

Any intermediate probability will trigger an action (or not) depending on whether the outcome of random(0,255) is equal or smaller than the probability value. This query 
updates the internal RNG state once per frame, when the guard is in the middle of a fight (not when he is simply en garde).

! Falling Tiles (Level 13)

At the very beginning of level 13, the tiles on the ceiling will start breaking and falling over the running hero. This is assumed to be Jaffar stomping on them to hurt and bothering you (indeed, many runs have been lost to these tiles breaking the player's run). 

[https://i.ibb.co/ZWnjWHK/image6.png]

To determine the order in which these tiles are activated, the RNG is queried once per each.

!! Passive RNG Elements

! Loose Tiles

If the game runs on Sound Blaster or another synthetized sound device (not PC Speaker), the game's loose tiles will affect the RNG. Every time once such tile is activated, either by disturbing from afar it or pressing on it until it falls, the RNG will be queried to determine which of the 3 possible sounds will play. This randomness contributes to the game's innovative life-full feel. However, the logic affects the RNG state non-deterministically.

[https://i.ibb.co/176nM8z/image12.png]

The game stores the value of the last loose sound produced, to prevent reproducing it twice consecutively. This value is stored as a single byte that starts uninitialized at the beginning of the game. Once the first loose tile is activated (either in level one or the demonstration level), its value will oscillate between the values: 19, 20, and 21. These are indexes to the synthetized sample corresponding to three slightly different tile sounds. 

To determine the new loose tile sound, the game uses the following [https://github.com/NagyD/SDLPoP/blob/7aeacd83167ab407272ab539ed5190fffc1d80f7/src/seg007.c#L865|logic], simplified as:

%%SRC_EMBED
do 
 looseTileSound = random(0:2) + 19
while looseTileSound == lastLooseTileSound
lastLooseTileSound = looseTileSound 
%%END_EMBED

Since it can be expected that the initial value of this variable is zero at the beginning of the game, this aspect of RNG cannot be manipulated a priori. However, it plays a crucial role in reproducibility as, even if you enter the level with the same RNG state, it might diverge upon hitting one of these tiles.

! Torches

Every torch visible on screen triggers, each, an RNG update to determine the shape of its flame in the next frame. Every update is deterministic as this selection allows the flame shape to repeat between two consecutive frames. 

[https://i.ibb.co/VxzFCWW/image16.png]

On the frame where the hero enters a new room, the number of torches in the new room is the number of RNG updates that will be calculated.

! Potions

Potions produce a smoke effect that repeats deterministically and does not affect RNG at every frame. Instead, the only update on the RNG is produced upon entering a room where a potion exists, to determine the initial phase of the pattern. This is done to prevent multiple potions to all produce the same pattern at the same time. This is the case for all types of potions (+1HP, -1HP, +1MaxHP, Slowfall).

[https://i.ibb.co/j6hhMh0/image7.png]

The RNG will not be affected upon drinking the potion, but it also won't be updated upon reentering the room.

! Sword

Twice in the game you can observe a pickable sword laying on the ground: once in level 1 --the sword you use to fight throughout the game--, and; once in level 12 -- the shadow's sword. To denote its sharpness, the game will add a twinkling effect on the sword every so often. 

[https://i.ibb.co/2nN6pPN/image14.png]

To determine whether the sword should twinkle, the game checks:
* If the sword is currently twinkling or has twinkled in the previous N frames , do not twinkle in the next frame
* Otherwise, check the RNG value to make it twinkle now and set N to the random number.

The fact that updating the RNG or not depends on a previous result, makes this a non-deterministic effect on its internal state. This is not a problem, however, as current TAS skips the first sword, and picks up the second one without any RNG-based effects on the actions required to do so.

! Stars

What? Stars?. Yes, these ones:

[https://i.ibb.co/GxYY3r5/image9.png]

They twinkle every 3 or so frames. Which one of them twinkles is determined by the game's RNG. This check updates its internal state once. 

!! Other RNG Elements

! Copy Protection Letters

The position and letters hovering over the potions in level 15 are determined at the start of the game. The index of each of the potions is shown below.

[https://i.ibb.co/f4FGVsG/image17.png]

The letter positioning uses (and affects) the RNG internal state a non-deterministic amount of times, given the following pseudo-code:


%%SRC_EMBED
// N is the size of the vector containing letters from A to Z
// Letters might be repeated in this vector to increase their selection likelihood
N = 39 

// Advances the RNG state by one
correctPotion = random(0,13) 

for idx in 0:13
  do letter = random(0,N) while letter has not been picked up before
  PotionLetter[idx] = letter
end for
%%END_EMBED

The important aspect of the code above is that the letter selection code above will access the RNG a non-deterministic number of times (i.e., until the letter picked has not been picked before). This is of crucial aspect of manipulating RNG in a TAS, as the process needs to be replicated exactly as in the [https://github.com/NagyD/SDLPoP/blob/7aeacd83167ab407272ab539ed5190fffc1d80f7/src/seg000.c#L215|game code].
 

! Cutscenes

The RNG state is modified even during cutscenes, since they contain two torches and a set of stars in the window. For some reason, the number of RNG state updates per frame is inconsistent on different cutscenes. Here is an exact progression of the number of RNG updates:

%%SRC_EMBED
/* 01->02 */  {3, 3, 3, 3, 3, 3, 3, 3, 3, 5, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3},
/* 03->04 */  {3, 3, 3, 3, 3, 3, 3, 3, 3, 5, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3},
/* 05->06 */  {3, 3, 3, 3, 3, 3, 3, 3, 3, 5, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3},
/* 07->08 */  {3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3},
/* 08->09 */  {3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 3, 2, 3, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3},
/* 11->12 */  {1, 3, 3, 3, 3, 3, 3, 3, 3, 5, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3}
%%END_EMBED

! Invariant Rooms

There are rooms where no RNG elements exist and therefore will keep the RNG state fixed until you exit.

[https://i.ibb.co/k6mTpKv/invariant.png]