Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- How instapasses work and how to find them in game script
- I'll start with basic info on game script (main.scm) and how it works.
- The most important for this trick part of game script is threads code. Basically script is split into a pretty big number of "threads" - which run in quasiparallel mode. Game updates everything once per frame, including all the script threads. When main loop gives control to script processor, it starts interpreting each running thread of current gamestate. Game keeps instruction pointer (which point a thread should be executed from), local variables and 2 timers in memory and also in a save if the game is saved. The processor executes instructions starting from saved instruction pointer until it reaches "wait" opcode. "wait" has a parameter, time during which game doesn't execute any instructions of given thread. If it is 0, then game will continue executing it during next frame processing.
- Some of these threads are unusual, they are espescially marked as mission threads. They are created by special command, that differs from regular thread creation. And as it was found out that game only keeps 1 mission thread in memory, a fact which will be used a bit later.
- So how a thread look like? Luckily we know exactly how developers saw it. Original script files were left over in mobile version of GTA III, so here they are:
- http://gtamodding.ru/wiki/%D0%9A%D0%B0%D1%82%D0%B5%D0%B3%D0%BE%D1%80%D0%B8%D1%8F%3A%D0%98%D1%81%D1%85%D0%BE%D0%B4%D0%BD%D0%B8%D0%BA%D0%B8
- Basically, usual mission script looks like a bunch of loops that end on some events (mission targets) with creation of objects between them and with checks inside them. Mission ends with three labels: mission_failed, mission_passed and mission_cleanup. Last of them is supposed to be executed in any case, as well as one of first two. Mission starting code is very small. It calls main mission code, which basically is all mission logic, and receives control back in one of two cases. First one is "return" instruction of the main mission code and second one happens if player was busted or wasted and this script is marked as active mission thread (there is only one such a thread at any point of the game). So game checks if player was busted or wasted, calls mission_failed subroutine and then in any case makes a cleanup.
- Now let's get to the point. If a mission is started while the other mission is still running, an interesting thing happens. As stated above, any thread keeps instruction pointer. It keeps an offset from the beginning of its thread at least for mission threads. Mission thread just adds the offset to current active mission base address (start address). Then during one of frames a new mission has started. The active mission base address has changed and now game will try to execute the instruction at the same offset as it was originally for old mission, but the base address will now be different, so it will execute instructions of newly run mission.
- Let's get to examples. We'll take Taxi Driver instapass, which was found by Powdinet. I use SCRLog to get log of the script to show what had happened.
- http://i.imgur.com/ySTQmz0.png
- This is a script log from a last frame that was still running Cipriani's Chauffeur. It starts with offset 6064 as we see for the first instruction at the left. Luckily for us, Sanny Builder shows the label as offsets from the start of the thread. Let's see how it looks like there. We can use Lightnat0r's version with variable names from original script.
- https://raw.githubusercontent.com/Lighnat0r/GTA-III-SCM-Converted/master/GTA%203%20Main%20Variables%20Named.txt
- Here is the fragment that starts at the offset 6008:
- :JOEY4_6008
- 00D6: if or
- 81A0: not player $PLAYER_CHAR stopped $BLOB_FLAG 1215.0 -326.875 25.0 1220.188 -330.5 27.0
- 010F: player $PLAYER_CHAR wanted_level > 0
- 80DC: not is_player_in_car $PLAYER_CHAR car $TONIS_RIDE
- 004D: goto_if_false @JOEY4_6683
- 0001: wait 0 ms
- 00D6: if
- 0118: actor $TONI_CIPRIANI dead
- 004D: goto_if_false @JOEY4_6102
- What do we see? The "wait 0" command, that stopped previous script execution is obviously is this fragment. By the starting condition we can guess that that this is the loop that waits for player to be at the ending marker in Toni's car with no wanted level. But let's find this loop in the original script to make it clear.
- WHILE NOT IS_PLAYER_STOPPED_IN_AREA_IN_CAR_3D player 1215.0 -326.9 25.0 1220.2 -330.5 27.0 blob_flag
- OR IS_WANTED_LEVEL_GREATER Player 0
- OR NOT IS_PLAYER_IN_CAR player tonis_ride
- WAIT 0
- IF IS_CHAR_DEAD toni
- PRINT_NOW ( JM4_8 ) 5000 1
- GOTO mission_joey4_failed
- ENDIF
- //...
- Indeed it is. So this is the loop after cutscene at laudromat.
- The loop stops at the same offset, since player still didn't arrive.
- What happens next? We start the mission, and here is what the log says:
- http://i.imgur.com/8TJ5TNw.png
- So, game launched mission 14 (which is Taxi Driver). It changes the active mission script. Let's see what happens with our copy.
- http://i.imgur.com/6v49lP2.png
- Well, the offset is the same, but the instruction is different. It's actually an unknown instruction which is interpreted as "wait 0" command. Let's omit some unknown instructions until something relevant.
- http://i.imgur.com/BqCaaSw.png
- This makes more sense. "player_made_progress" implies that this is Taxi Driver passing section. Let's have a look on its script.
- :TAXI_5987
- 0109: player $PLAYER_CHAR money += $SCORE_FOR_THIS_FARE
- 01E3: text_1number_styled 'TSCORE2' number $SCORE_FOR_THIS_FARE duration 6000 ms style 6 // $~1~
- 018C: play_sound 94 at 0.0 0.0 0.0
- 0058: $TAXI_SCORE += $SCORE_FOR_THIS_FARE
- 0008: $TAXI_MISSION_DELIVERIES += 1
- 0315: increment_taxi_dropoffs
- 0008: $TAXI_PASSED_THIS_SHOT += 1
- 00D6: if and
- 0038: $NEW_TAXI_CREATED_BEFORE == 0
- 0038: $TAXI_MISSION_DELIVERIES == 100
- 004D: goto_if_false @TAXI_6110
- 014D: text_pager 'NEW_TAX' 140 2 0 // BIGGER! FASTER! HARDER! new Borgnine taxis open for business in Harwood. Call 555-BORGNINE today!
- 014C: set_parked_car_generator $SWANK_TAXI cars_to_generate_to 101
- 030C: set_mission_points += 1
- 0004: $NEW_TAXI_CREATED_BEFORE = 1
- :TAXI_6110
- 0008: $TAXI_COUNTDOWN += 10000
- 0050: gosub @TAXI_6484
- 00D6: if
- 003A: $TAXI_PASSED_THIS_SHOT == $IN_A_ROW_NUMBER
- 004D: goto_if_false @TAXI_6196
- 036D: text_2numbers_styled 'IN_ROW' numbers $TAXI_PASSED_THIS_SHOT $IN_A_ROW_CASH duration 5000 ms style 6 // ~1~ IN A ROW bonus! $~1~
- 0109: player $PLAYER_CHAR money += $IN_A_ROW_CASH
- 0058: $TAXI_SCORE += $IN_A_ROW_CASH
- 0008: $IN_A_ROW_NUMBER += 5
- 0008: $IN_A_ROW_CASH += 2000
- So we started at "004D: goto_if_false @TAXI_6110" instruction. So we skipped the check for number of deliveries and started right from the point of reward. Nice!
- Now to the explaination of how to find new instapasses. I will explain on trivial ways of doing so. The example is "Ride in the Park" instapass.
- Let's start with script of this mission, a part that rewards player.
- :T4X4_2_3159
- 00D6: if
- 0038: $A_RIDE_IN_THE_PARK_COMPLETED == 0
- 004D: goto_if_false @T4X4_2_3203
- 0004: $A_RIDE_IN_THE_PARK_BEST_TIME = 120000
- 0060: $A_RIDE_IN_THE_PARK_BEST_TIME -= $TIMER_4X4
- 0014: $A_RIDE_IN_THE_PARK_BEST_TIME /= 1000
- :T4X4_2_3203
- 00D6: if
- 0038: $A_RIDE_IN_THE_PARK_COMPLETED == 1
- 004D: goto_if_false @T4X4_2_3274
- 0004: $RECORD_TEMP = 120000
- 0060: $RECORD_TEMP -= $TIMER_4X4
- 0014: $RECORD_TEMP /= 1000
- 00D6: if
- 001C: $A_RIDE_IN_THE_PARK_BEST_TIME > $RECORD_TEMP
- 004D: goto_if_false @T4X4_2_3274
- 0084: $A_RIDE_IN_THE_PARK_BEST_TIME = $RECORD_TEMP
- :T4X4_2_3274
- 01E3: text_1number_styled 'M_PASS' number 30000 duration 5000 ms style 1 // MISSION PASSED! $~1~
- 0394: play_mission_passed_music 1
- 0110: clear_player $PLAYER_CHAR wanted_level
- 0109: player $PLAYER_CHAR money += 30000
- 03FE: save_offroadII_time $A_RIDE_IN_THE_PARK_BEST_TIME
- 00D6: if
- 0038: $A_RIDE_IN_THE_PARK_COMPLETED == 0
- 004D: goto_if_false @T4X4_2_3353
- 0318: set_latest_mission_passed 'T4X4_2' // 'A RIDE IN THE PARK'
- 0004: $A_RIDE_IN_THE_PARK_COMPLETED = 1
- 030C: set_mission_points += 1
- :T4X4_2_3353
- Okay, so for mission to count (opcode 0318 for AM, 030C for 100%) we need to find a loop at approximately 3159 to 3330 offset. Let's find suspicious loops.
- Here is first assumption: a loop in Arms Shortage. Here we go:
- :RAY2_3142
- 00D6: if
- 001A: 12 > $COUNTER_DEAD_VARMINTS
- 004D: goto_if_false @RAY2_4907
- 0001: wait 0 ms
- 01BD: $TIMER_NOW_RM2 = current_time_in_ms
- 0084: $TIMER_DIF_RM2 = $TIMER_NOW_RM2
- 0060: $TIMER_DIF_RM2 -= $TIMER_START_RM2
- 0050: gosub @RAY2_9333
- 00D6: if and
- 0018: $TIMER_DIF_RM2 > 2000
- 0038: $FLAG_SENTINEL_CREATED == 0
- 004D: goto_if_false @RAY2_3347
- 00A5: $SENTINEL1_RM2 = create_car #COLUMB at $VARMINT_GEN1_X $VARMINT_GEN1_Y -100.0
- 02AA: set_car $SENTINEL1_RM2 immune_to_nonplayer 1
- 020A: set_car $SENTINEL1_RM2 door_status_to 2
- 0186: $BLIP_SENTINEL1 = create_marker_above_car $SENTINEL1_RM2
- 0129: $VARMINT_1 = create_actor 12 #GANG12 in_car $SENTINEL1_RM2 driverseat
- 01C8: $VARMINT_2 = create_actor 12 model #GANG12 in_car $SENTINEL1_RM2 passenger_seat 0
- 01C8: $VARMINT_3 = create_actor 12 model #GANG12 in_car $SENTINEL1_RM2 passenger_seat 1
- 01C8: $VARMINT_4 = create_actor 12 model #GANG11 in_car $SENTINEL1_RM2 passenger_seat 2
- 00AD: set_car_cruise_speed $SENTINEL1_RM2 to 10.0
- 00AE: set_car_driving_style $SENTINEL1_RM2 to 3
- 02C2: car $SENTINEL1_RM2 drive_to_point $STAGE_1_X $STAGE_1_Y 11.5625
- 0004: $FLAG_SENTINEL_CREATED = 1
- Easily it is recognised as a loop that waits until we kill all enemies after their spawn. Its "wait 0" is somewhere after 3159, so let's have a look...
- Game crashed. Why did it happen?
- This time I omit normal old mission's loop, will only note that the offset was 3164 - kinda what we needed. But unfortunately there is not an opcode at this offset in the SCM, it's just a data interpreted as opcode, much like in Taxi Driver instapass, but with worse circumstances.
- [0200] LOCATE_PLAYER_ON_FOOT_CAR_3D [UNKNOWN] 4 -725.375 22.75 -1.#QNAN 0
- This "unknown" ruins it all. Game tries to read a parameter which it interprets as a string. It uses its address as a index in players' array and goes way out of bounds, out of its own memory. RIP :(
- I will also add that this mission is instapassed with Grand Theft Aero from this loop:
- :LOVE4_3219
- 00D6: if
- 80DC: not is_player_in_car $PLAYER_CHAR car $WINGLESS_CESSNA
- 004D: goto_if_false @LOVE4_3325
- 0001: wait 0 ms
- 00D6: if
- 0119: car $WINGLESS_CESSNA wrecked
- 004D: goto_if_false @LOVE4_3280
- 00BC: print_now 'LOVE4_9' duration 5000 ms flag 1 // ~r~The plane has been destroyed!
- 0002: goto @LOVE4_9982
- Which is somewhere at 3240, which is also is within our bounds. Instapass works.
- This is how it is possible to look for obvious instapasses. They work exactly the same way in VC. It is also possible to instapass missions that are further is mission code if loop offset is further that the last instruction of new mission.
- Also there may be skips of some missions' parts, if we hit right into loop of a new mission and there will be no conflict (like not created objects). In some cases it will still be instapass, like Gone Fishing. This instapass actually jumps right into one of instructions sequence after which enters a loop. It checks for a boat to be destroyed. However since it was never created, it decides that mission objective is passed and completes the mission. This is not so obvious instapass and isn't very easy to look for.
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement