RoboPhred / oni-save-parser

Parses save files for the game "Oxygen Not Included".
MIT License
47 stars 23 forks source link

Contents of storage bins? #3

Closed bxbd closed 6 years ago

bxbd commented 6 years ago

I'd like to be able to extract all the material counts from the save files, to get the same numbers you can see on the side of the game screen. I tried querying the saveData.body.gameObjects for a given material, but I don't think the data there includes counts of that material found within storage bins.

Using a save file from a freshly created game, I believe I can confirm the above by noting that FieldRation (aka nutrient bars) are not found in the gameObjects list, however if you query the RationBox object you will find that there's 2k+ bytes of data remaining in the extraData field for the behavior named "Storage". I tried dumping this data, but I do not know how to parse it, but it does seem to contain the string 'FieldRation" and I suspect it also includes a count here.

I would like to help you extend this library but I'm unfamiliar with how the typescript files describing the save file format operate. I did try to duplicate one of the behaviors to parse the "Storage" behavior here, but I had no luck. If you can point me towards documentation on how to understand this framework better perhaps I can try to be of more help.

RoboPhred commented 6 years ago

The current design of the parser is frankly atrocious. While the IoC approach was good, the use of a dependency injection container was overkill, coupling the parser to their data classes harmed flexibility, and a design flaw prevents me from actually taking advantage of the decoupling for versioning support.

I am working on a redesign over at simplify-rewrite, and will take the opportunity to set up a system for parsing the custom "extraData" block on behaviors.

This new version will be far easier to understand and work with, and I would love your help extending its feature set. It might take some time for me to get enough structure in to make contributions feasible; I will update this issue when behavior parsing is ready.

bxbd commented 6 years ago

ok sounds good

also, fyi, there's an update coming out today and save files from the preview build don't seem to load, i checked this new branch too, and it also chokes on the cosmic save files.

but would love to help, thanks for the lead

RoboPhred commented 6 years ago

Yep, Klei changed how type data is stored in a way that isn't compatible with the current version. That's what triggered the rewrite in the first place; the new design should let me support changes like this going forward.

The new branch has some notes in the Todo with technical details on what changed if you are interested

khakulov commented 6 years ago

I have patched following code into game-object-manager.ts:_parseGameObjectBehavior

if (name === 'Storage') {
      const objectCount = reader.readInt32();
      extraData = new Array(objectCount);
      for (let i = 0; i < objectCount; i++) {
        extraData[i] = {};
        extraData[i].name = reader.readKleiString();
        extraData[i].position = reader.readVector3();
        extraData[i].rotation = reader.readQuaternion();
        extraData[i].scale = reader.readVector3();
        extraData[i].folder = reader.readByte();

        const behaviorCount = reader.readInt32();

        extraData[i].behaviors = new Array(behaviorCount);
        for (let j = 0; j < behaviorCount; j++) {
          extraData[i].behaviors[j] = this._parseGameObjectBehavior(reader);
        }

        const name = extraData[i].name;
        const PrimaryElement = extraData[i]['behaviors'].find(
          (x: any) => x['name'] === 'PrimaryElement'
        );
        if (GameObjectManagerImpl.objectID[name] === undefined) {
          GameObjectManagerImpl.objectID[name] = PrimaryElement.parsedData.ElementID;
        }
      }
    }
    if (name === 'MinionModifiers') {
      extraData = {};
      const needCount = reader.readInt32();
      extraData.needs = new Array(needCount);
      for (let i = 0; i < needCount; i++) {
        extraData.needs[i] = {
          name: reader.readKleiString(),
          valueLength: reader.readInt32(),
          value: reader.readSingle(),
        };
      }
      const effectCount = reader.readInt32();
      extraData.effects = new Array(effectCount);
      for (let i = 0; i < effectCount; i++) {
        extraData.effects[i] = {
          name: reader.readKleiString(),
          value1: reader.readInt32(),
          value2: reader.readInt32(),
          messageId: reader.readKleiString(),
          messageString: reader.readKleiString(),
        };
      }
    }

and game-object-manager.ts:_writeGameObjectBehavior


    if (extraData) {
      if (name === 'Storage') {
        const storedObjects: any = extraData;
        dataWriter.writeInt32(storedObjects.length);
        for (let storedObject of storedObjects) {
          dataWriter.writeKleiString(storedObject.name);
          dataWriter.writeVector3(storedObject.position);
          dataWriter.writeQuaternion(storedObject.rotation);
          dataWriter.writeVector3(storedObject.scale);
          dataWriter.writeByte(storedObject.folder);

          dataWriter.writeInt32(storedObject.behaviors.length);
          for (let behavior of storedObject.behaviors) {
            this._writeGameObjectBehavior(dataWriter, behavior);
          }
        }
      } else if (name === 'MinionModifiers') {
        const needs = (extraData as any).needs;
        dataWriter.writeInt32(needs.length);
        for (let i = 0; i < needs.length; i++) {
          dataWriter.writeKleiString(needs[i].name);
          dataWriter.writeInt32(4);
          dataWriter.writeSingle(needs[i].value);
        }
        const effects = (extraData as any).effects;
        dataWriter.writeInt32(effects.length);
        for (let i = 0; i < effects.length; i++) {
          dataWriter.writeKleiString(effects[i].name);
          dataWriter.writeInt32(effects[i].value1);
          dataWriter.writeInt32(effects[i].value2);
          dataWriter.writeKleiString(effects[i].messageId);
          dataWriter.writeKleiString(effects[i].messageString);
        }
      } else {
        dataWriter.writeBytes(extraData);
      }
    }

This enables me to change storage content and Minion properties like SlimeLung

Example:

const storageLockers = saveData.body.gameObjects.StorageLocker;

for (let storageLocker of storageLockers) {
  const storage = storageLocker['behaviors'].find(x => x['name'] === 'Storage');
  for (let storedObject of storage.extraData) {
    const PrimaryElement = storedObject['behaviors'].find(x => x['name'] === 'PrimaryElement');
    if (PrimaryElement.parsedData._Temperature > 295) {
      PrimaryElement.parsedData._Temperature = 295.0;
    }
    if (PrimaryElement.parsedData.diseaseCount > 0) {
      PrimaryElement.parsedData.diseaseCount = 0;
      PrimaryElement.parsedData.diseaseID.hash = 0;
    }
  }
}
const minions = saveData.body.gameObjects.Minion;

for (let minion of minions) {
  const minionModifiers = minion['behaviors'].find(x => x['name'] === 'MinionModifiers');
  const calories = minionModifiers.extraData.needs.find(n => n.name == 'Calories');
  calories.value = 24000000.0;
  const slimeLung = minionModifiers.extraData.needs.find(n => n.name == 'SlimeLung');
  slimeLung.value = 0.0;
  const stress = minionModifiers.extraData.needs.find(n => n.name == 'Stress');
  stress.value = 0.0;
  minionModifiers.extraData.effects = [];
}
RoboPhred commented 6 years ago

Thanks for looking into this! The parser rewrite is almost done; I just need to test the template saving. I should have it functioning on both rancher and cosmos by the end of the weekend. I should be able to work this in after I get the duplicity site updated.

RoboPhred commented 6 years ago

Support for storage has been added, and will be included in the next release. Remember that if you add a new item, you need to be sure and handle the KPrefabID behavior, including incrementing the global ID counter.

I split off MinionModifiers to issue #4