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

    1. GTANet.com

    2. GTANet 20th Anniversary

    1. GTA Online

      1. The Cayo Perico Heist
      2. Find Lobbies & Players
      3. Guides & Strategies
      4. Vehicles
      5. Content Creator
      6. Help & Support
    2. Red Dead Online

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

    1. Red Dead Redemption 2

      1. PC
      2. Help & Support
    2. Red Dead Redemption

    1. Grand Theft Auto Series

      1. St. Andrews Cathedral
    2. GTA VI

    3. GTA V

      1. Guides & Strategies
      2. Help & Support
    4. GTA IV

      1. The Lost and Damned
      2. The Ballad of Gay Tony
      3. Guides & Strategies
      4. Help & Support
    5. GTA San Andreas

      1. Guides & Strategies
      2. Help & Support
    6. GTA Vice City

      1. Guides & Strategies
      2. Help & Support
    7. GTA III

      1. Guides & Strategies
      2. Help & Support
    8. Portable Games

      1. GTA Chinatown Wars
      2. GTA Vice City Stories
      3. GTA Liberty City Stories
    9. Top-Down Games

      1. GTA Advance
      2. GTA 2
      3. GTA
    1. GTA Mods

      1. GTA V
      2. GTA IV
      3. GTA III, VC & SA
      4. Tutorials
    2. Red Dead Mods

      1. Documentation
    3. Mod Showroom

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

      1. Design Your Own Mission
      2. OpenIV
      3. GTA: Underground
      4. GTA: Liberty City
      5. GTA: State of Liberty
    1. Rockstar Games

    2. Rockstar Collectors

    1. Off-Topic

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

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

    2. Support

      1. Court House
    3. Suggestions

Embedding Scripts in Save Files


OrionSR

Recommended Posts

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
Link to post
Share on other sites

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
  • Like 3
Link to post
Share on other sites

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
  • Like 4
Link to post
Share on other sites

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
  • Like 2
Link to post
Share on other sites
  • 9 months later...

Updates: Embedding Local Addressing and Custom GXT

 

I got these tricks working in a test environment and wanted to document the basic strategies.

 

Running scripts can be modified to use local (negative) addressing like missions, external scripts and, more importantly, cleo scripts, by setting the base IP to the start of the script. Scripts with the appropriate codes added to the beginning can be compiled as cleo scripts and embedded directly. 

 

In this example, the test script was embedded to $8410, which is safely within the Roulette's cash won array. (8410 * 4 = 0x8368)

:Embed_Addressing
[email protected] = -11  // local aDMA offset to BaseIP
[email protected]([email protected]@,1i) = 0xA51CC8 // 0x8368 + 0xA49960 (SCM_offset + SCM_start)

In this example the script offset is found by reading the currentIP of the script and subtracting the length of compiled code from the start to the end of the read currentIP command. 

{$CLEO .es}
// BaseIP41.txt

:Embed_Addressing
[email protected] = -10  // local aDMA offset to CurrentIP
[email protected] = -11  // local aDMA offset to BaseIP
0085: [email protected]([email protected],1i) = [email protected]([email protected],1i)  // BaseIP = CurrentIP
[email protected]([email protected],1i) -= 30 // -= offset to current line

:Script_Body
03A4: name_thread 'BaseIP41'
wait 4000
00BA: show_text_styled GXT 'FESZ_LS' time 4000 style 4  //  Load Successful.
wait 4000
jump @Jump_Test
004E: end_thread

:Jump_Test
00BA: show_text_styled GXT 'STAT145' time 4000 style 4  // Unique Jumps done
wait 4000
004E: end_thread

Future plans involve using just a gosub at the start and reading the return stack for the BaseIP (+ 7 for the gosub). The offset of the subroutine can be shared by multiple scripts and discarded from temp space once the embedded scripts have been launched.

Embedded GXT

 

I'm still learning my way around TheText and GXT, but I finally managed to display some custom text. My original thought was to overwrite existing text in memory, but changing the pointer for the hash of a particular key to the offset of my text string eliminated any concerns about string length, and should make repairing the standard text a lot easier to manage.

 

References, since TheText isn't terribly well documented:

TheText (PCv1) 0xC1B340

  • +0x0 Pointer to TKEY
  • +0x8 Pointer to TDAT
  • +0x12C TABL

It looks like "TheText" will be the appropriate search label for mobile offsets.
Changes to TheText will be reset to default when loading a save (running scripts will need to be aware of reloading).

 

[email protected] = 0xC1B340  // Pointer to TKEY 
0A8D: [email protected] = read_memory [email protected] size 4 virtual_protect 0
[email protected] += 0xA598   // offset to FELM_WR		// Loading Mission Pack Game, please wait...

0AD3: v$8410 = format "Custom GXT by OrionSR~n~May ~1~, ~1~"
[email protected] = 8410
[email protected] *= 4
[email protected] += 0xA49960 //SCM_offset

0A8C: write_memory [email protected] size 4 value [email protected] virtual_protect 0  
036D: show_text_2numbers_styled GXT 'FELM_WR' numbers 21 2020 time 5000 style 5

EqtY0lFl.jpg

 

There, now if my hard disk crashes then at least I've got the basics documented online.

These two strategies make embedded scripts a much more viable strategy. I'll be looking for an opportunity to implement embedded scripts on PS2 and update this documentation with a more coherent strategy.

_________________________________________________

 

Updates: The strategy of using local variables with negative array indexes to manipulate the local script memory doesn't work on PS2. The baseIP strategy of adjusting the jump offsets works reasonably well when the launch process is skipped by editing new running scripts into the save, and adding scripts was generally less trouble than managing the launch processes anyway. This strategy makes the script much easier to test.

 

However, fixing the baseIP means the v2 save will only be compatible with NTSC or PAL. I'm currently testing strategies to post-process cleo scripts with relative jump offsets, and to use a sniffer script to discover the current base offset to SCM whenever a save is loaded so pointers can be used while maintaining regional compatibility between saves.

Implementing on PS2v2

Edited by OrionSR
Link to post
Share on other sites
  • 9 months later...
Noel salazar
On 26/7/2019 at 08.44, OrionSR said:

Menyematkan Skrip dalam Simpan File

 

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.

 

Rusak: Beberapa strategi yang dijelaskan dalam dokumentasi ini tidak berfungsi seperti yang diharapkan. Terutama, ketika variabel global digunakan dalam skrip yang disematkan maka ruang variabel global ditambahkan di awal SCM dan kode yang disematkan diimbangi yang sesuai, yang mengarah ke alamat lompatan yang salah. Menghindari semua variabel global atau menyesuaikan setiap alamat lompatan secara manual adalah pilihan yang buruk. Saya mencari solusi lain.

 

Gambaran:

 

Hijacking a Running Script: Skrip kustom dikompilasi dengan Sanny Builder. Editor hex digunakan untuk menyalin kode biner dalam skrip yang dikompilasi dan menempelkannya ke dalam ruang variabel global dari permainan yang disimpan. Skrip yang sedang berjalan dimodifikasi dengan editor hex sehingga instruksi berikutnya (relativeIP) akan dimulai di alamat kode yang tertanam di ruang variabel global. Kode kustom diakhiri dengan lompatan ke relativeIP asli sehingga skrip yang berjalan dapat melanjutkan operasi normal.

 

Mengambil Kembali Ruang Variabel Global dari Memori SCM : Beberapa larik standar menyediakan sedikit ruang untuk kode sementara dalam penyimpanan standar, tetapi ada sedikit ruang untuk kode permanen. Namun, banyak skrip SCM di luar ruang variabel global tidak lagi diperlukan saat menyimpan data dimuat. Beberapa ribu byte memori SCM dapat dipulihkan dengan secara permanen memperluas ruang variabel global dan digunakan untuk menyimpan skrip yang dikompilasi.

 

Mengelola Alamat Langsung: Sanny Builder akan secara akurat menyandikan alamat lompat yang tepat untuk label jika skrip dikompilasi dengan kode yang terletak di offset yang tepat dalam file main.scm khusus. Utama "NOPped" ini hanya mencakup apa yang diperlukan untuk mengkompilasi dan mendekompilasi dengan benar sebagai file SCM, alamat lompatan untuk lokasi skrip di masa mendatang, dan ribuan instruksi 0000: NOP. Ketika nops utama didekompilasi, custom label dimasukkan ke dalam lautan nops dan memberikan referensi untuk penempatan kode yang akurat.

 

 

Contoh Skrip:

 

Tes Dasar: Membajak skrip yang sedang berjalan untuk menampilkan pesan. Tes sederhana dari strategi dasar.

Perluas Ruang Variabel: Membajak skrip yang sedang berjalan untuk meningkatkan ukuran ruang variabel global dan menginisialisasi variabel.

Luncurkan Peluncur: Membajak skrip yang sedang berjalan untuk meluncurkan skrip baru yang berjalan secara permanen yang dapat dengan mudah meluncurkan skrip lain.

Save Anywhere: Alat yang berguna menggunakan kode scm dasar dan uji input pengguna. Harus bekerja sama terlepas dari versinya.

Warp to Marker: Membaca memori menggunakan pengalamatan ADMA . Sangat bergantung pada versi.

 

 

Menguji dengan Cleo: Tujuannya adalah untuk menghindari penggunaan opcode cleo dan menjalankan skrip khusus di lingkungan tanpa skrip cleo, tetapi saya sama sekali tidak malu menggunakan skrip cleo saat menguji. Secara khusus, semua skrip kompleks diuji sebagai skrip cleo di PC dengan Cleo4, atau Android dengan CleoA. Ketika skrip bekerja dengan baik, kode dapat disalin ke nop utama (tanpa direktif {cleo. Cs}) dan dikompilasi tanpa modifikasi tambahan.

 

Bekerja Dengan Editor Hex: Seharusnya mungkin untuk menyematkan skrip dengan editor hex gratis seperti HxD , dan saya berharap dapat memberikan referensi yang cukup sehingga pemain dapat menemukan offset yang sesuai, tetapi ini adalah tugas yang rumit dan saya tidak ingin cacat saya sendiri terlalu banyak jadi saya akan menjelaskan proses pengeditan hex berdasarkan 010 Editor . Alat berpemilik ini mendukung templat biner yang dapat mengidentifikasi penyimpanan berbagai versi dan sistem, mengurai penyimpanan file ke dalam menu yang tertata, dan menampilkan serta mengedit informasi dalam format yang sudah dikenal. Untungnya, 010 memiliki masa uji coba yang banyak. Ada dokumentasi yang cukup baik untuk menyimpan file PC SA yang tersedia, tetapi hanya templat biner SA yang dapat mendeskripsikan penyimpanan PS2 dan seluler dengan benar.

 

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:

 

Skrip akan menunggu cukup lama hingga layar memudar dan memverifikasi penyimpanan dimuat dengan benar, lalu menampilkan pesan, memutar lagu, dan melompat ke perintah end_thread. Alamat lompat akan dimodifikasi agar sesuai dengan relativeIP asli dari skrip yang dibajak. Pernyataan end_thread diperlukan di sini agar versi cleo berfungsi, tetapi tidak akan dijalankan dalam versi yang disematkan.

 



 

Ini mengkompilasi ke:


 

E0 FF FF FF adalah alamat lompat yang perlu diubah. Secara kebetulan, nilai ini jatuh secara merata dalam satu variabel global, yang membuatnya lebih mudah untuk diedit dengan template 010. Alamatnya khusus untuk setiap penyimpanan jadi saya melakukan pengeditan ini setelah menyematkan kode.

 

$ Roulette_Cash_Won Array (151i, 604 bytes) * tidak ditentukan dalam INIs

 

VarNum * 4 = Offset SCM + Offset dari awal file = * Offset Global 

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

$ 9765 * 4 = 39060 + 442 = 39502 Seluler

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

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

* Global dalam hal ini adalah pengaturan GoTo untuk memulai file.

 

Array $ Roulette_Cash_Won adalah ruang kerja pilihan saya untuk kode sementara. Ini sedikit lebih kecil dari beberapa array string misi tetapi menyediakan metode dalam game untuk menghapus kode sementara - cukup mulai misi roulette. Kode yang disematkan tampaknya tidak memengaruhi pembayaran dan array diinisialisasi ulang di bagian akhir.

 

 

Menempelkan Kode Biner ke Ruang Variabel Global:

 

010: Buka Save Blocks> Block 01: CTheScripts> GlobalVars dan gulir ke variabel yang sesuai untuk versi Anda. Memilih variabel di template akan menyorot variabel di jendela hex. Tempel kode yang dikompilasi dan timpa data yang ada di jendela hex, dimulai dari awal variabel ini.

 

HxD: Ruang variabel global berada di lokasi statis untuk setiap jenis penyimpanan tertentu, 4 byte setelah penanda BLOK kedua yang memisahkan setiap blok penyimpanan. SimpleVars yang disimpan di blok 0 selalu berukuran sama, sehingga offset GoTo dari variabel global dapat dihitung sebagai $ VarNum * 4 + StartOfVarSpace. Namun, karena ruang variabel dapat, dan akan, mengubah ukuran, dan jumlah skrip yang berjalan akan bervariasi, offset statis tidak banyak berguna untuk apa pun yang ditemukan nanti dalam penyimpanan.


 

Membajak Running Script :

 

010: Buka Save Blocks> Block 01: CTheScripts> tRunningScripts> Script [1], biasanya 'oddveh' (skrip 2 pada PS2). RelativeIP ada di dekat bagian bawah. Catat nilai saat ini; itu diperlukan sebagai alamat lompat di akhir kode yang disematkan. Ubah RelativeIP ke offset lokal untuk memulai larik Cash Won. 010 akan mengelola konversi heksadesimal dan desimal, jadi format tidak menjadi masalah.

 

HxD: Menghitung offset dari menjalankan skrip dalam penyimpanan agak canggung, saya akan bekerja untuk memberikan referensi lengkap. Skrip yang sedang berjalan berada di dekat akhir blok TheScripts dan biasanya berisi nama skrip teks sehingga mudah ditemukan dengan mencari BLOK ketiga (blok 2) dan menggulir ke belakang hingga nama skrip terlihat. Atau cari saja nama skripnya. Offset untuk nama skrip di PC dan PS2 adalah +10 dari awal skrip berjalan, dan offset ke relativeIP adalah +226, jadi +216 dari awal nama skrip. Offset pada ponsel adalah +14 untuk nama skrip dan +262 untuk relativeIP, jadi +248 untuk nama skrip.

 

Perbaiki Lompatan Tersemat:

 

010: Kembali ke tempat kode disematkan di ruang variabel dan temukan versi cleo dari alamat lompat: E0 FF FF FF. Klik kanan pada data hex dan pilih Lompat ke variabel Template - ini akan membawa Anda langsung ke variabel global yang sesuai dengan alamat ini. Masukkan nilai yang direkam dari relativeIP asli.

 

HxD: Prosesnya hampir sama. Versi terbaru HxD memiliki alat Inspektur yang dapat menampilkan dan mengedit data hex dalam format yang sudah dikenal.

 

Perbaiki checksum, simpan, dan uji. Ada sedikit yang bisa salah dengan kode yang disematkan, jadi ini seharusnya menjadi tes untuk mengerjakan strategi dasar dan mengumpulkan semua bagian ke dalam skrip kerja.

 

 

Perpanjang Ruang Variabel:

 

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: Ini adalah file scm yang tidak dapat digunakan. Tujuannya adalah untuk mengenkode alamat lompat dengan benar untuk skrip yang disematkan. Untuk mendapatkan akses ke memori sebanyak mungkin, main yang dilucuti telah dimodifikasi untuk menghapus kode sebanyak mungkin tetapi tetap merupakan file scm yang valid yang dapat dikompilasi dan didekompilasi dengan Sanny.

 







 

File ini perlu diisi dengan banyak instruksi 0000: NOP (tanpa operasi) untuk menyediakan ruang kerja untuk skrip yang disematkan. Banyak, seperti sekitar 100.000 diperlukan untuk memperhitungkan semua 200.000 byte memori SCM. Sekitar sepertiganya sudah cukup. Namun, jauh lebih mudah untuk mengkompilasi versi pendek ini dan menyisipkan 200.000 byte di akhir file menggunakan editor hex daripada menyalin dan menempelkan opcode. Dekompilasi file yang lebih besar.

 

Tambahkan tujuan lompatan yang sesuai setelah garis yang dipasang di atas. Ini akan digunakan dalam kombinasi dengan customlabels.ini untuk menyisipkan label di alamat lompat yang benar.

 


 


 

Tambahkan ke CustomLabels.ini.

Aktifkan Label Kustom di Alat> Opsi> Format


 


 

Perhatikan bahwa ketika kode ditambahkan setelah label, offset untuk label nanti dalam skrip tidak akan lagi disejajarkan pada offset yang tepat. Saya terus kembali ke scm yang disimpan untuk skrip baru.

 

 

VarSpace Script:

 

& 3 ( 1 @ , 1i) menggunakan format ADMA untuk mengubah kata sandi yang dimulai dari $ 0 +3 byte. Nilai ini mengontrol seberapa banyak memori yang akan disimpan untuk variabel global, dan mencakup $ 0 dan $ 1. Karena kedua global ini dilindungi oleh Sanny, ADMA adalah cara termudah untuk mengelola variabel ini. (VarSpaceSize disimpan tepat sebelum variabel global dalam file penyimpanan menentukan seberapa banyak data yang disimpan akan dimuat ke dalam memori.)

 

Untuk meningkatkan kompatibilitas, saya membatasi ruang variabel yang diperluas ke apa yang tersedia untuk PC v2. Sedikit lebih banyak ruang dapat diperas dari v1.

 

Padding ditambahkan sehingga alamat lompatan terakhir akan mendarat secara merata di global, tetapi ini tidak sepenuhnya diperlukan.

 

Dikerjakan ulang untuk menghindari penggunaan variabel global.










 









 

Dekompilasi NOPmain.scm dengan label khusus. Masukkan semuanya kecuali petunjuk cleo ke dalam skrip scm tepat setelah label kustom untuk array Uang Tunai. Kompilasi skrip dan buka dengan editor hex. Temukan kode yang dikompilasi dalam scm yang sebagian besar kosong, atau pada offset yang ditentukan, dan salin ke array Cash Won di ruang variabel global penyimpanan. Perbarui relativeIP dari skrip yang sedang berjalan dan alamat lompatan terakhir dalam kode yang disematkan untuk menyelesaikan proses pembajakan seperti sebelumnya.

 

Perbarui, simpan, uji, dan simpan game. Buka penyimpanan yang dimodifikasi dengan editor hex untuk mengamati perubahan pada ruang variabel.

 

Satu masalah yang saya temui pada penyimpanan seluler, jika cukup skrip yang diluncurkan, objek, mobil atau ped yang ditambahkan oleh sistem pos pemeriksaan yang kacau, atau muatan (dan lompatan aksi) yang ditambahkan oleh mod, tidak terlalu sulit untuk penyimpanan tipe safehouse normal menjadi sangat membengkak sehingga penyimpanan akan dinaikkan ke ukuran berikutnya yang lebih besar. Ini tidak mempengaruhi apa pun selain template 010, yang mendeteksi penyimpanan besar sebagai penyimpanan "MissionThread" dan membingungkan bagaimana hal-hal diuraikan, jadi saya belum mengalokasikan ruang variabel maksimum yang memungkinkan pada penyimpanan seluler.

 

Saya tidak yakin bagaimana saya ingin mengelola lebih dari 16.000 byte memori SCM. Saya ingin menyediakan banyak ruang untuk variabel global yang dapat digunakan oleh skrip, dan masuk akal bagi mereka untuk bersebelahan dengan variabel global standar jadi saya tidak ingin menambahkan kode di awal. Saya juga tidak ingin terlalu jauh ke dalam ruang yang tersedia karena saya tidak yakin dengan ruang variabel optimal yang harus dipesan untuk seluler karena masalah dengan ukuran penyimpanan. Jadi untuk contoh ini saya telah memutuskan untuk mulai mengkompilasi kode pada variabel global $ 13500, offset 54000. Ini akan memungkinkan hampir 1200 variabel global yang bisa umum di antara semua versi skrip, dan skrip baru dapat menempati ruang yang sama pada sistem apa pun.

 

 

Meluncurkan Launch Running Script:

 

Proses ini memerlukan penyematan dua skrip, skrip peluncuran yang berjalan secara permanen yang disimpan dalam ruang variabel yang diperluas, dan skrip yang dibajak untuk meluncurkannya. Pembajakan menjadi membosankan, jadi skrip peluncuran akan berguna, tetapi tidak banyak hasil sampai skrip yang berjalan berikutnya siap.

 

Skrip peluncur lebih pendek dari yang telah ditulis ke array Uang Tunai. Mungkin lebih mudah untuk mengidentifikasi alamat lompat yang tepat jika larik dihapus dari data yang ada. Kompilasi Launcher sebagai skrip cleo dan sematkan ke array Uang Tunai. Membajak skrip untuk menjalankan kode. Selesaikan skrip berikutnya sebelum menguji.

 



 

 

Luncurkan sebagai Running Script:

 

Skrip ini diinstal ke $ 13500 menggunakan nopped main dan akan meluncurkan skrip di alamat bukan nol yang ditetapkan ke $ 13499, dan kemudian menyetel ulang variabel. Skrip akan tetap aktif dalam penyimpanan dan akan meluncurkan semua skrip baru yang tersisa.

 


 

 

Simpan Di Mana Saja:

 

Skrip penyimpanan diinstal dengan main nopped ke $ 13510, offset 54040. Luncurkan skrip dengan mengubah $ 13499 menjadi 54040. Aktifkan skrip dengan menahan tombol grup mundur, biasanya H saat menggunakan kontrol keyboard, atau ke bawah pada D-pad saat menggunakan pengontrol. Skrip ini harus bekerja sama pada PC dan PS2.

 





 

Variasi dalam versi seluler untuk kontrol input yang berbeda - pengguna harus memegang widget senjata. Seluler memiliki lebih banyak variabel lokal daripada PC dan PS2, jadi pengatur waktu lokal pertama adalah 40 @, bukan 32 @. (Sedikit padding ditambahkan untuk menjaga jumlah byte yang seimbang. Tujuannya adalah agar setiap celah di antara skrip diisi dengan NOP 2-byte sehingga seluruh blok dapat didekompilasi dengan Sanny tanpa menimbulkan kesalahan apa pun.)

 




 

 

Warp ke Marker:

 

Skrip ini menggunakan metode ADMA untuk membaca memori sebagai pengganti perintah cleo. Bekerja dengan memori membuat skrip ini khusus untuk v1 dapat dieksekusi di PC, dan Android 1.08. Saya tidak memiliki informasi atau perangkat keras yang diperlukan untuk mengonversi dan menguji skrip ini dengan benar pada sistem lain - selain PS2, mungkin, jika saya dapat membuat semuanya berfungsi sekali lagi. Penyimpanan PS2 canggung untuk dikelola, jadi saya akan menundanya sambil menunggu minat.

 

OSRWarp disematkan pada $ 13530 dan diluncurkan pada 54120. Perbedaan antara skrip adalah variabel timer lokal, penekanan tombol dan pemeriksaan widget, dan alamat memori. CJ akan melengkung ke penanda peta hanya jika disetel saat Conversation No - biasanya N, diadakan.

(Sekali lagi terima kasih kepada ZAZ untuk skrip teleportasi aslinya.)

 









 
   

 






 


   

 

Referensi:

 

Catatan:

  • File penyimpanan PC dengan skrip tertanam tidak akan dikonversi dengan benar antara v1 dan v2.
  • Simpan file dengan ruang variabel yang diperluas mungkin tidak lagi berfungsi dengan beberapa editor penyimpanan (Editor Savegame 3.x).

 

Dokumentasi ini sedang dalam proses.

 

my game always force close saat menggunakan code 0A51

Link to post
Share on other sites
  • 2 weeks later...

I have created a basic Go program to automate embedding the "Load Successful" script. Source is up on GitHub: https://github.com/Squ1dd13/SCEmbed

 

I'm still actively working on this at the moment, so it should become a bit more flexible in terms of what it embeds in the future, but it does currently work. I've tested it on iOS and Windows (through Wine, but that shouldn't make a difference), and it works the same on both. The program hijacks the second script in the array of running scripts, and stores the custom code in an extended area of the global variable store. It currently extends the global store to 60028 bytes on all platforms. It isn't really ready for use yet, and you'll need Go to build and run it. If you manage that, pass the path to a save file as an argument, and the output will have the same path but with ".modded" appended to it.

 

I have not tested on PS2 (Japanese or otherwise) or Android, so if anyone is able to do that I would be very grateful. I expect there will be issues on both platforms, but in theory they should work.

 

iOS save file: https://gtasnp.com/ZfeHni

 

I can't upload the PC save file to GTASnP because it has the wrong output size, which is something I need to fix. The game will load it, and 5-10 minutes of gameplay went without issues.

  • Like 2
Link to post
Share on other sites
Posted (edited)
3 hours ago, Squ1dd13 said:

I have created a basic Go program to automate embedding

Wow. Nice work. This is a significant development. The major limiting factor of embedding scripts was the proprietary nature of the tool I use - the 010 Editor. Your Go program sounds like an excellent solution. I'm anxious you test what you've done so far but it's going to take a while for me to come up to speed on this project. Also, I want to document some of the lessons learned from a PS2 project so you've got the most current information available as you progress with your project.

 

First, a couple of comments on your initial report: 

 

I recommend limiting the total variable space to 60000 bytes on all platforms ($14999), for now. Some of these always-running scripts need to be aware of when a save has been reloaded or the game has just started so they can hunt down dynamic data and pointers to, for example, reinitialize custom GXT. The unused extended variables at $15000 plus a few more are a useful way to detect when data has reset and to pass session, system, region and version info between scripts. 

 

Not only SnP, but the PC exes are very picky about the size of the save file. Sometimes they can handle something that's a little off but in general PC saves should always be a fixed length of 202752 bytes. PC save have a great deal of slack after all the save blocks that is just filler and can be safely trimmed as necessary. Just fix the checksum at the end and the save should be good to go. PC can handle larger save files but they are expected to grow by chunks of... 64000(?) bytes. Also, FLA has a strategy for managing PC saves of any length. 

 

IIRC, mobile games can handle irregular save sizes with less trouble. Mobile saves tend to change size anyway. Checkpoint saves are a bit larger than save slot saves, and mission saves larger still - growing by large chunks. The problem with mobile saves size is that there is less slack at the end of the file, the save blocks tend to be a bit larger, and there's an additional save block that I've never accounted for in my save templates. Add to that, expanded varspace, extra running scripts, glitched extra objects, peds and cars in the pools, and maybe some new cargens or jumps, and it's not too hard for a normal slot save to grow to the size of a checkpoint save - which will work just fine but SnP will probably want you to save it in slot 9 or 10.

 

There are a few lessons from a PS2 project that I want to document before I start digging into your new tool. I'll put more details in later posts as I gather information. Soon, an example of post-processing the addresses of compiled custom scripts. A major headache involved with embedded scripts has been managing the addressing for labels. Post-processing removed all the hassle of address management. 

 

Also, I found it easier to add new running scripts to the save file than to hijack a script to launch them. It'll review my files and document the process but it'll take a little longer to come up to speed again.

 

 

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

Post-Processing Custom Scripts to Embedded Scripts

 

Overall Goals - Embedded Scripts should be as similar as possible to Custom Scripts (.cs - cleo scripts without Cleo opcodes). Few modifications should be necessary to change a custom script from PC to an embedded script on PS2, or an Android script to iOS. Preferred strategies will work basically the same way on all systems. 

 

With that in mind, addressing should be a seamless process. User shouldn't need to know anything about this. 

 

History of embedded addressing:

  • First test scripts had no jumps, so were extremely limited in scope.
  • Early tests with jumps involved manually calculating and hardcoding the offsets, a process prone to errors.
  • More flexible addressing was achieved my compiling the text of a script at a specific offset in a large and blank "main.txt" and compiling as an SCM file. This process is awkward, wasteful, and does not lend itself well to automation. Also, Sanny wants to handle some things differently in SCM, like defining objects. 
  • The next stage involved tweaking the BaseIP of a script so it would use the local addressing scheme used in cleo, external scripts, and missions. The process worked well on PC, but the trick of using a [email protected] as an array with a negative index to adjust the BaseIP of a launched script didn't work on PS2. Also, I never like the idea of having a little extra overhead code in each script launched. (This idea might be worth revisiting in combination with adding scripts instead of launching them.
  • The most recent addressing strategy is post-processing a compiled custom script to search for all jump, elsejump and gosub commands and fix the address as required. There is some risk of false detections but these were minimized by restricting the jump offset to within the length of the file. In the case of jumps, opcode 0002 is easily confused with local variable [email protected] I have yet to code special handling for the jump table opcode.

 

Global vs Local Addressing (terms as described by Sanny's format options):

  • Global, positive int32 relative to the Start of SCM, $0.
  • Local, negative int32 relative to the start of the script recorded in the BaseIP field.

To convert from local to global addressing for embedded scripts, invert the local offset to positive and add the offset where the script has been embedded. I usually start with an extended global variable as a write destination multiply by 4 bytes. Might be easier to work with offsets directly. 

 

010 Script for Post-Processing: Range checking should probably be added to elsejump and gosub - hasn't been a problem yet. Jump tables are not handled.

Spoiler

 

//--------------------------------------
//--- 010 Editor v6.0.3 Script File
//
// File: PostProcessCleo
// Author: OrionSR
// Revision: 3/14/2021
// Purpose: Change local addressing of cleo scripts to global addressing for embedding
//--------------------------------------

/*
int64 FindFirst( 
    <datatype> data, 
    int matchcase=true, 
    int wholeword=false, 
    int method=0, 
    double tolerance=0.0, 
    int dir=1, 
    int64 start=0, 
    int64 size=0, 
    int wildcardMatchLength=24 )
*/

int i;
int64 r;
int64 globalvar;  // 11050
int offset; // = globalvar * 4;
int address;

// Input a number
globalvar = InputNumber( "Input Variable Number", 
    "Enter the global variable number where this script is embedded.", "" );
if( globalvar == BAD_VALUE )
    return -1; // cancelled
    offset = globalvar * 4;

    TFindResults jump = FindAll( "020001,h" );
    Printf( "\njumps %d\n", jump.count );
    for( i = 0; i < jump.count; i++ )
      {
      address = ReadInt( jump.start[i]+jump.size[i] );
      if ( address < 0 )
        {
        address *= -1 ;
        if ( address < FileSize() )
          {
          address += offset ;
          WriteInt ( jump.start[i]+jump.size[i], address );    
          Printf( "%Ld %Ld\n", jump.start[i], address );
          };
        };
      };


    TFindResults elsejump = FindAll( "4D0001,h" );
    Printf( "\nelsejumps %d\n", elsejump.count );
    for( i = 0; i < elsejump.count; i++ )
      {
      address = ReadInt( elsejump.start[i]+elsejump.size[i] );
      if ( address < 0 )
        {
        address *= -1 ;
        address += offset ;
        WriteInt ( elsejump.start[i]+elsejump.size[i], address );    
        Printf( "%Ld %Ld\n", elsejump.start[i], address );
        };
      };

    TFindResults gosub = FindAll( "500001,h" );
    Printf( "\ngosubs %d\n", gosub.count );
    for( i = 0; i < gosub.count; i++ )
      {
      address = ReadInt( gosub.start[i]+gosub.size[i] );
      if ( address < 0 )
        {
        address *= -1 ;
        address += offset ;
        WriteInt ( gosub.start[i]+gosub.size[i], address );    
        Printf( "%Ld %Ld\n", gosub.start[i], address );
        };
      };


//0871: init_jump_table $Video_Game total_jumps 8 default_jump 0 @End_Case_Video_Game jumps 0 @MS_Game_TheyCrawledFromUranus 1 @MS_Game_Duality 2 @MS_Game_GoGoSpaceMonkey 3 @MS_Game_LetsGetReadyToBumble 4 @MS_Game_TrackBetting 5 @MS_Game_Pool 6 @MS_Game_Lowrider 
//0872: jump_table_jumps 7 @MS_Game_BeefyBaron -1 @End_Case_Video_Game -1 @End_Case_Video_Game -1 @End_Case_Video_Game -1 @End_Case_Video_Game -1 @End_Case_Video_Game -1 @End_Case_Video_Game -1 @End_Case_Video_Game -1 @End_Case_Video_Game 

 

 

 

 

 

Edited by OrionSR
Link to post
Share on other sites

It has struck me that the ability to embed scripts in save files represents somewhat of a security hole in the game. We'll just have to trust people to not abuse this. I don't think it will ever be a real problem.

 

On a different topic, the post-processing system looks promising; I will have a go at implementing that in the Go tool once I'm done fixing the current bugs.

 

On 3/14/2021 at 11:40 PM, OrionSR said:

This idea might be worth revisiting in combination with adding scripts instead of launching them.

 

I seem to have missed the definition of "launching" a script. Is launching the same as hijacking, or are you referring to having a single script responsible for starting others (or something else entirely)? Sorry, I'm still very new to embedded scripts.

Edited by Squ1dd13
details--
Link to post
Share on other sites
1 hour ago, Squ1dd13 said:

embed scripts in save files represents somewhat of a security hole in the game

Your security concerns are valid. Before I discussed the embedding strategy publicly I consulted with forum administration and was encouraged to continue with the project. I also chatted with Silent about improving security on PC through the SilentPatch - specifics of security measures would necessarily need to remain private. There are a number of strategies that could be used by SnP to detect embedded scripts, but this would be mostly a courtesy service that might reduce trolling from script kiddies but wouldn't discouraged a truly skilled coder determined to cause trouble. In my opinion, the risk won't change much if we continue with the project, but there is quite a bit of potential benefit to players, especially on PS2, iOS and WinStore systems.

 

On a related topic (FYI): I've got general permission from Seeman and Deji to adapt Cleo 3 and 4 opcodes for embedding on PS2 if I can figure out how. I don't know Alexander Blade as well so I haven't tried bugging him about CleoA opcodes. No progress has been made beyond the test with Cleo1 on PC.

 

2 hours ago, Squ1dd13 said:

"launching" a script

004F: create_thread @MS_BIKE_MISSIONS // SB
004F: start_new_script @MS_BIKE_MISSIONS // SCR

I'm not sure where I picked up the launch term. In the context of editing, Start New and Create are terms that are too generic. Launch is intended to describe the specific process of making the script active by using SCM coding to add it to the running scripts pool. Previous examples have used hijacking, the process of redirecting a running script to execute custom code, to launch scripts embedded in the varspace. 

 

But there's another way to activate a script; copy and insert a running script to the end of the pool, adjusted the data as required, increase the number of running scripts count and trim the file to length. For later tests I saved a template script with all the data cleaned so I could more easily adjust the data. However, a new running script (the active part saved at the end of block 1) is mostly blank. It would be easy enough to create one out of whole cloth. I'm currently trying to document the process of how to Insert a running script. (Care to comment on Insert as the chosen term?)

 

I expect that the concept seems weird, but during my last project it was preferred over fussing with hijack coding. The main problem with documentation is describing the structure of block 1 for PS2, PC and Mobile. Do you have the 010 Editor and the binary template to parse the saves? The PC structure is described on the SA Save Wiki, there are only minor difference with block 0 on PS2 and mobile, and "normal" mobile running scripts have one extra dword and a bunch of extra local variables, but the structure is essentially the same. My current plan is to start describing the fields and contents; I could use some input on the information you need to calculate their positions.

 

Link to post
Share on other sites
Posted (edited)
12 hours ago, Squ1dd13 said:

the post-processing system looks promising

Yeah, I think this is going to be the default strategy for addressing. I just remember why local addressing wasn't used for PS2 - compatibility. NTSC and PAL saves are normally compatible on PS2; the expectation is that embedded scripts would be compatible too. However, NTSC and PAL have different base offsets. If the BaseIP is adjusted to allow local addressing in embedded scripts then the save will no longer be compatible between regions. On PS2, version compatibility is not an issue; the scripts are so different that conversion is impractical. 

_______________________________________

 

Added: A comment regarding automating the embedding process.

 

The basic process of embedding the script usually involves writing the script to the save and activating the script with launch commands or inserting a running script. In practice, however, the activation step isn't always necessary and should be an optional process of an automated routine. The most common example occurs while debugging. If broken code needs to be replaced it may not be necessary to adjust the launch process or running script. Or, if a running script worked and was saved, but needs an update anyway, it might be necessary to reset the relativeIP and clear the local variables.

 

Not all embedded scripts need to be activated. I often use small "sniffer" scripts to launch a larger tool when a button is pressed, CJ enters a certain area, or to perform a common helper function. 

I have found it useful to write non-script data to extended memory. Examples include filling a large array of data, a process that avoids wasting space with codes that duplicate the data in an unusable format. And string arrays for custom GXT. Dn'to sweat this part too much though; it's only intended as something to think about as you develop your tool.

 

I'm curious about what the user interface is like. What I'm imagining is something that could used as a External Tool (Tools, IDE tools, User tools). Sanny can pass the active filename with the special word $SB_FileName. 

 

Edited by OrionSR
Link to post
Share on other sites
On 3/15/2021 at 9:57 PM, OrionSR said:

I could use some input on the information you need to calculate their positions.

I'm confused as to what you're asking here: the only unknown fields in block 0 which don't just look like padding are what I assume to be the mobile/PS2 equivalents to the SystemTime field that is in the PC saves. For block 1, like you say: there are very few differences there, and they are all in the template. Also yes, I am using 010 Editor along with the templates and scripts provided (which are very helpful).

 

On 3/15/2021 at 9:57 PM, OrionSR said:

Care to comment on Insert as the chosen term?

Insert seems quite descriptive, and it certainly makes sense to me (as someone who is not very familiar with this process).

 

On 3/16/2021 at 2:21 AM, OrionSR said:

I'm curious about what the user interface is like.

The interface is just CLI at the moment, but I was planning to add some sort of GUI once the core functionality is finished. As for Sanny Builder integration: I don't imagine it would be very difficult, so I'll have a look at it at some point. I've only ever used Sanny once, and that was a few years ago, so I'm not very familiar with it. I've never actually had to write a script, only code that works with compiled script data. Given that Sanny is pretty much the standard script compiler, it makes sense to get this tool working with it.

 

Edit

I have made some of your suggested changes (60000 bytes of variable space and changed PC save output size to 202752). I've also created a release on GitHub with attached binaries for Linux, macOS and Windows. The tool has no dependencies (which is one of the best things about Go), so anyone can just download a binary and try it out easily.

 

Another Edit

The tool now inserts the script rather than hijacking other ones. Once I've implemented the jump address translation system in Go, I think it should be possible to start embedding arbitrary scripts (rather than the current "Load Successful" one).

Edited by Squ1dd13
Link to post
Share on other sites
Posted (edited)
On 3/16/2021 at 10:25 AM, Squ1dd13 said:

The tool now inserts the script rather than hijacking other ones.

Sweet, and here I was trying to come up with a good argument for going through the extra work. Hijacking is still a useful strategy. I use it as a buff script for CJ and to set certain parameters for the tests; all inline codes, no jumps or conditions.

 

Sorry for the late response... I'd better document the process anyway.

 

Insert a Running Script

 

The terming Insert is intended to describe the process of activating a script altering the structure and data of script information in save block 1 instead of launching the script with opcode commands. The structure of block 1 is too complex to describe here. The format is basically the same for PS2 and PC, but with slightly different offsets, and mobile is only a little different overall if normal slot saves are edited. Please reference the binary templates for the 010 Editor for specific details of the save structure.

 

Inserting vs Launch by Hijacking: Hijacking seems easy enough, but in practice proved to be awkward. I would add each script, test, and add another, but I didn't resave between tests. The hijack script either needed to be adjusted to launch multiple scripts, or more than one script needed to be hijacked. Inserting the running scripts avoided these issues, and left the hijacking strategy available for player buffs and save tweaks. 

 

Another advantage to Inserting running scripts is that very small scripts can be embedded to the local VarSpace. Something I can't image how to activate with SCM code. To work properly, the jump offsets would need to be calculated based on a specific script index.

 

Block 0 - DWORD TimeInMillisecond The only useful data from this block. Adjust int WakeupTime with this value plus milliseconds to cause a delay in the execution of embedded scripts. I found it helpful to stagger the delay of embedded scripts to help establish that the save loads properly and to identify which script might be causing any problems.

 

Block 1 - TheScripts

 

int VarSpaceSize - The size of the VarSpace is the only thing that will need to be tracked to determine the offsets to the other fields describe below (on normal slot saves). RunningScripts is the only other structure that will vary in size.

 

int nRunningScripts - increment this value for each inserted script.

 

Within struct tRunning Scripts for each inserted script

  • all fields can be 0 unless otherwise noted. Unmentioned fields can be 0 on new scripts.
  • short index - each script must have a unique index for one of the 96 available slots for running scripts. Unlike other structures, running scripts are usually indexed in reverse; MAIN is usually at index 95 and later scripts have increasing lower indexes. Depending on progress, save files usually have about 30 to 40 active scripts always running. During game play, several external scripts will run and end as needed. The running scripts is a dynamic structure, so index gaps may appear between scripts. My strategy was to start adding scripts at index 20 and working down by Inserting new scripts at the end of the structure.
  • int streamedScriptIndex - mobile and later only; always seems to be -1. I'm not sure how it's used.
  • int pPrev - unused, 0 is fine for inserted scripts.
  • char Name[8] - Best practice is a 7 character script name plus a null terminator. This can be left blank if the script will name itself, I prefer to name my scripts here so it's easier to track which is which, and usually skip naming in the script to save a few bytes of varspace.
  • int BaseIP - Usually 0. Setting the BaseIP to the offset of the start of the script will allow the game to use the local addressing scheme used for missions, external scripts and cleo scripts. This would simplify the process of embedding a script but can lead to compatibility issues on PS2, and probably mobile too.
  • int CurrentIP - Usused, 0 is fine. Populated based on RelativeIP when save is loaded. For PC and PS2 it's calculated as RelativeIP + Offset to SCM. On mobile it's a bit more complicated. The offset to ImageBase is also included, and is a dynamic value. Offset to SCM hasn't been confirmed for anything other than Android v1.08, and this value is needed to calculate the ImageBase.
  • int ReturnStack[8] - Unused; populated based on RelativeReturnStack when loaded. 
  • short StackID - Index for the return stack used with gosub. See previous reports regarding hijacking scripts with gosub-return.
  • int Locals[40] - 32 for PC and PS2, mobile+ has 40.
  • byte IsActive - Always 1. I'm pretty sure a value of 0 will end an unwanted script. I usually set the relativeIP to the offset of an End Thread command to terminate a running script.
  • byte ScriptAttachType - Always -1 . I don't know what it does.
  • int WakeupTime - 0, or Time_In_Millisecond from block 0 plus a delay.
  • int RelativeIP - Set this value to the offset from the Start of SCM ($0) to where the compiled script has been embedded (var number * 4).
  • int RelativeReturnStack[8] - 0 for new scripts. Stack can be adjusted for hijacking with gosub-return.

 

Editing the Script

 

Increment nRunningScripts

 

Insert 262 bytes for PC and PS2, or 298 bytes for mobile, after all other running scripts or between running scripts. This will increase the size of the file, it'll need to be trimmed to size.

 

Within the running script

  • index = unique and descending
  • streamedScriptIndex = -1 (mobile only, field doesn't exist on PC and PS2)
  • Name (optional)
  • IsActive = 1
  • ScriptAttachType = -1
  • WakeupTime (optional)
  • RelativeIP = offset of embedded code
  • Everything else can be 0

 

 

Edited by OrionSR
Link to post
Share on other sites
On 3/14/2021 at 11:40 PM, OrionSR said:

Post-Processing Custom Scripts to Embedded Scripts

I've added this to the tool, so it can now take (theoretically, but not really) any script as an argument and embed it. I've tested with the "save anywhere" script on PC, and it seems to work perfectly.

Link to post
Share on other sites
2 hours ago, Squ1dd13 said:

it seems to work perfectly

Great progress. I'm afraid I'm having a hard time keeping up with you. 

 

It would probably be worth the trouble to run some additional tests against [email protected] and the jump opcode 0002. I had some initial conflicts early on. Adding the filesize limitation to jump offsets reduced the false positives, but I also started avoiding [email protected] or restricting it's use to non-int32 data types. There shouldn't be a problem unless a small negative integer is assigned, but I would expect values like -1 or -999 to be typed as bytes or words, so a false positive would be unlikely. IIRC, the original conflicts occurred when using hex values. We should test the current algorithm to hunt for false positives so we can provide proper warnings to the user.

 

Does your post processor manage the two switch/jump table opcodes? 0871: and 0872:. My thought was that I couldn't expect a consistent data type byte after the opcode word and a two byte search pattern would be even more prone to false positives. However, these opcodes will have 9 jump destinations at specific offsets that each conforms to the expectation of a valid jump offset for processing. 

 

Link to post
Share on other sites
Posted (edited)

I've been trying to track down the information needed to make a v2.00 version of the teleport script but I quickly ran into trouble. This script is intended as a test for relative addressing by reading data from fixed offsets but I can't find the expected labels in my database. 

Start of SCM
CTheScripts::ScriptSpace
_ZN11CTheScripts11ScriptSpaceE
apk108 71FFB0
apk200 A49960

start of radar pool
CRadar::ms_RadarTrace
_ZN6CRadar13ms_RadarTraceE
apk108 8F0A80
apk200 can't find

marker handle
gMobileMenu + 0x48
apk108 63E090
apk200 can't find

 

With Cleo for Android the standard practice is to find label offsets instead of hardcoding the offsets. This practice is supposed to version proof the scripts. 

0DD0: [email protected] = get_label_addr @_ZN6CRadar13ms_RadarTraceE
0DD1: [email protected] = get_func_addr_by_cstr_name [email protected] // start of marker structure
...
:_ZN6CRadar13ms_RadarTraceE
hex
"_ZN6CRadar13ms_RadarTraceE" 00
end

 

So I find it odd that I can't find these labels in my database. And I'm not finding reports of issues with old teleport scripts with the new cleo and 2.00. So perhaps the problem is with my database, or my lack of skills with these tools. I'm not sure how to continue. It might be necessary to work up another test of working with relative addressing.

 

Edited by OrionSR
Link to post
Share on other sites
13 hours ago, OrionSR said:

false positives

The post-processor disassembles the script and patches specific instructions rather than looking for specific bytes, so false positives should never happen. The only issue with this is that if there are issues with the disassembler, they will be carried forward to the post-processor, so it's important to make sure that both are robust. Theoretically, a bug could lead to a false positive, but the conditions under which one would be picked up would be very different to those in the 010 script, due to the difference of the systems.

 

13 hours ago, OrionSR said:

Does your post processor manage the two switch/jump table opcodes?

Not at the moment, but that shouldn't be too difficult to do due to the disassembly step. I'll look for a streamed script that uses switches and test on those (even though they won't be embeddable), or see if I can write a simple script that uses a switch and embed that.

 

11 hours ago, OrionSR said:

I can't find the expected labels in my database.

If all you need is the address of a particular symbol, a search of the dynamic symbol table should suffice:

> nm --demangle --dynamic /path/to/libGTASA.so | grep "gMobileMenu"
006e006c B gMobileMenu

The above works on my Ubuntu machine, but I don't know how you would do the equivalent on Windows. Although I've never needed to be able to do this (since the iOS build has no symbol names from the game code, only from what War Drum Studios wrote), I can imagine it would be quite useful for finding an address without opening a massive reverse engineering suite such as IDA or Ghidra.

Edited by Squ1dd13
hyphen
Link to post
Share on other sites
9 hours ago, Squ1dd13 said:

The post-processor disassembles the script

Wow. I'm impressed, and a bit humbled.  This strategy is the obvious solution but I couldn't talk Seeman into adapting Sanny with such a special purpose tool; it was just me at the time. The 010 strategy was the best I could manage, and was good enough to prove proof of concept. I am no longer concerned about false positives.

 

9 hours ago, Squ1dd13 said:

address of a particular symbol

Okay..., so the implication is; if opcodes 0DD0 and 0DD1 were "borrowed" from CleoA and embedded into the game using the Cleo1 strategy then the codes still couldn't find the required offsets on iOS. 

Can you speculate on the possibility of embedding opcodes on mobile saves? An obvious problem is that existing opcodes are probably version specific. 

 

11 hours ago, Squ1dd13 said:

> nm --demangle --dynamic /path/to/libGTASA.so | grep "gMobileMenu"

Can this function be performed with SCM codes?
 

Version Detection

0DD6: [email protected] = get_game_version
if
  [email protected] == 17 // 2.00

If we could run this opcode on iOS 2.00 and WinStore 2.00, would it produce different version IDs? In terms of relative offsets, is there any similarity between your iOS version and Android 2.00 (with FLA and Cleo)? My hunch is that relative offsets are unique to each... version+os. 

Link to post
Share on other sites
14 hours ago, OrionSR said:

Can you speculate on the possibility of embedding opcodes on mobile saves?

I'm not actually sure how this is done – is there a topic where this has been previously discussed that I can refer to for information? (I could also have missed something further up this thread, but I can't see anything.) At the moment I'm just imagining that the post-processor would embed SCM implementations and call them in place of the assigned opcode, but this is only a guess.

 

14 hours ago, OrionSR said:

Can this function be performed with SCM codes?

I meant it more as something for finding offsets statically, but it should be possible with SCM. I believe both the iOS and Android binaries have symbol tables that could be read with memory hacking from SCM code, but this would only be possible if the ranges of memory access from SCM would allow it. I don't know about the structure of other platforms' binaries, but I think WinStore is probably similar. I don't know about the older versions (PC and PS2), because I'm not at all familiar with the PS2's architecture and I'm uncertain about the contents of .exe files with regards to symbol tables (although they must have some sort of equivalent). In other words: theoretically, but only memory read range permitting. Please could you briefly outline the memory reading capabilities we have available from inside embedded scripts?

 

14 hours ago, OrionSR said:

would it produce different version IDs?

We can't run this opcode on iOS at the moment because I couldn't find enough details on the return value of that instruction to implement it. I think our best bet for platform detection is finding some area of memory that we can read which we know will be different between the platforms but that will be consistent in value on each individual platform so that we can just hardcode values in to check what the platform is. The issue with this is that values may change between versions on the same platform, so we would either have to check for a set of values that would verify each platform (each value found in the same location on the same platform but on different versions).

 

The method above doesn't solve the problem of version detection, to which the only solution I can think of is finding a number somewhere in memory that can reliably tell us the version. This number almost certainly doesn't exist under the conditions that it must be readable in the same way between platforms – unless we check the platform first and then choose an address to read based on the platform we find – and that it must be in the same place for each version. This system is unlikely to be possible at all, but I've added it here just in case I'm being unnecessarily pessimistic.

 

14 hours ago, OrionSR said:

My hunch is that relative offsets are unique to each... version+os

Yes; this is definitely true for Android and iOS 64-bit, because the Android build is 32-bit, so every time a pointer comes up the two offsets will be put out of sync by 4 bytes (on top of whatever the current out-of-sync-ness is). PC builds are also all 32-bit IIRC, but I may be wrong on this. In all honesty I had not thought about this until now. I suppose this means that it would be substantially easier to get relative addressing right for Android and PC than it will be for the same plus iOS.

Link to post
Share on other sites
16 hours ago, Squ1dd13 said:

I'm not actually sure how this is done

Check this post for more information on implementing Cleo1 through SCM. I'm not sure how it works but I was able to adapt the CLEO RUN section of the script into a subroutine I could embed in a PC save file. Cleo1 is mentioned briefly in ancient topics, which can be an interesting read but I doubt you'll learn anything more about Cleo1. It was a proof of concept that was quickly replaced the Cleo3 Library.

 

Cleo1 installs fully operational opcodes. I could use the same commands used in a script for Cleo3 (limited command set). These appear to be identical to cleo commands. As I understand it, the assembly instructions are crammed into the same function location. Once loaded, the opcode persist in memory during that session. I was worried about losing too much script space to cleo so I made a special save that loaded the opcodes into memory, then I could load saves that used those commands.


A short example:

{
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     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

So given this basic strategy on PC, is it possible to pull a CleoA opcode out of memory and paste it into a hex construct in an embedded script?

 

16 hours ago, Squ1dd13 said:

structure of other platforms' binaries

Don't worry about version detection for PS2 or PC. Those are easy enough to manage. The mobile versions are more problematic since the saves are still compatible even when the version or system are different. We don't need to solve this problem right away.

 

Can you find the information you need to adapt the teleport script to iOS using hard coded offsets? 

 

Memory Offsets for 1.08

 

I put together this google sheet for another project. It might provide useful examples of working with aDMA, relative addressing, and pointers. The data in this sheet could be adapted to hold offsets for other versions and systems. The idea is to build something that can produce lists of constants that Sanny can compile. I've been thinking about doing this anyway. I've got offsets for structures of many systems scattered in old scripts. 

 

 

Link to post
Share on other sites
8 hours ago, OrionSR said:

Can you find the information you need to adapt the teleport script to iOS using hard coded offsets? 

Finding the offsets shouldn't be too hard for iOS, but there are still two major issues: ASLR and 64-bit pointers. Given a way to find the ASLR slide, translating Android scripts to iOS would be easy on a 32-bit device. However, most users are not on 32-bit devices. As far as I am aware we can't easily work with 64-bit numbers in SCM, but the biggest problem is that I don't know how to read from pointers that are too big for normal script numbers. I am aware that we could just use two adjacent locals to store the two halves, but I can't think of a way to get the game to then read those two adjacent locals as a single pointer and dereference it.

 

I can't find anything on https://docs.sannybuilder.com about the syntax you're using for reading memory. Please could you clarify what lines such as the following are doing under the hood?

008B: 0@ = &0(1@,1i)

I get that this is reading from memory into [email protected], but how? What happens to the pointer value in [email protected]? The syntax looks similar to array access syntax, but I'm confused by the & symbol before the parentheses.

 

I'm hoping that this syntax is doing something that might allow us to use smaller values in place of pointers (e.g. array indices so that a large part of the pointer values could be replaced by the same divided by the size of the array element, such as pointer / 4 for an array of integers) with some other method. It's probably worth noting that reading memory on iOS 64-bit will always have to be from some offset or array because, by design, every pointer in the 64-bit binary is above 0x100000000 – one above the maximum value for a 32-bit unsigned integer – so that 32-bit pointers are never valid (to prevent people accidentally introducing confusing bugs by putting pointers in 32-bit values and then trying to dereference them). This is a clever idea, but unfortunately it means that we can't pass raw pointers from SCM to the game code since we'll never be able to store a pointer in SCM in the first place.

 

TL;DR: All the pointers are too big to store in a 32-bit value, so we need to get the game to create a pointer from some smaller number we give in the script and do the memory reading for us.

 

I am currently in the process of trying to find a way to read memory, but I'm not very far in yet. I think I may end up building CLEO iOS with an extra opcode added in for reading memory that will take a 32-bit value and read a number of bytes from the equivalent 64-bit address so that I can continue with other things while working out a real solution.

Edited by Squ1dd13
Link to post
Share on other sites
Posted (edited)
4 hours ago, Squ1dd13 said:

I am currently in the process of trying to find a way to read memory, but I'm not very far in yet.

There are two basic SCM hooks for reading and writing to game memory; stats and global variables. Strategies involving stats are limited in scope. The range of global variables can be extended by using arrays with a variable index. Since the index increments the offset by 4 byte I would expect the max range to be something like, signed int32 * 4.

 

Due to the nature of global variables, all offsets are inherently relative to the start of SCM, the "ScriptSpace" at $0. The teleport script is designed to test relative addressing, but it will only work if RadarTrace and gMobileMenu are static structures. Static structures are always found at the same relative offset. Dynamic structure get loaded to different memory locations at runtime. A static structure on PC may be a dynamic structure on PS2 or mobile. At this point we don't need to know ASLR/image base offsets, and technically we don't need to know the offset to SCM/ScriptSpace either, except...

 

Almost all documentation for hard coded offsets uses the "base" offset format. Image base, or game base; these are the offsets used by memory opcodes. These are the offsets I see if I open the game process on PC with a hex editor or memory tool. Similar offsets apply when viewing memory dumps for PS2 emulators, or viewing IDA databases of the executable files. So in practice, the relative offset if usually calculated as the base offset of the structure or data minus the offset to ScriptSpace. 

 

The next calculation is to divide the relative offset by 4 to account for the indexing in the array. Special handling will be required to read offsets that are not doubly-even. All values read are dwords. Special handling is required to work with bytes and words.

 

& vs $ - aDMA and Global Variables: These are basically the same thing. Note that in this example that the opcode is for assigning @ = $. 

008B: [email protected] = &0([email protected],1i)

 

This is basically the same thing as [email protected] = $0([email protected],1i) but $0 and $1 are reserved variables. They hold SCM instructions to jump around the varspace, and Sanny tries to protect them from tampering.  The main difference between aDMA and globals is that the number is for bytes (&) instead of dwords ($). So, &8 is the same as $2. Usually I just work with &0, but &1, &2 and &3 could be used to read offsets that are not doubly-even (hasn't come up in practice). Or, a series of values like XYZ can be read without changing the index; &0([email protected],1i) &4([email protected],1i) &8([email protected],1i). Another advantage to aDMA over memory opcodes is that, as global variables, you don't need to read the value into another variable to use it in another opcode.

 

Um... I'll post this so you can get started and review your other comments. Hopefully, we don't need to work with pointers just yet. However, if the structures for warping are dynamic we can design another test on other structures.

Edited by OrionSR
Link to post
Share on other sites
5 hours ago, Squ1dd13 said:

every pointer in the 64-bit binary is above 0x100000000

I'm not sure what to think about this. There's nothing I can do to test anything and there are far too many unknowns. I'm not even sure if my iOS saves were last touched by iOS, and hadn't even considered a difference between 32 and 64 bit OS. 

 

Can you decode the offsets used in running scripts? In the save these are 32-bit. BaseIP and RelativeIP are moving targets, it's easier to work the return stacks because waits are harder to track in the scripts and returns from gosubs linger in data after use. On PC, the base offset is the relative offset plus offset to ScriptSpace. On Android it's image base + ScriptSpace + relative offset. The addressing format used for instruction pointers on Android match the format used for pointers (or a memory opcode with the appropriate add ib / fix ib flag). 

 

What calculations are required to find the same stack offset using the 64-bit format used for iOS pointers. Could it be something like ASLR + image base + script space + relative offset?
 

Link to post
Share on other sites
MrNobodyx88

Hello OrionSR 😀 I can't send you a private message, I don't know why, so I'm writing from here, my problem is: I have a gtasa save file and blips disappear in that file. I used the modification tool in gtasnp but got a "Stray blips not detected" warning. What can I do? Or I will send you my save file by e-mail and you can fix it.

Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • 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.