OrionSR 2,427 Posted July 26, 2019 Share Posted July 26, 2019 (edited) Embedding Scripts in Save Files Embedded scripts operate on the premise that the global variable space occupies the beginning of the same SCM script space used for running scripts, so any code written to the global variable space can be executed, or launched and remain active like any other running script. The purpose of this topic is to document strategies used with San Andreas on PC and Android in the hope that these methods can be adapted to other GTA games, environments without custom cleo scripts, or as an alternative to a custom main.scm. Busted: Some strategies described in this documentation are not working as expected. Most notably, when a global variable is used in an embedded script then global variable space is added at the start of SCM and embedded codes are offset accordingly, leading to incorrect jump addresses. Avoiding all global variables or manually adjusting each jump address are both terrible options. I am looking for other solutions. Overview: Hijacking a Running Script: Custom scripts are compiled with Sanny Builder. A hex editor is used to copy the binary code in the compiled script and paste it into the global variable space of a saved game. A currently running script is modified with a hex editor so the next instruction (relativeIP) will begin at the address of the codes embedded in the global variable space. The custom codes end with a jump to the original relativeIP so the running script can resume normal operation. Reclaiming Global Variable Space from SCM Memory: Several standard arrays provide a little room for temporary code in a standard save, but there is little room for permanent code. However, much of the SCM script beyond the global variable space is no longer needed when save data is loaded. Several thousand bytes of SCM memory can be recovered by permanently extending the global variable space and used to store compiled scripts. Managing Jump Addresses: Sanny Builder will accurately encode the proper jump addresses for labels if the script is compiled with the codes located at the proper offset in a custom main.scm file. This “NOPped” main includes only what is necessary to compile and decompile properly as an SCM file, jump addresses for the future location of the scripts, and thousands of 0000: NOP instructions. When the nopped main is decompiled, custom labels are inserted into the sea of nops and provide reference for the accurate placement of the codes. Example Scripts: Basic Test: Hijack a running script to display a message. A simple test of the basic strategy. Extend Variable Space: Hijack a running script to increase the size of the global variable space and initialize the variables. Launch a Launcher: Hijack a running script to launch a new permanently running script that can easily launch other scripts. Save Anywhere: A useful tool using basic scm code and test of user input. Should work the same regardless of version. Warp to Marker: Reading memory using ADMA addressing. Highly depended on version. Testing with Cleo: The goal is to avoid using cleo opcodes and to run custom scripts in environments without cleo scripts, but I'm not the least bit shy about using cleo scripts while testing. In particular, all complex scripts are tested as cleo scripts on PC with Cleo4, or Android with CleoA. When the script is working properly the codes can be copied to the nopped main (without the {cleo. cs} directive) and compile without any additional modification. Working With a Hex Editor: It should be possible to embed scripts with a free hex editor like HxD, and I hope to provide enough reference that players can find the appropriate offsets, but this is a complex task and I don't want to handicap myself too much so I'll be describing the hex editing process based on the 010 Editor. This proprietary tool supports binary templates that can identify saves of different versions and systems, parse save files into organized menus, and display and edit information in familiar formats.Fortunately, 010 has a generous trial period. There is reasonably good documentation for SA PC save files available, but only the SA binary templates can describe PS2 and mobile saves properly. Fixing the Checksum: Whenever a save file is modified the 32-bit CRC checksum occupying the last 4 bytes of the save must be updated to the sum of all bytes or a corruption message will be displayed in the slot and the save can't be loaded. This is such an automatic process for me that I often forget to mention it, so this might be the only time I'll bring it up. Edit the save, update the checksum, every time before saving. The binary template package for 010 contains a checksum script; just run it on the save. HxD has a checksum-32 tool. Run it on the whole file after clearing the last 4 bytes, and encode in little endian. Most save editors, including GTASnP.com, will update the checksum when saving. Basic Test Script: The script will wait just long enough for the screen to fade in and to verify the save loaded properly, then display a message, play a tune and jump to the end_thread command. The jump address will be modified to match the original relativeIP of the hijacked script. The end_thread statement is needed here for the cleo version to work, but won't be executed in the embedded version. {$CLEO .cs} // {$CLEO .csa} // BasicTest.txt 0001: wait 4000 ms 00BA: show_text_styled GXT 'FESZ_LS' time 4000 style 4 // Load Successful. 0394: play_music 2 // Mission Complete! 0002: jump @End :End 004E: end_thread This compiles to: 01 00 05 A0 0F BA 00 09 46 45 53 5A 5F 4C 53 00 05 A0 0F 04 04 94 03 04 02 02 00 01 E0 FF FF FF 4E 00 E0 FF FF FF is the jump address that needs to be changed. By happy accident, this value falls evenly within a single global variable, which makes it a bit easier to edit with the 010 template. The address is specific to each save so I make this edit after embedding the code. $Roulette_Cash_Won Array (151i, 604 bytes) * not defined in INIs VarNum* 4 = SCM Offset + Offset from start of file = *Global Offset $8398 * 4 = 33592 + 326 = 33918 PC v1/v2 $9765 * 4 = 39060 + 442 = 39502 Mobile $8397 * 4 = 33588 + 342 = 33930 PS2 v1 $8402 * 4 = 33608 + 342 = 33950 PS2 v2 *Global in this case is a GoTo setting for start of file. The $Roulette_Cash_Won Array is my preferred workspace for temporary codes. It's a bit smaller than some of the mission string arrays but provides an in-game method to erase the temporary code – just start the roulette mission. The embedded codes don't seem to effect the payout and the array is reinitialized at the end. Pasting Binary Code into the Global Variable Space: 010: Open Save Blocks > Block 01: CTheScripts > GlobalVars and scroll to the appropriate variable for your version. Selecting the variable in the template will highlight the variable in the hex window. Paste the compiled code and overwrite the existing data in the hex window, starting at the beginning of this variable. HxD: The global variable space is in a static location for each specific type of save, 4 bytes after the second BLOCK marker that separate each save block. The SimpleVars stored in block 0 are always the same size, so the GoTo offset of a global variable can be calculated as $VarNum * 4 + StartOfVarSpace. However, since the variable space can, and will, change size, and the number of running scripts will vary, static offsets are of little use for anything found later in the save. Hijacking a Running Script: 010: Open Save Blocks > Block 01: CTheScripts > tRunningScripts > Script[1], usually 'oddveh' (script 2 on PS2). RelativeIP is near the bottom. Record the current value; it is needed as the jump address at the end of the embedded code. Change the RelativeIP to the local offset for the start of the Cash Won array. 010 will manage hex and decimal conversions, so format doesn't matter. HxD: Calculating the offsets of running scripts in a save is a bit awkward, I'll work towards providing full reference. Running scripts is near the end of TheScripts block and usually contain a text script name so they are easy to find by searching for the third BLOCK (block 2) and scrolling backward until the script name is seen. Or just search for the script name. The offset for the script name on PC and PS2 is +10 from the start of the running script, and the offset to the relativeIP is +226, so +216 from the start of the script name. Offsets on mobile are +14 for script name and +262 for relativeIP, so +248 from script name. Fix the Embedded Jump: 010: Return to where the the codes were embedded in the variable space and find the cleo version of the jump address: E0 FF FF FF. Right-click on the hex data and select Jump to Template variable – this will take you directly to the global variable that corresponds to this address. Enter the value recorded from the original relativeIP. HxD: Pretty much the same process. The latest versions of HxD have an Inspector tool that can display and edit the hex data in familiar formats. Fix the checksum, save and test. There is little that can go wrong with the embedded codes, so this should simply be a test of working out the basic strategies and putting together all of the pieces into a working script. Extend Variable Space: The next goal is to recover enough space to store larger scripts. The script will extend the variable space, initialize the variables to 0, and display a success message and tune before jumping back to the hijacked script using the same strategies described above. The main difference between this script and the Basic Test is that the initialization process will include a loop, so jump_to and jump_if addresses will need to correspond to the variable space. Note: There is a direct relationship between the variable space in a save, SCM memory, and the beginning of an SCM file when viewed with a hex editor. First the scm file is loaded into SCM memory, then the global variable space is written over the beginning of SCM memory when the save is loaded. NOPmain.scm: This is a non-viable scm file. It's purpose is to properly encode jump addresses for embedded scripts. To gain access to as much memory as possible, a stripped main was modified to remove as much code as possible but still remain a valid scm file that can be compiled and decompiled with Sanny. // This file is intended to be used with embedded scripts in GTA San Andreas. It is not a viable main.scm. DEFINE OBJECTS 1 DEFINE OBJECT SANNY BUILDER 3.2.2 DEFINE MISSIONS 0 DEFINE EXTERNAL_SCRIPTS -1 // Use -1 in order not to compile AAA script DEFINE UNKNOWN_EMPTY_SEGMENT 0 DEFINE UNKNOWN_THREADS_MEMORY 0 //-------------MAIN--------------- 0000: NOP :MAIN 0001: wait 250 ms 0002: jump @MAIN 0000: NOP This file needs to be padded with lots of 0000: NOP (no operation) instructions to provide working space for the embedded script. Lots, as in about 100,000 are needed to account for all 200,000 bytes of SCM memory. About one third of that is plenty. Still, it's a lot easier to compile this short version and insert 200,000 bytes at the end of the file using a hex editor than it is to copy and paste the opcodes. Decompile the larger file. Add the appropriate jump destinations after the lines posted above. These will be used in combination with customlabels.ini to insert labels at the correct jump address. // SA PC ------- Nopped Main -------- 0002: jump 33592 // = Roulette_Cash_Won $8398 * 4 0002: jump 43808 // = Extended_VarSpace $10952 * 4 0002: jump 54000 // = Launch $13500 * 4 0002: jump 54040 // = OSRSave $13510 * 4 0002: jump 54120 // = OSRWarp $13530 * 4 // SA Mobile ------- Nopped Main -------- 0002: jump 39060 // = Roulette_Cash_Won $9765 * 4 0002: jump 49212 // = Extended_VarSpace $12303 * 4 0002: jump 54000 // = Launch $13500 * 4 0002: jump 54040 // = OSRSave $13510 * 4 0002: jump 54120 // = OSRWarp $13530 * 4 Add to CustomLabels.ini. Enable Custom Labels in Tools > Options > Formats ; SA PC ------- Nopped Main -------- 33592 = Roulette_Cash_Won // $8398 * 4 43808 = Extended_VarSpace // $10952 * 4 54000 = Launch // $13500 * 4 54040 = OSRSave // $13510 * 4 54120 = OSRWarp // $13530 * 4 ; SA Mobile ------- Nopped Main -------- 39060 = Roulette_Cash_Won // $9765 * 4 49212 = Extended_VarSpace // $12303 * 4 54000 = Launch // $13500 * 4 54040 = OSRSave // $13510 * 4 54120 = OSRWarp // $13530 * 4 Note that when codes are added after a label, the offsets for labels later in the script will no longer be aligned at the proper offset. I keep reverting to a saved scm for new scripts. VarSpace Script: &3([email protected],1i) uses ADMA formatting to change the dword that starts at $0 +3 bytes. This value controls how much memory will be saved for the global variables, and spans $0 and $1. Since these two globals are protected by Sanny, ADMA is the easiest way to manage this variable anyway. (The VarSpaceSize stored just before the global variables in the save file determines how much save data will be loaded into memory.) To increase compatibility, I'm restricting the expanded variable space to what is available to PC v2. A little more space could be squeezed out of v1. The padding is added so the last jump address will land evenly on a global, but this isn't strictly necessary. Reworked to avoid using global variables. {$CLEO .cs} //PCVarSpace.txt // Reclaim Global Variable Space from SCM // GTASA PC v1/v2 // $10952 thru $15006 (+4055) wait 4000 ms [email protected] = 0 &3([email protected],1i) = 60028 // Set size of variable space for [email protected] = 10952 to 15006 step 1 &0([email protected],1i) = 0 end //padding 0000: NOP 00BA: show_text_styled GXT 'FESZ_LS' time 4000 style 4 // Load Successful. 0394: play_music 2 // Mission Complete! 0002: jump @End :End 004E: end_thread {$CLEO .csa} //MobileVarSpace.txt // Reclaim Global Variable Space from SCM // GTASA Android 1.08 // $12303 thru $16809 (+4507) wait 4000 [email protected] = 0 &3([email protected],1i) = 67240 // Set size of variable space for [email protected] = 0 to 4505 step 1 $12303([email protected],1i) = 0 end //padding wait 256 ms 0000: NOP 00BA: show_text_styled GXT 'FESZ_LS' time 4000 style 4 // Load Successful. 0394: play_music 2 // Mission Complete! 0002: jump @End :End 004E: end_thread Decompile NOPmain.scm with custom labels. Insert everything except the cleo directive into the scm script just after the custom label for the Cash Won array. Compile the script and open with a hex editor. Find the compiled codes within a mostly blank scm, or at the specified offset, and copy to the Cash Won array in the global variable space of the save. Update the relativeIP of a running script and the final jump address in the embedded code to complete the hijacking process as before. Update, save, test and save the game. Open the modified save with a hex editor to observe the changes to the variable space. One issue I've encountered on mobile saves, if enough scripts are launched, objects, cars or peds added by the screwy checkpoint system, or cargens (and stunt jumps) added by mods, it's not too difficult for a normal safehouse-type save to become so bloated that the save gets bumped up into the next larger size. This doesn't effect anything other than the 010 template, which detects the large save as “MissionThread” save and confuses how things are parsed, so I have not been allocating the max possible variable space on mobile saves. I'm not sure how I want to manage over 16,000 bytes of SCM memory. I want to reserve a lot of space for global variables that can be used by scripts, and it makes sense for them to be contiguous with the standard global variables so I don't want to add code at the beginning. I also don't want to get too deep into the available space as I'm not sure of the optimal variable space to reserve for mobile due to issues with the size of the save. So for these examples I've decided to start compiling code at global variable $13500, offset 54000. This will allow for almost 1200 global variables that can be common between all script versions, and new scripts can occupy the same space on any system. Launching a Launch Running Script: This process will require embedding two scripts, a permanently running launch script stored in the expanded variable space, and a hijacked script to launch it. Hijacking gets tedious, so a launch script will be handy, but there isn't much of a payoff until the next running script is ready. The launcher script is shorter than what has already been written to the Cash Won array. It might be easier to identify the proper jump address if the array was cleared of existing data. Compile Launcher as a cleo script and embed to the Cash Won array. Hijack a script to run the code. Finish the next script before testing. {$CLEO .cs} //Launcher.txt // Hijack strategy to launch a script wait 4000 004F: create_thread 54000 // Launch 00BA: show_text_styled GXT 'LAUNCH' time 4000 style 4 // Launch 0002: jump @End :End 004E: end_thread Launch as a Running Script: This script is installed to $13500 using a nopped main and will launch a script at the non-zero address assigned to $13499, and then reset the variable. The script will remain active in the save and will launch all new remaining scripts. {$CLEO .cs} //Launch.txt 03A4: name_thread 'LAUNCH' :Launch 0001: wait 4000 00D6: if 8038: not $13499 == 0 004D: jump_if_false @Launch 004F: create_thread $13499 00BB: show_text_lowpriority GXT 'LAUNCH' time 4000 flag 1 // Launch 0004: $13499 = 0 0002: jump @Launch Save Anywhere: The save script is installed with a nopped main to $13510, offset 54040. Launch the script by changing $13499 to 54040. Activate the script by holding the group backwards button, usually H when using keyboard controls, or down on the D-pad when using a controller. This script should work the same on PC and PS2. {$CLEO .cs} 03A4: name_thread 'OSRSAVE' :OSRSAVE 0006: [email protected] = 0 :Holding 0001: wait 0 ms 00D6: if 00E1: player 0 pressed_key 9 //~k~~GROUP_CONTROL_BWD~ 004D: jump_if_false @OSRSAVE 00D6: if and 0038: $ONMISSION == 0 0019: [email protected] > 4000 004D: jump_if_false @Holding 03D8: show_save_screen 0002: jump @OSRSAVE The variations in mobile version are for the different input controls – the user must hold the weapon widget. Mobile has more local variables than PC and PS2, so the first local timer is [email protected] instead of [email protected] (A little padding was added to keep the byte count even. The goal is to have any gaps between scripts filled with 2-byte NOPs so the whole block could be decompiled with Sanny without throwing any errors.) {$CLEO .csa} 03A4: name_thread 'OSRSAVE' :OSRSAVE 0006: [email protected] = 0 :Holding 0001: wait 0 ms 00D6: if 0A51: is_widget_pressed 159 // Weapon Widget 004D: jump_if_false @OSRSAVE 00D6: if and 0038: $ONMISSION == 0 0019: [email protected] > 3000 004D: jump_if_false @Holding 03D8: show_save_screen 0002: jump @OSRSAVE Warp to Marker: This script uses an ADMA method to read memory as a substitute for cleo commands. Working with memory makes these scripts specific to a v1 executable on PC, and Android 1.08. I don't have the information or hardware needed to properly convert and test this script on other systems -- other than PS2, maybe, if I can get it all working one more time. PS2 saves are awkward to manage, so I'll put this off pending any interest. OSRWarp is embedded at $13530 and launched at 54120. The differences between the scripts are the local timer variable, key press and widget checks, and memory addresses. CJ will warp to the map marker only if it is set when Conversation No – usually N, is held. (Thanks again to ZAZ for the original teleport script.) {$CLEO .cs} 03A4: name_thread 'osrwarp' //osrwarp.txt //PC v1 :OSRWARP 0006: [email protected] = 0 :Holding 0001: wait 0 ms 00D6: if 00E1: player 0 pressed_key 10 //~k~~CONVERSATION_NO~ 004D: jump_if_false @OSRWARP 00D6: if 0019: [email protected] > 2000 004D: jump_if_false @Holding 0006: [email protected] = 0xBA6774 // read address // marker handle 000E: [email protected] -= 0xA49960 // start of scm 0016: [email protected] /= 4 // ADMA index 008B: [email protected] = &0([email protected],1i) // read marker handle 00D6: if 8039: not [email protected] == 0 004D: jump_if_false @OSRWARP 0012: [email protected] *= 0x10000 // extract index 0016: [email protected] /= 0x10000 0006: [email protected] = 40 // size of record 006A: [email protected] *= [email protected] // index 000A: [email protected] += 8 // offet to coords 000A: [email protected] += 0xBA86F0 // read address // start of radar pool 000E: [email protected] -= 0xA49960 // start of scm 0016: [email protected] /= 4 // ADMA index 008B: [email protected] = &0([email protected],1i) // read X 008B: [email protected] = &4([email protected],1i) // read Y 0172: [email protected] = get_char_heading $PLAYER_ACTOR 01B4: set_player_control $PLAYER_CHAR to 0 04BB: set_area_visible 0 04FA: clear_extra_colours 0 057E: set_radar_as_interior 0 04E4: request_collision [email protected] [email protected] 03CB: load_scene [email protected] [email protected] 0.0 00D6: if 0256: is_player_playing $PLAYER_CHAR 004D: goto_if_false @Finish 0860: set_char_area_visible $PLAYER_ACTOR to 0 00A1: set_char_coordinates $PLAYER_ACTOR to [email protected] [email protected] -100.0 0173: set_char_heading $PLAYER_ACTOR to [email protected] :Finish 0001: wait 0 00D6: if 0256: is_player_playing $PLAYER_CHAR 004D: goto_if_false @Finish 01B4: set_player_control $PLAYER_CHAR to 1 0373: set_camera_behind_player 02EB: restore_camera_jumpcut 0002: jump @OSRWARP 0001: wait 256 {$CLEO .csa} 03A4: name_thread 'osrwarp' //osrwarp.txt //Android v1.08 :OSRWARP 0006: [email protected] = 0 :Holding 0001: wait 0 ms 00D6: if 0A51: is_widget_pressed 160 // Radar Widget 004D: jump_if_false @OSRWARP 00D6: if 0019: [email protected] > 2000 004D: jump_if_false @Holding 0006: [email protected] = 0x63E090 // read address // marker handle 000E: [email protected] -= 0x71FFB0 // start of scm 0016: [email protected] /= 4 // ADMA index 008B: [email protected] = &0([email protected],1i) // read marker handle 00D6: if 8039: not [email protected] == 0 004D: jump_if_false @OSRWARP 0012: [email protected] *= 0x10000 // extract index 0016: [email protected] /= 0x10000 0006: [email protected] = 40 // size of record 006A: [email protected] *= [email protected] // index 000A: [email protected] += 8 // offet to coords 000A: [email protected] += 0x8F0A80 // read address // start of radar pool 000E: [email protected] -= 0x71FFB0 // start of scm 0016: [email protected] /= 4 // ADMA index 008B: [email protected] = &0([email protected],1i) // read X 008B: [email protected] = &4([email protected],1i) // read Y 0172: [email protected] = get_char_heading $PLAYER_ACTOR 01B4: set_player_control $PLAYER_CHAR to 0 04BB: set_area_visible 0 04FA: clear_extra_colours 0 057E: set_radar_as_interior 0 04E4: request_collision [email protected] [email protected] 03CB: load_scene [email protected] [email protected] 0.0 00D6: if 0256: is_player_playing $PLAYER_CHAR 004D: goto_if_false @Finish 0860: set_char_area_visible $PLAYER_ACTOR to 0 00A1: set_char_coordinates $PLAYER_ACTOR to [email protected] [email protected] -100.0 0173: set_char_heading $PLAYER_ACTOR to [email protected] :Finish 0001: wait 0 00D6: if 0256: is_player_playing $PLAYER_CHAR 004D: goto_if_false @Finish 01B4: set_player_control $PLAYER_CHAR to 1 0373: set_camera_behind_player 02EB: restore_camera_jumpcut 0002: jump @OSRWARP References: GTASA 010 Binary Template 2019 - Parses data for all known GTASA save files. [Sanny] Custom Edit Modes - Info on creating custom profiles for PS2 and SA Mobile with version specific custom variables and full IDE support. Using Pointers and Image Base on Android 1.08 and ADMA. Seemann suggested this post and this post for pre-cleo strategies for managing protected memory (assembly method). Sanny's Archive of MAIN.SCM versions. When possible, archive dates match file date of script.img. 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 April 23, 2020 by OrionSR 4 Link to post Share on other sites
OrionSR 2,427 Posted July 26, 2019 Author Share Posted July 26, 2019 (edited) Alternate Strategies for Expanding the Global Variable Space: I'm working on a strategy for embedding scripts that should be more compatible with the current version of Sanny Builder (v3.2.3). This will restrict most embedded scripts to the expanded variable space but should provide a reliable method of tricking Sanny into compiling the proper addresses. One problem, the script that expands and initializes the expanded variable space still needs a place to run. It should be possible to manage this one script, but there are alternative strategies. If you are editing for PC or Android with Cleo installed, use the cleo version of the script in the first post. Use a hex editor to manually increase the size of the saved variable space by editing the dword starting at $0+3. then... Either load and save to let the game expand the varspace, then use an editor to initialize the variables, Or, also increase the VarSpaceSize at $0 -4 to match, insert bytes at the end of the varspace, and trim the file to length. Use an 010 script to automate the hex editing process. ExpandedVarSpace.1sc - 010 Script Spoiler //-------------------------------------- //--- 010 Editor v6.0.3 Script File // // File: ExpandVarSpace.1sc // Author: OrionSR // Revision: v0.1 // Purpose: Expand the variable space in GTA San Andreas save files. //-------------------------------------- // The maximum VarSpace is not assigned for mobile because data might get trimmed. // Refresh the template to make sure it can still complete execution after editing. //Find Start of VarSpace FSeek( FindFirst( "BLOCK*",1,0,1,0.0,1,FTell(),0,5 ) ); FSeek( FindNext(1)+9 ); int VarSpaceStart = FTell(); int OldVarSpaceSize = Save.CTheScripts.VarSpaceSize; // read from template variable int FileEnd = FileSize(); int NewVarSpaceSize = 60028; // PCv2 default if (isMobile == 1) NewVarSpaceSize = 60028; // max for mobile is 67240 if (isPS2 == 1) NewVarSpaceSize = 60176; // PS2v1 // Set VarSpace to Load Save.CTheScripts.VarSpaceSize = NewVarSpaceSize; // Set VarSpace to Save WriteInt(VarSpaceStart +3, NewVarSpaceSize); // Insert extra VarSpace and Trim File to Original Length InsertBytes( VarSpaceStart + OldVarSpaceSize, NewVarSpaceSize - OldVarSpaceSize, 0 ); DeleteBytes( FileEnd, NewVarSpaceSize - OldVarSpaceSize ); // Fix Checksum WriteUInt(FileSize()-4, Checksum(CHECKSUM_BYTE, 0, FileSize()-4)); ____________________________________________________ Can anyone offer any suggestions for custom text (GXT) in this context? This is a major limitation for embedded scripts. How to edit protected memory? The VP strategies suggested by Seemann involve editing opcodes with assembly code, which is not likely to be of much help on anything other than PC. Still, I really like the idea of read and write memory scripts, or common subroutines, especially when managing image base on mobile-based games. SCM Data Types - the byte before data (useful for reading compiled SCM code) Spoiler From Cleo.h //operand types #define globalVar 2 //$ #define localVar 3 //@ #define globalArr 7 //$(,) #define localArr 8 //@(,) #define imm8 4 //char #define imm16 5 //short #define imm32 1 //long, unsigned long #define imm32f 6 //float #define vstring 0x0E //"" #define sstring 9 //'' #define globalVarVString 0x10 //v$ #define localVarVString 0x11 //@v #define globalVarSString 0x0A //s$ #define localVarSString 0x0B //@s Edited August 2, 2019 by OrionSR 2 Link to post Share on other sites
OrionSR 2,427 Posted August 1, 2019 Author Share Posted August 1, 2019 (edited) Embedding Cleo in Save Files Seemann suggested Cleo1, an SCM version of Cleo with a limited set of opcodes, as a template for creating an embedded version of Cleo for PC v1. This script was easy to convert to this purpose with few modifications. I'm still having problems with my jump addresses so I won't include instructions for installation. However, I have a demo save. This demo save can be used to load cleo for scripts in your saves. https://gtasnp.com/GjL2TN The Embedded Cleo1 save will launch 3 scripts - wait until it settles. Cleo_Run will set the cleo opcodes and end. Ewarp will warp to marker when N is held. Esave will save when H is held. Ewarp uses the Cleo opcode to read memory - the primary test. Esave uses standard SCM, but doesn't check $OnMission because globals are still confusing my jump address. If this save is resaved and immediately loaded, the warp script still works, so the cleo opcodes persist during the session. Restart the game and reload the re-saved version - the save script still works but warping will crash the game. This saved is based on a debug version of a Chain Game save which has been heavily modified, so ignore or enjoy the weirdness. CJ has been buffed and the map has been opened to help with testing, but the phone calls are bugged so the early LS strand can't be finished.CLEO_RUN as Embedded: Contains reference on which opcodes were added. This limited set of opcodes are compatible with the current sascm.ini for PC. Spoiler Basically, I added the end_thread and commented out the stripped MAIN codes. I was afraid to touch anything else. //{$Cleo .s} //{$VERSION 3.1.0020} // Provides executing each time the game started gosub @CLEO_RUN wait 250 end_thread /* 03A4: name_thread 'MAIN' var $PLAYER_CHAR: Player end // var 01F0: set_max_wanted_level_to 6 0111: toggle_wasted_busted_check 0 00C0: set_current_time 15 0 04E4: unknown_refresh_game_renderer_at 824.0641 -1794.8955 Camera.SetAtPos(2488.562, -1666.865, 12.8757) $PLAYER_CHAR = Player.Create(#NULL, 2488.562, -1666.865, 12.8757) $PLAYER_ACTOR = Actor.EmulateFromPlayer($PLAYER_CHAR) 07AF: $PLAYER_GROUP = player $PLAYER_CHAR group Camera.SetBehindPlayer set_weather 0 wait 0 ms $PLAYER_CHAR.SetClothes("PLAYER_FACE", "HEAD", Head) $PLAYER_CHAR.SetClothes("JEANSDENIM", "JEANS", Legs) $PLAYER_CHAR.SetClothes("SNEAKERBINCBLK", "SNEAKER", Shoes) $PLAYER_CHAR.SetClothes("VEST", "VEST", Torso) $PLAYER_CHAR.Build $PLAYER_CHAR.CanMove = True fade 1 (out) 0 ms select_interior 0 0629: change_stat 181 (islands unlocked) to 4 016C: restart_if_wasted at 2027.77 -1420.52 15.99 angle 137.0 for_town_number 0 016D: restart_if_busted at 1550.68 -1675.49 14.51 angle 90.0 for_town_number 0 0180: set_on_mission_flag_to $ONMISSION // Note: your missions have to use the variable defined here ($ONMISSION) 03E6: remove_text_box 0330: toggle_player $PLAYER_CHAR infinite_run 1 // show custom text 0A8E: [email protected] = 0xA49964 + @_TextPtr // MainScm + 2 bytes of opcode + 2 bytes of datatype and string length + label 0AA5: call 0x588BE0 num_params 4 pop 4 0 0 0 [email protected] // create a car with the only opcode 0AA5: call 0x43A0B6 num_params 1 pop 1 #INFERNUS end_thread :_TextPtr 0900: "Custom Text Here" 0000: null-terminator */ { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Project "CLEO" v1.29 Final by Seemann (c) 2007 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ http://www.gtaforums.com/index.php?showtopic=268976 http://www.sannybuilder.com/forums/viewtopic.php?id=62 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Opcodes List: 0A8C: write_memory <dword> size <byte> value <dword> virtual_protect <bool> 0A8D: <var> = read_memory <int32> size <byte> virtual_protect <bool> 0A8E: <var> = <valA> + <valB> // int 0A8F: <var> = <valA> - <valB> // int 0A90: <var> = <valA> * <valB> // int 0A91: <var> = <valA> / <valB> // int 0A96: <var> = actor <handle> struct 0A97: <var> = car <handle> struct 0A98: <var> = object <handle> struct 0A99: chdir <flag> 0A9A: <var> = openfile "path" mode <dword> 0A9B: closefile <hFile> 0A9C: <var> = file <hFile> size 0A9D: readfile <hFile> size <dword> to <var> 0A9E: writefile <hFile> size <dword> from <var> 0A9F: <var> = current_thread_address 0AA0: gosub_if_false <label> 0AA1: return_if_false 0AA2: <var> = load_library "path" 0AA3: free_library <hLib> 0AA4: <var> = get_proc_address "name" library <hLib> 0AA5: call <address> num_params <byte> pop <byte> [param1, param2...] 0AA6: call_method <address> struct <address> num_params <byte> pop <byte> [param1, param2...] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SASCM.INI Lines 0A8C=4,write_memory %1d% size %2d% value %3d% virtual_protect %4d% 0A8D=4,%4d% = read_memory %1d% size %2d% virtual_protect %3d% 0A8E=3,%3d% = %1d% + %2d% ; int 0A8F=3,%3d% = %1d% - %2d% ; int 0A90=3,%3d% = %1d% * %2d% ; int 0A91=3,%3d% = %1d% / %2d% ; int 0A96=2,%2d% = actor %1d% struct 0A97=2,%2d% = car %1d% struct 0A98=2,%2d% = object %1d% struct 0A99=1,chdir %1b:userdir/rootdir% 0A9A=3,%3d% = openfile %1s% mode %2d% // IF and SET 0A9B=1,closefile %1d% 0A9C=2,%2d% = file %1d% size 0A9D=3,readfile %1d% size %2d% to %3d% 0A9E=3,writefile %1d% size %2d% from %3d% 0A9F=1,%1d% = current_thread_pointer 0AA0=1,gosub_if_false %1p% 0AA1=0,return_if_false 0AA2=2,%2h% = load_library %1s% // IF and SET 0AA3=1,free_library %1h% 0AA4=3,%3d% = get_proc_address %1s% library %2d% 0AA5=-1,call %1d% num_params %2h% pop %3h% 0AA6=-1,call_method %1d% struct %2d% params %3h% pop %4h% ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } :CLEO_RUN [email protected] = -429566 &0([email protected],1i) == 4611680 jf @CLEO_v2 // 1.0 [email protected] = -429539 &0([email protected],1i) = 0xA49960 &0([email protected],1i) += @CLEO_HANDLER 0485: return_true return :CLEO_v2 059A: return_false return { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ DO NOT EVER CHANGE THE HANDLER CODE! ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } :CLEO_HANDLER // 0A8C - 0AEF hex { EAX = CurrentOpcode ECX = ThreadPointer EDX = EIP ESI = ECX ESP +0 = ret_addr +4 = opcode +8 = ecx DO NOT CHANGE ESI! } 8B 44 24 04 // mov eax, [esp+4+Opcode] 66 2D 8C 0A // sub ax, 0x0A8C BA @CLEO_Pointers // mov edx, @CLEO_Pointers 8B 94 82 60 99 A4 00 // mov edx, [0xA49960+eax*4+edx] 81 C2 60 99 A4 00 // add edx, 0xA49960 FF E2 // jmp edx end { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ CLEO Pointers Table ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } :CLEO_Pointers hex @CLEO_Opcode0A8C @CLEO_Opcode0A8D @CLEO_Opcode0A8E @CLEO_Opcode0A8F @CLEO_Opcode0A90 @CLEO_Opcode0A91 @CLEO_Opcode0A92 // reserved @CLEO_Opcode0A93 // reserved @CLEO_Opcode0A94 // reserved @CLEO_Opcode0A95 // reserved @CLEO_Opcode0A96 @CLEO_Opcode0A97 @CLEO_Opcode0A98 @CLEO_Opcode0A99 @CLEO_Opcode0A9A @CLEO_Opcode0A9B @CLEO_Opcode0A9C @CLEO_Opcode0A9D @CLEO_Opcode0A9E @CLEO_Opcode0A9F @CLEO_Opcode0AA0 @CLEO_Opcode0AA1 @CLEO_Opcode0AA2 @CLEO_Opcode0AA3 @CLEO_Opcode0AA4 @CLEO_Opcode0AA5 @CLEO_Opcode0AA6 end { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Opcode 0A8C 0A8C: write_memory (1) size (2) value (3) virtual_protect (4) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } :CLEO_Opcode0A8C hex 6A 04 // push 4 B8 80 40 46 00 FF D0 // call CollectNumberParams 83 3D 84 3C A4 00 01 // cmp VirtualProtect, 1 (p4) 75 08 // jnz @MOVedx 6A 04 // push 4 E8 40 00 00 00 // call VirtualProtect+64 58 // pop eax 8B 15 78 3C A4 00 // mov edx, Address (p1) 8B 0D 7C 3C A4 00 // mov ecx, Size (p2) 8B 05 80 3C A4 00 // mov eax, Value (p3) 83 F9 01 // cmp Size, 1 75 04 // jnz @word 88 02 // mov [Address], al EB 0C // jmp @ret 83 F9 02 // cmp Size, 2 75 05 // jnz @dword 66 89 02 // mov [Address], ax EB 02 // jmp @ret 89 02 // mov [Address], eax 83 3D 84 3C A4 00 01 // cmp VirtualProtect, 1 (p4) 75 0C // jnz @ret FF 35 F4 3C A4 00 // push oldProtect E8 04 00 00 00 // call VirtualProtect+4 58 // pop eax 32 C0 // xor al, al C2 04 00 // retn 4 end { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ CLEO Virtual Protect Subroutine ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } hex 68 F4 3C A4 00 // push offset oldProtect FF 74 24 08 // push [esp+8+NewProtect] FF 35 7C 3C A4 00 // push Size FF 35 78 3C A4 00 // push Address FF 15 2C 80 85 00 // call VirtualProtect C3 // ret end { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Opcode 0A8D 0A8D: (1) = read_memory (2) size (3) virtual_protect (4) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } :CLEO_Opcode0A8D hex 6A 03 // push 3 B8 80 40 46 00 FF D0 // call CollectNumberParams 83 3D 80 3C A4 00 01 // cmp VirtualProtect, 1 (p3) 75 08 // jnz @MOVedx 6A 04 // push 4 E8 CB FF FF FF // call VirtualProtect-53 58 // pop eax 8B 05 78 3C A4 00 // mov eax, Address (p1) 31 C9 // xor ecx, ecx BA 78 3C A4 00 // mov edx, offset Result (p1) 89 0A // mov [edx], ecx 8B 0D 7C 3C A4 00 // mov ecx, Size (p2) 83 F9 01 // cmp ecx, 1 75 06 // jnz @2 8A 00 // mov al, [eax] 88 02 // mov [edx], al EB 11 // jmp @write 83 F9 02 // cmp ecx, 2 75 08 // jnz @4 66 8B 00 // mov ax, [eax] 66 89 02 // mov [edx], ax EB 04 // jmp @write 8B 00 // mov eax, [eax] 89 02 // mov [edx], eax 83 3D 80 3C A4 00 01 // cmp VirtualProtect, 1 (p3) 75 0C // jnz @ret FF 35 F4 3C A4 00 // push oldProtect E8 85 FF FF FF // call VirtualProtect-123 58 // pop eax 6A 01 // push 1 8B CE // mov ecx, esi B8 70 43 46 00 FF D0 // call WriteResult 32 C0 // xor al, al C2 04 00 // retn 4 end { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Opcode 0A8E 0A8E: (1) = (2) + (3) // int ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } :CLEO_Opcode0A8E { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Opcode 0A8F 0A8F: (1) = (2) - (3) // int ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } :CLEO_Opcode0A8F { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Opcode 0A90 0A90: (1) = (2) * (3) // int ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } :CLEO_Opcode0A90 { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Opcode 0A91 0A91: (1) = (2) / (3) // int ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } :CLEO_Opcode0A91 hex 50 // push eax 6A 02 // push 2 B8 80 40 46 00 FF D0 // call CollectNumberParams A1 78 3C A4 00 // mov eax, p1 8B 15 7C 3C A4 00 // mov edx, p2 59 // pop ecx // ADD EAX, EDX 83 F9 02 // cmp ecx, 2 (opcode 0A8E) 75 04 // jnz @opcode0A8F 01 D0 // add eax, edx EB 19 // jmp @write // SUB EAX, EDX 83 F9 03 // cmp ecx, 3 (opcode 0A8F) 75 04 // jnz @opcode0A90 29 D0 // sub eax, edx EB 10 // jmp @write // MUL EAX, EDX 83 F9 04 // cmp ecx, 4 (opcode 0A90) 75 04 // jnz @opcode0A91 F7 EA // imul edx EB 07 // jmp @write // DIV EAX, P2 99 // cdq F7 3D 7C 3C A4 00 // idiv p2 (opcode 0A91) A3 78 3C A4 00 // mov p1, eax 6A 01 // push 1 8B CE // mov ecx, esi B8 70 43 46 00 FF D0 // call WriteResult 32 C0 // xor al, al C2 04 00 // retn 4 end { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Opcode 0A92 0A92: (1) = (2) + (3) // float ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } :CLEO_Opcode0A92 { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Opcode 0A93 0A93: (1) = (2) - (3) // float ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } :CLEO_Opcode0A93 { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Opcode 0A94 0A94: (1) = (2) * (3) // float ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } :CLEO_Opcode0A94 { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Opcode 0A95 0A95: (1) = (2) / (3) // float ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } :CLEO_Opcode0A95 hex 32 C0 // xor al, al C2 04 00 // ret 4 end { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Opcode 0A96 0A96: (1) = actor (2) struct ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } :CLEO_Opcode0A96 { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Opcode 0A97 0A97: (1) = car (2) struct ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } :CLEO_Opcode0A97 { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Opcode 0A98 0A98: (1) = object (2) struct ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } :CLEO_Opcode0A98 hex 50 // push eax 6A 01 // push 1 B8 80 40 46 00 FF D0 // call CollectNumberParams 59 // pop ecx FF 35 78 3C A4 00 // push Handle (p1) // 0A96: ACTOR.STRUCT 83 F9 0A // cmp ecx, 10 (opcode 0A96) 75 0F // jnz @opcode0A97 8B 0D 90 44 B7 00 // mov ecx, @CActors B8 10 49 40 00 FF D0 // call GetActorPointer EB 21 // jmp @write // 0A97: CAR.STRUCT 83 F9 0B // cmp ecx, 11 (opcode 0A97) 75 0F // jnz @opcode0A98 8B 0D 94 44 B7 00 // mov ecx, @CVehicles B8 E0 48 40 00 FF D0 // call GetCarPointer EB 0D // jmp @write // 0A98: OBJECT.STRUCT 8B 0D 9C 44 B7 00 // mov ecx, @CObjects (opcode 0A98) B8 40 50 46 00 FF D0 // call GetObjectPointer 6A 01 // push 1 8B CE // mov ecx, esi A3 78 3C A4 00 // mov p1, eax B8 70 43 46 00 FF D0 // call WriteResult 32 C0 // xor al, al C2 04 00 // ret 4 end { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Opcode 0A99 0A99: chdir <flag> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } :CLEO_Opcode0A99 hex 6A 01 // push 1 B8 80 40 46 00 FF D0 // call CollectNumberParams // test flag A1 78 3C A4 00 // mov eax, Flag (p1) 85 C0 // test eax, eax 74 0E // jz @root 83 F8 01 // cmp eax, 1 74 15 // jnz @exit // Flag=1; UserDir B8 60 88 53 00 FF D0 // call SetUserDirToCurrent EB 0F // jmp @exit // Flag=0; RootDir 68 54 8B 85 00 // push offset Null B8 D0 87 53 00 FF D0 // call ChDir 83 C4 04 // add esp, 4 32 C0 // xor al, al C2 04 00 // ret 4 end { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Opcode 0A9A 0A9A: <var> = openfile "path" mode <dword> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } :CLEO_Opcode0A9A hex 81 EC 80 00 00 00 // sub esp, 128 6A 64 // push 100 8D 44 24 04 // lea eax, [esp+4] 50 // push eax B8 50 3D 46 00 FF D0 // call GetStringParam 6A 01 // push 1 8B CE // mov ecx, esi B8 80 40 46 00 FF D0 // call CollectNumberParams 68 78 3C A4 00 // push offset p1 8D 44 24 04 // lea eax, [esp+4] 50 // push eax B8 00 89 53 00 FF D0 // call fopen 6A 01 // push 1 8B CE // mov ecx, esi A3 78 3C A4 00 // mov p1, eax 31 D2 // xor edx, edx 85 C0 // test eax, eax 0F 95 C2 // setnz dl 52 // push edx B8 D0 59 48 00 FF D0 // call SetConditionResult B8 70 43 46 00 FF D0 // call WriteResult 81 C4 88 00 00 00 // add esp, 136 32 C0 // xor al, al C2 04 00 // ret 4 end { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Opcode 0A9B 0A9B: closefile <hFile> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } :CLEO_Opcode0A9B hex 6A 01 // push 1 B8 80 40 46 00 FF D0 // call CollectNumberParams FF 35 78 3C A4 00 // push hFile (p1) B8 D0 89 53 00 FF D0 // call CloseFile 58 // pop eax 32 C0 // xor al, al C2 04 00 // ret 4 end { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Opcode 0A9C 0A9C: <var> = file <hFile> size ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } :CLEO_Opcode0A9C hex 6A 01 // push 1 B8 80 40 46 00 FF D0 // call CollectNumberParams FF 35 78 3C A4 00 // push hFile (p1) B8 E0 89 53 00 FF D0 // call GetFileSize 6A 01 // push 1 8B CE // mov ecx, esi A3 78 3C A4 00 // mov p1, eax B8 70 43 46 00 FF D0 // call WriteResult 58 // pop eax 32 C0 // xor al, al C2 04 00 // ret 4 end { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Opcode 0A9D 0A9D: readfile <hFile> size <dword> to <var> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } :CLEO_Opcode0A9D { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Opcode 0A9E 0A9E: writefile <hFile> size <dword> from <var> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } :CLEO_Opcode0A9E hex 50 // push eax 6A 02 // push 2 B8 80 40 46 00 FF D0 // call CollectNumberParams 6A 02 // push 2 B8 90 47 46 00 FF D0 // call GetVariablePos 59 // pop ecx FF 35 7C 3C A4 00 // push Size (p2) 50 // push eax FF 35 78 3C A4 00 // push hFile (p1) 83 F9 11 // cmp ecx, 17 (opcode 0A9D) 75 07 // jnz @opcode0A9E B8 50 89 53 00 // mov eax, BlockRead EB 05 // jmp @call B8 70 89 53 00 // mov eax, BlockWrite FF D0 // call eax 83 C4 0C // add esp, 12 32 C0 // xor al, al C2 04 00 // ret 4 end { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Opcode 0A9F 0A9F: <var> = current_thread_address ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } :CLEO_Opcode0A9F hex 6A 01 // push 1 89 35 78 3C A4 00 // mov p1, esi B8 70 43 46 00 FF D0 // call WriteResult 32 C0 // xor al, al C2 04 00 // ret 4 end { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Opcode 0AA0 0AA0: gosub_if_false <label> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } :CLEO_Opcode0AA0 hex 6A 01 // push 1 B8 80 40 46 00 FF D0 // call CollectNumberParams 8A 86 C5 00 00 00 // mov al, [esi+0xC5+IfResult] 84 C0 // test al, al 75 1C // jnz @true 0F B6 46 38 // movzx eax, [esi+0x38+THREAD.StackCounter] 8B 56 14 // mov edx, [esi+0x14+THREAD.CurrentIP] 89 54 86 18 // mov [esi+eax*4+THREAD.ReturnStack], edx 66 FF 46 38 // inc word ptr [esi+0x38+THREAD.StackCounter] FF 35 78 3C A4 00 // push label (p1) B8 A0 4D 46 00 FF D0 // call SetJumpLocation 32 C0 // xor al, al C2 04 00 // ret 4 end { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Opcode 0AA1 0AA1: return_if_false ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } :CLEO_Opcode0AA1 hex 8A 86 C5 00 00 00 // mov al, [esi+0xC5+IfResult] 84 C0 // test al, al 75 0F // jnz @true 66 FF 4E 38 // dec word ptr [esi+0x38+THREAD.StackCounter] 0F B6 46 38 // movzx eax, [esi+0x38+THREAD.StackCounter] 8B 54 86 18 // mov edx, [esi+eax*4+0x18+THREAD.ReturnStack] 89 56 14 // mov [esi+0x14+CurrentIP], edx 32 C0 // xor al, al C2 04 00 // ret 4 end { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Opcode 0AA2 0AA2: <var> = load_library <path> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } :CLEO_Opcode0AA2 hex 81 EC 80 00 00 00 // sub esp, 128 6A 64 // push 100 8D 44 24 04 // lea eax, [esp+4] 50 // push eax B8 50 3D 46 00 FF D0 // call GetStringParam 8D 04 24 // lea eax, esp 50 // push eax FF 15 70 80 85 00 // call LoadLibraryA 6A 01 // push 1 8B CE // mov ecx, esi A3 78 3C A4 00 // mov p1, eax 31 D2 // xor edx, edx 85 C0 // test eax, eax 0F 95 C2 // setnz dl 52 // push edx B8 D0 59 48 00 FF D0 // call SetConditionResult B8 70 43 46 00 FF D0 // call WriteResult 81 C4 80 00 00 00 // add esp, 128 32 C0 // xor al, al C2 04 00 // ret 4 end { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Opcode 0AA3 0AA3: free_library <hLib> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } :CLEO_Opcode0AA3 hex 6A 01 // push 1 B8 80 40 46 00 FF D0 // call CollectNumberParams FF 35 78 3C A4 00 // push hLib (p1) FF 15 10 81 85 00 // call FreeLibrary 32 C0 // xor al, al C2 04 00 // ret 4 end { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Opcode 0AA4 0AA4: <var> = get_proc_address "name" library <hLib> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } :CLEO_Opcode0AA4 hex 81 EC 80 00 00 00 // sub esp, 128 6A 64 // push 100 8D 44 24 04 // lea eax, [esp+4] 50 // push eax B8 50 3D 46 00 FF D0 // call GetStringParam 6A 01 // push 1 8B CE // mov ecx, esi B8 80 40 46 00 FF D0 // call CollectNumberParams 8D 04 24 // lea eax, esp 50 // push eax FF 35 78 3C A4 00 // push hLib (p1) FF 15 6C 80 85 00 // call GetProcAddress 6A 01 // push 1 8B CE // mov ecx, esi A3 78 3C A4 00 // mov p1, eax 31 D2 // xor edx, edx 85 C0 // test eax, eax 0F 95 C2 // setnz dl 52 // push edx B8 D0 59 48 00 FF D0 // call SetConditionResult B8 70 43 46 00 FF D0 // call WriteResult 81 C4 80 00 00 00 // add esp, 128 32 C0 // xor al, al C2 04 00 // ret 4 end { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Opcode 0AA5 0AA5: call <address> num_params <byte> pop <byte> [param1, param2...] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } :CLEO_Opcode0AA5 hex 6A 03 // push 3 B8 80 40 46 00 FF D0 // call CollectNumberParams 53 // push ebx 57 // push edi 8B 1D 78 3C A4 00 // mov ebx, addr (p1) 8B 3D 7C 3C A4 00 // mov edi, Number (p2) 85 FF // test edi, edi 74 12 // jz @call 6A 01 // push 1 B8 80 40 46 00 FF D0 // call CollectNumberParams FF 35 78 3C A4 00 // push param (p1) 4F // dec edi EB EA // jmp @loop FF D3 // call ebx A1 80 3C A4 00 // mov eax, pop 6B C0 04 // imul eax, 4 01 C4 // add esp, eax 5F // pop edi 5B // pop ebx FF 46 14 // inc dword ptr [esi+0x14+CurrentIP] 32 C0 // xor al, al C2 04 00 // ret 4 end { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Opcode 0AA6 0AA6: call_method <address> struct <address> num_params <byte> pop <byte> [param1, param2...] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } :CLEO_Opcode0AA6 hex 6A 04 // push 4 B8 80 40 46 00 FF D0 // call CollectNumberParams 53 // push ebx 57 // push edi 51 // push ecx 8B 1D 78 3C A4 00 // mov ebx, addr (p1) 8B 3D 80 3C A4 00 // mov edi, Number (p3) 85 FF // test edi, edi 74 12 // jz @call 6A 01 // push 1 B8 80 40 46 00 FF D0 // call CollectNumberParams FF 35 78 3C A4 00 // push param (p1) 4F // dec edi EB EA // jmp @loop 8B 0D 7C 3C A4 00 // mov ecx, Struct (p2) FF D3 // call ebx A1 84 3C A4 00 // mov eax, pop (p4) 6B C0 04 // imul eax, 4 01 C4 // add esp, eax 59 // pop ecx 5F // pop edi 5B // pop ebx FF 46 14 // inc dword ptr [esi+0x14+CurrentIP] 32 C0 // xor al, al C2 04 00 // ret 4 end { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ END OF CLEO ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ } Edited August 1, 2019 by OrionSR 3 Link to post Share on other sites
OrionSR 2,427 Posted August 7, 2019 Author Share Posted August 7, 2019 (edited) Hijacking Scripts with Gosub-Return: The original method of hijacking a script to run custom code is to modify the relativeIP of a running script to resume execution at the location of embedded codes, and return the hijacked script to normal operation by ending with a jump statement to the original relativeIP. This strategy is fairly intuitive but editing the compiled jump address is a bit awkward. Gosub-Return: End the hijack codes with Return and simulate a Gosub with save edits. A gosub-return strategy eliminates the need to modify the compiled code. Embed the compiled code in the variable space as before. Copy the RelativeIP of a running script to the end of it's RelativeReturnStack (RelativeReturnStack[StackID]). Set the RelativeIP to the offset of the embedded script. Increment the StackID. Automatically Embed a Blackboard Glitch Repair BlackboardFix.1sc - 010 Script that embeds pre-compiled repair codes to fix the Blackboard Glitch. The blackboard glitch is (rarely) caused by driving school. When a driving school is started after unlocking the Import/Export mission (going back for gold, or starting bike school for the first time), sometimes it'll delete the blackboard. This will cause the game to crash when CJ is close to the drydock in San Fierro. The Blackboard glitch is one of the few standard glitches that cannot be repaired with current tools. Goals: Demonstrate the Gosub-Return strategy for hijacking scripts. Provide an automated tool for players to repair their own broken saves. Document the process well enough that it can be adapted to non-proprietary tools. Gain experience using 010 scripts to edit save files. Repair Scripts: Compile as cleo scripts and save in the same location as the BlackboardFix.1sc script. There are no jump addresses so the cleo version can be embedded without modification. However, don't try to run this version as a cleo script; the unexpected Return will cause a crash. Also, the local variables used won't assign the object handle to the proper variable of the impexpm thread without additional commands. These repair scripts will only work properly if the impexpm thread is hijacked. Spoiler PC/PS2 {$CLEO .cs} // BlackboardRepair.txt // PCv1, PCv2, PS2v1, PS2v2 // For use with BlackboardFix.1sc 0001: wait 4000 029B: [email protected] = init_object #NF_BLACKBOARD at -1573.881 135.3845 2.535 0177: set_object [email protected] Z_angle_to 180.0 0550: keep_object [email protected] in_memory 1 0392: make_object [email protected] moveable 0 01C7: remove_object_from_mission_cleanup_list [email protected] 09F1: play_audio_at_actor $PLAYER_ACTOR event 1133 // SOUND_BUY_CAR_MOD 0051: return Mobile {$CLEO .csa} // BlackboardMobile.txt // Android, iOS, WinStore // For use with BlackboardFix.1sc 0001: wait 4000 029B: [email protected] = init_object #NF_BLACKBOARD at -1573.881 135.3845 2.535 0177: set_object [email protected] Z_angle_to 180.0 0550: keep_object [email protected] in_memory 1 0392: make_object [email protected] moveable 0 01C7: remove_object_from_mission_cleanup_list [email protected] 09F1: play_audio_at_actor $PLAYER_ACTOR event 1133 // SOUND_BUY_CAR_MOD 0051: return BlackboardFix.1sc Run on a save that has been parsed with the GTASA.bt template. Track the progress in the output Window. Spoiler //--- 010 Editor v6.0.3 Script File // // File: BlackboardFix.1sc // Author: OrionSR // Revision: v0.1 // Purpose: Replace the blackboard object deleted by driving school. //-------------------------------------- // Use: // First run the GTASA.bt binary template on the save to parse the save data. // Then run this Blackboard Fix script on the glitched save. // Load the repair save to complete the fix. // A sound will play to indicate that the repair script has executed. // //======================================================================== int i; // index for loops int sizeToInsert = 0; // size of script to embed int scriptIndex = -1; // index of running script to modify int objectIndex = -1; // index of target object in Pools int varSpaceStart = 0; // offset of global variable space char hijackScript[8] = "impexpm"; // Running script to hijack int saveFileNum = GetFileNum(); // for use with FileSelect() char saveFileName[512] = GetFileName(); // strings for save name management char tempSaveName[512] = FileNameSetExtension( saveFileName, ".tmp" ); char backupSaveName[512] = FileNameSetExtension( saveFileName, ".bak" ); char saveTemplate[512] = "GTASA.bt"; // required binary template char templateName[512] = GetTemplateName(); // active template name // Verify template has been run on save if ( templateName != saveTemplate ) { Printf( "%s has not parsed the save data!\n", saveTemplate ); Printf( "Run %s on this save before executing the fix script.\n", saveTemplate ); return; }; // Version specific variables int cashWonOffset = 33592; // PC default if (isMobile == 1) cashWonOffset = 39060; int localIndex = 27; // PC default if (isMobile == 1) localIndex = 28; char fileName[512] = "BlackboardRepair.cs"; if (isMobile == 1) fileName = "BlackboardRepair.csa"; char repairFile[512] = FileNameGetPath(GetScriptFileName() ); repairFile += fileName; //======================================================================== //Find Start of VarSpace FSeek( FindFirst( "BLOCK*",1,0,1,0.0,1,FTell(),0,5 ) ); FSeek( FindNext(1)+9 ); varSpaceStart = FTell(); // Find script to hijack for( i = 0; i < Save.CTheScripts.nRunningScripts; i++ ) { if ( Save.CTheScripts.Scripts.Script[i].Name == hijackScript ) { scriptIndex = i; }; }; if ( scriptIndex == -1 ) { Printf( "%s script not active! Blackboard Glitch is not possible.\n", hijackScript ); } else { Printf( "%s script found at index: %d. Checking object...\n", hijackScript, scriptIndex ); // Check for exisiting #NF_BLACKBOARD 3077 for( i = 0; i < Save.CPools.nObjects; i++ ) { if ( Save.CPools.Objects[i].Handle == Save.CTheScripts.Scripts.Script[scriptIndex].Locals[localIndex] ) { if ( Save.CPools.Objects[i].ModelID == NF_BLACKBOARD) // enum { objectIndex = i; }; }; }; if ( objectIndex != -1 ) { Printf( "Blackboard found at index: %d. Blackboard Glitch is not active.\n", objectIndex ); } else { Printf( "Blackboard object is missing! Looking for repair file...\n" ); if ( FileExists( repairFile ) ) { // Copy repair script to clipboard Printf( "Repair script found: %s. Backing up files...\n", FileNameGetBase(repairFile) ); FileOpen( repairFile, false, "Hex", false ); sizeToInsert = FileSize(); SetSelection( 0, sizeToInsert ); CopyToClipboard(); FileClose(); // Backup original save game FileSelect( saveFileNum ); if ( FileSave( tempSaveName ) == -1) { Printf( "Temp file did not save.\n" ); } else { Printf( "Temp file saved as: %s\n", FileNameGetBase( tempSaveName ) ); if ( FileExists( backupSaveName ) ) DeleteFile( backupSaveName ); if ( RenameFile( saveFileName, backupSaveName ) == -1 ) { Printf( "Backup file could not be renamed.\n" ); } else { Printf( "Backup file renamed to: %s\n", FileNameGetBase( backupSaveName ) ); // Paste repair script into save file SetSelection( varSpaceStart + cashWonOffset, sizeToInsert ); PasteFromClipboard(); // Hijack script with Gosub-Return Save.CTheScripts.Scripts.Script[scriptIndex].RelativeReturnStack[Save.CTheScripts.Scripts.Script[scriptIndex].StackID] = Save.CTheScripts.Scripts.Script[scriptIndex].RelativeIP; Save.CTheScripts.Scripts.Script[scriptIndex].RelativeIP = cashWonOffset; Save.CTheScripts.Scripts.Script[scriptIndex].StackID +=1; // Fix Checksum WriteUInt(FileSize()-4, Checksum(CHECKSUM_BYTE, 0, FileSize()-4)); Printf( "Repair script has been embedded successfully.\n" ); // Clean up FileSave( saveFileName ); DeleteFile( tempSaveName ); Printf( "Saving repair save as: %s.\n", FileNameGetBase( saveFileName ) ); Printf( "Final repair will be completed when %s is loaded.\n", FileNameGetBase( saveFileName ) ); }; }; } else { Printf( "Repair script not found with %s\n", GetScriptName() ); }; }; }; The BlackboardFix script requires that the GTASA.bt binary template has been run on the save file. Check the first post for more information on this template. Edited April 23, 2020 by OrionSR 2 Link to post Share on other sites
OrionSR 2,427 Posted May 25, 2020 Author Share Posted May 25, 2020 (edited) 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: DKpac22's SDK-Plugin CText GTAmods Wiki - GXT GTA_San_Andreas_CRC32_Calculator - needed to find the hash associated with a particular text key. Updated GXT Text not sorted by tables Older GXT Text sorted by tables 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 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 June 21, 2020 by OrionSR Link to post Share on other sites