-
Notifications
You must be signed in to change notification settings - Fork 24
Expand file tree
/
Copy pathparseSavegameWorker.ts
More file actions
157 lines (144 loc) · 5.21 KB
/
Copy pathparseSavegameWorker.ts
File metadata and controls
157 lines (144 loc) · 5.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
import { ReadableStreamParser } from '@etothepii/satisfactory-file-parser';
import { JSONParser } from '@streamparser/json';
import { loglev } from '@/core/logger/log';
import {
createInfrastructureAccumulator,
finalizeInfrastructure,
ingestEntity,
} from './infrastructure/extractInfrastructure';
import {
createInspectAccumulator,
finalizeInspect,
inspectObject,
} from './inspectSavegame';
import {
collectInfrastructureTransferables,
type IParseSavegameRequest,
type IParseSavegameResponse,
type ParsedSatisfactorySave,
} from './ParseSavegameMessages';
import { installSatisfactoryParserPatches } from './parserPatches';
installSatisfactoryParserPatches();
const logger = loglev.getLogger('parse-savegame');
// `postMessage` inside a Worker module accepts a `transfer` array, but
// the default DOM lib in tsconfig types it as the window-scoped variant
// (which expects a `targetOrigin` string). Locally re-typed to avoid
// pulling the WebWorker lib into the whole project.
const workerPostMessage = postMessage as (
message: IParseSavegameResponse,
transfer?: ArrayBuffer[],
) => void;
/**
* Streaming-friendly parse: pipes the parser library's
* `ReadableStream<string>` of JSON through a SAX-style
* `JSONParser` TransformStream that emits one fully-formed object
* per `levels.*.objects.*` match. Each object is fed into the
* inspect / infrastructure accumulators and then dropped, keeping
* the worker heap bounded (the parser's WHATWG backpressure pauses
* production once the consumer falls behind). This replaces an
* earlier eager `Parser.ParseSave` path that materialised the
* entire save graph in memory and OOMed on endgame saves.
*/
async function parseSavegame(
file: File,
options: { extractInfrastructure?: boolean },
) {
try {
const buffer = await file.arrayBuffer();
const { stream, startStreaming } =
ReadableStreamParser.CreateReadableStreamFromSaveToJson('Save', buffer, {
onProgress: (progress: number, message?: string) => {
postMessage({
type: 'progress',
progress,
message,
} as IParseSavegameResponse);
},
});
const wantInfrastructure = options.extractInfrastructure === true;
const inspectAcc = createInspectAccumulator();
const infraAcc = wantInfrastructure
? createInfrastructureAccumulator()
: null;
// Consume the parser library's `ReadableStream<string>` directly and
// feed each chunk into a `JSONParser` (the non-WHATWG variant). The
// WHATWG `TransformStream` wrapper from `@streamparser/json-whatwg`
// cannot be used here: its `cloneParsedElementInfo` does
// `JSON.parse(JSON.stringify(parent))` on every emit, and with
// `keepStack: false` the parent array grows monotonically (deleted
// entries leave holes). The clone cost is O(N) per emit, total O(N²)
// — effectively hangs on endgame saves with hundreds of thousands
// of objects. Driving the parser via the callback path skips the
// clone entirely.
const jsonParser = new JSONParser({
paths: ['$.levels.*.objects.*'],
keepStack: false,
});
jsonParser.onValue = info => {
const obj = info.value;
inspectObject(inspectAcc, obj);
if (infraAcc) ingestEntity(infraAcc, obj);
};
const reader = (stream as ReadableStream<string>).getReader();
const streamingDone = startStreaming();
streamingDone.catch(err => {
// If the parser library throws, surface the error through the
// reader as well so the await loop below exits.
reader.cancel(err).catch(() => {});
});
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
jsonParser.write(value);
}
} finally {
if (!jsonParser.isEnded) jsonParser.end();
}
// Wait for the parser library's startStreaming to settle. If it
// rejected this re-throws here.
await streamingDone;
const { availableRecipes, usedNodeIds, players } =
finalizeInspect(inspectAcc);
const save: ParsedSatisfactorySave = {
availableRecipes,
usedNodeIds,
players,
};
let transfer: ArrayBuffer[] = [];
if (infraAcc) {
postMessage({
type: 'progress',
progress: 0.99,
message: 'Finalising infrastructure...',
} as IParseSavegameResponse);
save.infrastructure = finalizeInfrastructure(infraAcc);
transfer = collectInfrastructureTransferables(save.infrastructure);
logger.log(
'Infrastructure extracted:',
save.infrastructure.buildings.count,
'buildings,',
save.infrastructure.splines.reduce((sum, s) => sum + s.count, 0),
'spline polylines',
);
}
workerPostMessage(
{ type: 'parsed', save } as IParseSavegameResponse,
transfer,
);
} catch (e) {
logger.error(`Error while parsing`, e);
postMessage({
type: 'error',
message: e instanceof Error ? e.message : e,
} as IParseSavegameResponse);
}
}
addEventListener('message', (event: MessageEvent<IParseSavegameRequest>) => {
const { data } = event;
if (data.type === 'parse') {
parseSavegame(data.file, {
extractInfrastructure: data.extractInfrastructure ?? false,
});
}
});