Micro Machines 2 SRAM Documentation (and a MD RAM comparison utility)

Discussion in 'Technical Discussion' started by Overlord, Feb 8, 2021.

  1. Overlord

    Overlord

    Now playable in Smash Bros Ultimate Moderator
    18,115
    304
    63
    Berkshire, England
    Learning Cymraeg
    Post 18,000 =P

    So the last week or so I've been playing around with Micro Machines 2 on Mega Drive again. My original plan was to try and get different cars playable in other levels but after some work for a while looking at savestate addresses I realised I was getting nowhere, so gave up on this idea. That said, I did write up a quick utility for checking bytes between Mega Drive 68K ROM dumps from Gens KMod, which then reports back on matches of a specified pattern: that's at http://overlord.digibase.ca/hex_analyser_1.0.rar for anyone who's interested. It's GPL v3, source is included. Basically you specify between 2 and 5 filenames that are in the same directory and the bytes you want to look for in each of the files, and if the bytes are at the same address in the files it'll report a match. e.g. if I search ramdump1.ram and ramdump2.ram for 01 in file 1 and 02 in file 2, it'll return a match if say 0x0123 in file 1 is 01 and in file 2 is 02. Useful if you're looking for a value that you know is changing after a specific action (i.e. you're on a screen, take a RAM dump, then do something to make it change and take the RAM dump again) and you aren't sure where in the MD's memory a single byte of changes is happening, or if it's in multiple places. I was trying to find a vehicle, difficulty or speed flag - with no luck. Ah well.

    With this going nowhere, I decided to document the SRAM format instead. Micro Machines 2 is one of three games in the series that can save: MM96, the game that came out afterwards, has a massive 64K SRAM due to the track editor, and my Micro Machines Military ROM wasn't generating an SRAM file - I've only just realised now after checking that it appears to have been the non-J-cart version, that'd be why. =P File size seems about the same as MM2's, maybe I'll do this one at some point, Military does store some extra things 2 doesn't.

    Anyway! I loaded up an emulator, got a clean copy of the SRAM, then started working my way through, comparing the changes the game was making after doing things. One thing I learnt quickly is that editing the file to try and speed things up was a nogo - the SRAM has a checksum on the end, and if you change any single bit in the file, the game wipes the entire lot back to default settings. This was an annoying thing to find out, but fortunately it was fairly early on. One issue I did have fairly early on was with the time trial data - the format made no sense at all at first glance, with slightly different names or characters generating wildly different byte entries, even when I did one track with all racers with the same name (though on reflection, there was a pattern here I should have seen at the time, that I missed). What I needed was a save entry that had the exact same time, down to the centisecond, that I could change just the racer or the name for, given it seemed to save all 3 of these things in that 6 bytes.

    Enter Rim Runners.

    [​IMG]

    This track on 1-lap is the shortest time trial in the game, and can be completed in less than six seconds. It's a toilet seat, how delightful. I did this over and over using savestates, until I had a set of times with the same racer and the same name that differed by only 1 centisecond - well, 2, as it turns out, but I'll get to that later on. From here I was able to crack the rest of it.

    Right, to the save format!

    Code (Text):
    1. 0x000: lowercase ASCII string "hasreset" (8 bytes)
    2.  
    3. All bytes ending in 0, 2, 4, 6, 8, A, C, E between 0x008 and 0x0C9: 00, not used
    4. All bytes ending in 1, 3, 5, 7, 9, B, D, F between 0x008 and 0x0C9: FF, not used
    5.  
    6. 0x009: changed to 16 on setting 1st driver name? (1 byte)
    7.  
    8. 0x00B: Highest Challenge beaten for JEREMY (1 byte)
    9. 0x00D: Highest Head to Head beaten for JEREMY (1 byte)
    10. 0x00F: Super League Division reached by JEREMY (1 byte)  
    11. 0x011: Seasons taken to do so by JEREMY (1 byte)
    12.  
    13. 0x017: Highest Challenge beaten for WALTER (1 byte)
    14. 0x019: Highest Head to Head beaten for WALTER (1 byte)
    15. 0x01B: Super League Division reached by WALTER (1 byte)  
    16. 0x01D: Seasons taken to do so by WALTER (1 byte)
    17.  
    18. 0x023: Highest Challenge beaten for LISA (1 byte)
    19. 0x025: Highest Head to Head beaten for LISA (1 byte)
    20. 0x027: Super League Division reached by LISA (1 byte)  
    21. 0x029: Seasons taken to do so by LISA (1 byte)
    22.  
    23. 0x02F: Highest Challenge beaten for EMILE (1 byte)
    24. 0x031: Highest Head to Head beaten for EMILE (1 byte)
    25. 0x033: Super League Division reached by EMILE (1 byte)  
    26. 0x035: Seasons taken to do so by EMILE (1 byte)
    27.  
    28. 0x03B: Highest Challenge beaten for DAVEY (1 byte)
    29. 0x03D: Highest Head to Head beaten for DAVEY (1 byte)
    30. 0x03F: Super League Division reached by DAVEY (1 byte)  
    31. 0x041: Seasons taken to do so by DAVEY (1 byte)
    32.  
    33. 0x047: Highest Challenge beaten for DELORA (1 byte)
    34. 0x049: Highest Head to Head beaten for DELORA (1 byte)
    35. 0x04B: Super League Division reached by DELORA (1 byte)  
    36. 0x04D: Seasons taken to do so by DELORA (1 byte)
    37.  
    38. 0x053: Highest Challenge beaten for CHEN (1 byte)
    39. 0x055: Highest Head to Head beaten for CHEN (1 byte)
    40. 0x057: Super League Division reached by CHEN (1 byte)  
    41. 0x059: Seasons taken to do so by CHEN (1 byte)
    42.  
    43. 0x05F: Highest Challenge beaten for SPIDER (1 byte)
    44. 0x061: Highest Head to Head beaten for SPIDER (1 byte)
    45. 0x063: Super League Division reached by SPIDER (1 byte)
    46. 0x065: Seasons taken to do so by SPIDER (1 byte)
    47.  
    48. 0x06B: Highest Challenge beaten for BRUNO (1 byte)
    49. 0x06D: Highest Head to Head beaten for BRUNO (1 byte)
    50. 0x06F: Super League Division reached by BRUNO (1 byte)
    51. 0x071: Seasons taken to do so by BRUNO (1 byte)
    52.  
    53. 0x077: Highest Challenge beaten for DWAYNE (1 byte)
    54. 0x079: Highest Head to Head beaten for DWAYNE (1 byte)
    55. 0x07B: Super League Division reached by DWAYNE (1 byte)  
    56. 0x07D: Seasons taken to do so by DWAYNE (1 byte)
    57.  
    58. 0x083: Highest Challenge beaten for CHERRY (1 byte)
    59. 0x085: Highest Head to Head beaten for CHERRY (1 byte)
    60. 0x087: Super League Division reached by CHERRY (1 byte)
    61. 0x089: Seasons taken to do so by CHERRY (1 byte)
    62.  
    63. 0x08F: Highest Challenge beaten for JETHRO (1 byte)
    64. 0x091: Highest Head to Head beaten for JETHRO (1 byte)
    65. 0x093: Super League Division reached by JETHRO (1 byte)
    66. 0x095: Seasons taken to do so by JETHRO (1 byte)
    67.  
    68. 0x09B: Highest Challenge beaten for MARIA (1 byte)
    69. 0x09D: Highest Head to Head beaten for MARIA (1 byte)
    70. 0x09F: Super League Division reached by MARIA (1 byte)  
    71. 0x0A1: Seasons taken to do so by MARIA (1 byte)
    72.  
    73. 0x0A7: Highest Challenge beaten for VIOLET (1 byte)
    74. 0x0A9: Highest Head to Head beaten for VIOLET (1 byte)
    75. 0x0AB: Super League Division reached by VIOLET (1 byte)  
    76. 0x0AD: Seasons taken to do so by VIOLET (1 byte)
    77.  
    78. 0x0B3: Highest Challenge beaten for EDINA (1 byte)
    79. 0x0B5: Highest Head to Head beaten for EDINA (1 byte)
    80. 0x0B7: Super League Division reached by EDINA (1 byte)  
    81. 0x0B9: Seasons taken to do so by EDINA (1 byte)
    82.  
    83. 0x0BF: Highest Challenge beaten for SUELEE (1 byte)
    84. 0x0C1: Highest Head to Head beaten for SUELEE (1 byte)
    85. 0x0C3: Super League Division reached by SUELEE (1 byte)  
    86. 0x0C5: Seasons taken to do so by SUELEE (1 byte)
    Values for the above:
    Highest Challenge goes from FF (not set), starting at 01 (1 track), through 0A (10 races completed) up to 19 (champion, with 25 courses completed).
    Highest Head to Head goes from FF (not set), starting at 01 (1 track), upwards. I've actually only just realised that I missed finishing this mode. XD I would assume it does the same thing Challenge does.
    Super League Division reached: FF for not set, Then starting at 04 and going downwards as you reach the higher leagues. Never goes lower than 01.
    Seasons taken to do so: FF for not set, the highest I got it was 04 (4 seasons) when I completed the Super League mode (it takes at least 4 seasons to do). The game itself has a hardcoded space for double digits so I'd assume this could potentially go as high as 99 seasons.

    Code (Text):
    1. 0x0CA: Driver name for JEREMY (6 bytes)
    2. 0x0D0: Driver name for WALTER (6 bytes)
    3. 0x0D6: Driver name for LISA   (6 bytes)
    4. 0x0DC: Driver name for EMILE  (6 bytes)
    5. 0x0E2: Driver name for DAVEY  (6 bytes)
    6. 0x0E8: Driver name for DELORA (6 bytes)
    7. 0x0EE: Driver name for CHEN   (6 bytes)
    8. 0x0F4: Driver name for SPIDER (6 bytes)
    9. 0x0FA: Driver name for BRUNO  (6 bytes)
    10. 0x100: Driver name for DWAYNE (6 bytes)
    11. 0x106: Driver name for CHERRY (6 bytes)
    12. 0x10C: Driver name for JETHRO (6 bytes)
    13. 0x112: Driver name for MARIA  (6 bytes)
    14. 0x118: Driver name for VIOLET (6 bytes)
    15. 0x11E: Driver name for EDINA  (6 bytes)
    16. 0x124: Driver name for SUELEE (6 bytes)
    Values for the above: ASCII caps & space only.

    Code (Text):
    1. 0x12A to 0x285: 3A 97 DE F7 BD EC, repeated, as if blank time trial times. Unused (348 bytes)
    Code (Text):
    1. 0x286: 1 Roller Coaster - 3-lap time trial record (6 bytes)
    2. 0x28C: 1 Roller Coaster - 1-lap time trial record (6 bytes)
    3. 0x292: 2 Tiny Treehouse - 3-lap time trial record (6 bytes)
    4. 0x298: 2 Tiny Treehouse - 1-lap time trial record (6 bytes)
    5. 0x29E: 3 Ferry Fiasco - 1-lap time trial record (6 bytes)
    6. 0x2A4: 3 Ferry Fiasco - 1-lap time trial record (6 bytes)
    7. 0x2AA: 4 Basement Bumps - 3-lap time trial record (6 bytes)
    8. 0x2B0: 4 Basement Bumps - 1-lap time trial record (6 bytes)
    9. 0x2B6: 5 Whine On! - 3-lap time trial record (6 bytes)
    10. 0x2BC: 5 Whine On! - 1-lap time trial record (6 bytes)
    11. 0x2C2: 6 Chainsaw Chase! - 3-lap time trial record (6 bytes)
    12. 0x2C8: 6 Chainsaw Chase! - 1-lap time trial record (6 bytes)
    13. 0x2CE: 7 Garage Games - 3-lap time trial record (6 bytes)
    14. 0x2D4: 7 Garage Games - 1-lap time trial record (6 bytes)
    15. 0x2DA: 8 Water Chase - 3-lap time trial record (6 bytes)
    16. 0x2E0: 8 Water Chase - 1-lap time trial record (6 bytes)
    17. 0x2E6: 9 Turbo Turns - 3-lap time trial record (6 bytes)
    18. 0x2EC: 9 Turbo Turns - 1-lap time trial record (6 bytes)
    19. 0x2F2: 10 Pool Slalom - 3-lap time trial record (6 bytes)
    20. 0x2F8: 10 Pool Slalom - 1-lap time trial record (6 bytes)
    21. 0x2FE: 11 Bathtub Burnoff - 3-lap time trial record (6 bytes)
    22. 0x304: 11 Bathtub Burnoff - 1-lap time trial record (6 bytes)
    23. 0x30A: 12 Dervish Danger - 3-lap time trial record (6 bytes)
    24. 0x310: 12 Dervish Danger - 1-lap time trial record (6 bytes)
    25. 0x316: 13 Frosty Reception - 3-lap time trial record (6 bytes)
    26. 0x31C: 13 Frosty Reception - 1-lap time trial record (6 bytes)
    27. 0x322: 14 Pinball Panic - 3-lap time trial record (6 bytes)
    28. 0x328: 14 Pinball Panic- 1-lap time trial record (6 bytes)
    29. 0x32E: 15 Pipe Patience - 3-lap time trial record (6 bytes)
    30. 0x334: 15 Pipe Patience - 1-lap time trial record (6 bytes)
    31. 0x33A: 16 Banked Oval - 3-lap time trial record (6 bytes)
    32. 0x340: 16 Banked Oval - 1-lap time trial record (6 bytes)
    33. 0x346: 17 Ups N Downs - 3-lap time trial record (6 bytes)
    34. 0x34C: 17 Ups N Downs - 1-lap time trial record (6 bytes)
    35. 0x352: 18 Ball Chase - 3-lap time trial record (6 bytes)
    36. 0x358: 18 Ball Chase - 1-lap time trial record (6 bytes)
    37. 0x35E: 19 Musical Hits - 3-lap time trial record (6 bytes)
    38. 0x364: 19 Musical Hits - 1-lap time trial record (6 bytes)
    39. 0x36A: 20 Step On It - 3-lap time trial record (6 bytes)
    40. 0x370: 20 Step On It - 1-lap time trial record (6 bytes)
    41. 0x376: 21 Crossing Chaos - 3-lap time trial record (6 bytes)
    42. 0x37C: 21 Crossing Chaos - 1-lap time trial record (6 bytes)
    43. 0x382: 22 Vice Squad - 3-lap time trial record (6 bytes)
    44. 0x388: 22 Vice Squad - 1-lap time trial record (6 bytes)
    45. 0x38E: 23 Stream Struggle - 3-lap time trial record (6 bytes)
    46. 0x394: 23 Stream Struggle - 1-lap time trial record (6 bytes)
    47. 0x39A: 24 Rim Runners - 3-lap time trial record (6 bytes)
    48. 0x3A0: 24 Rim Runners - 1-lap time trial record (6 bytes)
    49. 0x3A6: 25 Treehouse Tiles - 3-lap time trial record (6 bytes)
    50. 0x3AC: 25 Treehouse Tiles - 1-lap time trial record (6 bytes)
    51. 0x3B2: 26 Garden Jumps - 3-lap time trial record (6 bytes)
    52. 0x3B8: 26 Garden Jumps - 1-lap time trial record (6 bytes)
    53. 0x3BE: 27 Driller Killer - 3-lap time trial record (6 bytes)
    54. 0x3C4: 27 Driller Killer - 1-lap time trial record (6 bytes)
    TIME TRIAL DATA FORMAT
    ======================
    Here is a full set of times, one for each character, arranged in numerical order by the first byte and then the last. This matches up with the same order used for the characters in the Challenge/Head To Head/Super League portion of the data - this is not a coincidence. They all have the same name set.

    Code (Text):
    1. 01 23 13 DE 29 80   0.05.82 (jeremy)
    2. 01 1A 13 DE 29 81   0.05.64 (walter)
    3. 01 23 13 DE 29 82   0.05.82 (lisa)
    4. 01 10 13 DE 29 83   0.05.44 (emile)
    5. 41 21 13 DE 29 80   0.05.78 (davey)
    6. 41 18 13 DE 29 81   0.05.60 (delora)
    7. 41 15 13 DE 29 82   0.05.54 (chen)
    8. 41 1A 13 DE 29 83   0.05.64 (spider)
    9. 81 15 13 DE 29 80   0.05.54 (bruno)
    10. 81 1B 13 DE 29 81   0.05.66 (dwayne)
    11. 81 1C 13 DE 29 82   0.05.68 (cherry)
    12. 81 15 13 DE 29 83   0.05.54 (jethro)
    13. C1 15 13 DE 29 80   0.05.54 (maria)
    14. C1 14 13 DE 29 81   0.05.52 (violet)
    15. C1 0E 13 DE 29 82   0.05.40 (edina)
    16. C1 14 13 DE 29 83   0.05.52 (suelee)
    --------

    First 2 bits of first byte, last 2 bits of last byte: two separate 2-bit bit-fields which together indicate the character. Why did Supersonic Software feel the need to split them like this to each end of the save data, rather than put them together into a single nybble? I have no idea.

    Code (Text):
    1. 0         0
    2. 0000   0000
    3.  
    4. 4         1
    5. 0100   0001
    6.  
    7. 8         2
    8. 1000   0010
    9.  
    10. C         3
    11. 1100   0011
    12.  
    13. 00 00   JEREMY
    14. 00 01   WALTER
    15. 00 10   LISA
    16. 00 11   EMILE
    17. 01 00   DAVEY
    18. 01 01   DELORA
    19. 01 10   CHEN
    20. 01 11   SPIDER
    21. 10 00   BRUNO
    22. 10 01   DWAYNE
    23. 10 10   CHERRY
    24. 10 11   JETHRO
    25. 11 00   MARIA
    26. 11 01   VIOLET
    27. 11 10   EDINA
    28. 11 11   SUELEE
    So for example:

    Code (Text):
    1. 52 36 33 DD A4 97   1.33.24   FONZIE (SPIDER)
    2. 52             97
    3. 01010010 10010111
    4. 01             11
    5.  
    6. 01 11 = SPIDER

    --------

    Last 6 bits of first byte, all of second byte: The course time, in the number of centiseconds since 0.00.00, divided by 2 (14 bytes)

    As mentioned elsewhere in the text, the times in this game only ever end in even numbers. Hence this value is the number of centiseconds, divided by 2, since the timer started counting. 14 bits is 65534 (32767 x2) centiseconds, which is 10 minutes, 55 seconds and 34 centiseconds; and as such is well over what's required. Below is a sample set of times for Rim Runners 1-Lap, the raw data stripped of the name & racer ID, and the resultant conversion from the nybble & hex value into one long binary number, then a decimal, then times 2 to get the centiseconds value. If the value is 6000 or larger, we take that away, add 1 minute to the time, and keep doing it until the value is under 6000, at which point we have the part after the minutes figure.

    Code (Text):
    1. 0.05.40    1 0E   000001 00001110       00000100001110   270       540
    2. 0.05.44    1 10   000001 00010000       00000100010000   272       544
    3. 0.05.52    1 14   000001 00010100       00000100010100   276       552
    4. 0.05.54    1 15   000001 00010101       00000100010101   277       554
    5. 0.05.56    1 16   000001 00010110       00000100010110   278       556
    6. 0.05.58    1 17   000001 00010111       00000100010111   279       558
    So if we take our example record again:

    Code (Text):
    1. 52 36 33 DD A4 97   1.33.24   FONZIE (SPIDER)
    2. 52 36
    3. 00010010 00110110
    4.   010010 00110110
    5. 01001000110110
    6. 4662
    7. 9324
    8. 1 minute, plus 3324 centiseconds = 1 minute 33 seconds 24 centiseconds, or 1.33.24
    --------

    Third byte, fourth byte, fifth byte, first 6 bits of sixth byte: The racer's name

    Each record stores what the racer's name was at the time the record was set. These stay what they were at the time it was done, even if the racer's renamed. This is stored as six 5-bit fields, with the values as follows:

    Code (Text):
    1. 00000   (space, only used to pad the name out to be 6 characters. You can't set a space in a name)
    2. 00001   A
    3. 00010   B
    4. 00011   C
    5. 00100   D
    6. 00101   E
    7. 00110   F
    8. 00111   G
    9. 01000   H
    10. 01001   I
    11. 01010   J
    12. 01011   K
    13. 01100   L
    14. 01101   M
    15. 01110   N
    16. 01111   O
    17. 10000   P
    18. 10001   Q
    19. 10010   R
    20. 10011   S
    21. 10100   T
    22. 10101   U
    23. 10110   V
    24. 10111   W
    25. 11000   X
    26. 11001   Y
    27. 11010   Z
    28. Unused combinations: 11011, 11100, 11101, 11110, 11111
    On our example record:

    Code (Text):
    1. 52 36 33 DD A4 97   1.33.24   FONZIE (SPIDER)
    2. 33 DD A4 97
    3. 00110011 11011101 10100100 10010111
    4. 00110011110111011010010010010111
    5. 00110 01111 01110 11010 01001 00101 11
    6. 6     15    14    26    9     5
    7. F     O     N     Z     I     E
    And finally, the rest of the file:

    Code (Text):
    1. 0x3CA: 00, repeated (20 bytes)
    2. 0x3DE: Mixed-case ASCII text string "Supersonic Software Ltd." (24 bytes)
    3. 0x3F6: 00, repeated (6 bytes)
    4. 0x3FC: Checksum? (2 bytes)
    5. 0x3FE: 00 00 (2 bytes)
    Extra notes and ruminations
    ===========================
    * In-game shows the no record set time as being 4.59.98, on the records screen it shows "NO RECORD".

    * Blank time trial field hex values: 3A 97 DE F7 BD EC. This is equal to Jeremy (the first racer in list order), named to a letter combination 6 characters long that doesn't exist (it'd be the next letter after Z (11011) six times), getting a time of 0.54.22.

    * Multiple racers can have the same name without time trial records getting confused.

    * If a racer's name is changed, their time trial records under the old name no longer show in their number of records in View Stats, however the records themselves remain saved in Time Trial mode (under the original name). If the name is changed back, the original records will be reassigned in View Stats.

    * Time trial records always end in an even number of milliseconds. This would mean at 50Hz, 100 centiseconds pass every second: an actual second. 60Hz games will run faster - 120 centiseconds per second of gameplay.

    * 324 bytes are used for saving time trial data. 348 bytes above it are left in the same default format, but aren't used. Given this massive amount of unused space, I question why Supersonic Software felt the need to crush the time trial store down to 6 bytes of usage per record. The total amount of space available for times is 672 bytes, as such there are 12 theoretical bytes available for each to use, with 24 bytes still left over. Given this, you could store the character name as straight ASCII (6 bytes), the character selection as an entire byte rather than two spread 2-bit bit-fields, and still have 5 bytes left per time trial entry to store the time: given all times in this game are 5 characters long (never higher than 4.59.98) and as such could be stored as 1 byte each, there's no need to have done the compression down to 6 bytes at all. Why, lads? Even with the compression they used, there's still no need to have split the racer identification nybble in half like they ended up storing it, so many weird decisions were made here.

    * If you fail to complete a single race on Challenge or Head to Head, the stat stays as "no statistic".

    * If a character's name is changed, their Challenge and Head to Head records are ERASED from the save file (and by extension, View Stats). There is no way to retrieve this.

    * If racers are under their original name when chosen, the option to rename will always be set to Yes. This applies even when either named that manually after choosing yes, or by naming them something else and then named back - as such there is no "renamed" flag stored for each character in the save file.

    * Super League stores your best effort. A higher tier result will overwrite a lower one even if it takes more tries, i.e. Division 3 after 2 seasons will overwrite Division 4 after 1 season. Less seasons will overwrite higher seasons, i.e. Division 3 after 1 season will overwrite Division 3 after 2 seasons. If you reach a Division and then get demoted, the time it took you to get to the highest Division achieved will be what gets saved, i.e. if you reach Division 3 after 1 season but then go down again on the 2nd, it will store Division 3 after 1 season, not Division 4 after 2.

    * For obvious reasons of you starting there, Division 4 will only ever be saved with 1 season's attempt.

    * Every 3rd Challenge win gets you the Bonus Life mini-game with the monster truck. There are 4 of these - after this, if you get the game again it repeats the 4th track every time.

    * The Challenge lives visibly cap at 9 - however if you get more than this and then lose one, the number that reappears will still be 9. 12 is possible to have if you come first in every race: a Walter token is hidden on at least one level for a 13th.

    * The "Ultimate Race!" banner is broken on the final Challenge course. It alternates between "ULTIMATE RACE! S" and "U FOUR BY FOURS". Presumably this was missed due to how hard it is to get to this point (I have never done it legitimately and in order to do so for this I was heavily abusing savestates. God knows how they expected kids to be able to finish this game!)
     
    • Informative Informative x 3
    • Like Like x 1
    • Useful Useful x 1
    • List
  2. nineko

    nineko

    I am the Holy Cat Tech Member
    Unless I misunderstood what you were trying to do, a much easier way to do that is the "RAM watch" feature you can find in emulators with TAS capabilities, such as Gens ReRecording. It does take some time to understand how to set it up, but it's a pretty useful tool.