Slime Rancher 2 is a game by Monomi Park, and a sequel to their 2016 game Slime Rancher. Slime Rancher 2 was released to early access in September 2022, and has a known bug where one can accidentally lock oneself out of possible upgrades: these upgrades require certain items only found in treasure pods, and either duplicate copies are not counted, or purchasing the upgrade spends all copies of the item. The result is that if the player acquires two copies of the upgrade item before purchasing the upgrade, they will now be permenantly short one copy of the upgrade item, and unable to upgrade fully.
This bug is acknowledged by Monomi Park, and they hope to fix it in the next update, but aren't sure that they'll be able to retroactively correct it in existing saves. Dissatisfied with this (and not wanting to potentially wait months for a fix), I decided to see if I could edit my save file.
Reverse-engineering the format wasn't my first choice of approach, but I couldn't see any editors available online. Using a hex editor plus some bespoke tooling, I started picking apart the file's format.
Saves are built of five major components: primitive values, like i32s
or timestamps; strings; arrays; maps; and objects.
The two most commmon primitive values are i32
s and f32s
. All values are encoded little-endian. In addition to these, there are also some 64-bit values:
f64
s, and measure the number of in-game seconds since midnight of day 1. (Note that the game starts at 9 AM, or 32400 seconds.) One in-game hour equals one real-world minute, and dying or sleeping in your house will also advance time.On top of that, booleans are encoded as a single byte with a value of 0x00
(false) or 0x01
(true).
Strings are serialized by first writing a byte containing their length, followed by the bytes of the string itself. One consequence is that no string can be longer than 255 characters, but it did make deciphering the format very straight-forward. (This format is often known as a Pascal string, and as someone who once wrote a lot of Turbo Pascal, the 255 byte limit is very familiar.)
I don't know what encoding the strings use: UTF-8? ISO 8859-1? Strict ASCII? Since there's no user-provided text in the save file, I can't experiment to find out.
Arrays are encoded by first writing the length of the array as a 32-bit integer, followed by the array elements in sequence. Decoding is thus straight-forward, provided one can figure out the type of value in the array.
Like arrays, maps are preceded by a 32-bit integer encoding its length. In this case, the length is the total number of key/value pairs, rather than values written. Unlike arrays, after each key/value pair, the byte sequence 0x00 0x20
is written, making maps easier to recognize in a hex dump.
Objects open with a string containing the object's name, using the string encoding described above. This is followed by a 32-bit integer containing, I presume, a version number, followed by the byte sequence 0x00 0x30
, and then the object's fields. The end of the object is indicated by the byte sequence 0x00 0x40
.
Most objects have a version of number of 1. Two have a version number of 2, and one has a version number of 0. (It also has an empty name.)
Although the 0x00 0x30
and 0x00 0x40
markers make it easy to guess where an object starts and ends, an object could contain the either sequence as e.g. part of an integer, so one can't use the markers to do simple bracket-matching style parsing. On the other hand, if you've finished parsing the fields of an object and there isn't a close marker immediately following, you know that you've made a mistake.
Relatedly, the save file format is not self-describing: there is nothing encoding the number of fields an object has, or their types. Writing a decoder involves a lot of guess work and testing hypotheses. Even then, it's impossible to distinguish between an integer (or float) that's always 0 from an array or map that's empty in every sample file you've looked at.
Object names are always written in capital letters, and with two exceptions their names begin with SR
. Some names have obvious meanings (SRGAME
, SRRANCH
). Others are more opaque (SRPL
stores player data; SRV3
is a 3-float vector). Others are still a mystery to me (e.g. SRRCD
, which seems to relate to vegetable and plant growth and decay.)
Here are the formats of the objects, to the best of my ability to decode them so far. One limiting factor is that I only have access to my nearly complete save file, and two recently just-started ones. As such, I don't have any saves representing mid-game.
Slime Rancher 1 appears to have a very similar save format, and reverse engineering that would probably provide some insight into the Slime Rancher 2 one, but this has sucked up enough of my time already.
The entire save is an SRGAME
object.
SRGAME
Name | Type | Notes |
---|---|---|
Identifiable Type Index | SRITI | |
Garden patch index | SRRGI | R-something Garden Index? |
Game icon index | SRGAMEICONINDEX | Sometimes the object names do make things easy |
Zone index | SRZONEINDEX | |
Game name | String | This is the base name of the save, and is comprised of the time (down to seconds) that the game was started, followed by an underscore and then which save slot this corresponds to. E.g. 20221020134518_2 is the game I started on October 20, 2022, in the second game slot. |
Game slot? | String | This appears to just be the game slot again |
??? | i32 | Possibly the slot number again, but this time encoded as an integer and 0-indexed? |
Game summary | SRGSUMM | Data shown to the user on the load screen |
Game settings | SRGAMESETTINGS | |
Scene group index | SRSGI | |
World | SRW | Data concerning the world at large |
Player | SRPL | Player data, including the refinery inventory |
Ranch | SRRANCH | Data about the ranch and its plots |
??? | Two bytes | Or maybe two bools? 0x00 0x10 in both of the games I've looked at. It might also be a control code (similar to 0x00 0x20 , 0x00 0x30 , and 0x00 0x40 ) that I haven't deciphered yet. |
Actors | SRAD [] | State data for all 'actors': slimes, hens, botanicals, containers, and plorts |
Discoveries | SRPED | Player E-something Discoveries? PEDagological opportunities? |
??? | SRAPP | |
Secret style discs | SRSECRETSTYLEDISC | I don't think SR2 has these yet? |
Upgrade component index | SRUCI | |
??? index | SRSEI | Just contains a pair of hex strings |
SRGAME
is large, but there are very few mysterious byte sequences.
SRV3
: Vector (3D)Name | Type | Notes |
---|---|---|
x | f32 | |
y | f32 | |
z | f32 |
Axes might not actually be in that order.
By embedding an index into the save file mapping numerical IDs to strings, it makes it easier for Monomi Park to reorder, etc. IDs used by the game itself.
It also made my life reverse-engineering the file format a bit easier since I can look up values in these indices and see if the results make sense. Usually you can guess which index to use based on the range of the values: there are around 480 entries in SRITI
, and the other indices are much smaller.
SRITI
: Identifiable Type IndexName | Type | Notes |
---|---|---|
Identifiable types | String[] |
Type names are all in the format of Category.Specifier
, e.g. SlimeDefinition.Pink
or IdentifiableType.Hen
.
SRRGI
: Ranch growable index?Name | Type | Notes |
---|---|---|
Garden patch type | String[] |
All names start with patch
(for vegetables) or tree
(for fruit), e.g. patchbeet01
or treepear01
.
Curiously, there are some plants in the index that appeared in SR1 but not yet in SR2.
SRGAMEICONINDEX
: Game icon indexName | Type | Notes |
---|---|---|
Game icon | String[] |
Maps which game icon is used to represent the save. All icons are slimes.
SRZONEINDEX
: Zone indexName | Type | Notes |
---|---|---|
Zone name | String[] |
I haven't found yet where these are being used
SRSGI
: Scene group indexName | Type | Notes |
---|---|---|
Scene group | String[] |
All scene group names start with SceneGroup.
.
SRUCI
: Upgrade component indexName | Type | Notes |
---|---|---|
Upgrade components | String[] |
All upgrade component names start with UpgradeComponent.
.
SRSEI
Name | Type | Notes |
---|---|---|
??? | String[] |
I'm not even sure this is an index, and if so what of. There are only two entries, and both are just opaque hexadecimal strings.
SRGSUMM
: Game summaryName | Type | Notes |
---|---|---|
Build | String | The build number matching the one on the title screen |
In-game time | In-game time (f64 ) | |
Save file timestamp | Save file time (i64 ) | The one place with an epoch of 1/1/1 |
Money | i32 | Current player wealth |
Discoveries | i32 | Number of slimepedia entries |
??? | 23 bytes | Probably various game settings, e.g. tarrs enabled, ferals enabled, but I haven't deciphered them all yet |
Game icon | i32 | References SRGAMEICONINDEX |
Summarized save info so that browsing the load-game screen doesn't require fully parsing each save.
SRGAMESETTINGS
: Game settingsName | Type | Notes |
---|---|---|
Game settings | StringPair[] | "StringPair" being my name for the anonymous object this actually uses |
Game icon | i32 | References SRGAMEICONINDEX |
This is the only place where the anonymous object is used: it has a 0-length name, a version number of 0, and contains two strings. I don't know why a map wasn't used instead here. Legacy reasons?
The game settings are all stored as strings, e.g a key of setting.GameIcon
mapping a string 14
, or setting.FeralEnabled
mapping to on
. Curiously, this means that game icon is stored twice, first as a string and then later as an integer.
SRW
: WorldName | Type | Notes |
---|---|---|
World time | In-game time | Current in-game clock |
??? | i32 /f32 ? | Number varies betweens save slots, but seems to remain steady in any given game. As an integer, it always seems to be a 10 digit number starting with 122 . As a float, values I've seen range from to 467392.46875 to 670484.0 |
??? | f64 ? | Always seems to be inf |
??? | f64 ? | Always seems to be 0 |
??? | In-game time? | If so, it always seems to point to a time on the first day around or shortly after noon. |
??? | i32 ? | Always 0 |
Plort market saturation | f32[i32] | Keys are references to the item index; values are the number of plorts the market has left unsold: unsold plorts suppress plort prices. |
Hen spawn timers? | In-game time[SRV3] | The locations indicated by the keys seem to correspond to nest locations, although some are also floating in the air, e.g. in one of the ranch extensions. This extension also has a bunch of hens wandering around. I think the values are when the spawner last ran. In a new game, they're all 0. |
Liquid sources | i32[String] | Keys all start with LiquidSource then a number. Values are all zero. |
Slime spawn timers? | In-game time[SRV3] | Similar to hen-spawn timers, but for slimes |
Gordos | SRG[String] | Gordo state data. Keys are gordo followed by a 10-digit number |
??? | SRRW[SRV3] | ??? |
Player-placed gadgets | SRPG[String] | Keys are site followed by a 10-digit number |
Pods | SRTP[String] | Keys are of the form pod followed by a 10-digit number |
Switches | i32[String] | Keys are of the form pod followed by a 10-digit number. Value is current state? |
Puzzles | bool[String] | Keys are puz followed by a 10-digit number. Value is whether the puzzle is solved? |
??? | i32 | Always 0, so could be an empty map or array |
Research drones | SRREDRONE[String] | Keys are descriptive, e.g. ResearchDroneGully . |
??? | i32 | Again, always 0 |
Resource nodes | SRRESNODE[String] | Keys are resource_node followed by a 10-digit number |
There are still a lot of mysteries here. Some I'm unravelling by mapping the SRV3s alongside the various actors, visualizing it, and using that to find the world coordinates they refer to.
SRG
: GordoName | Type | Notes |
---|---|---|
Fed | i32 | Units of food fed to the Gordo. Favourite foods count double. When the Gordo is fully fed and bursts, this value is -1. |
??? | i32 | Always 0? |
Found | bool | Has the player discovered this gordo? |
Position | SRV3 | Position in world coordinates |
Facing | SRV3 | Rotation around the x, y and z axes in degrees |
Gordo type | i32 | References item index |
Gordos only appear on this list when the player has discovered the zone they live in.
SRRW
: ???Name | Type | Notes |
---|---|---|
Time stamp? | In-game time | |
??? | i32 ? |
This is still a mystery. Timestamps are 'recent', so it's probably another spawn time, but with some additional state attached. The value is usually (but not always) 0, which makes it harder to decypher.
SRPG
: Player gadgetName | Type | Notes |
---|---|---|
Item ID | i32 | References item index |
Actor ID | i32 | References actor list in SRGAME |
??? | i32 | Location ID? |
Angle | f32 | Placement angle in degrees |
??? | 32 bytes | Not yet decoded; could contain, e.g. inventory of warp depots, or which gadget is this one's sibling |
Scene group ID | i32 | References scene group index; where in the world is this gadget? |
??? | 5 bytes | |
Position? | SRV3 |
Still more work to go to figure this one out.
SRTP
: Treasure podName | Type | Notes |
---|---|---|
Opened? | i32 | 0 or 1 |
??? | i32 | Always 0 |
Data on treasure pods
SRREDRONE
: Research droneName | Type | Notes |
---|---|---|
Discovered | bool | |
Name | String |
SRRESNODE
: Resource nodeName | Type | Notes |
---|---|---|
Name | String | |
??? | i32 | |
Timestamp | In-game time | A future time. When the node will despawn? |
Resource type | String | Often blank |
??? | i32 | |
Contents | i32[] | References item index. |
SRPL
: PlayerName | Type | Notes |
---|---|---|
HP | i32 | Current HP, not max |
Energy | i32 | Current energy, not max |
??? | i32 | |
Money | i32 | |
??? | i32 | |
Total money earned | i32 | |
Build | String | |
Position | SRV3 | |
Facing | SRV3 | |
Upgrades | SRPU | |
Upgrade components | SRUPGRADECOMPONENTS | |
Inventory | SRAD[] | All simple-variant SRAD s |
Gadgets unlocked | i32[] | References item index |
??? | 4 bytes | |
Refinery contents | i32[i32] | Keys are item indices, values are counts. Refinery contents is not just inputs (plorts) but also unbuilt gadgets and decorations |
??? | bool | |
??? | 8 bytes | |
??? | bool | |
Decorizer | SRDZR | Not yet in SR2, but purpose discovered from looking at SR1 saves |
SRPU
: Player upgradesName | Type | Notes |
---|---|---|
Purchased upgrades? | i32[i32] | Keys are all multiples of 16. Values start at -1 for unpurchased, and then increase by 1 for each level purchased. |
Yes, at this point I could have just cheated my way past the bug and stopped.
SRUPGRADECOMPONENTS
: Upgrade componentsName | Type | Notes |
---|---|---|
Owned components | u32[UpgradeComponentId] | A count of how many of each upgrade component the player has |
SRDZR
Name | Type | Notes |
---|---|---|
??? | 8 bytes | Always seem to be 0s |
SRRANCH
: RanchName | Type | Notes |
---|---|---|
Plots | SRLP[] | All plots, not just accessible ones |
Doors | i32[String] | Has the player unlocked the door to the ranch extension yet? Keys are of the form door followed by a 10-digit number |
??? | i32[i32] | Keys are all distinct integers from 0 to 9. Values are 0, 21, 1000, 1001, and 1002, and don't seem to vary. |
??? | In-game Time[String] | Keys are all ranch followed by a 10-digit number. Values are timestamps: in a new game, there are 4 ranches, and they're all 0d 9:00:00. In a more established game, the timestamps update, and a 5th ranch is added. Last visited? |
SRLP
: PlotName | Type | Notes |
---|---|---|
Ran feeder | In-game time | When was the auto-feeder (if any) last run? 0 if there is no autofeeder. |
??? | 8 bytes | |
Ran vaccuum | In-game time | When was the plort/hen vaccuum last run? |
??? | 8 bytes | Always 0s |
Plot type | i32 | 1 = empty, 2 = slime corral, 3 = hen coop, 4 = garden, 5 = silo, 6 = pond?, 7 = incinerator |
Plant type | i32 | References garden patch index |
Name | String | plot followed by, you guessed it, a 10 digit number |
Upgrades | i32[] | Which upgrades have been purchased for the plot, e.g. sun-shield (13)? |
Plot inventories | SRAD[][String] | A map of strings to SRAD arrays. SRADs are the simple variant. Strings draw from a fixed pool, and indicate the type of inventory: silo, plort, food, or aged fowl. |
??? | i32[] | The length is always 4, and values default to 0, but sometimes are 1 or 2 instead. |
Incinerator pit fullness? | f32 | |
??? | bool | |
Tracked actor list | TRACKEDACTORLIST |
TRACKEDACTORLIST
: Tracked actor listName | Type | Notes |
---|---|---|
Actor IDs | f64[] | Complex SRAD variants, as used in the Actors field of SRGAME , have an index field. Although that field is an i32 , we store them here as an f64 and I have no idea why. |
SRAD
: ActorThere are two variants of SRAD
: a simple one for inactive actors, stored in inventories or silos; and a more complex one for actors that can interact with the world.
SRAD
Name | Type | Notes |
---|---|---|
Item ID | i32 | References item index |
Count | i32 | How many there are |
??? | i32 | |
??? | SRSED |
SRAD
Name | Type | Notes |
---|---|---|
Position | SRV3 | |
Facing | SRV3 | |
Index | i32 | Monotonically incrementing ID used to reference the actor, independent of its position in this list. Counting starts at 100. |
??? | SRSED | |
Chick timer | In-game time | Possibly counts time until chick matures? NaN for grown fowl, and 0 for non-fowl. |
Hen timer | In-game timer | Possibly counts time until the hen tries to lay a new egg? NaN for chicks and roosters, and 0 for non-fowl. |
Plort despawn | In-game time | When will a plort despawn from age. 0 for non-plorts |
Plant growth data? | SRRCD | |
Has extra timestamp | bool | Is there an extra timestamp immediately after this, growing this object by an extra 8 bytes? Only set on slimes, but not all slimes. |
??? | In-game time | Only set on some slimes, and references a past time. |
Feral | bool | Is this a feral slime? |
??? | 5 bytes | All zero |
Scene group ID | i32 | Where in the world is this actor? References scene group index |
Petrified | bool | If this is a ringtail or ringtail largo, is it currently petrified by the sun? If so, it's not rendered, and a statue is put in its place. |
Unpetrified actor for statue | i32 | For ringtail statues, when night comes, which actor should it unpetrify to? |
??? | 4 bytes | |
??? | In-game time | Set on non-slimes, 0 otherwise. Spawning date? |
??? | SRSE[] |
SRSED
Name | Type | Notes |
---|---|---|
??? | f32[i32] | Keys are 0, 1 and 2 but rarely saved in that order. Hunger, and some other metrics? |
This seems to be slime-specific.
SRRCD
Name | Type | Notes |
---|---|---|
Condition | i32 | 0, 1 or 2. I think this indicates growing, ripe and decayed, not neccesarily in that order. |
??? | In-game time | Future time. When to make the next state transition? |
SRSE
Name | Type | Notes |
---|---|---|
??? | 4 bytes | Always 0 |
Timestamp | ??? | Sometimes future, sometimes past |
I don't even see these in recently started games.
SRPED
Name | Type | Notes |
---|---|---|
??? | 4 bytes | always 0; could be an empty array? |
Discoveries | String[] | Array of discovery names |
SRAPP
Name | Type | Notes |
---|---|---|
??? | (i32, i32)[i32] | Keys are references to the item index. Values might actually be single-element arrays, instead, all containing just the number 1. |
??? | i32[i32] | Keys are references to the item index. Values are all 1. |
In both fields, the keys enumerate the different non-largo slimes.
SRSECRETSTYLEDISC
Name | Type | Notes |
---|---|---|
??? | 4 bytes | Empty array? |
SREVENTRECORD
Name | Type | Notes |
---|---|---|
Events | SREVENTENTRY[] |
SREVENTENTRY
Name | Type | Notes |
---|---|---|
Category | String | Category of event |
Subcategory | String | |
Count | i32 | How many times has this event occurred? |
Entry created (IGT) | In-game time | When did this event first occur |
Last updated (IGT) | In-game time | When did this event last occur |
Entry created (wall time) | Real-world time | 1601 epoch |
Last updated (wall time) | Real-world time | 1601 epoch |
In addition to using HxD, I wrote up a quick TUI hex viewer. My thoughts were that I could write something to add persistent annotations to the file. The annotation component didn't get too far, but I added a few commands to do things like tag a length-prefixed string starting from a given byte, and move the cursor to the byte after the string. This made it easier to do some early exploratory work: if something was highlighted, I could ignore it for now. This was followed by another command to tag objects.
At this stage, I was treating objects as generic objects: I didn't know at that time that a given field would have a consistent type. I had an enum of different value types, and had a hashmap mapping object names to a Vec
of types. When I'd press o
at the start of an object, it would look up the object type and try to annotate its contents. Sometimes this would fail, because the object contained another object type without a hashmap entry, or because an object's entry was wrong.
The SRAD
object was particularly frustrating in this respect, as it broke the rules all other objects seemed to follow: there were two major layouts, with no clues as to which to use beyond checking the first few bytes and hoping that there would be no cases where the first variant leads with the int 0x56525304. There was also a byte, buried in the middle, where if it was 1 the object would be 8 bytes longer. Rather than trying to encode this in my simple hashmap of strings to vecs, I just created an 'SRAD' field type.
At this stage, I was worrying less about what any of the values meant rather than just getting the structure correct. I would notice that an object might have a run of similar adjacent strings, preceded by an integer whose value could conceivably match their count. In this case, I would add ObjField::array(ObjField::String)
to the field list, and see if it would get any further parsing.
The hex viewer was still useful for seeing things in context, but to rapidly test my hypotheses, I added a command line argument that took the name of an object, find all instances of that object in the file, and try to parse them. It would also dump their representations so I could compare them side-by-side.
I managed to get many of the simpler objects done this way, but the larger objects could be thousands of bytes long, containing other objects I'd already worked out. I realized that I'd need to output objects in a more structured manner if I wanted to work them out. Thus was born the first of the pretty printers. There were no field names at this point, just lists of types. I already had code to parse and annotate objects for the hex viewer; all I had to do was make a copy of that code and have it print out the parsed values instead of adding them to the annotations database, and take an indentation amount as an argument.
This new approach helped enormously. It was now more obvious when, say, something was an array, rather than an int followed by a handful of objects. I could run it on an incompletely defined object, and it could tell me "I expected to see an 0x00 0x40
but saw these bytes instead:", giving me a lead on what the next field(s) chould be.
At one point, I considered dumping all of the object field definitions to a file and parsing them. Then I considered creating a meta-language so I could also define things like arrays this way. Then I realized I was a few steps away from reinventing Forth, so stopped. In retrospect, this would have been wasted work, so I'm glad I managed to restrain myself.
About 24 hours after I started, I finally had every object parsing. I didn't necessarily understand what most of the values meant, but I ad least had a handle on the structure.
I now had a handle on the structure of the file, but I wanted to be able to edit it! My next step was to create structs for each of the different object types, and re-implement all of the parsing from the ground up. This sadly meant also giving up on my pretty-printer, but I at least would have debug representations to tide me over.
The objects definitions translated into structs easily, and in the process I also realized that I didn't need to worry about having generic 'objects': if the third field in an object was an SRV3
, it would always be an SRV3
. Because this wasn't a distinction I originally made, I had to spend some time examining output to add proper types to fields. Along the way, I also started naming fields based on my guesses of their contents.
I started writing a parser by hand, before realizing that I was just going to re-invent nom poorly, so used that instead. I created a Parseable
trait and started implementing it for everything. To simplify parsing objects, I also added an Obj
trait, and a blanket implementation of Parseable
that would handle parsing the object prefix and suffix: objects would then just need to implement parsing the body.
Similarly, I had implementations of Parseable
for types like Vec
, that could read arrays.
There was a point where I realized how Rust's type inference could greatly simplify writing my code. Let's use SRG
, the object representing a Gordo, as an example:
#[derive(Clone, Default, Debug, PartialEq)]
pub struct SRG {
pub fed: i32,
pub unknown: i32,
pub found: bool,
pub pos: Srv3,
pub facing: Srv3,
pub gordo_type: ItemId,
}
We have a couple of nested objects, but could also have Vec
s as well. Assuming that all the struct's fields have types that implement Parseable
, I can implement Obj
for SRG
as follows:
impl Obj for SRG {
const NAME: &'static str = "SRG";
fn parse_body(input: &[u8]) -> IResult<&[u8], Self> {
let (input, fed) = Parseable::parse(input)?;
let (input, unknown) = Parseable::parse(input)?;
let (input, found) = Parseable::parse(input)?;
let (input, pos) = Parseable::parse(input)?;
let (input, facing) = Parseable::parse(input)?;
let (input, gordo_type) = Parseable::parse(input)?;
Ok((input, Self { fed, unknown, found, pos, facing, gordo_type }));
}
}
I don't need to tell Rust what implementation of Parseable
to call, since it can work backwords to figure it out on its own. From there, I could replace the entire Obj
impl with a macro:
simple_obj!(SRG, "SRG", fed, unknown, found, pos, facing, gordo_type);
The only catch is that I need to list the fields in the order they appear in the save file.
This was all well and good, but since my goal was to edit save files, I would need to write them too. This was actually surprisingly simple to do: I added a write
method to Parseable
, hand-implemented it for the primitive types, and then updated my simple_obj
macro to implement write
as well. Similar to parse
, it just needed to write each field in order.
After making sure that I could round-trip save files properly with no modifications, it was time for the moment of truth: did the save file contain a check-sum to guard against people like me? I created a new game, saved and quit, bumped my money from the initial 250 to 100250, wrote out the updated save, and swapped it out for the original. Although the load game screen showed me having only 250 money still, once the game loaded my balance was the expected 100250. There were no check-sums.
The problem with using Rust's {:#?}
debug formatter is that it wants to put everything on its own line, even when some things (like SRV3
) are conceptually a single value.
This time I split the pretty printing into two parts: a Printer
, responsible for actually writing to the output stream and maintaining state like indentation levels, and a PPrintable
trait to be implemented by everything.
After some false starts, I ended up with a useful set of printing operations. Most primitive types would just print simple representations of themselves. Some (like ItemId
) would look up values in the various indices stored in the save file and print those instead.
Objects would print their fields and corresponding values. There were separate methods for field names I was confident about, and those I was not. The printer would keep indentation straight, even when values might span multiple lines.
With the output now nicely readable, decoding values became easier. I could do diffs between saves and see what values would change. Monotonically increasing values were likely to be timestamps, and once I had worked out one instance, it was easy to find other places with similar-looking values and test how well interpretting them as timestamps would work.
Working out the different timestamp formats helped significantly: spotting an in-game timestamp in a stretch of mystery bytes would act as an anchor point, and allow me to split the span of bytes into two smaller spans.
Printing out item names instead of IDs, based on the in-file index, also helped by providing considerable context to the surrounding data. For example, all of the IDs used by SRPG
objects referenced gadgets. From there, it would be a reasonable assumption that the float with values ranging between 0 and 359 might be the angle at which the gadget was installed. Similarly, a map from item ID to float where the keys were all plort types would be tracking the plort market's saturation.
At this point, there were several mysterious maps in SRW
: the keys were all SRV3
s, and the values timestamps. The locations never seemed to move, although later-game save files would have more entries than earlier-game saves. I decided my best bet would be to visualize the data, plotting both the unknown SRV3
s alongside known values: the positions of SRAD
actors. I used kiss3d
to create a simple 3D map I could navigate around. The ranch was sufficiently distinctive that I was able to orient myself quickly, and was able to hunt down the positions of some of the keys.
As far as I can tell, these points corresponded to hen and slime spawners, with associated timers to prevent hens or slimes from clustering around a single spawner. A later experiment where I removed all actors in the field suggests that the spawners also look at the local hen/slime density before deciding whether to fire.
I didn't really want to do it, but I eventually concluded that also examining my Slime Rancher 1 saves could give me additional context.
Unsurprisingly, Slime Rancher 1 seemed to use the same set of primitives as Slime Rancher 2, but unlike SR2, the saves didn't have indices of item types, scene groups, game icons, etc. Relatedly, my hypothesis that the number right after object name but before the 0x00 0x30
marker was a version number became much more certain. While in SR2 there were only two objects with a version number greater than 1, most objects in SR1 seemed to have been iterated on.
In addition to the lack of indices, there seemed to be more optional fields and nested data structures. Assuming that various objects retained their purpose between the two versions, it's also possible to get insights into how object numbering worked: the SRAPP
object uses the various slime types as keys. This number isn't strictly consecutive in Slime Rancher 2, due to the inclusion of the hybrid Largo types, but in Slime Rancher 1, the IDs include 1, 2, 3, and end with 16000.
While parsing the Slime Rancher 1 save wasn't as productive as I had been hoping, I did learn in the process that SRDZR
refers to the decorizer, a feature not yet added to Slime Rancher 2. I also took this opportunity to improve error reporting when parsing failed: in addition to providing context on where the parsing failed, it would now also do a hex dump of the recently consumed input and to-be-consumed input, with annotations to make start-of-object, end-of-object and "comma" markers easier to spot.
Monomi Park released a bug-fix update, which fixed the bug that motivated writing this library. With the bug fixed, I could now see what the SRUPGRADECOMPONENTS
object was supposed to look like when non-empty, and updated the parser accordingly.
The library is still very much a work-in-progress, as the save file format will likely change over following updates: new fields being added, or existing-but-empty fields gaining values. There is also a high probability that there are fields that I am currently mis-intepretting, which could cause current saves from players with play-styles sufficiently different from mine to also mis-parse.
While I could solicit save files from other players, deliberately engineer novel situations in-game, or even just make changes to my saves and see what happens, working further on this project is a low priority.