Jump to content

Recommended Posts

A browser-based tool for editing ONI save files

 

Version 3.15.0 is Here!

Features:

  • Compatility follows the most recent stable branch
  • Open Source
  • Local editing: The save file is never sent to any server; all edits are performed in browser
  • Offline editing: Under settings, enable "Offline Mode" for use when no internet is available.
  • Split into Standard and Advanced editors
  • Loading and Saving progress reports
  • Editors
    • Difficulty
    • Duplicant
      • Name
      • Gender
      • Appearance
      • Health, stamina, diseases, infections, other values
      • Health state (Alive, Dead, Invulnurable)
      • Skills
      • Experience
      • Job mastery
      • Interests
      • Add and remove Traits
      • Apply and remove status effects*
    • Geysers
      • Emission type (Now with visual changes!)
      • Emission rate
      • Lifecycle time
      • Active time
      • Emission time
    • Materials list
      • View total stored and loose material
      • Delete loose material for performance
    • Raw data editor
      • Edit All the Things

* While you can find effects like "Vomit" and "NarcolepticSleep" in here, they are purely the "effect" of such conditions.  They are what changes the dup's stress or stanima, but do not trigger the actual behavior.

Use the save editor at your own risk!  Use it sparingly, and do not report crashes for saves modified in uncanny ways.

The editor can be found athttps://robophred.github.io/oni-duplicity/

Please report bugs and make feature requests at: https://github.com/RoboPhred/oni-duplicity/issues

The site is built in Typescript using the React framework, with Redux as a backing store.

Both projects are written in Typescript, and can be found here:

Save Parser Library

Web Editor Source

The editor and its underlying library is fully open source under the MIT license, meaning you can do whatever with it so long as you include the license and my copyright notice with it.

 


 

 

Link to comment
Share on other sites

I think I have access to edit the appearance but I need to investigate it more.  I will try to get the bare minimal function in tomorrow, but hopefully we can get a nice visual Sims style editor on that tab eventually.

Link to comment
Share on other sites

This is amazing!

What I'm missing in game is experience display. Always wondered how far dupe is from getting skill up. Should I let Nisbet research a bit longer so she can get that +1 to learning?

Now I can check! Anyone knows what are the experience breakpoints btw?

Link to comment
Share on other sites

MAX_GAINED_ATTRIBUTE_LEVEL = 20
EXPERIENCE_LEVEL_POWER = 1.7f;
TARGET_MAX_LEVEL_CYCLE = 400;

num = (
    Mathf.Pow(
        currentLevel / MAX_GAINED_ATTRIBUTE_LEVEL,
        EXPERIENCE_LEVEL_POWER
    ) * TARGET_MAX_LEVEL_CYCLE * 600.0);

nextLevelExperience = (
    Mathf.Pow(
        (currentLevel + 1) / MAX_GAINED_ATTRIBUTE_LEVEL,
        EXPERIENCE_LEVEL_POWER
    ) * TARGET_MAX_LEVEL_CYCLE * 600.0
) - num;

I'm sure this math can be explained easier, but its too early in the morning for me to attempt to understand what its really doing.

In my hazy, sleep deprived state, it sort of maybe looks like Triangular Numbers, which I have used when writing similar leveling systems.

 

...upon closer look, I would guess that the total experience required to go from level 0 to level TargetLevel is

( (TargetLevel / 20) ^ 1.7 ) * 400 * 600

The experience to the next level from the current one would naturally be the experience from 0 to next, subtracted by the experience from 0 to current (the "num" variable above)

I keep seeing references to 600 as the game-time of 1 cycle as well.

Link to comment
Share on other sites

What edits are you performing?  Can you send me a save file and the details of what you are changing so I can reproduce it?

Feel free to contact me on the ONI discord for more support, I hang out in the modding section.

 

Edit: Keep in mind that its possible to use this to place the game into states it was never intended to handle.  Make sure your edits are consistent with how the game operates (For example, don't give someone a job they aren't capable of doing due to previous jobs not being mastered)

Link to comment
Share on other sites

16 hours ago, Esquimerisflay said:

I just modified the dupes skill points !! as an example 10 points in the architect's ability to construct and 10 points in the skill of the conzinheiro, following this example when I modify more than 3 dupes, the game crashScreenshot.png

I tried modifying any number of skills on many duplicants.  It works fine, so the issue is not simply modify any 3.  Something about your save file or the game state does not like one of the changes you are making. 

Again, I will need your save file and exactly what you are modifying in order to figure out the issue.

 

10 hours ago, TLW said:

Is there a description of the save format anywhere?

I tried to grock the parser, but... you are in a maze of twisty files, all alike. I'm a C person, not a TS person.

 

I had started on documenting it at one point, you can see the remaining bits of documentation over at https://github.com/RoboPhred/oni-save-parser/blob/master/src/versions/7_3/interfaces.ts.

Thats generally a good file to work through, as it contains the major structures of the save, and I tried to keep the fields in order.

The actual parsing is done in the implementations folder, through the parse() function.  The weird bit is the template data, which has been split into multiple (too many) files.  This section comes near the start of the file, lists the name/fields/properties of many of the objects used in the file, and allows the parser to handle most of the data without understanding it ahead of time.  You might find it easier to understand in its pre-refactor monolith form.

If you want to hop into the discord modding channel, I would be happy to help you dig into it more!

Link to comment
Share on other sites

6 hours ago, RoboPhred said:

Eu tentei modificar qualquer número de habilidades em muitos duplicantes. Funciona bem, então o problema não é simplesmente modificar qualquer 3. Algo sobre o seu arquivo salvo ou o estado do jogo não gosta de uma das mudanças que você está fazendo. 

Mais uma vez, vou precisar do seu arquivo salvo e  exatamente o  que você está modificando para descobrir o problema.

 

 

Eu comecei a documentá-lo em um ponto, você pode ver os restantes pedaços de documentação em  https://github.com/RoboPhred/oni-save-parser/blob/master/src/versions/7_3/interfaces.ts .

Isso geralmente é um bom arquivo para trabalhar, pois contém as principais estruturas do save, e eu tentei manter os campos em ordem.

A análise real é feita na   pasta de implementações , através da função parse (). O bit estranho é o  modelo de dados , que foi dividido em vários arquivos (muitos). Esta seção chega perto do início do arquivo, lista o nome / campos / propriedades de muitos dos objetos usados no arquivo e permite que o analisador manipule a maioria dos dados sem compreendê-lo antecipadamente. Você pode achar mais fácil de entender em sua forma de monólito pré-refatoradora  .

Se você quiser entrar no canal de modulação de discórdia, eu ficaria feliz em ajudá-lo a investigar mais!

ok,what's your discord address? and how do I send you my file for you to look at what may be causing the game to fail?

Link to comment
Share on other sites

14 hours ago, RoboPhred said:

I had started on documenting it at one point, you can see the remaining bits of documentation over at https://github.com/RoboPhred/oni-save-parser/blob/master/src/versions/7_3/interfaces.ts.

Thats generally a good file to work through, as it contains the major structures of the save, and I tried to keep the fields in order.

The actual parsing is done in the implementations folder, through the parse() function.  The weird bit is the template data, which has been split into multiple (too many) files.  This section comes near the start of the file, lists the name/fields/properties of many of the objects used in the file, and allows the parser to handle most of the data without understanding it ahead of time.  You might find it easier to understand in its pre-refactor monolith form.

If you want to hop into the discord modding channel, I would be happy to help you dig into it more!

The particular issue I've got is actually fairly early on.

It goes <0x11,"Klei.SaveFileRoot">, <numFields:int32_t=3>, <numProps:int32_t=0>, <0xc,"WidthInCells">, which all makes sense.

But then there's five (5) bytes between the end of WidthInCells and the start of HeightInCells. The last 4 are the length of the string, as expected... Which would seem to imply that WidthInCells is a byte [0x06? Which seems wrong]. Or that I'm missing something when scanning through. But the only definition I see of "widthInCells" in your template is here - and it simply declares it as a number.

In general, I am finding it very difficult to trace through and find what types map to which deserializers. When the names match it's relatively simple... but what deserializer maps to e.g. Map<string, Uint8Array>? Dictionary? If so, how does that map? Or even 'number'? Ditto, take e.g. Enumeration. Is it simply syntactic sugar around TypeSerializer<number>?

...actually, retracing through the code I think I've just realized that I've been parsing it all wrong - or rather I've been parsing templates as though they were actual data. Am I right in saying that 0x6 means that it's an int32, as in TypeId? If so that makes a whole lot more sense.

Also: what's the value of UserDefinedGeneric and how is it distinguished from SByte?

Edit: just successfully parsed the templates... now on to the actual file.

Link to comment
Share on other sites

@TLW Glad you are having success!  The thing you need to worry about now is the compression.  There's a flag in the header that indicates if it is compressed, and everything after templates would be deflated if so.  The game uses Ionic.Zlib for this purpose.

After that, just pay attention to the data length values for the prefabs and game objects; some game object behaviors have additional data stored after their template which they have custom parse logic for.  oni-save-parser currently just shoves that into a byte array until I get around to writing the logic for those.

 

3 hours ago, watermelen671 said:

Hm...if I were to take a pre-thermal file, edit it, and then put it back into my saves, would it still be classified as pre-thermal, or is it "modernized", so to speak. 

I'm not really sure if I'm making any sense or not. :wilson_drool:

 

This might be possible, but the library will not attempt to "upgrade" the header.  The thing I would be worried about is if they took the opportunity of the breaking save to add any extra data to some of the behaviors that the game will expect to see.  It might be loadable in this state, but has a high potential for Strange Bugs to occur.  If you are willing to put the effort in, you could probably try to shim in this missing state from a more recent save, but there is a lot of data that ties together I have yet to untangle (mainly, the stuff I shove into the extraData field on behaviors).

If you want to give this a try, you can make a simple node program that uses oni-save-parser which rewrites the header like this:

Warning: The following code is For Science, and shouldn't be countenanced by the sane or rational.

// ...load old save into ArrayBuffer

const saveGame = parseSaveGame(fileData);
saveGame.header.headerVersion = 1
saveGame.header.isCompressed = true;
saveGame.header.gameInfo.saveMajorVersion = 7;
saveGame.header.gameInfo.saveMinorVersion = 3;

const newFileData = writeSaveGame(saveGame);

// ...write newFileData into a new file.

 

Link to comment
Share on other sites

Pushed an update that fixed save corruption if accent characters were found.  This can happen even if you do not rename anything, as language mods can affect internal text to the save.

If you had game crashes before, try the new version.

Link to comment
Share on other sites

4 hours ago, RoboPhred said:

This might be possible, but the library will not attempt to "upgrade" the header.  The thing I would be worried about is if they took the opportunity of the breaking save to add any extra data to some of the behaviors that the game will expect to see.  It might be loadable in this state, but has a high potential for Strange Bugs to occur.  If you are willing to put the effort in, you could probably try to shim in this missing state from a more recent save, but there is a lot of data that ties together I have yet to untangle (mainly, the stuff I shove into the extraData field on behaviors).

If you want to give this a try, you can make a simple node program that uses oni-save-parser which rewrites the header like this:

Warning: The following code is For Science, and shouldn't be countenanced by the sane or rational.


// ...load old save into ArrayBuffer

const saveGame = parseSaveGame(fileData);
saveGame.header.headerVersion = 1
saveGame.header.isCompressed = true;
saveGame.header.gameInfo.saveMajorVersion = 7;
saveGame.header.gameInfo.saveMinorVersion = 3;

const newFileData = writeSaveGame(saveGame);

// ...write newFileData into a new file.

I mean, I suppose I could try...but I'm terrified that if I do, I'd end up somehow causing the game to be deleted. 

Spoiler

inb4 you say that couldn't happen, it already did. My first attempt at doing this caused my AV engine to pick it up as malware...and it "cleaned" my ONI saves. :wilson_cry:

Tbh I'm only asking because a certain friend just wants Gabriel back. eh.png.05cc785e7a527feca82bb5471f36d55c.png

Spoiler

bodybuilder.png.aafb1dc881715d8e23ae1318a18bdf5e.png

 

Link to comment
Share on other sites

@RoboPhred - success! (Well, at least on a brand-new save). Though I need to clean it up a lot - among other things it currently takes about a minute to dump a save (partly because e.g. arrays parse byte-by-byte, partly because Python is Python, and partly because it's just really hacked together).

FYI, python's zlib library works perfectly for the decompression (zbits 15, separate decompression object). That was actually one of the easiest parts, weirdly enough.

Now to get a decompiler working so I can figure out the few remaining odd cases (SolidConduitSerializer, StateMachineController, Klei.AI.Modifiers, Storage, MinionModifiers, Navigator). I'll post the results here.

Alas, there aren't really any good C# decompilers on Linux - that I've found anyway.

Link to comment
Share on other sites

Wait, I can imagine why you would want to remake the parser because javascript, but you made it in python of all things?

Kidding aside, theres quite a lot of extra data bolted onto those behaviors.  If you manage to find a good decompiler, look for references to "ISaveLoadableDetails", those are the ones that store additional information in additon to their templates.  I would definitely be interested in getting info on these.  I plan to get to them myself eventually, but i'm mostly focused on exposing the information I do have access to in the UI.

 

Edit:

On the subject of arrays, some of the nastier larger ones are arrays of bytes.  You tend to be able to handle those as special cases; I load them in a single gulp with ArrayBuffer in javascript, and Klei makes use of the .Net array block copy ability to eat up whole primitive arrays in one go.  There's a lot of opportunity for performance improvement here.

Link to comment
Share on other sites

First template (do you want these here? As issues? Other? Better format?):

SolidConduitSerializer

count = int32 >= 0

[parseTemplateType() for i in range count]

Simple enough. The actual code has a lot more wrinkles - it only saves data if for cells that have content, and since it saves the count first it actually loops over everything twice (once to get the count and once to save everything). But if you're just doing a load/save roundtrip all of that work is done for you already.

Link to comment
Share on other sites

As issues in oni-save-parser would probably be better.  Also, what template does it parse?  Is it one of those that writes the template name before each entry, or does it assume the parser already knows the name and parses the template named in the code?

Link to comment
Share on other sites

I have 9 Dups inside my savegame and the editor shows me 12 Dups. Some name of them are double but with different jobs they learned. Some of my having a few different jobs because some are never ever needed later like an artist so I switched them to something else. All charakters who have learrned 2 oder more jobs got for each an own profile inside the save game editor.

For changing hair and body it would be nice to see how it looks like at the dups without testing inside the game how he is looking.

Link to comment
Share on other sites

Archived

This topic is now archived and is closed to further replies.

Please be aware that the content of this thread may be outdated and no longer applicable.

×
  • Create New...