满意的文件解析器
这是类型脚本 github 项目 以解析满意保存/蓝印。 满意是一款由Coffee Stain Studios发行的游戏. 重写完全用 TypeScript 写,并捆绑在 国家预防机制。 。 。 . 这里列举的例子有Node.js. 也应该在浏览器里工作.
此解析器可以读取,修改并写入 :
- 保存文件
.sav - 蓝图文件
.sbp, (中文(简体) ),.sbpcfg
解析器是用于深度保存编辑,因为它只是解析到JSON和后. 拥有可编辑结构,游戏逻辑的目的不详. 建议您查看解析后保存/ 蓝印, 以获取您想要编辑的内容 .
支持的版本
游戏更新版的版本支持如下所示. 解析器与更古老的保存和蓝图都相当相容(例如来自U8). 如果你还碰到错误,告诉我或者提出一个问题. U5及以下的游戏版本文件不支持.
| 游戏版本 | 支助 |
|---|---|
| U5型导弹 | 无法兼容 |
| U6 级 | qq 基本兼容 |
| U7型导弹 | qq 基本兼容 |
| 铀8 | 兼容 |
| 铀1.0 | 兼容 |
| 铀1.1 | 兼容 |
| 铀1.2 | 兼容 |
版本迁移
由于兼容性原因,解析器不会从一个游戏版本到另一个版本的MIGRATE. 如果您分析一个U8保存/蓝印,解析器也会将它序列化回一个U8保存/蓝印.
安装
无
npm install @etothepii/satisfactory-file-parser
线条
yarn add @etothepii/satisfactory-file-parser
错误报告或反馈
你总是可以提出一个问题 在链接 github 项目 或以不和之相击我 etothepii。 。 。 .
我所知道的每一个错误 都会让解析器更坚固 没有很多人的帮助,它就不会在现在的位置!
模块支持 ++ 1.1 +++
通过默认,大多数Mods只是再利用了基础游戏的属性和Structs. 如果一个Mod不能工作, 或只是有对象 有很多跟踪数据 无法解析.
一些经过明确测试的模型包括: Ficit-Cam 结构解决方案 线性运动 集装箱屏幕 接通器墙洞 X3-Signs, X3-Roads
例外: Ficsit Networks 更复杂了 还没把一切都画出来 数据保存为原始字节数组. 如果你想的话 我可以看看你的救赎.
正在读取保存
在记忆中读取保存 .
import * as fs from 'fs';
import { Parser } from '@etothepii/satisfactory-file-parser';
const file = new Uint8Array(fs.readFileSync('./MySave.sav')).buffer;
const save = Parser.ParseSave('MySave', file);
// write to json file
fs.writeFileSync('MySave.json', JSON.stringify(save));
也可以通过流来读取“保存”,以保存RAM.
整个保存的二进制数据仍然会存在内存中,但转换后的JSON可以被流出. (你当然可以继续读取溪流的记忆).
回来的 stream 是一个可读的类型字符串的WhatWG流并代表一个 SatisfactorySave 对象。此对象可以再次被序列化.
WhatWG被浏览器默认使用. 节点js可以使用它们 Writable.toWeb() 财务报告和审定财务报表 Writable.fromWeb() 举个例子.
import * as fs from 'fs';
import { Writable } from 'stream';
import { WritableStream } from 'stream/web';
import { ReadableStreamParser } from '@etothepii/satisfactory-file-parser';
const file = fs.readFileSync('./MySave.sav');
const jsonFileStream = fs.createWriteStream('./MySave.json', { highWaterMark: 1024 * 1024 * 200 }); // your outgoing JSON stream. In this case directly to file.
const whatwgWriteStream = Writable.toWeb(jsonFileStream) as WritableStream<string>; // convert the file stream to WHATWG-compliant stream
const { stream, startStreaming } = ReadableStreamParser.CreateReadableStreamFromSaveToJson('MySave', file);
stream.pipeTo(whatwgWriteStream);
jsonFileStream.on('close', () => {
// write stream finished
});
startStreaming();
写入保存
因此,写一个解析后保存的文件同样容易. SaveParser在进程的不同场合同步时有回调帮助. 例如,写出标题或写出保存正文块时. 将二进制数据分割成单个块, 就可以更容易地将二进制数据流到其他地方.
import * as fs from 'fs';
import { Parser } from "@etothepii/satisfactory-file-parser";
let fileHeader: Uint8Array;
const bodyChunks: Uint8Array[] = [];
Parser.WriteSave(save, header => {
console.log('on save header.');
fileHeader = header;
}, chunk => {
console.log('on save body chunk.');
bodyChunks.push(chunk);
});
// write complete sav file back to disk
fs.writeFileSync('./MyModifiedSave.sav', new Uint8Array(Buffer.concat([fileHeader!, ...bodyChunks]));
阅读蓝图
蓝图由2个文件组成. 这个 .sbp 主文件和配置文件 .sbpcfg。 。 。 .
import * as fs from 'fs';
import { Parser } from "@etothepii/satisfactory-file-parser";
const file = new Uint8Array(fs.readFileSync('./MyBlueprint.sbp')).buffer;
const configFile = new Uint8Array(fs.readFileSync('./MyBlueprint.sbpcfg')).buffer;
const blueprint = Parser.ParseBlueprintFiles('Myblueprint', mainFile, configFile);
// write to json file
fs.writeFileSync('Myblueprint.json', JSON.stringify(blueprint));
写入蓝图
因此,将蓝图写入二进制数据,与以保存解析的相同风格获得回调相同.
import * as fs from 'fs';
import { Parser } from "@etothepii/satisfactory-file-parser";
let mainFileHeader: Uint8Array;
const mainFileBodyChunks: Uint8Array[] = [];
const summary = Parser.WriteBlueprintFiles(blueprint,
header => {
console.log('on main file header.');
mainFileHeader = header;
},
chunk => {
console.log('on main file body chunk.');
mainFileBodyChunks.push(chunk);
}
);
// write complete .sbp file back to disk
fs.writeFileSync('./MyBlueprint.sbp', new Uint8Array(Buffer.concat([mainFileHeader!, ...mainFileBodyChunks])));
// write .sbpcfg file back to disk, we get that data from the result of WriteBlueprintFiles
fs.writeFileSync('./MyBlueprint.sbpcfg', new Uint8Array(summary.configFileBinary));
关于解析方法的其他选项
对于每个解析器呼叫,您可以通过可选的回调来接收额外的信息.
就像在被解压的救生机上回叫 解析保存为报告进度提供了回调 [0] 和不定期的信息.
若为 throwErrors ,解析器将在无法解析的数据上立即中止.
const save = Parser.ParseSave('MySave', file.buffer, {
onDecompressedSaveBody: (body) => console.log('on decompressed body', body.byteLength),
onProgressCallback: (progress, msg) => console.log(progress, msg),
throwErrors: false
});
const { stream, startStreaming } = ReadableStreamParser.CreateReadableStreamFromSaveToJson(savename, file, {
onProgress: (progress, msg) => console.log(`progress`, progress, msg);
});
const blueprint = Parser.ParseBlueprintFiles('Myblueprint', file, configFile, {
onDecompressedBlueprintBody: (body) => console.log('on decompressed body', body.byteLength),
throwErrors: false
});
保存编辑实例(JS/TS)
import { SaveComponent, SaveEntity, StructArrayProperty, Int32Property, ObjectProperty, StrProperty, StructProperty, InventoryItemStructPropertyValue, DynamicStructPropertyValue } from '@etothepii/satisfactory-file-parser';
// method to overwrite save objects
// currently quite inefficient to loop through everything, so theres room to improve in a future version. Feel free to raise an issue.
const modifyObjects = (...modifiedObjects: (SaveEntity | SaveComponent)[]) => {
for (const modifiedObject of modifiedObjects) {
for (const level of Object.values(save.levels)) {
for (let i = 0; i < level.objects.length; i++) {
if (level.objects[i].instanceName === modifiedObject.instanceName) {
level.objects[i] = modifiedObject;
}
}
}
}
}
const objects = Object.values(save.levels).flatMap(level => level.objects);
const collectables = Object.values(save.levels).flatMap(level => level.collectables);
示例打印枢纽终端位置
// get hub terminals. Beware that filter returns a COPIED array and not the original objects.
const hubTerminals = objects.filter(obj => obj.typePath === '/Game/FactoryGame/Buildable/Factory/HubTerminal/Build_HubTerminal.Build_HubTerminal_C') as SaveEntity[];
const firstHubPosition = hubTerminals[0].transform.translation;
console.log(`Hub terminal is located at ${firstHubPosition.x}, ${firstHubPosition.y}, ${firstHubPosition.z}`);
修改玩家位置的示例
const players = objects.filter(obj => obj.typePath === '/Game/FactoryGame/Character/Player/Char_Player.Char_Player_C') as SaveEntity[];
for (const player of players) {
const name = (player.properties.mCachedPlayerName as StrProperty).value;
player.transform.translation = {
x: player.transform.translation.x + 5000,
y: player.transform.translation.y + 5000,
z: player.transform.translation.z,
}
console.log(`Player ${name} is now located at ${player.transform.translation.x}, ${player.transform.translation.y}, ${player.transform.translation.z}`);
}
// modify original save objects
modifyObjects(...players);
在存储容器中覆盖项目堆叠的示例
// get the first storage container, either mk1 or mk2.
const storageContainers = objects.filter(obj =>
obj.typePath === '/Game/FactoryGame/Buildable/Factory/StorageContainerMk1/Build_StorageContainerMk1.Build_StorageContainerMk1_C'
|| obj.typePath === '/Game/FactoryGame/Buildable/Factory/StorageContainerMk2/Build_StorageContainerMk2.Build_StorageContainerMk2_C'
);
const firstContainer = storageContainers[0];
// the container has a reference name to an inventory component.
const inventoryReference = firstContainer.properties.mStorageInventory as ObjectProperty;
const inventory = objects.find(obj => obj.instanceName === inventoryReference.value.pathName) as SaveComponent;
const inventoryStacks = inventory.properties.mInventoryStacks as StructArrayProperty;
const firstStack = inventoryStacks.values[0];
// Items within ItemStacks are quite nested. And StructProperties can basically be anything.
// overwrite first item stack with 5 Rotors.
((firstStack as DynamicStructPropertyValue).properties.Item as InventoryItemStructPropertyValue).itemReference = {
levelName: '',
pathName: '/Game/FactoryGame/Resource/Parts/Rotor/Desc_Rotor.Desc_Rotor_C'
};
((firstStack as DynamicStructPropertyValue).properties.NumItems as IntProperty).value = 5;
// modify original save object
modifyObjects(inventory);
错误处理( 从 3. 3. 0 起)
从版本 3.3.0,解析器将不再默认丢出每个错误.
只有您明确指定 throwErrors: true 当调用保存/蓝印上的解析器时,当遇到无法解析的数据时,解析器会立即中止.
否则, throwErrors: false 默认值.
何时 throwErrors 实值 false 行为如下:
- 对于未知的属性,它们的数据土地位于
rawBytes区域. - 如果错误更为严重,则影响整体属性结构:
trailingData输入数据的字段. - 否则,整个尺度的物体可能腐败,将完全从所解析的JSON中被排除在外.
- 更广泛的保存结构(头顺序、身体顺序、身体结构)的错误,无论怎样,仍然被扔出。 那你的救赎是无法解释的.
自动生成类型文件参考。 。 。 .
索引桶文件也使用桶比自动生成. npx barrelsby --directory src --delete --recursive --verbose --exclude src/test
基本指南。 。 。 .
更详细地解释一些基本的东西 在剖析器.