Deciphering Slime Rancher 2's Save File Format

2022-10-22
/documentation#slime rancher 2#reverse engineering

Background

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.

Building blocks

Saves are built of five major components: primitive values, like i32s or timestamps; strings; arrays; maps; and objects.

Primitives

The two most commmon primitive values are i32s and f32s. All values are encoded little-endian. In addition to these, there are also some 64-bit values:

On top of that, booleans are encoded as a single byte with a value of 0x00 (false) or 0x01 (true).

Strings

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

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.

Maps

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

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.)

The format itself

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

NameTypeNotes
Identifiable Type IndexSRITI
Garden patch indexSRRGIR-something Garden Index?
Game icon indexSRGAMEICONINDEXSometimes the object names do make things easy
Zone indexSRZONEINDEX
Game nameStringThis 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?StringThis appears to just be the game slot again
???i32Possibly the slot number again, but this time encoded as an integer and 0-indexed?
Game summarySRGSUMMData shown to the user on the load screen
Game settingsSRGAMESETTINGS
Scene group indexSRSGI
WorldSRWData concerning the world at large
PlayerSRPLPlayer data, including the refinery inventory
RanchSRRANCHData about the ranch and its plots
???Two bytesOr 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.
ActorsSRAD[]State data for all 'actors': slimes, hens, botanicals, containers, and plorts
DiscoveriesSRPEDPlayer E-something Discoveries? PEDagological opportunities?
???SRAPP
Secret style discsSRSECRETSTYLEDISCI don't think SR2 has these yet?
Upgrade component indexSRUCI
??? indexSRSEIJust contains a pair of hex strings

SRGAME is large, but there are very few mysterious byte sequences.

SRV3: Vector (3D)

NameTypeNotes
xf32
yf32
zf32

Axes might not actually be in that order.

Indexes

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 Index

NameTypeNotes
Identifiable typesString[]

Type names are all in the format of Category.Specifier, e.g. SlimeDefinition.Pink or IdentifiableType.Hen.

SRRGI: Ranch growable index?

NameTypeNotes
Garden patch typeString[]

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 index

NameTypeNotes
Game iconString[]

Maps which game icon is used to represent the save. All icons are slimes.

SRZONEINDEX: Zone index

NameTypeNotes
Zone nameString[]

I haven't found yet where these are being used

SRSGI: Scene group index

NameTypeNotes
Scene groupString[]

All scene group names start with SceneGroup..

SRUCI: Upgrade component index

NameTypeNotes
Upgrade componentsString[]

All upgrade component names start with UpgradeComponent..

SRSEI

NameTypeNotes
???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 summary

NameTypeNotes
BuildStringThe build number matching the one on the title screen
In-game timeIn-game time (f64)
Save file timestampSave file time (i64)The one place with an epoch of 1/1/1
Moneyi32Current player wealth
Discoveriesi32Number of slimepedia entries
???23 bytesProbably various game settings, e.g. tarrs enabled, ferals enabled, but I haven't deciphered them all yet
Game iconi32References SRGAMEICONINDEX

Summarized save info so that browsing the load-game screen doesn't require fully parsing each save.

SRGAMESETTINGS: Game settings

NameTypeNotes
Game settingsStringPair[]"StringPair" being my name for the anonymous object this actually uses
Game iconi32References 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: World

NameTypeNotes
World timeIn-game timeCurrent 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 saturationf32[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 sourcesi32[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
GordosSRG[String]Gordo state data. Keys are gordo followed by a 10-digit number
???SRRW[SRV3]???
Player-placed gadgetsSRPG[String]Keys are site followed by a 10-digit number
PodsSRTP[String]Keys are of the form pod followed by a 10-digit number
Switchesi32[String]Keys are of the form pod followed by a 10-digit number. Value is current state?
Puzzlesbool[String]Keys are puz followed by a 10-digit number. Value is whether the puzzle is solved?
???i32Always 0, so could be an empty map or array
Research dronesSRREDRONE[String]Keys are descriptive, e.g. ResearchDroneGully.
???i32Again, always 0
Resource nodesSRRESNODE[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: Gordo

NameTypeNotes
Fedi32Units of food fed to the Gordo. Favourite foods count double. When the Gordo is fully fed and bursts, this value is -1.
???i32Always 0?
FoundboolHas the player discovered this gordo?
PositionSRV3Position in world coordinates
FacingSRV3Rotation around the x, y and z axes in degrees
Gordo typei32References item index

Gordos only appear on this list when the player has discovered the zone they live in.

SRRW: ???

NameTypeNotes
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 gadget

NameTypeNotes
Item IDi32References item index
Actor IDi32References actor list in SRGAME
???i32Location ID?
Anglef32Placement angle in degrees
???32 bytesNot yet decoded; could contain, e.g. inventory of warp depots, or which gadget is this one's sibling
Scene group IDi32References 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 pod

NameTypeNotes
Opened?i320 or 1
???i32Always 0

Data on treasure pods

SRREDRONE: Research drone

NameTypeNotes
Discoveredbool
NameString

SRRESNODE: Resource node

NameTypeNotes
NameString
???i32
TimestampIn-game timeA future time. When the node will despawn?
Resource typeStringOften blank
???i32
Contentsi32[]References item index.

SRPL: Player

NameTypeNotes
HPi32Current HP, not max
Energyi32Current energy, not max
???i32
Moneyi32
???i32
Total money earnedi32
BuildString
PositionSRV3
FacingSRV3
UpgradesSRPU
Upgrade componentsSRUPGRADECOMPONENTS
InventorySRAD[]All simple-variant SRADs
Gadgets unlockedi32[]References item index
???4 bytes
Refinery contentsi32[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
DecorizerSRDZRNot yet in SR2, but purpose discovered from looking at SR1 saves

SRPU: Player upgrades

NameTypeNotes
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 components

NameTypeNotes
Owned componentsu32[UpgradeComponentId]A count of how many of each upgrade component the player has

SRDZR

NameTypeNotes
???8 bytesAlways seem to be 0s

SRRANCH: Ranch

NameTypeNotes
PlotsSRLP[]All plots, not just accessible ones
Doorsi32[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: Plot

NameTypeNotes
Ran feederIn-game timeWhen was the auto-feeder (if any) last run? 0 if there is no autofeeder.
???8 bytes
Ran vaccuumIn-game timeWhen was the plort/hen vaccuum last run?
???8 bytesAlways 0s
Plot typei321 = empty, 2 = slime corral, 3 = hen coop, 4 = garden, 5 = silo, 6 = pond?, 7 = incinerator
Plant typei32References garden patch index
NameStringplot followed by, you guessed it, a 10 digit number
Upgradesi32[]Which upgrades have been purchased for the plot, e.g. sun-shield (13)?
Plot inventoriesSRAD[][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 listTRACKEDACTORLIST

TRACKEDACTORLIST: Tracked actor list

NameTypeNotes
Actor IDsf64[]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: Actor

There 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.

Simple SRAD

NameTypeNotes
Item IDi32References item index
Counti32How many there are
???i32
???SRSED

Complex SRAD

NameTypeNotes
PositionSRV3
FacingSRV3
Indexi32Monotonically incrementing ID used to reference the actor, independent of its position in this list. Counting starts at 100.
???SRSED
Chick timerIn-game timePossibly counts time until chick matures? NaN for grown fowl, and 0 for non-fowl.
Hen timerIn-game timerPossibly counts time until the hen tries to lay a new egg? NaN for chicks and roosters, and 0 for non-fowl.
Plort despawnIn-game timeWhen will a plort despawn from age. 0 for non-plorts
Plant growth data?SRRCD
Has extra timestampboolIs 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 timeOnly set on some slimes, and references a past time.
FeralboolIs this a feral slime?
???5 bytesAll zero
Scene group IDi32Where in the world is this actor? References scene group index
PetrifiedboolIf 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 statuei32For ringtail statues, when night comes, which actor should it unpetrify to?
???4 bytes
???In-game timeSet on non-slimes, 0 otherwise. Spawning date?
???SRSE[]

SRSED

NameTypeNotes
???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

NameTypeNotes
Conditioni320, 1 or 2. I think this indicates growing, ripe and decayed, not neccesarily in that order.
???In-game timeFuture time. When to make the next state transition?

SRSE

NameTypeNotes
???4 bytesAlways 0
Timestamp???Sometimes future, sometimes past

I don't even see these in recently started games.

SRPED

NameTypeNotes
???4 bytesalways 0; could be an empty array?
DiscoveriesString[]Array of discovery names

SRAPP

NameTypeNotes
???(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

NameTypeNotes
???4 bytesEmpty array?

SREVENTRECORD

NameTypeNotes
EventsSREVENTENTRY[]

SREVENTENTRY

NameTypeNotes
CategoryStringCategory of event
SubcategoryString
Counti32How many times has this event occurred?
Entry created (IGT)In-game timeWhen did this event first occur
Last updated (IGT)In-game timeWhen did this event last occur
Entry created (wall time)Real-world time1601 epoch
Last updated (wall time)Real-world time1601 epoch

Process

Custom hex editor

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.

Early pretty-printing

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.

Moving to Rust structs

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 Vecs 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.

Writing output

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.

Pretty-printing, take 2

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.

Major progress

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.

Further visualization

At this point, there were several mysterious maps in SRW: the keys were all SRV3s, 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 SRV3s 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.

Slime Rancher 1

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.

Current state

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.