Quantcast
Jump to content
Search In
  • More options...
Find results that contain...
Find results in...
    1. Welcome to GTAForums!

    1. Red Dead Redemption 2

      1. PC
      2. Gameplay
      3. Missions
      4. Help & Support
    2. Red Dead Online

      1. Gameplay
      2. Find Lobbies & Outlaws
      3. Help & Support
      4. Frontier Pursuits
    1. Crews & Posses

      1. Recruitment
    2. Events

    1. GTA Online

      1. Diamond Casino & Resort
      2. DLC
      3. Find Lobbies & Players
      4. Guides & Strategies
      5. Vehicles
      6. Content Creator
      7. Help & Support
    2. Grand Theft Auto Series

    3. GTA 6

    4. GTA V

      1. PC
      2. Guides & Strategies
      3. Help & Support
    5. GTA IV

      1. Episodes from Liberty City
      2. Multiplayer
      3. Guides & Strategies
      4. Help & Support
      5. GTA Mods
    6. GTA Chinatown Wars

    7. GTA Vice City Stories

    8. GTA Liberty City Stories

    9. GTA San Andreas

      1. Guides & Strategies
      2. Help & Support
      3. GTA Mods
    10. GTA Vice City

      1. Guides & Strategies
      2. Help & Support
      3. GTA Mods
    11. GTA III

      1. Guides & Strategies
      2. Help & Support
      3. GTA Mods
    12. Top Down Games

      1. GTA Advance
      2. GTA 2
      3. GTA
    13. Wiki

      1. Merchandising
    1. GTA Modding

      1. GTA V
      2. GTA IV
      3. GTA III, VC & SA
      4. Tutorials
    2. Mod Showroom

      1. Scripts & Plugins
      2. Maps
      3. Total Conversions
      4. Vehicles
      5. Textures
      6. Characters
      7. Tools
      8. Other
      9. Workshop
    3. Featured Mods

      1. DYOM
      2. OpenIV
      3. GTA: Underground
      4. GTA: Liberty City
      5. GTA: State of Liberty
    1. Red Dead Redemption

    2. Rockstar Games

    1. Off-Topic

      1. General Chat
      2. Gaming
      3. Technology
      4. Programming
      5. Movies & TV
      6. Music
      7. Sports
      8. Vehicles
    2. Expression

      1. Graphics / Visual Arts
      2. GFX Requests & Tutorials
      3. Writers' Discussion
      4. Debates & Discussion
    1. News

    2. Forum Support

    3. Site Suggestions

Sign in to follow this  
OrionSR

Embedding Scripts in Save Files

Recommended Posts

OrionSR
Posted (edited)

Embedding Scripts in Save Files

 

Embedded scripts operate on the premise that the global variable space occupies the beginning of the same SCM script space used for running scripts, so any code written to the global variable space can be executed, or launched and remain active like any other running script. The purpose of this topic is to document strategies used with San Andreas on PC and Android in the hope that these methods can be adapted to other GTA games, environments without custom cleo scripts, or as an alternative to a custom main.scm.

 

Busted: Some strategies described in this documentation are not working as expected. Most notably, when a global variable is used in an embedded script then global variable space is added at the start of SCM and embedded codes are offset accordingly, leading to incorrect jump addresses. Avoiding all global variables or manually adjusting each jump address are both terrible options. I am looking for other solutions.

 

Overview:

 

Hijacking a Running Script: Custom scripts are compiled with Sanny Builder. A hex editor is used to copy the binary code in the compiled script and paste it into the global variable space of a saved game. A currently running script is modified with a hex editor so the next instruction (relativeIP) will begin at the address of the codes embedded in the global variable space. The custom codes end with a jump to the original relativeIP so the running script can resume normal operation.

 

Reclaiming Global Variable Space from SCM Memory: Several standard arrays provide a little room for temporary code in a standard save, but there is little room for permanent code. However, much of the SCM script beyond the global variable space is no longer needed when save data is loaded. Several thousand bytes of SCM memory can be recovered by permanently extending the global variable space and used to store compiled scripts.

 

Managing Jump Addresses: Sanny Builder will accurately encode the proper jump addresses for labels if the script is compiled with the codes located at the proper offset in a custom main.scm file. This “NOPped” main includes only what is necessary to compile and decompile properly as an SCM file, jump addresses for the future location of the scripts, and thousands of 0000: NOP instructions. When the nopped main is decompiled, custom labels are inserted into the sea of nops and provide reference for the accurate placement of the codes.

 

 

Example Scripts:

 

Basic Test: Hijack a running script to display a message. A simple test of the basic strategy.

Extend Variable Space: Hijack a running script to increase the size of the global variable space and initialize the variables.

Launch a Launcher: Hijack a running script to launch a new permanently running script that can easily launch other scripts.

Save Anywhere: A useful tool using basic scm code and test of user input. Should work the same regardless of version.

Warp to Marker: Reading memory using ADMA addressing. Highly depended on version.

 

 

Testing with Cleo: The goal is to avoid using cleo opcodes and to run custom scripts in environments without cleo scripts, but I'm not the least bit shy about using cleo scripts while testing. In particular, all complex scripts are tested as cleo scripts on PC with Cleo4, or Android with CleoA. When the script is working properly the codes can be copied to the nopped main (without the {cleo. cs} directive) and compile without any additional modification.

 

Working With a Hex Editor: It should be possible to embed scripts with a free hex editor like HxD, and I hope to provide enough reference that players can find the appropriate offsets, but this is a complex task and I don't want to handicap myself too much so I'll be describing the hex editing process based on the 010 Editor. This proprietary tool supports binary templates that can identify saves of different versions and systems, parse save files into organized menus, and display and edit information in familiar formats.Fortunately, 010 has a generous trial period. There is reasonably good documentation for SA PC save files available, but only the SA binary templates can describe PS2 and mobile saves properly.

 

Fixing the Checksum: Whenever a save file is modified the 32-bit CRC checksum occupying the last 4 bytes of the save must be updated to the sum of all bytes or a corruption message will be displayed in the slot and the save can't be loaded. This is such an automatic process for me that I often forget to mention it, so this might be the only time I'll bring it up. Edit the save, update the checksum, every time before saving. The binary template package for 010 contains a checksum script; just run it on the save. HxD has a checksum-32 tool. Run it on the whole file after clearing the last 4 bytes, and encode in little endian. Most save editors, including GTASnP.com, will update the checksum when saving.

 

 

Basic Test Script:

 

The script will wait just long enough for the screen to fade in and to verify the save loaded properly, then display a message, play a tune and jump to the end_thread command. The jump address will be modified to match the original relativeIP of the hijacked script. The end_thread statement is needed here for the cleo version to work, but won't be executed in the embedded version.

 

{$CLEO .cs} // {$CLEO .csa}
// BasicTest.txt

0001: wait 4000 ms
00BA: show_text_styled GXT 'FESZ_LS' time 4000 style 4 // Load Successful.
0394: play_music 2 // Mission Complete!
0002: jump @End

:End
004E: end_thread

 

This compiles to:

01 00 05 A0 0F BA 00 09 46 45 53 5A 5F 4C 53 00
05 A0 0F 04 04 94 03 04 02 02 00 01 E0 FF FF FF
4E 00

 

E0 FF FF FF is the jump address that needs to be changed. By happy accident, this value falls evenly within a single global variable, which makes it a bit easier to edit with the 010 template. The address is specific to each save so I make this edit after embedding the code.

 

$Roulette_Cash_Won Array (151i, 604 bytes) * not defined in INIs

 

VarNum* 4  = SCM Offset + Offset from start of file = *Global Offset 

$8398  *  4  =  33592  +  326  =  33918  PC v1/v2

$9765  *  4  =  39060  +  442  =  39502  Mobile

$8397  *  4  =  33588  +  342  =  33930  PS2 v1

$8402  *  4  =  33608  +  342  =  33950  PS2 v2

*Global in this case is a GoTo setting for start of file.

 

The $Roulette_Cash_Won Array is my preferred workspace for temporary codes. It's a bit smaller than some of the mission string arrays but provides an in-game method to erase the temporary code – just start the roulette mission. The embedded codes don't seem to effect the payout and the array is reinitialized at the end.

 

 

Pasting Binary Code into the Global Variable Space:

 

010: Open Save Blocks > Block 01: CTheScripts > GlobalVars and scroll to the appropriate variable for your version. Selecting the variable in the template will highlight the variable in the hex window. Paste the compiled code and overwrite the existing data in the hex window, starting at the beginning of this variable.

 

HxD: The global variable space is in a static location for each specific type of save, 4 bytes after the second BLOCK marker that separate each save block. The SimpleVars stored in block 0 are always the same size, so the GoTo offset of a global variable can be calculated as $VarNum * 4 + StartOfVarSpace. However, since the variable space can, and will, change size, and the number of running scripts will vary, static offsets are of little use for anything found later in the save.


 

Hijacking a Running Script:

 

010: Open Save Blocks > Block 01: CTheScripts > tRunningScripts > Script[1], usually 'oddveh' (script 2 on PS2). RelativeIP is near the bottom. Record the current value; it is needed as the jump address at the end of the embedded code. Change the RelativeIP to the local offset for the start of the Cash Won array. 010 will manage hex and decimal conversions, so format doesn't matter.

 

HxD: Calculating the offsets of running scripts in a save is a bit awkward, I'll work towards providing full reference. Running scripts is near the end of TheScripts block and usually contain a text script name so they are easy to find by searching for the third BLOCK (block 2) and scrolling backward until the script name is seen. Or just search for the script name. The offset for the script name on PC and PS2 is +10 from the start of the running script, and the offset to the relativeIP is +226, so +216 from the start of the script name. Offsets on mobile are +14 for script name and +262 for relativeIP, so +248 from script name.

 

Fix the Embedded Jump:

 

010: Return to where the the codes were embedded in the variable space and find the cleo version of the jump address: E0 FF FF FF. Right-click on the hex data and select Jump to Template variable – this will take you directly to the global variable that corresponds to this address. Enter the value recorded from the original relativeIP.

 

HxD: Pretty much the same process. The latest versions of HxD have an Inspector tool that can display and edit the hex data in familiar formats.

 

Fix the checksum, save and test. There is little that can go wrong with the embedded codes, so this should simply be a test of working out the basic strategies and putting together all of the pieces into a working script.

 

 

Extend Variable Space:

 

The next goal is to recover enough space to store larger scripts. The script will extend the variable space, initialize the variables to 0, and display a success message and tune before jumping back to the hijacked script using the same strategies described above. The main difference between this script and the Basic Test is that the initialization process will include a loop, so jump_to and jump_if addresses will need to correspond to the variable space.

 

Note: There is a direct relationship between the variable space in a save, SCM memory, and the beginning of an SCM file when viewed with a hex editor. First the scm file is loaded into SCM memory, then the global variable space is written over the beginning of SCM memory when the save is loaded.

 

 

NOPmain.scm: This is a non-viable scm file. It's purpose is to properly encode jump addresses for embedded scripts. To gain access to as much memory as possible, a stripped main was modified to remove as much code as possible but still remain a valid scm file that can be compiled and decompiled with Sanny.

 

// This file is intended to be used with embedded scripts in GTA San Andreas. It is not a viable main.scm.
DEFINE OBJECTS 1
DEFINE OBJECT SANNY BUILDER 3.2.2

DEFINE MISSIONS 0

DEFINE EXTERNAL_SCRIPTS -1 // Use -1 in order not to compile AAA script

DEFINE UNKNOWN_EMPTY_SEGMENT 0

DEFINE UNKNOWN_THREADS_MEMORY 0

//-------------MAIN---------------
0000: NOP

:MAIN
0001: wait 250 ms
0002: jump @MAIN
0000: NOP

 

This file needs to be padded with lots of 0000: NOP (no operation) instructions to provide working space for the embedded script. Lots, as in about 100,000 are needed to account for all 200,000 bytes of SCM memory. About one third of that is plenty. Still, it's a lot easier to compile this short version and insert 200,000 bytes at the end of the file using a hex editor than it is to copy and paste the opcodes. Decompile the larger file.

 

Add the appropriate jump destinations after the lines posted above. These will be used in combination with customlabels.ini to insert labels at the correct jump address.

 

// SA PC ------- Nopped Main --------
0002: jump 33592 // = Roulette_Cash_Won $8398 * 4
0002: jump 43808 // = Extended_VarSpace $10952 * 4
0002: jump 54000 // = Launch $13500 * 4
0002: jump 54040 // = OSRSave $13510 * 4
0002: jump 54120 // = OSRWarp $13530 * 4

 

// SA Mobile ------- Nopped Main --------
0002: jump 39060 // = Roulette_Cash_Won $9765 * 4
0002: jump 49212 // = Extended_VarSpace $12303 * 4
0002: jump 54000 // = Launch $13500 * 4
0002: jump 54040 // = OSRSave $13510 * 4
0002: jump 54120 // = OSRWarp $13530 * 4

 

Add to CustomLabels.ini.

Enable Custom Labels in Tools > Options > Formats

; SA PC ------- Nopped Main --------
33592 = Roulette_Cash_Won // $8398 * 4
43808 = Extended_VarSpace // $10952 * 4
54000 = Launch // $13500 * 4
54040 = OSRSave // $13510 * 4
54120 = OSRWarp // $13530 * 4

 

; SA Mobile ------- Nopped Main --------
39060 = Roulette_Cash_Won // $9765 * 4
49212 = Extended_VarSpace // $12303 * 4
54000 = Launch // $13500 * 4
54040 = OSRSave // $13510 * 4
54120 = OSRWarp // $13530 * 4

 

Note that when codes are added after a label, the offsets for labels later in the script will no longer be aligned at the proper offset. I keep reverting to a saved scm for new scripts.

 

 

VarSpace Script:

 

&3([email protected],1i) uses ADMA formatting to change the dword that starts at $0 +3 bytes. This value controls how much memory will be saved for the global variables, and spans $0 and $1. Since these two globals are protected by Sanny, ADMA is the easiest way to manage this variable anyway. (The VarSpaceSize stored just before the global variables in the save file determines how much save data will be loaded into memory.)

 

To increase compatibility, I'm restricting the expanded variable space to what is available to PC v2. A little more space could be squeezed out of v1.

 

The padding is added so the last jump address will land evenly on a global, but this isn't strictly necessary.

 

Reworked to avoid using global variables.

{$CLEO .cs}
//PCVarSpace.txt

// Reclaim Global Variable Space from SCM
// GTASA PC v1/v2
// $10952 thru $15006 (+4055)

wait 4000 ms

[email protected] = 0
&3([email protected],1i) = 60028 // Set size of variable space

for [email protected] = 10952 to 15006 step 1
  &0([email protected],1i) = 0
end

//padding
0000: NOP

00BA: show_text_styled GXT 'FESZ_LS' time 4000 style 4 // Load Successful.
0394: play_music 2 // Mission Complete!
0002: jump @End

:End
004E: end_thread

 

{$CLEO .csa}
//MobileVarSpace.txt

// Reclaim Global Variable Space from SCM
// GTASA Android 1.08
// $12303 thru $16809 (+4507)

wait 4000

[email protected] = 0
&3([email protected],1i) = 67240 // Set size of variable space

for [email protected] = 0 to 4505 step 1
  $12303([email protected],1i) = 0
end

//padding
wait 256 ms
0000: NOP

00BA: show_text_styled GXT 'FESZ_LS' time 4000 style 4 // Load Successful.
0394: play_music 2 // Mission Complete!
0002: jump @End

:End
004E: end_thread

 

Decompile NOPmain.scm with custom labels. Insert everything except the cleo directive into the scm script just after the custom label for the Cash Won array. Compile the script and open with a hex editor. Find the compiled codes within a mostly blank scm, or at the specified offset, and copy to the Cash Won array in the global variable space of the save. Update the relativeIP of a running script and the final jump address in the embedded code to complete the hijacking process as before.

 

Update, save, test and save the game. Open the modified save with a hex editor to observe the changes to the variable space.

 

One issue I've encountered on mobile saves, if enough scripts are launched, objects, cars or peds added by the screwy checkpoint system, or cargens (and stunt jumps) added by mods, it's not too difficult for a normal safehouse-type save to become so bloated that the save gets bumped up into the next larger size. This doesn't effect anything other than the 010 template, which detects the large save as “MissionThread” save and confuses how things are parsed, so I have not been allocating the max possible variable space on mobile saves.

 

I'm not sure how I want to manage over 16,000 bytes of SCM memory. I want to reserve a lot of space for global variables that can be used by scripts, and it makes sense for them to be contiguous with the standard global variables so I don't want to add code at the beginning. I also don't want to get too deep into the available space as I'm not sure of the optimal variable space to reserve for mobile due to issues with the size of the save. So for these examples I've decided to start compiling code at global variable $13500, offset 54000. This will allow for almost 1200 global variables that can be common between all script versions, and new scripts can occupy the same space on any system.

 

 

Launching a Launch Running Script:

 

This process will require embedding two scripts, a permanently running launch script stored in the expanded variable space, and a hijacked script to launch it. Hijacking gets tedious, so a launch script will be handy, but there isn't much of a payoff until the next running script is ready.

 

The launcher script is shorter than what has already been written to the Cash Won array. It might be easier to identify the proper jump address if the array was cleared of existing data. Compile Launcher as a cleo script and embed to the Cash Won array. Hijack a script to run the code. Finish the next script before testing.

 

{$CLEO .cs}
//Launcher.txt
// Hijack strategy to launch a script

wait 4000
004F: create_thread 54000 // Launch
00BA: show_text_styled GXT 'LAUNCH' time 4000 style 4 // Launch
0002: jump @End

:End
004E: end_thread

 

 

Launch as a Running Script:

 

This script is installed to $13500 using a nopped main and will launch a script at the non-zero address assigned to $13499, and then reset the variable. The script will remain active in the save and will launch all new remaining scripts.

 

{$CLEO .cs}
//Launch.txt
03A4: name_thread 'LAUNCH'

:Launch
0001: wait 4000
00D6: if
8038:   not $13499 == 0
004D: jump_if_false @Launch
004F: create_thread $13499
00BB: show_text_lowpriority GXT 'LAUNCH' time 4000 flag 1 // Launch
0004: $13499 = 0
0002: jump @Launch

 

 

Save Anywhere:

 

The save script is installed with a nopped main to $13510, offset 54040. Launch the script by changing $13499 to 54040. Activate the script by holding the group backwards button, usually H when using keyboard controls, or down on the D-pad when using a controller. This script should work the same on PC and PS2.

 

{$CLEO .cs}
03A4: name_thread 'OSRSAVE'

:OSRSAVE
0006: [email protected] = 0

:Holding
0001: wait 0 ms
00D6: if
00E1:   player 0 pressed_key 9 //~k~~GROUP_CONTROL_BWD~
004D: jump_if_false @OSRSAVE

00D6: if and
0038:   $ONMISSION == 0
0019:   [email protected] > 4000
004D: jump_if_false @Holding

03D8: show_save_screen
0002: jump @OSRSAVE

 

The variations in mobile version are for the different input controls – the user must hold the weapon widget. Mobile has more local variables than PC and PS2, so the first local timer is [email protected] instead of [email protected] (A little padding was added to keep the byte count even. The goal is to have any gaps between scripts filled with 2-byte NOPs so the whole block could be decompiled with Sanny without throwing any errors.)

 

{$CLEO .csa}
03A4: name_thread 'OSRSAVE'

:OSRSAVE
0006: [email protected] = 0

:Holding
0001: wait 0 ms
00D6: if
0A51:   is_widget_pressed 159 // Weapon Widget
004D: jump_if_false @OSRSAVE

00D6: if and
0038:   $ONMISSION == 0
0019:   [email protected] > 3000
004D: jump_if_false @Holding
03D8: show_save_screen
0002: jump @OSRSAVE

 

 

Warp to Marker:

 

This script uses an ADMA method to read memory as a substitute for cleo commands. Working with memory makes these scripts specific to a v1 executable on PC, and Android 1.08. I don't have the information or hardware needed to properly convert and test this script on other systems -- other than PS2, maybe, if I can get it all working one more time. PS2 saves are awkward to manage, so I'll put this off pending any interest.

 

OSRWarp is embedded at $13530 and launched at 54120. The differences between the scripts are the local timer variable, key press and widget checks, and memory addresses. CJ will warp to the map marker only if it is set when Conversation No – usually N, is held.

(Thanks again to ZAZ for the original teleport script.)

 

{$CLEO .cs}
03A4: name_thread 'osrwarp'
//osrwarp.txt
//PC v1

:OSRWARP
0006: [email protected] = 0

:Holding
0001: wait 0 ms
00D6: if
00E1:   player 0 pressed_key 10 //~k~~CONVERSATION_NO~
004D: jump_if_false @OSRWARP

00D6: if
0019:  [email protected] > 2000
004D: jump_if_false @Holding

0006: [email protected] = 0xBA6774 // read address // marker handle
000E: [email protected] -= 0xA49960 // start of scm
0016: [email protected] /= 4 // ADMA index

008B: [email protected] = &0([email protected],1i) // read marker handle

00D6: if
8039:   not [email protected] == 0
004D: jump_if_false @OSRWARP

0012: [email protected] *= 0x10000 // extract index
0016: [email protected] /= 0x10000
 
0006: [email protected] = 40 // size of record
006A: [email protected] *= [email protected] // index
000A: [email protected] += 8 // offet to coords
000A: [email protected] += 0xBA86F0 // read address // start of radar pool
000E: [email protected] -= 0xA49960 // start of scm
0016: [email protected] /= 4 // ADMA index
008B: [email protected] = &0([email protected],1i) // read X
008B: [email protected] = &4([email protected],1i) // read Y

0172: [email protected] = get_char_heading $PLAYER_ACTOR
01B4: set_player_control $PLAYER_CHAR to 0
04BB: set_area_visible 0
04FA: clear_extra_colours 0
057E: set_radar_as_interior 0
04E4: request_collision [email protected] [email protected]
03CB: load_scene [email protected] [email protected] 0.0
00D6: if
0256:   is_player_playing $PLAYER_CHAR
004D: goto_if_false @Finish
0860: set_char_area_visible $PLAYER_ACTOR to 0
00A1: set_char_coordinates $PLAYER_ACTOR to [email protected] [email protected] -100.0
0173: set_char_heading $PLAYER_ACTOR to [email protected]

:Finish
0001: wait 0
00D6: if
0256:   is_player_playing $PLAYER_CHAR
004D: goto_if_false @Finish

01B4: set_player_control $PLAYER_CHAR to 1
0373: set_camera_behind_player
02EB: restore_camera_jumpcut
0002: jump @OSRWARP
0001: wait 256

 

{$CLEO .csa}
03A4: name_thread 'osrwarp'
//osrwarp.txt
//Android v1.08

:OSRWARP
0006: [email protected] = 0

:Holding
0001: wait 0 ms
00D6: if
0A51:   is_widget_pressed 160 // Radar Widget
004D: jump_if_false @OSRWARP

00D6: if
0019:   [email protected] > 2000
004D: jump_if_false @Holding

0006: [email protected] = 0x63E090 // read address // marker handle
000E: [email protected] -= 0x71FFB0 // start of scm
0016: [email protected] /= 4 // ADMA index
 
008B: [email protected] = &0([email protected],1i) // read marker handle
00D6: if
8039:   not [email protected] == 0
004D: jump_if_false @OSRWARP

0012: [email protected] *= 0x10000 // extract index
0016: [email protected] /= 0x10000

0006: [email protected] = 40 // size of record
006A: [email protected] *= [email protected] // index
000A: [email protected] += 8 // offet to coords
000A: [email protected] += 0x8F0A80 // read address // start of radar pool
000E: [email protected] -= 0x71FFB0 // start of scm
0016: [email protected] /= 4 // ADMA index
008B: [email protected] = &0([email protected],1i) // read X
008B: [email protected] = &4([email protected],1i) // read Y

0172: [email protected] = get_char_heading $PLAYER_ACTOR
01B4: set_player_control $PLAYER_CHAR to 0
04BB: set_area_visible 0
04FA: clear_extra_colours 0
057E: set_radar_as_interior 0
04E4: request_collision [email protected] [email protected]
03CB: load_scene [email protected] [email protected] 0.0
00D6: if
0256:   is_player_playing $PLAYER_CHAR
004D: goto_if_false @Finish
0860: set_char_area_visible $PLAYER_ACTOR to 0
00A1: set_char_coordinates $PLAYER_ACTOR to [email protected] [email protected] -100.0
0173: set_char_heading $PLAYER_ACTOR to [email protected]

:Finish
0001: wait 0
00D6: if
0256:   is_player_playing $PLAYER_CHAR
004D: goto_if_false @Finish
01B4: set_player_control $PLAYER_CHAR to 1
0373: set_camera_behind_player
02EB: restore_camera_jumpcut
0002: jump @OSRWARP

 

References:

 

Notes:

  • PC save files with embedded scripts won't convert properly between v1 and v2.
  • Save files with expanded variable space may no longer work with some save editors (Savegame Editor 3.x).

 

This documentation is a work in progress.

 

Edited by OrionSR

Share this post


Link to post
Share on other sites
OrionSR
Posted (edited)

Alternate Strategies for Expanding the Global Variable Space:

 

I'm working on a strategy for embedding scripts that should be more compatible with the current version of Sanny Builder (v3.2.3). This will restrict most embedded scripts to the expanded variable space but should provide a reliable method of tricking Sanny into compiling the proper addresses.

 

One problem, the script that expands and initializes the expanded variable space still needs a place to run. It should be possible to manage this one script, but there are alternative strategies.

 

  • If you are editing for PC or Android with Cleo installed, use the cleo version of the script in the first post.
  • Use a hex editor to manually increase the size of the saved variable space by editing the dword starting at $0+3. then...
    Either load and save to let the game expand the varspace, then use an editor to initialize the variables,
    Or, also increase the VarSpaceSize at $0 -4 to match, insert bytes at the end of the varspace, and trim the file to length.
  • Use an 010 script to automate the hex editing process.

 

ExpandedVarSpace.1sc - 010 Script

Spoiler


//--------------------------------------
//--- 010 Editor v6.0.3 Script File
//
// File: ExpandVarSpace.1sc
// Author: OrionSR
// Revision: v0.1
// Purpose: Expand the variable space in GTA San Andreas save files.
//--------------------------------------

// The maximum VarSpace is not assigned for mobile because data might get trimmed.
// Refresh the template to make sure it can still complete execution after editing.


//Find Start of VarSpace
FSeek( FindFirst( "BLOCK*",1,0,1,0.0,1,FTell(),0,5 ) );
FSeek( FindNext(1)+9 );

int VarSpaceStart = FTell();
int OldVarSpaceSize = Save.CTheScripts.VarSpaceSize; // read from template variable
int FileEnd = FileSize();
int NewVarSpaceSize = 60028; // PCv2 default

if (isMobile == 1) NewVarSpaceSize = 60028; // max for mobile is 67240 
if (isPS2 == 1) NewVarSpaceSize = 60176; // PS2v1

// Set VarSpace to Load
Save.CTheScripts.VarSpaceSize = NewVarSpaceSize;


// Set VarSpace to Save
WriteInt(VarSpaceStart +3, NewVarSpaceSize);

// Insert extra VarSpace and Trim File to Original Length
InsertBytes( VarSpaceStart + OldVarSpaceSize, NewVarSpaceSize - OldVarSpaceSize, 0 );
DeleteBytes( FileEnd, NewVarSpaceSize - OldVarSpaceSize );

// Fix Checksum
WriteUInt(FileSize()-4, Checksum(CHECKSUM_BYTE, 0, FileSize()-4));

 

 

____________________________________________________

 

Can anyone offer any suggestions for custom text (GXT) in this context? This is a major limitation for embedded scripts.

 

How to edit protected memory? The VP strategies suggested by Seemann involve editing opcodes with assembly code, which is not likely to be of much help on anything other than PC. Still, I really like the idea of read and write memory scripts, or common subroutines, especially when managing image base on mobile-based games.


SCM Data Types - the byte before data (useful for reading compiled SCM code)

Spoiler

From Cleo.h

//operand types
#define	globalVar			2		//$
#define	localVar			3		//@
#define	globalArr			7		//$(,)
#define	localArr 			8		//@(,)
#define	imm8 				4		//char
#define	imm16 				5		//short
#define	imm32 				1		//long, unsigned long
#define	imm32f 				6		//float
#define	vstring 			0x0E	//""
#define	sstring 			9		//''
#define	globalVarVString 	0x10	//v$
#define	localVarVString 	0x11	//@v
#define	globalVarSString 	0x0A	//s$
#define	localVarSString 	0x0B	//@s

 

 

 

Edited by OrionSR

Share this post


Link to post
Share on other sites
OrionSR
Posted (edited)

Embedding Cleo in Save Files

 

Seemann suggested Cleo1, an SCM version of Cleo with a limited set of opcodes, as a template for creating an embedded version of Cleo for PC v1. This script was easy to convert to this purpose with few modifications. I'm still having problems with my jump addresses so I won't include instructions for installation. However, I have a demo save. This demo save can be used to load cleo for scripts in your saves.

 

https://gtasnp.com/GjL2TN

 

The Embedded Cleo1 save will launch 3 scripts - wait until it settles. 

 

Cleo_Run will set the cleo opcodes and end.

Ewarp will warp to marker when N is held. 

Esave will save when H is held.

 

Ewarp uses the Cleo opcode to read memory - the primary test.

Esave uses standard SCM, but doesn't check $OnMission because globals are still confusing my jump address.


If this save is resaved and immediately loaded, the warp script still works, so the cleo opcodes persist during the session. Restart the game and reload the re-saved version - the save script still works but warping will crash the game.

 

This saved is based on a debug version of a Chain Game save which has been heavily modified, so ignore or enjoy the weirdness. CJ has been buffed and the map has been opened to help with testing, but the phone calls are bugged so the early LS strand can't be finished.

CLEO_RUN as Embedded:

  • Contains reference on which opcodes were added.
  • This limited set of opcodes are compatible with the current sascm.ini for PC.
     
Spoiler

Basically, I added the end_thread and commented out the stripped MAIN codes. I was afraid to touch anything else.

//{$Cleo .s}
//{$VERSION 3.1.0020}

// Provides executing each time the game started
gosub @CLEO_RUN
wait 250
end_thread
/*
03A4: name_thread 'MAIN'
var              
 $PLAYER_CHAR: Player 
end // var
01F0: set_max_wanted_level_to 6 
0111: toggle_wasted_busted_check 0 
00C0: set_current_time 15 0 
04E4: unknown_refresh_game_renderer_at 824.0641 -1794.8955 
Camera.SetAtPos(2488.562, -1666.865, 12.8757)
$PLAYER_CHAR = Player.Create(#NULL, 2488.562, -1666.865, 12.8757)
$PLAYER_ACTOR = Actor.EmulateFromPlayer($PLAYER_CHAR)
07AF: $PLAYER_GROUP = player $PLAYER_CHAR group
Camera.SetBehindPlayer
set_weather 0          
wait 0 ms                                
$PLAYER_CHAR.SetClothes("PLAYER_FACE", "HEAD", Head)
$PLAYER_CHAR.SetClothes("JEANSDENIM", "JEANS", Legs)
$PLAYER_CHAR.SetClothes("SNEAKERBINCBLK", "SNEAKER", Shoes)
$PLAYER_CHAR.SetClothes("VEST", "VEST", Torso)
$PLAYER_CHAR.Build
$PLAYER_CHAR.CanMove = True
fade 1 (out) 0 ms                     
select_interior 0
0629: change_stat 181 (islands unlocked) to 4
016C: restart_if_wasted at 2027.77 -1420.52 15.99 angle 137.0 for_town_number 0
016D: restart_if_busted at 1550.68 -1675.49 14.51 angle 90.0 for_town_number 0
0180: set_on_mission_flag_to $ONMISSION // Note: your missions have to use the variable defined here ($ONMISSION)
03E6: remove_text_box
0330: toggle_player $PLAYER_CHAR infinite_run 1

// show custom text
0A8E: [email protected] = 0xA49964 + @_TextPtr // MainScm + 2 bytes of opcode + 2 bytes of datatype and string length + label
0AA5: call 0x588BE0 num_params 4 pop 4 0 0 0 [email protected]

// create a car with the only opcode
0AA5: call 0x43A0B6 num_params 1 pop 1 #INFERNUS

end_thread

:_TextPtr
0900: "Custom Text Here"
0000: null-terminator

*/
{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

     Project "CLEO"

      v1.29 Final
 
   by Seemann (c) 2007

 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 http://www.gtaforums.com/index.php?showtopic=268976
 http://www.sannybuilder.com/forums/viewtopic.php?id=62
       
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~    
      Opcodes List:
 
  0A8C: write_memory <dword> size <byte> value <dword> virtual_protect <bool>
  0A8D: <var> = read_memory <int32> size <byte> virtual_protect <bool>
  0A8E: <var> = <valA> + <valB> // int
  0A8F: <var> = <valA> - <valB> // int
  0A90: <var> = <valA> * <valB> // int
  0A91: <var> = <valA> / <valB> // int
  
  0A96: <var> = actor <handle> struct
  0A97: <var> = car <handle> struct    
  0A98: <var> = object <handle> struct

  0A99: chdir <flag>
  0A9A: <var> = openfile "path" mode <dword>
  0A9B: closefile <hFile>   
  0A9C: <var> = file <hFile> size
  0A9D: readfile <hFile> size <dword> to <var>  
  0A9E: writefile <hFile> size <dword> from <var>

  0A9F: <var> = current_thread_address
  0AA0: gosub_if_false <label>
  0AA1: return_if_false

  0AA2: <var> = load_library "path"
  0AA3: free_library <hLib>
  0AA4: <var> = get_proc_address "name" library <hLib>

  0AA5: call <address> num_params <byte> pop <byte> [param1, param2...]
  0AA6: call_method <address> struct <address> num_params <byte> pop <byte> [param1, param2...]
          
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      SASCM.INI Lines
 
  0A8C=4,write_memory %1d% size %2d% value %3d% virtual_protect %4d%
  0A8D=4,%4d% = read_memory %1d% size %2d% virtual_protect %3d%
  0A8E=3,%3d% = %1d% + %2d% ; int
  0A8F=3,%3d% = %1d% - %2d% ; int
  0A90=3,%3d% = %1d% * %2d% ; int
  0A91=3,%3d% = %1d% / %2d% ; int

  0A96=2,%2d% = actor %1d% struct
  0A97=2,%2d% = car %1d% struct
  0A98=2,%2d% = object %1d% struct

  0A99=1,chdir %1b:userdir/rootdir%
  0A9A=3,%3d% = openfile %1s% mode %2d% // IF and SET
  0A9B=1,closefile %1d%
  0A9C=2,%2d% = file %1d% size
  0A9D=3,readfile %1d% size %2d% to %3d%
  0A9E=3,writefile %1d% size %2d% from %3d%
  0A9F=1,%1d% = current_thread_pointer

  0AA0=1,gosub_if_false %1p%
  0AA1=0,return_if_false
  
  0AA2=2,%2h% = load_library %1s% // IF and SET
  0AA3=1,free_library %1h%
  0AA4=3,%3d% = get_proc_address %1s% library %2d%
  0AA5=-1,call %1d% num_params %2h% pop %3h%
  0AA6=-1,call_method %1d% struct %2d% params %3h% pop %4h%  
                       
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
}
         
:CLEO_RUN
[email protected] = -429566
&0([email protected],1i) == 4611680
jf @CLEO_v2 // 1.0
[email protected] = -429539
&0([email protected],1i) =  0xA49960
&0([email protected],1i) += @CLEO_HANDLER
0485: return_true
return

:CLEO_v2
059A: return_false
return

{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

 DO NOT EVER CHANGE THE HANDLER CODE!

 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
}
:CLEO_HANDLER //  0A8C - 0AEF
hex
 {
  EAX = CurrentOpcode
  ECX = ThreadPointer
  EDX = EIP
  ESI = ECX
  ESP +0 = ret_addr
      +4 = opcode
      +8 = ecx
        
  DO NOT CHANGE ESI!
 } 
 8B 44 24 04            // mov eax, [esp+4+Opcode]
 66 2D 8C 0A            // sub ax, 0x0A8C
 BA @CLEO_Pointers      // mov edx, @CLEO_Pointers
 8B 94 82 60 99 A4 00   // mov edx, [0xA49960+eax*4+edx]
 81 C2 60 99 A4 00      // add edx, 0xA49960
 FF E2                  // jmp edx
end

{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    CLEO Pointers Table
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
}
:CLEO_Pointers
hex
 @CLEO_Opcode0A8C
 @CLEO_Opcode0A8D
 @CLEO_Opcode0A8E
 @CLEO_Opcode0A8F
 @CLEO_Opcode0A90
 @CLEO_Opcode0A91
 @CLEO_Opcode0A92 // reserved
 @CLEO_Opcode0A93 // reserved
 @CLEO_Opcode0A94 // reserved  
 @CLEO_Opcode0A95 // reserved
 @CLEO_Opcode0A96     
 @CLEO_Opcode0A97
 @CLEO_Opcode0A98  
 @CLEO_Opcode0A99 
 @CLEO_Opcode0A9A
 @CLEO_Opcode0A9B
 @CLEO_Opcode0A9C 
 @CLEO_Opcode0A9D 
 @CLEO_Opcode0A9E
 @CLEO_Opcode0A9F
 @CLEO_Opcode0AA0
 @CLEO_Opcode0AA1
 @CLEO_Opcode0AA2
 @CLEO_Opcode0AA3   
 @CLEO_Opcode0AA4 
 @CLEO_Opcode0AA5 
 @CLEO_Opcode0AA6 
end

{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     Opcode 0A8C
 0A8C: write_memory (1) size (2) value (3) virtual_protect (4)
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
}

:CLEO_Opcode0A8C
hex
 6A 04                  // push 4
 B8 80 40 46 00 FF D0   // call CollectNumberParams

 83 3D 84 3C A4 00 01   // cmp VirtualProtect, 1  (p4)
 75 08                  // jnz @MOVedx
 6A 04                  // push 4
 E8 40 00 00 00         // call VirtualProtect+64
 58                     // pop eax

 8B 15 78 3C A4 00      // mov edx, Address (p1)
 8B 0D 7C 3C A4 00      // mov ecx, Size    (p2)
 8B 05 80 3C A4 00      // mov eax, Value   (p3)

 83 F9 01               // cmp Size, 1 
 75 04                  // jnz @word
 88 02                  // mov [Address], al
 EB 0C                  // jmp @ret  
 83 F9 02               // cmp Size, 2
 75 05                  // jnz @dword
 66 89 02               // mov [Address], ax
 EB 02                  // jmp @ret
 89 02                  // mov [Address], eax

 83 3D 84 3C A4 00 01   // cmp VirtualProtect, 1  (p4)
 75 0C                  // jnz @ret
 FF 35 F4 3C A4 00      // push oldProtect
 E8 04 00 00 00         // call VirtualProtect+4
 58                     // pop eax
 32 C0                  // xor al, al 
 C2 04 00               // retn 4
end

{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 CLEO Virtual Protect Subroutine
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
}
hex
 68 F4 3C A4 00         // push offset oldProtect
 FF 74 24 08            // push [esp+8+NewProtect]
 FF 35 7C 3C A4 00      // push Size
 FF 35 78 3C A4 00      // push Address
 FF 15 2C 80 85 00      // call VirtualProtect
 C3                     // ret
end
{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     Opcode 0A8D
 0A8D: (1) = read_memory (2) size (3) virtual_protect (4)
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
}
:CLEO_Opcode0A8D
hex
 6A 03                  // push 3
 B8 80 40 46 00 FF D0   // call CollectNumberParams

 83 3D 80 3C A4 00 01   // cmp VirtualProtect, 1  (p3)
 75 08                  // jnz @MOVedx
 6A 04                  // push 4
 E8 CB FF FF FF         // call VirtualProtect-53
 58                     // pop eax

 8B 05 78 3C A4 00      // mov eax, Address (p1)
 31 C9                  // xor ecx, ecx
 BA 78 3C A4 00         // mov edx, offset Result (p1)
 89 0A                  // mov [edx], ecx
 8B 0D 7C 3C A4 00      // mov ecx, Size (p2)

 83 F9 01               // cmp ecx, 1
 75 06                  // jnz @2
 8A 00                  // mov al, [eax]
 88 02                  // mov [edx], al
 EB 11                  // jmp @write 

 83 F9 02               // cmp ecx, 2
 75 08                  // jnz @4
 66 8B 00               // mov ax, [eax]
 66 89 02               // mov [edx], ax
 EB 04                  // jmp @write

 8B 00                  // mov eax, [eax]
 89 02                  // mov [edx], eax

 83 3D 80 3C A4 00 01   // cmp VirtualProtect, 1  (p3)
 75 0C                  // jnz @ret
 FF 35 F4 3C A4 00      // push oldProtect
 E8 85 FF FF FF         // call VirtualProtect-123
 58                     // pop eax

 6A 01                  // push 1
 8B CE                  // mov ecx, esi
 B8 70 43 46 00 FF D0   // call WriteResult
 32 C0                  // xor al, al  
 C2 04 00               // retn 4
end


{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     Opcode 0A8E
  0A8E: (1) = (2) + (3) // int
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
}
:CLEO_Opcode0A8E
{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     Opcode 0A8F
  0A8F: (1) = (2) - (3) // int
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
}
:CLEO_Opcode0A8F
{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     Opcode 0A90
  0A90: (1) = (2) * (3) // int
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
}
:CLEO_Opcode0A90
{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     Opcode 0A91
  0A91: (1) = (2) / (3) // int
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
}
:CLEO_Opcode0A91
hex
 50                     // push eax
 6A 02                  // push 2
 B8 80 40 46 00 FF D0   // call CollectNumberParams
 A1 78 3C A4 00         // mov eax, p1
 8B 15 7C 3C A4 00      // mov edx, p2 
 59                     // pop ecx
 // ADD EAX, EDX
 83 F9 02               // cmp ecx, 2 (opcode 0A8E)
 75 04                  // jnz @opcode0A8F 
 01 D0                  // add eax, edx
 EB 19                  // jmp @write
 // SUB EAX, EDX
 83 F9 03               // cmp ecx, 3 (opcode 0A8F)
 75 04                  // jnz @opcode0A90 
 29 D0                  // sub eax, edx
 EB 10                  // jmp @write
 // MUL EAX, EDX
 83 F9 04               // cmp ecx, 4 (opcode 0A90)
 75 04                  // jnz @opcode0A91 
 F7 EA                  // imul edx
 EB 07                  // jmp @write
 // DIV EAX, P2
 99                     // cdq
 F7 3D 7C 3C A4 00      // idiv p2    (opcode 0A91)
 A3 78 3C A4 00         // mov p1, eax
 6A 01                  // push 1
 8B CE                  // mov ecx, esi
 B8 70 43 46 00 FF D0   // call WriteResult
 32 C0                  // xor al, al
 C2 04 00               // retn 4
end 

{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     Opcode 0A92
  0A92: (1) = (2) + (3) // float
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
}
:CLEO_Opcode0A92
{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     Opcode 0A93
  0A93: (1) = (2) - (3) // float
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
}
:CLEO_Opcode0A93
{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     Opcode 0A94
  0A94: (1) = (2) * (3) // float
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
}
:CLEO_Opcode0A94
{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     Opcode 0A95
  0A95: (1) = (2) / (3) // float
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
}
:CLEO_Opcode0A95
hex
 32 C0                  // xor al, al
 C2 04 00               // ret 4
end

{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     Opcode 0A96
  0A96: (1) = actor (2) struct
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
}
:CLEO_Opcode0A96
{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     Opcode 0A97
  0A97: (1) = car (2) struct
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
}
:CLEO_Opcode0A97
{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     Opcode 0A98
  0A98: (1) = object (2) struct
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
}
:CLEO_Opcode0A98
hex
 50                     // push eax
 6A 01                  // push 1
 B8 80 40 46 00 FF D0   // call CollectNumberParams
 59                     // pop ecx

 FF 35 78 3C A4 00      // push Handle (p1)
 // 0A96: ACTOR.STRUCT
 83 F9 0A               // cmp ecx, 10 (opcode 0A96)
 75 0F                  // jnz @opcode0A97 
 8B 0D 90 44 B7 00      // mov ecx, @CActors
 B8 10 49 40 00 FF D0   // call GetActorPointer
 EB 21                  // jmp @write 
 // 0A97: CAR.STRUCT
 83 F9 0B               // cmp ecx, 11 (opcode 0A97)
 75 0F                  // jnz @opcode0A98 
 8B 0D 94 44 B7 00      // mov ecx, @CVehicles
 B8 E0 48 40 00 FF D0   // call GetCarPointer
 EB 0D                  // jmp @write 
 // 0A98: OBJECT.STRUCT
 8B 0D 9C 44 B7 00      // mov ecx, @CObjects (opcode 0A98)
 B8 40 50 46 00 FF D0   // call GetObjectPointer

 6A 01                  // push 1
 8B CE                  // mov ecx, esi
 A3 78 3C A4 00         // mov p1, eax
 B8 70 43 46 00 FF D0   // call WriteResult
 32 C0                  // xor al, al
 C2 04 00               // ret 4
end

{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     Opcode 0A99
  0A99: chdir <flag>
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
}
:CLEO_Opcode0A99
hex
 6A 01                  // push 1
 B8 80 40 46 00 FF D0   // call CollectNumberParams
 // test flag
 A1 78 3C A4 00         // mov eax, Flag (p1)
 85 C0                  // test eax, eax
 74 0E                  // jz @root
 83 F8 01               // cmp eax, 1
 74 15                  // jnz @exit
 // Flag=1; UserDir
 B8 60 88 53 00 FF D0   // call SetUserDirToCurrent
 EB 0F                  // jmp @exit                       
 // Flag=0; RootDir
 68 54 8B 85 00         // push offset Null
 B8 D0 87 53 00 FF D0   // call ChDir 
 83 C4 04               // add esp, 4
 32 C0                  // xor al, al
 C2 04 00               // ret 4    
end

{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     Opcode 0A9A
  0A9A: <var> = openfile "path" mode <dword>
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
}
:CLEO_Opcode0A9A
hex       
 81 EC 80 00 00 00      // sub esp, 128
 6A 64                  // push 100
 8D 44 24 04            // lea eax, [esp+4]
 50                     // push eax

 B8 50 3D 46 00 FF D0   // call GetStringParam
 6A 01                  // push 1
 8B CE                  // mov ecx, esi
 B8 80 40 46 00 FF D0   // call CollectNumberParams

 68 78 3C A4 00         // push offset p1
 8D 44 24 04            // lea eax, [esp+4]
 50                     // push eax

 B8 00 89 53 00 FF D0   // call fopen

 6A 01                  // push 1
 8B CE                  // mov ecx, esi
 A3 78 3C A4 00         // mov p1, eax
 
 31 D2                  // xor edx, edx
 85 C0                  // test eax, eax
 0F 95 C2               // setnz dl
 52                     // push edx
 B8 D0 59 48 00 FF D0   // call SetConditionResult

 B8 70 43 46 00 FF D0   // call WriteResult
 81 C4 88 00 00 00      // add esp, 136
 32 C0                  // xor al, al
 C2 04 00               // ret 4
end

{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     Opcode 0A9B
  0A9B: closefile <hFile>
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
}
:CLEO_Opcode0A9B
hex
 6A 01                  // push 1
 B8 80 40 46 00 FF D0   // call CollectNumberParams
 FF 35 78 3C A4 00      // push hFile (p1)
 B8 D0 89 53 00 FF D0   // call CloseFile
 58                     // pop eax
 32 C0                  // xor al, al
 C2 04 00               // ret 4
end

{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     Opcode 0A9C
  0A9C: <var> = file <hFile> size 
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
}
:CLEO_Opcode0A9C
hex
 6A 01                  // push 1
 B8 80 40 46 00 FF D0   // call CollectNumberParams

 FF 35 78 3C A4 00      // push hFile (p1)
 B8 E0 89 53 00 FF D0   // call GetFileSize
 6A 01                  // push 1
 8B CE                  // mov ecx, esi
 A3 78 3C A4 00         // mov p1, eax
 B8 70 43 46 00 FF D0   // call WriteResult

 58                     // pop eax
 32 C0                  // xor al, al
 C2 04 00               // ret 4
end


{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     Opcode 0A9D
  0A9D: readfile <hFile> size <dword> to <var>
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
}
:CLEO_Opcode0A9D
{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     Opcode 0A9E
  0A9E: writefile <hFile> size <dword> from <var>
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
}
:CLEO_Opcode0A9E
hex
 50                     // push eax                                   
 6A 02                  // push 2
 B8 80 40 46 00 FF D0   // call CollectNumberParams
 6A 02                  // push 2
 B8 90 47 46 00 FF D0   // call GetVariablePos
 59                     // pop ecx 

 FF 35 7C 3C A4 00      // push Size (p2)
 50                     // push eax
 FF 35 78 3C A4 00      // push hFile (p1)

 83 F9 11               // cmp ecx, 17 (opcode 0A9D)
 75 07                  // jnz @opcode0A9E 
 B8 50 89 53 00         // mov eax, BlockRead
 EB 05                  // jmp @call
 B8 70 89 53 00         // mov eax, BlockWrite

 FF D0                  // call eax
 83 C4 0C               // add esp, 12
 32 C0                  // xor al, al
 C2 04 00               // ret 4
end

{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     Opcode 0A9F
  0A9F: <var> = current_thread_address
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
}
:CLEO_Opcode0A9F
hex
 6A 01                  // push 1
 89 35 78 3C A4 00      // mov p1, esi
 B8 70 43 46 00 FF D0   // call WriteResult
 32 C0                  // xor al, al
 C2 04 00               // ret 4
end

{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     Opcode 0AA0
  0AA0: gosub_if_false <label>
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
}
:CLEO_Opcode0AA0
hex
 6A 01                  // push 1
 B8 80 40 46 00 FF D0   // call CollectNumberParams
 
 8A 86 C5 00 00 00      // mov al, [esi+0xC5+IfResult]
 84 C0                  // test al, al
 75 1C                  // jnz @true
  
 0F B6 46 38            // movzx eax, [esi+0x38+THREAD.StackCounter]
 8B 56 14               // mov edx, [esi+0x14+THREAD.CurrentIP]
 89 54 86 18            // mov [esi+eax*4+THREAD.ReturnStack], edx
 66 FF 46 38            // inc word ptr [esi+0x38+THREAD.StackCounter]

 FF 35 78 3C A4 00      // push label (p1)                                   
 B8 A0 4D 46 00 FF D0   // call SetJumpLocation
 
 32 C0                  // xor al, al
 C2 04 00               // ret 4
end

{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     Opcode 0AA1
  0AA1: return_if_false
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
}
:CLEO_Opcode0AA1
hex
 8A 86 C5 00 00 00      // mov al, [esi+0xC5+IfResult]
 84 C0                  // test al, al
 75 0F                  // jnz @true

 66 FF 4E 38            // dec word ptr [esi+0x38+THREAD.StackCounter]
 0F B6 46 38            // movzx eax, [esi+0x38+THREAD.StackCounter] 
 8B 54 86 18            // mov edx, [esi+eax*4+0x18+THREAD.ReturnStack]
 89 56 14               // mov [esi+0x14+CurrentIP], edx
 
 32 C0                  // xor al, al
 C2 04 00               // ret 4
end

{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     Opcode 0AA2
  0AA2: <var> = load_library <path>
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
}
:CLEO_Opcode0AA2
hex
 81 EC 80 00 00 00      // sub esp, 128
 6A 64                  // push 100
 8D 44 24 04            // lea eax, [esp+4]
 50                     // push eax

 B8 50 3D 46 00 FF D0   // call GetStringParam
 8D 04 24               // lea eax, esp
 50                     // push eax
 FF 15 70 80 85 00      // call LoadLibraryA

 6A 01                  // push 1
 8B CE                  // mov ecx, esi
 A3 78 3C A4 00         // mov p1, eax
 
 31 D2                  // xor edx, edx
 85 C0                  // test eax, eax
 0F 95 C2               // setnz dl
 52                     // push edx
 B8 D0 59 48 00 FF D0   // call SetConditionResult

 B8 70 43 46 00 FF D0   // call WriteResult
 81 C4 80 00 00 00      // add esp, 128
 
 32 C0                  // xor al, al
 C2 04 00               // ret 4
end

{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     Opcode 0AA3
  0AA3: free_library <hLib>
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
}
:CLEO_Opcode0AA3
hex
 6A 01                  // push 1
 B8 80 40 46 00 FF D0   // call CollectNumberParams
 FF 35 78 3C A4 00      // push hLib (p1)
 FF 15 10 81 85 00      // call FreeLibrary
 32 C0                  // xor al, al
 C2 04 00               // ret 4
end

{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     Opcode 0AA4
  0AA4: <var> = get_proc_address "name" library <hLib>
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
}
:CLEO_Opcode0AA4
hex
 81 EC 80 00 00 00      // sub esp, 128
 6A 64                  // push 100
 8D 44 24 04            // lea eax, [esp+4]
 50                     // push eax

 B8 50 3D 46 00 FF D0   // call GetStringParam
 6A 01                  // push 1
 8B CE                  // mov ecx, esi
 B8 80 40 46 00 FF D0   // call CollectNumberParams

 8D 04 24               // lea eax, esp
 50                     // push eax
 FF 35 78 3C A4 00      // push hLib (p1)
 FF 15 6C 80 85 00      // call GetProcAddress
 
 6A 01                  // push 1
 8B CE                  // mov ecx, esi
 A3 78 3C A4 00         // mov p1, eax
 
 31 D2                  // xor edx, edx
 85 C0                  // test eax, eax
 0F 95 C2               // setnz dl
 52                     // push edx
 B8 D0 59 48 00 FF D0   // call SetConditionResult

 B8 70 43 46 00 FF D0   // call WriteResult
 81 C4 80 00 00 00      // add esp, 128
 32 C0                  // xor al, al
 C2 04 00               // ret 4
end

{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     Opcode 0AA5
  0AA5: call <address> num_params <byte> pop <byte> [param1, param2...]
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
}
:CLEO_Opcode0AA5
hex
 6A 03                  // push 3
 B8 80 40 46 00 FF D0   // call CollectNumberParams
 53                     // push ebx
 57                     // push edi
 8B 1D 78 3C A4 00      // mov ebx, addr (p1)
 8B 3D 7C 3C A4 00      // mov edi, Number (p2)

 85 FF                  // test edi, edi
 74 12                  // jz @call
 6A 01                  // push 1
 B8 80 40 46 00 FF D0   // call CollectNumberParams
 FF 35 78 3C A4 00      // push param (p1) 
 4F                     // dec edi
 EB EA                  // jmp @loop

 FF D3                  // call ebx
 
 A1 80 3C A4 00         // mov eax, pop
 6B C0 04               // imul eax, 4
 01 C4                  // add esp, eax
 
 5F                     // pop edi
 5B                     // pop ebx
 FF 46 14               // inc dword ptr [esi+0x14+CurrentIP]
 32 C0                  // xor al, al
 C2 04 00               // ret 4
end

{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     Opcode 0AA6
  0AA6: call_method <address> struct <address> num_params <byte> pop <byte> [param1, param2...]
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
}
:CLEO_Opcode0AA6
hex
 6A 04                  // push 4
 B8 80 40 46 00 FF D0   // call CollectNumberParams
 53                     // push ebx
 57                     // push edi
 51                     // push ecx
 8B 1D 78 3C A4 00      // mov ebx, addr (p1)
 8B 3D 80 3C A4 00      // mov edi, Number (p3)

 85 FF                  // test edi, edi
 74 12                  // jz @call
 6A 01                  // push 1
 B8 80 40 46 00 FF D0   // call CollectNumberParams
 FF 35 78 3C A4 00      // push param (p1) 
 4F                     // dec edi
 EB EA                  // jmp @loop

 8B 0D 7C 3C A4 00      // mov ecx, Struct (p2)
 FF D3                  // call ebx
 
 A1 84 3C A4 00         // mov eax, pop (p4)
 6B C0 04               // imul eax, 4
 01 C4                  // add esp, eax

 59                     // pop ecx 
 5F                     // pop edi
 5B                     // pop ebx
 FF 46 14               // inc dword ptr [esi+0x14+CurrentIP]
 32 C0                  // xor al, al
 C2 04 00               // ret 4
end

{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     END OF CLEO
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
}

 

 

 

 

Edited by OrionSR

Share this post


Link to post
Share on other sites
OrionSR
Posted (edited)

Hijacking Scripts with Gosub-Return:

 

The original method of hijacking a script to run custom code is to modify the relativeIP of a running script to resume execution at the location of embedded codes, and return the hijacked script to normal operation by ending with a jump statement to the original relativeIP. This strategy is fairly intuitive but editing the compiled jump address is a bit awkward.

 

Gosub-Return: End the hijack codes with Return and simulate a Gosub with save edits. A gosub-return strategy eliminates the need to modify the compiled code.

  • Embed the compiled code in the variable space as before.
  • Copy the RelativeIP of a running script to the end of it's RelativeReturnStack (RelativeReturnStack[StackID]).
  • Set the RelativeIP to the offset of the embedded script.
  • Increment the StackID. 

 

Automatically Embed a Blackboard Glitch Repair

 

BlackboardFix.1sc - 010 Script that embeds pre-compiled repair codes to fix the Blackboard Glitch. The blackboard glitch is (rarely) caused by driving school. When a driving school is started after unlocking the Import/Export mission (going back for gold, or starting bike school for the first time), sometimes it'll delete the blackboard. This will cause the game to crash when CJ is close to the drydock in San Fierro. The Blackboard glitch is one of the few standard glitches that cannot be repaired with current tools.

 

Goals:

  • Demonstrate the Gosub-Return strategy for hijacking scripts.
  • Provide an automated tool for players to repair their own broken saves.
  • Document the process well enough that it can be adapted to non-proprietary tools.
  • Gain experience using 010 scripts to edit save files.

 

Repair Scripts:

 

Compile as cleo scripts and save in the same location as the BlackboardFix.1sc script. There are no jump addresses so the cleo version can be embedded without modification. However, don't try to run this version as a cleo script; the unexpected Return will cause a crash. Also, the local variables used won't assign the object handle to the proper variable of the impexpm thread without additional commands. These repair scripts will only work properly if the impexpm thread is hijacked.

 

Spoiler

 

PC/PS2

{$CLEO .cs}
// BlackboardRepair.txt
// PCv1, PCv2, PS2v1, PS2v2
// For use with BlackboardFix.1sc
0001: wait 4000 
029B: [email protected] = init_object #NF_BLACKBOARD at -1573.881 135.3845 2.535 
0177: set_object [email protected] Z_angle_to 180.0 
0550: keep_object [email protected] in_memory 1 
0392: make_object [email protected] moveable 0 
01C7: remove_object_from_mission_cleanup_list [email protected] 
09F1: play_audio_at_actor $PLAYER_ACTOR event 1133  // SOUND_BUY_CAR_MOD 
0051: return

Mobile

{$CLEO .csa}
// BlackboardMobile.txt
// Android, iOS, WinStore
// For use with BlackboardFix.1sc
0001: wait 4000 
029B: [email protected] = init_object #NF_BLACKBOARD at -1573.881 135.3845 2.535 
0177: set_object [email protected] Z_angle_to 180.0 
0550: keep_object [email protected] in_memory 1 
0392: make_object [email protected] moveable 0 
01C7: remove_object_from_mission_cleanup_list [email protected] 
09F1: play_audio_at_actor $PLAYER_ACTOR event 1133  // SOUND_BUY_CAR_MOD 
0051: return

 

 

 


BlackboardFix.1sc

 

Run on a save that has been parsed with the GTASA.bt template. Track the progress in the output Window.

Spoiler

 

//--- 010 Editor v6.0.3 Script File
//
// File: BlackboardFix.1sc
// Author: OrionSR
// Revision: v0.1
// Purpose: Replace the blackboard object deleted by driving school.
//--------------------------------------
// Use:
//      First run the GTASA.bt binary template on the save to parse the save data.
//      Then run this Blackboard Fix script on the glitched save.
//      Load the repair save to complete the fix.
//      A sound will play to indicate that the repair script has executed.
//
//========================================================================

int i;                  // index for loops
int sizeToInsert = 0;   // size of script to embed
int scriptIndex = -1;   // index of running script to modify
int objectIndex = -1;   // index of target object in Pools
int varSpaceStart = 0;  // offset of global variable space

char hijackScript[8] = "impexpm";           // Running script to hijack

int saveFileNum = GetFileNum();             // for use with FileSelect()
char saveFileName[512] = GetFileName();     // strings for save name management
char tempSaveName[512] = FileNameSetExtension( saveFileName, ".tmp" );
char backupSaveName[512] = FileNameSetExtension( saveFileName, ".bak" );

char saveTemplate[512] = "GTASA.bt";        // required binary template
char templateName[512] = GetTemplateName(); // active template name


// Verify template has been run on save
if ( templateName != saveTemplate ) 
{
    Printf( "%s has not parsed the save data!\n", saveTemplate );
    Printf( "Run %s on this save before executing the fix script.\n", saveTemplate );
    return;
};

// Version specific variables
int cashWonOffset = 33592; // PC default
if (isMobile == 1) cashWonOffset = 39060; 

int localIndex = 27; // PC default
if (isMobile == 1) localIndex = 28; 

char fileName[512] = "BlackboardRepair.cs";
if (isMobile == 1) fileName = "BlackboardRepair.csa";

char repairFile[512] = FileNameGetPath(GetScriptFileName() );
repairFile += fileName;

//========================================================================

//Find Start of VarSpace
FSeek( FindFirst( "BLOCK*",1,0,1,0.0,1,FTell(),0,5 ) );
FSeek( FindNext(1)+9 );
varSpaceStart = FTell();

// Find script to hijack
for( i = 0; i < Save.CTheScripts.nRunningScripts; i++ )
{
    if ( Save.CTheScripts.Scripts.Script[i].Name == hijackScript ) 
    {    
        scriptIndex = i;
    };
};
 
if ( scriptIndex == -1 ) 
{
    Printf( "%s script not active! Blackboard Glitch is not possible.\n", hijackScript );
}
else
{
    Printf( "%s script found at index: %d. Checking object...\n", hijackScript, scriptIndex );  

//  Check for exisiting #NF_BLACKBOARD 3077
    for( i = 0; i < Save.CPools.nObjects; i++ )
    {
        if ( Save.CPools.Objects[i].Handle == Save.CTheScripts.Scripts.Script[scriptIndex].Locals[localIndex] ) 
        {    
            if ( Save.CPools.Objects[i].ModelID == NF_BLACKBOARD) // enum
            {
                objectIndex = i;
            };
        };
    };

    if ( objectIndex != -1 ) 
    {
        Printf( "Blackboard found at index: %d. Blackboard Glitch is not active.\n", objectIndex );
    }
    else
    {
        Printf( "Blackboard object is missing! Looking for repair file...\n" );
        if ( FileExists( repairFile ) )
        {
//          Copy repair script to clipboard
            Printf( "Repair script found: %s. Backing up files...\n", FileNameGetBase(repairFile) );
            FileOpen( repairFile, false, "Hex", false );
            sizeToInsert = FileSize();
            SetSelection( 0, sizeToInsert );
            CopyToClipboard();
            FileClose();

//          Backup original save game
            FileSelect( saveFileNum );

            if ( FileSave( tempSaveName ) == -1)
            {
                Printf( "Temp file did not save.\n" );
            }
            else
            {
                Printf( "Temp file saved as: %s\n", FileNameGetBase( tempSaveName ) );
                if ( FileExists( backupSaveName ) ) DeleteFile( backupSaveName );
                if ( RenameFile( saveFileName, backupSaveName ) == -1 )
                {
                    Printf( "Backup file could not be renamed.\n" );
                }
                else
                {
                    Printf( "Backup file renamed to: %s\n", FileNameGetBase( backupSaveName ) );

//                  Paste repair script into save file
                    SetSelection( varSpaceStart + cashWonOffset, sizeToInsert );
                    PasteFromClipboard();

//                  Hijack script with Gosub-Return
                    Save.CTheScripts.Scripts.Script[scriptIndex].RelativeReturnStack[Save.CTheScripts.Scripts.Script[scriptIndex].StackID] = Save.CTheScripts.Scripts.Script[scriptIndex].RelativeIP;
                    Save.CTheScripts.Scripts.Script[scriptIndex].RelativeIP = cashWonOffset;
                    Save.CTheScripts.Scripts.Script[scriptIndex].StackID +=1;

//                  Fix Checksum
                    WriteUInt(FileSize()-4, Checksum(CHECKSUM_BYTE, 0, FileSize()-4));
                    Printf( "Repair script has been embedded successfully.\n" );

//                  Clean up
                    FileSave( saveFileName );
                    DeleteFile( tempSaveName );
                    Printf( "Saving repair save as: %s.\n", FileNameGetBase( saveFileName ) );
                    Printf( "Final repair will be completed when %s is loaded.\n", FileNameGetBase( saveFileName ) );
                };
            };
        }
        else
        {
            Printf( "Repair script not found with %s\n", GetScriptName() );            
        };
    };
};

 

 

The BlackboardFix script requires that the GTASA.bt binary template has been run on the save file. Check the first post for more information on this template.

Edited by OrionSR

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Sign in to follow this  

  • 1 User Currently Viewing
    0 members, 0 Anonymous, 1 Guest

×
×
  • Create New...

Important Information

By using GTAForums.com, you agree to our Terms of Use and Privacy Policy.