air6ornepig Posted 3 hours ago Posted 3 hours ago (edited) Good evening, I've been spending some time romhacking soul silver for fun. I used some unused item slots to add new items to the game, but needed a way to make them accessible to the player so I've been reverse-engineering the game's shops and I created a list of their offsets. Also in this post will include a short tutorial on editing the shops in arm9.bin Find the offset list here (google sheet) Before I begin, I want to credit TheShadyRelapse. I used their method to find the offsets in arm9.bin, and my discoveries are built on their knowledge. Generally, you should read their tutorial before reading mine, as my tutorial won't be very in-depth and the methodology is essentially the same. The reason I created a new thread instead of responding to their old post is: I was concerned it might be considered necroposting, I'm offering a little bit more than just PokéMart edits, and TheShadyRelapse's tutorial explicitly states you can't use their method to edit dynamic shop inventories. Because I am explicitly offering dynamic shops, I don't want people to read that and leave before reaching my post. * * * * * Requirements: A method to extract/import and decompress/compress arm9.bin. I recommend NitroPacker. Other people seem to report success using CrystalTile2 and blz.exe, but I personally experience rom breakage with these. A Hex Editor of any sort. I prefer HxD. A HGSS ROM, of course. DS Pokémon ROM Editor Reloaded (DSPRE). Not a hard requirement, but very helpful and strongly recommended. Required for some tasks. PtHGSS's index of items. Not a downloadable app nor hard requirement, just a list you might need for item IDs. * * * * * 1. Unpack your ROM (instructions for NitroPacker on Windows) If you know how to unpack your ROM, or extract/import arm9.bin and uncompress/compress it using other means, feel free to skip this step if you prefer that method. Nitropacker's instructions are also on their github page; I'm basically just ripping from them. Spoiler Download Nitropacker and unpack it where you please (it's helpful to place the folder near your ROM). Open the command prompt, drag-and-drop NitroPacker.exe into the prompt, type " unpack -r ", drag-and-drop your rom into the command prompt, type " -o ", drag-and-drop the location you want to unpack the rom in, then type " -p " and give your project a name (I just name it the same as the game). Arm9.bin is compressed so you must also add -d, and add -f to preserve the order of files in the game. Your prompt should look something like this: "Path\To\NitroPacker.exe" unpack -r "Path/To/ROM.nds" -o "Path\To\Unpack\Folder" -p "Project Name with Spaces" -d -f Open the location you unpacked your rom in and you should find arm9.bin in the root of that folder. 2. Open arm9.bin with a hex editor of your choice, and start editing! I have provided a table of offsets for each of the shops that are hard-coded in the arm9 binary. Simply find on the table what shop you want to edit, choose to search by offset or by the shop's inventory code (dubbed 'full associated code' on the table) by copying it from the table and pasting it into the search function of your hex editor. In HxD: CTRL+G to search by offset, CTRL+F to search code (don't forget to select the "Hex-values" tab). Most shops or clerks that have a static inventory: Items are listed simply and in order by their 16-bit (little endian) item IDs. The hex value FFFF terminates the shop list. Once you've arrived at the shop code, use either the bulbapedia index or the hex values provided in the table (already in little endian form) to locate the shop item you want to replace. When you replace items, make sure it's in little endian form! I won't go into much detail over editing static inventory shops, you can read ShadyRelapse's post. Because the list won't terminate until it reaches FFFF, you can use this to do things like combine and expand shop inventories. I included the location of mart script files and the variable that the common script uses to call which inventory to load, and I'll explain more on that later. Dynamic PokéMart shop: Items have an added 16-bit hex value in their code that corresponds to the amount of badges you've earned. They are are arranged as: [Item hex ID][Story Variable]. Where [Item hex ID] is its 16-bit hex ID in a little endian form, which can be found in the same bulbapedia index from earlier. [Story Variable] is a 16-bit variable that changes based on the amount of badges you have: 0001 = 0 badges 0002 = 1 badge 0003 = 3 badges 0004 = 5 badges 0005 = 7 badges 0006 = 8 badges So a poke ball's code would be (always in little endian of course) 04000100, potion = 11000100, full restore = 17000600, etc.. The offset and full associated code with the pokemart is already provided in the google sheet. Change the first 4 hexes to change the item in the inventory, and the last 4 hexes to change when in the story it becomes available. Example: If you wanted to do something goofy and replace Great Balls with Master Balls and make them available ASAP, you would go to the correct offset and replace the code 03000300 ([Great Ball][3 Badges]) with 01000100 ([Master Ball][0 Badges]). The dynamic shop uniquely terminates at 0000, and not FFFF. This means the inventory count is defined somewhere else. Unfortunately this means I don't know how to expand it. Pokéathlon shop: Items are also arranged as two 16-bit values: [Item hex ID][Cost in Pokéathlon Points (AP)]. [Cost in AP] is a little endian hex value corresponding to the item's AP cost in that shop (obviously). So the code for a red apricorn would be E501C800 (01e5 = red apricorn, 00c8 = 200), moomoo milk is 21006400 (0021 = moomoo milk, 0064 = 100), and something like a fire stone would be 5200C409 (0052 = fire stone, 09c4 = 2500). Replacing this code is just like any other, except you also have control of the AP price. The Pokéathlon shop also has 14 different inventories, two for each day of the week. While most shop inventory calls are controlled by a common script, the call for the pokeathlon shop inventory seems to be an engine-level command, so I'm not sure how to change which inventory it loads based on the day of the week. Still, I have the offsets for each day listed on the google sheet. 3. Repack your ROM (instructions for NitroPacker on Windows) Again, if you have your own preferred method for compressing and importing arm9.bin, feel free to use that. Spoiler This step will repack your rom while recompressing arm9.bin; Open the command prompt, drag-and-drop NitroPacker.exe into the prompt, type "pack -p ", drag and drop the .json file at the root of the folder you unpacked your rom in, type " -r ", drag-and-drop a .nds file into the prompt or manually type a path that includes a .nds extension to create a new file. Append -c and -f (-f is probably not needed but it doesn't break anything) and you should have a prompt like: "Path\To\NitroPacker.exe" pack -p "Path/To/Project.json" -r "Path\To\ROM.nds" -c -f Now load your game up and you should see changes to the Marts. Good luck! * * * * * Script-based Shops (DSPRE required): A number of shops are actually script-based instead of stored on the arm9 binary. This makes modifying them much easier and less constrained. You can open the "Script-based Shops" tab in the google sheets table to find the list of script-based shops and their associated script files. Open your rom with DSPRE and select the "Script Editor" tab to access and edit the script files safely. People already familiar with IDEs and code editors probably won't need help editing scripts. For an example I'm going to add the Return TM to the Game Corner shop. To edit the Game Corner shops, load script file 910 (906 in HeartGold). The Pokemon Shop starts at script 5 and the Item/TM Shop starts at script 4. To add an item to the TM shop, navigate to script 4. You can mouse-over message lines of code to see what they say and their text archive location. Sometimes menu items are drawn from different text archives then their map headers would suggest. I've listed locations for known text strings in the google sheets table. In this case both messages and menu items are drawn from archive 603. This is the code for the Held Item/TM shop: Spoiler Script 4: PlayFanfare SEQ_SE_CONFIRM LockAll FacePlayer OverlayManager 3 0 Message 0 OpenTouchScreen ShowSpecialCurrency 0 20 2 MultiTouchLocalText 1 1 0 1 0x4000 CreateMultiTouchBox 12 255 0 CreateMultiTouchBox 13 255 1 CreateMultiTouchBox 11 255 2 CloseMultiTouch SetVarFromVariable 0x8008 0x4000 CompareVarValue 0x8008 0 JumpIf EQUAL Function#8 CompareVarValue 0x8008 1 JumpIf EQUAL Function#9 Jump Function#10 ShowSpecialCurrency [par1] [par2] [par3] shows either BP or Coins. I haven't done any testing to figure out what exactly the first 2 parameters specify, but I know for par3, 1 displays BP and 2 displays current Coins. MultiTouchLocalText [par1] [par2] [par3] [par4] [par5] seems to enable an interactable text list that assigns its input into a variable set in [par5] (Var 0x4000 in this case). I haven't tested what the first 4 parameters do, probably something to do with either box alignment or colour. CreateMultiTouchBox [par1] [par2] [par3] creates the actual text box that can be selected. [par1] = box text. [par2] = selection description (set to 255 in this case which is out of bounds for the text archive, which I believe the game interprets to exclude a description). [par3] = value between 0-7 assigned to that text box which is stored in variable [par5] from MultiTouchLocalText. So "CreateMultiTouchBox 12 255 0", which is the first option in the game shop, has par1 set to message 12 which is "TECHNICAL MACHINE", par2 set to 255 which is nothing, and par3 set to 0, so when TECHNICAL MACHINE is selected, Var 0x4000 is set to 0. SetVarFromVariable [par1] [par2] copies a variable from another variable. Comparing variable values is pretty much never done directly from the MultiTouchLocalText par5 variable for some reason. I don't know why the developers decided to code like this. Maybe the variable from MultiTouchLocalText isn't stored for long, or it prevents the variable from being unintentionally overwritten by similar script functions. I haven't done the testing to confirm this. In this case it just assigns Var 0x8008 the same value as was assigned to Var 0x4000 (0). CompareVarValue [par1] [par2] loads a variable and a value for the context of the next command JumpIf [operator] [function] jumps to a specified function if par1 from CompareVarValue is equal to, lesser to, or greater than (depending on the operator) par2. In this case it jumps to function 8 if Var 0x8008 = 0 (which is assigned after a player selects a text box from CreateMultiTouchBox), so having selected the first option (TECHNICAL MACHINE), the script jumps to function 8 which handles the TMs menu. Jump [function] as you can probably tell jumps to a function without a condition. Function #8, aka the TM shop's code looks like this: Spoiler Function 8: MultiTouchLocalText 1 1 0 1 0x4000 CreateMultiTouchBox 14 255 0 CreateMultiTouchBox 15 255 1 CreateMultiTouchBox 16 255 2 CreateMultiTouchBox 17 255 3 CreateMultiTouchBox 18 255 4 CreateMultiTouchBox 19 255 5 CreateMultiTouchBox 11 255 6 CloseMultiTouch SetVarFromVariable 0x8008 0x4000 CompareVarValue 0x8008 0 JumpIf EQUAL Function#20 CompareVarValue 0x8008 1 JumpIf EQUAL Function#21 CompareVarValue 0x8008 2 JumpIf EQUAL Function#22 CompareVarValue 0x8008 3 JumpIf EQUAL Function#23 CompareVarValue 0x8008 4 JumpIf EQUAL Function#24 CompareVarValue 0x8008 5 JumpIf EQUAL Function#25 Jump Function#26 To add a TM to this shop, simply add an extra line "CreateMultiTouchBox XX YYY Z" between the last two CreateMultiTouchBox's, and change the value of CreateMultiTouchBox 11 255 6 to CreateMultiTouchBox 11 255 7. For "CreateMultiTouchBox XX YYY Z", XX and YYY correspond to string indices in text archive 603. Your TM won't automatically be in text archive 603, so you'll have to navigate to the Text Editor tab and open Text Archive 603. Messages 29 and 30 are free to use, so this example will use 29 (The DSPRE text editor allows you to safely add new strings of text to archives, so if you have no free spaces, add strings with the "Append Line" button). I'm going to make it cost 1000 coins, so I'll type "RETURN\n1,000 Coins" into index 29 and save the text archive. Our extra line from earlier will actually look like "CreateMultiTouchBox 29 255 6". Now we need to create a command that jumps to an all-new function if Return is selected. Add "CompareVarValue 0x8008 6" and "JumpIf EQUAL Function#72". Function #8 should now look like: Spoiler Function 8: MultiTouchLocalText 1 1 0 1 0x4000 CreateMultiTouchBox 14 255 0 CreateMultiTouchBox 15 255 1 CreateMultiTouchBox 16 255 2 CreateMultiTouchBox 17 255 3 CreateMultiTouchBox 18 255 4 CreateMultiTouchBox 19 255 5 CreateMultiTouchBox 29 255 6 CreateMultiTouchBox 11 255 7 CloseMultiTouch SetVarFromVariable 0x8008 0x4000 CompareVarValue 0x8008 0 JumpIf EQUAL Function#20 CompareVarValue 0x8008 1 JumpIf EQUAL Function#21 CompareVarValue 0x8008 2 JumpIf EQUAL Function#22 CompareVarValue 0x8008 3 JumpIf EQUAL Function#23 CompareVarValue 0x8008 4 JumpIf EQUAL Function#24 CompareVarValue 0x8008 5 JumpIf EQUAL Function#25 CompareVarValue 0x8008 6 JumpIf EQUAL Function#72 Jump Function#26 Now, we've added a jump to a function that doesn't exist yet, so we still have to create Function #72. We can just copy any of the other TM functions 20-25 and paste it at the very end of the function editor. I'm going to copy Function #25 (which handles checking your inventory space and choosing to buy the Thunderbolt TM): Spoiler Function 25: SetVar 0x8004 351 SetVar 0x8005 1 CheckItemSpace 0x8004 0x8005 0x800C CompareVarValue 0x800C 0 JumpIf EQUAL Function#37 TextItem 0 0x8004 Message 3 YesNoTouchScreen 0x800C CompareVarValue 0x800C 0 JumpIf EQUAL Function#44 CloseMessage CompareVarValue 0x800C 1 JumpIf EQUAL Function#8 GetCoins 0x8006 CompareVarValue 0x8006 0x2710 JumpIf LESS Function#39 Message 4 PlayFanfare SEQ_SE_DP_REGI TakeCoins 0x2710 UpdateSpecialCurrency 0 TextItem 0 0x8004 GetItemPocket 0x8004 0x800C TextPocket 1 0x800C Message 10 GiveItem 0x8004 1 0x800C Jump Function#8 Edit "Function 25:" to read "Function 72:". The item ID for TM27 (Return) is 354. We don't need to translate it into a hex value, just replace "SetVar 0x8004 351" with "SetVar 0x8004 354" (hex values are prefixed with 0x). "JumpIf EQUAL Function#44" must be changed to "JumpIf EQUAL Function#73", as this is the function that actually handles checking your coins and adding the item to our inventory. Even though I think this part of the code is redundant (because both yes/no inputs jump to another function so this code never runs), it's probably wise to ensure the cost here for Return is correct; "CompareVarValue 0x8006 0x2710" and "TakeCoins 0x2710" should be changed to "CompareVarValue 0x8006 0x03E8" and "TakeCoins 0x03E8". Function 72 should look like: Spoiler Function 72: SetVar 0x8004 354 SetVar 0x8005 1 CheckItemSpace 0x8004 0x8005 0x800C CompareVarValue 0x800C 0 JumpIf EQUAL Function#37 TextItem 0 0x8004 Message 3 YesNoTouchScreen 0x800C CompareVarValue 0x800C 0 JumpIf EQUAL Function#73 CloseMessage CompareVarValue 0x800C 1 JumpIf EQUAL Function#8 GetCoins 0x8006 CompareVarValue 0x8006 0x03E8 JumpIf LESS Function#39 Message 4 PlayFanfare SEQ_SE_DP_REGI TakeCoins 0x03E8 UpdateSpecialCurrency 0 TextItem 0 0x8004 GetItemPocket 0x8004 0x800C TextPocket 1 0x800C Message 10 GiveItem 0x8004 1 0x800C Jump Function#8 Next we must copy Function 44: Spoiler Function 44: GetCoins 0x8006 CompareVarValue 0x8006 0x2710 JumpIf LESS Function#39 Message 4 PlayFanfare SEQ_SE_DP_REGI TakeCoins 0x2710 UpdateSpecialCurrency 0 TextItem 0 0x8004 GetItemPocket 0x8004 0x800C TextPocket 1 0x800C Message 10 GiveItem 0x8004 1 0x800C Jump Function#8 As mentioned before, this function checks if you have enough coins to buy the item and then adds it to your inventory. Change "CompareVarValue 0x8006 0x2710" (0x2710 = 10000) to "CompareVarValue 0x8006 0x03E8" (0x03E8 = 1000). Function 73 should look like: Spoiler Function 73: GetCoins 0x8006 CompareVarValue 0x8006 0x03E8 JumpIf LESS Function#39 Message 4 PlayFanfare SEQ_SE_DP_REGI TakeCoins 0x03E8 UpdateSpecialCurrency 0 TextItem 0 0x8004 GetItemPocket 0x8004 0x800C TextPocket 1 0x800C Message 10 GiveItem 0x8004 1 0x800C Jump Function#8 Now don't forget to "Save Current File" in the editor! DSPRE will automatically re-arrange and re-name any functions as it deems fit to, so the functions we added won't be labelled the same anymore. This is why we didn't save mid-progress, having the script re-arranged mid-editing is a hassle. Next you can save your ROM and voila! You should see changes reflected in the Game Corner TM shop! Important notes to consider when script editing shops: You can only create a maximum of 8 selection boxes with CreateMultiTouchBox at a time. If you have more than 8 lines of CreateMultiTouchBox at once, the game doesn't know how to display it and will hardlock. If you want to add more than just 1 TM to the Game Corner shop like in my example, you'll have to get a little creative and add a second page of options. I accomplish this by copying Function 8 and pasting it as a new function, then adding a "Next Page" button in Function 8 that jumps to the newly pasted function. The new function will need appropriate edits. Once you figure out how to do this, your script shop editing options are essentially limitless. Creating a MultiTouchLocalText with only 4 or less options will automatically create wide selection boxes, so you must format your text appropriately. The new line text function "\n" doesn't work on these wide boxes, so you have to use another command to create a space between an item/pokemon name and its cost. Just copy what they do with the Pokemon Shop text and append "{CURSOR_X, 0, ###}" to your text followed by its cost. Some examples from the game: Spoiler ABRA{CURSOR_X, 0, 103}200 Coins EKANS{CURSOR_X, 0, 103}700 Coins DRATINI{CURSOR_X, 0, 90}2,100 Coins SANDSHREW{CURSOR_X, 0, 103}700 Coins #If you had an pokemon with a 5 digit cost, {CURSOR_X, 0, 83} would preserve the alignment of this page. #Normal (capitalized?) characters are 7 pixels and punctuation is 6. * * * * * Tips, Tricks, Suggestions, Etc.: In case you haven't noticed, Goldenrod and Celadon City Dept. Stores possess their own space in the arm9 code, but most of their stores have the exact same inventory. The engine command for the static inventory shop overlay is called by a Common Script 2052, and I don't know where common scripts are stored right now so I can't edit them. However, Common Script 2052 retrieves a variable (Var 0x8004) which decides what inventory to load, and it is set just before the common script is called. This means we can change what shops load what inventories by changing SetVar 0x8004 in the script. You can use this to set Celadon to load Goldenrod inventories, freeing space on arm9.bin for custom marts! Each shop's associated script as well as what variable it's associated with is recorded on the google sheet. For example, Celadon Dept. Store's medicine shop script is located in script file 790, it is the first script in the file: Spoiler Script 1: PlayFanfare SEQ_SE_CONFIRM LockAll FacePlayer CommonScript 2011 FreezeMessage SetVar 0x8004 18 CommonScript 2052 ReleaseAll End Change "SetVar 0x8004 18" to "SetVar 0x8004 3" to end up with: Spoiler Script 1: PlayFanfare SEQ_SE_CONFIRM LockAll FacePlayer CommonScript 2011 FreezeMessage SetVar 0x8004 3 CommonScript 2052 ReleaseAll End Save your changes and you'll observe no tangible changes in-game, but you've freed offset 000FBC02 in the arm9 binary, which can now be used for a custom shop with up to 11 items! You'll notice the 2F Item shop, the vitamin shops, and the battle item shops also take up unnecessary space and can also be repurposed the same way. Because we know each offset that is called by Common Script 2052 variable points to in arm9.bin, we can effectively remap the entire game's mart system. Shop inventories can also by expanded if you remap them wisely. Shop inventories in arm9.bin terminate at FFFF. If you overwrite the termination code FFFF, the shop inventory will 'bleed' into the next mart inventory code, effectively 'combining' the inventories (the second shop being bled into should be unaffected, but the first shop would have its own inventory plus the second shop's inventory now). Or rather, with careful remapping, you could use the space freed from the redundant Celadon Dept. Store medicine shop code to expand the inventory in the shop preceding its code (which by default is the Goldenrod medicine shop, so you would be expanding both medicine shop inventories). * * * * * I think that is everything I wanted to share. Thank you for reading, I hope this helps other people with their romhacking. Please inform me of any errors or typos I've made. Edited 3 hours ago by air6ornepig
Recommended Posts
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 accountSign in
Already have an account? Sign in here.
Sign In Now