@terrafirmagreg-team/dump 0.0.0-stage → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +28 -3
- package/src/lib/dump/assets/atlas/dto.ts +51 -0
- package/src/lib/dump/assets/atlas/lookup.ts +207 -0
- package/src/lib/dump/assets/atlas/sprites.ts +132 -0
- package/src/lib/dump/assets/lang/load.ts +19 -0
- package/src/lib/dump/assets/lang/names.ts +244 -0
- package/src/lib/dump/assets/urls.ts +145 -0
- package/src/lib/dump/client/atlases.ts +85 -0
- package/src/lib/dump/client/dump.ts +132 -0
- package/src/lib/dump/client/groups.ts +27 -0
- package/src/lib/dump/client/keybinds.ts +16 -0
- package/src/lib/dump/client/mods.ts +18 -0
- package/src/lib/dump/client/registry.ts +121 -0
- package/src/lib/dump/client/tabs.ts +23 -0
- package/src/lib/dump/client/tags.ts +59 -0
- package/src/lib/dump/contract/kind.ts +62 -0
- package/src/lib/dump/contract/read.ts +76 -0
- package/src/lib/dump/contract/ref.ts +260 -0
- package/src/lib/dump/contract/text.ts +101 -0
- package/src/lib/dump/data/registry/dto.ts +57 -0
- package/src/lib/dump/data/registry/parse.ts +291 -0
- package/src/lib/dump/data/tab/dto.ts +14 -0
- package/src/lib/dump/data/tab/parse.ts +27 -0
- package/src/lib/dump/data/tag/dto.ts +8 -0
- package/src/lib/dump/data/tag/parse.ts +58 -0
- package/src/lib/dump/gen/data/guides/book.ts +57 -0
- package/src/lib/dump/gen/data/guides/category.ts +31 -0
- package/src/lib/dump/gen/data/guides/entry.ts +49 -0
- package/src/lib/dump/gen/data/models/model.ts +4 -0
- package/src/lib/dump/gen/data/quests/chapter.ts +127 -0
- package/src/lib/dump/gen/data/quests/chapter_groups.ts +12 -0
- package/src/lib/dump/gen/data/quests/data.ts +41 -0
- package/src/lib/dump/gen/data/recipes/recipe.ts +79 -0
- package/src/lib/dump/gen/index.ts +21 -0
- package/src/lib/dump/gen/keybinds.ts +4 -0
- package/src/lib/dump/gen/lang.ts +4 -0
- package/src/lib/dump/gen/manifest.ts +60 -0
- package/src/lib/dump/gen/registry/blocks.ts +61 -0
- package/src/lib/dump/gen/registry/entities.ts +50 -0
- package/src/lib/dump/gen/registry/fluids.ts +49 -0
- package/src/lib/dump/gen/registry/groups.ts +15 -0
- package/src/lib/dump/gen/registry/items.ts +52 -0
- package/src/lib/dump/gen/registry/recipe_categories.ts +65 -0
- package/src/lib/dump/gen/registry/recipes.ts +4 -0
- package/src/lib/dump/gen/registry/tabs.ts +47 -0
- package/src/lib/dump/gen/registry/tags.ts +12 -0
- package/src/lib/dump/json-reader.ts +254 -0
- package/src/lib/dump/manifest/dto.ts +12 -0
- package/src/lib/dump/manifest/parse.ts +168 -0
- package/src/lib/dump/mods/dto.ts +31 -0
- package/src/lib/dump/mods/index.ts +97 -0
- package/src/lib/dump/mods/load.ts +60 -0
- package/src/lib/dump/paths.ts +172 -0
- package/src/lib/dump/reader.ts +31 -0
- package/src/lib/dump-search.ts +125 -0
- package/src/lib/snbt.ts +247 -0
- package/README.md +0 -3
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { parseDumpManifest } from '@terrafirmagreg-team/dump/lib/dump/manifest/parse';
|
|
2
|
+
import {
|
|
3
|
+
dumpStorageUrl,
|
|
4
|
+
registerDumpLocation,
|
|
5
|
+
type DumpLocation,
|
|
6
|
+
} from '@terrafirmagreg-team/dump/lib/dump/paths';
|
|
7
|
+
import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
|
|
8
|
+
import type { DumpReader } from '@terrafirmagreg-team/dump/lib/dump/reader';
|
|
9
|
+
import { createAsyncCache } from '@terrafirmagreg-team/data/lib/async-cache';
|
|
10
|
+
|
|
11
|
+
const SAFE_LANG_CODE = /^[a-z0-9]+(_[a-z0-9]+)*$/i;
|
|
12
|
+
|
|
13
|
+
function cleanLangCode(code: string): string | undefined {
|
|
14
|
+
return SAFE_LANG_CODE.test(code) ? code : undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const MAX_INFLIGHT = 12;
|
|
18
|
+
const lanes = {
|
|
19
|
+
high: [] as Array<() => void>,
|
|
20
|
+
normal: [] as Array<() => void>,
|
|
21
|
+
};
|
|
22
|
+
let inflight = 0;
|
|
23
|
+
|
|
24
|
+
function pump() {
|
|
25
|
+
while (inflight < MAX_INFLIGHT) {
|
|
26
|
+
const run = lanes.high.shift() ?? lanes.normal.shift();
|
|
27
|
+
if (!run) return;
|
|
28
|
+
inflight++;
|
|
29
|
+
run();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function fetchLane<T>(task: () => Promise<T>, high: boolean): Promise<T> {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const run = () =>
|
|
36
|
+
task()
|
|
37
|
+
.then(resolve, reject)
|
|
38
|
+
.finally(() => {
|
|
39
|
+
inflight--;
|
|
40
|
+
pump();
|
|
41
|
+
});
|
|
42
|
+
(high ? lanes.high : lanes.normal).push(run);
|
|
43
|
+
pump();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const FETCH_TIMEOUT = 20_000;
|
|
48
|
+
|
|
49
|
+
async function request(url: string): Promise<Response | undefined> {
|
|
50
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
51
|
+
try {
|
|
52
|
+
return await fetch(url, {
|
|
53
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
54
|
+
});
|
|
55
|
+
} catch {
|
|
56
|
+
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const DUMP_POINTER_TTL = 60_000;
|
|
63
|
+
|
|
64
|
+
const fetchJsonLoader = async (url: string, high: boolean) =>
|
|
65
|
+
fetchLane(async () => {
|
|
66
|
+
try {
|
|
67
|
+
const res = await request(url);
|
|
68
|
+
if (!res?.ok) return undefined;
|
|
69
|
+
return (await res.json()) as unknown;
|
|
70
|
+
} catch {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
}, high);
|
|
74
|
+
|
|
75
|
+
const fetchJsonGzLoader = async (url: string, high: boolean) =>
|
|
76
|
+
fetchLane(async () => {
|
|
77
|
+
try {
|
|
78
|
+
const res = await request(url);
|
|
79
|
+
if (!res?.ok || !res.body) return undefined;
|
|
80
|
+
const text = await new Response(
|
|
81
|
+
res.body.pipeThrough(new DecompressionStream('gzip')),
|
|
82
|
+
).text();
|
|
83
|
+
return JSON.parse(text) as unknown;
|
|
84
|
+
} catch {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
}, high);
|
|
88
|
+
|
|
89
|
+
const fetchJson = createAsyncCache(fetchJsonLoader, (url) => url);
|
|
90
|
+
|
|
91
|
+
const fetchJsonGz = createAsyncCache(fetchJsonGzLoader, (url) => url);
|
|
92
|
+
|
|
93
|
+
const fetchJsonMutable = createAsyncCache(
|
|
94
|
+
fetchJsonLoader,
|
|
95
|
+
(url) => url,
|
|
96
|
+
DUMP_POINTER_TTL,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const fetchJsonGzMutable = createAsyncCache(
|
|
100
|
+
fetchJsonGzLoader,
|
|
101
|
+
(url) => url,
|
|
102
|
+
DUMP_POINTER_TTL,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
export const COMPRESSED_TREE = /^(?:data|meta)\//;
|
|
106
|
+
|
|
107
|
+
function toLangMap(raw: unknown): Record<string, string> {
|
|
108
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {};
|
|
109
|
+
const map: Record<string, string> = {};
|
|
110
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
111
|
+
map[key] = typeof value === 'string' ? value : JSON.stringify(value);
|
|
112
|
+
}
|
|
113
|
+
return map;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export class JsonDumpReader implements DumpReader {
|
|
117
|
+
private manifestCache: Promise<ReturnType<typeof parseDumpManifest>> | undefined;
|
|
118
|
+
private locationCache: Promise<DumpLocation> | undefined;
|
|
119
|
+
private langCache = createAsyncCache((code: string) => this.loadLang(code));
|
|
120
|
+
private langRawCache = createAsyncCache((code: string) =>
|
|
121
|
+
this.loadLangRaw(code),
|
|
122
|
+
);
|
|
123
|
+
private readonly packId: string;
|
|
124
|
+
private readonly version?: string;
|
|
125
|
+
|
|
126
|
+
constructor(private dataset: DatasetRef) {
|
|
127
|
+
const ref = dataset;
|
|
128
|
+
this.packId = ref.id;
|
|
129
|
+
this.version = ref.version;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
getJson(path: string, priority?: 'high'): Promise<unknown | undefined> {
|
|
133
|
+
const high = priority === 'high';
|
|
134
|
+
if (path === 'versions.json') {
|
|
135
|
+
return fetchJsonMutable(
|
|
136
|
+
dumpStorageUrl(`${this.packId}/versions.json`),
|
|
137
|
+
high,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return this.location().then((loc) => {
|
|
141
|
+
const key = loc.files?.[path];
|
|
142
|
+
if (key) {
|
|
143
|
+
return key.endsWith('.gz')
|
|
144
|
+
? fetchJsonGz(dumpStorageUrl(`${key}`), high)
|
|
145
|
+
: fetchJson(dumpStorageUrl(`${key}`), high);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (loc.files) return undefined;
|
|
149
|
+
const rel = `${loc.root}/${path}`;
|
|
150
|
+
const [json, jsonGz] =
|
|
151
|
+
loc.root === this.packId
|
|
152
|
+
? [fetchJsonMutable, fetchJsonGzMutable]
|
|
153
|
+
: [fetchJson, fetchJsonGz];
|
|
154
|
+
return COMPRESSED_TREE.test(path)
|
|
155
|
+
? jsonGz(dumpStorageUrl(`${rel}.gz`), high)
|
|
156
|
+
: json(dumpStorageUrl(`${rel}`), high);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
getLang(lang: string): Promise<Record<string, string>> {
|
|
161
|
+
const code = cleanLangCode(lang);
|
|
162
|
+
return code ? this.langCache(code) : Promise.resolve({});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
getLangRaw(lang: string): Promise<Record<string, string>> {
|
|
166
|
+
const code = cleanLangCode(lang);
|
|
167
|
+
return code ? this.langRawCache(code) : Promise.resolve({});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private location(): Promise<DumpLocation> {
|
|
171
|
+
const p = (this.locationCache ??= this.resolveLocation());
|
|
172
|
+
p.catch(() => {
|
|
173
|
+
if (this.locationCache === p) this.locationCache = undefined;
|
|
174
|
+
});
|
|
175
|
+
return p;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private async resolveLocation(): Promise<DumpLocation> {
|
|
179
|
+
if (this.version) {
|
|
180
|
+
const root = `${this.packId}/${this.version}`;
|
|
181
|
+
const loc: DumpLocation = { root, files: await this.objectsIndex(root) };
|
|
182
|
+
registerDumpLocation(this.dataset, loc);
|
|
183
|
+
return loc;
|
|
184
|
+
}
|
|
185
|
+
const versions = (await fetchJsonMutable(
|
|
186
|
+
dumpStorageUrl(`${this.packId}/versions.json`),
|
|
187
|
+
true,
|
|
188
|
+
)) as { latest?: string } | undefined;
|
|
189
|
+
if (versions?.latest) {
|
|
190
|
+
const root = `${this.packId}/${versions.latest}`;
|
|
191
|
+
|
|
192
|
+
const exists = await fetchJson(
|
|
193
|
+
dumpStorageUrl(`${root}/manifest.json`),
|
|
194
|
+
false,
|
|
195
|
+
);
|
|
196
|
+
if (exists) {
|
|
197
|
+
const loc: DumpLocation = { root, files: await this.objectsIndex(root) };
|
|
198
|
+
registerDumpLocation(this.dataset, loc);
|
|
199
|
+
registerDumpLocation({ id: this.packId, version: versions.latest }, loc);
|
|
200
|
+
return loc;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const loc: DumpLocation = { root: this.packId };
|
|
204
|
+
registerDumpLocation(this.dataset, loc);
|
|
205
|
+
return loc;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private async objectsIndex(
|
|
209
|
+
root: string,
|
|
210
|
+
): Promise<Record<string, string> | undefined> {
|
|
211
|
+
const raw = await fetchJson(dumpStorageUrl(`${root}/objects.json`), false);
|
|
212
|
+
const files =
|
|
213
|
+
raw && typeof raw === 'object' && !Array.isArray(raw)
|
|
214
|
+
? (raw as Record<string, unknown>).files
|
|
215
|
+
: undefined;
|
|
216
|
+
if (!files || typeof files !== 'object' || Array.isArray(files)) {
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
const out: Record<string, string> = {};
|
|
220
|
+
for (const [rel, key] of Object.entries(files)) {
|
|
221
|
+
if (typeof key === 'string') out[rel] = key;
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private manifest() {
|
|
227
|
+
const p = (this.manifestCache ??= this.getJson('manifest.json').then((raw) =>
|
|
228
|
+
parseDumpManifest(raw),
|
|
229
|
+
));
|
|
230
|
+
p.catch(() => {
|
|
231
|
+
if (this.manifestCache === p) this.manifestCache = undefined;
|
|
232
|
+
});
|
|
233
|
+
return p;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private async loadLang(lang: string): Promise<Record<string, string>> {
|
|
237
|
+
const codes = lang === 'en_us' ? ['en_us'] : ['en_us', lang];
|
|
238
|
+
const loc = await this.location();
|
|
239
|
+
const base = loc.files ? 'lang' : 'meta/lang';
|
|
240
|
+
const metaMaps = await Promise.all(
|
|
241
|
+
codes.map((code) =>
|
|
242
|
+
this.getJson(`${base}/${code}.json`).then(toLangMap),
|
|
243
|
+
),
|
|
244
|
+
);
|
|
245
|
+
return Object.assign({}, ...metaMaps);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private async loadLangRaw(code: string): Promise<Record<string, string>> {
|
|
249
|
+
const loc = await this.location();
|
|
250
|
+
const base = loc.files ? 'lang' : 'meta/lang';
|
|
251
|
+
return this.getJson(`${base}/${code}.json`).then(toLangMap);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import {
|
|
2
|
+
asNumber,
|
|
3
|
+
asRecord,
|
|
4
|
+
asString,
|
|
5
|
+
asStringList,
|
|
6
|
+
} from '@terrafirmagreg-team/dump/lib/dump/contract/read';
|
|
7
|
+
import type {
|
|
8
|
+
DumpIndexes,
|
|
9
|
+
DumpManifest,
|
|
10
|
+
DumpProducer,
|
|
11
|
+
DumpText,
|
|
12
|
+
NamespaceDisplay,
|
|
13
|
+
PackInfo,
|
|
14
|
+
RecipeTypeRow,
|
|
15
|
+
} from '@terrafirmagreg-team/dump/lib/dump/manifest/dto';
|
|
16
|
+
|
|
17
|
+
function asProducer(value: unknown): DumpProducer | undefined {
|
|
18
|
+
const row = asRecord(value);
|
|
19
|
+
if (!row) return undefined;
|
|
20
|
+
const name = asString(row.name);
|
|
21
|
+
const platform = asString(row.platform);
|
|
22
|
+
const minecraft = asString(row.minecraft);
|
|
23
|
+
if (!name || !platform || !minecraft) return undefined;
|
|
24
|
+
return { name, platform, minecraft };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function asPackInfo(value: unknown): PackInfo | undefined {
|
|
28
|
+
const row = asRecord(value);
|
|
29
|
+
if (!row) return undefined;
|
|
30
|
+
const id = asString(row.id);
|
|
31
|
+
if (!id) return undefined;
|
|
32
|
+
return { id, version: typeof row.version === 'string' ? row.version : null };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function asDumpText(value: unknown): DumpText | undefined {
|
|
36
|
+
const row = asRecord(value);
|
|
37
|
+
if (!row) return undefined;
|
|
38
|
+
const out: DumpText = {};
|
|
39
|
+
const text = asString(row.text);
|
|
40
|
+
const translate = asString(row.translate);
|
|
41
|
+
const keybind = asString(row.keybind);
|
|
42
|
+
if (text !== undefined) out.text = text;
|
|
43
|
+
if (translate !== undefined) out.translate = translate;
|
|
44
|
+
if (keybind !== undefined) out.keybind = keybind;
|
|
45
|
+
if (Array.isArray(row.with)) {
|
|
46
|
+
out.with = row.with.flatMap((entry) => {
|
|
47
|
+
const part = asDumpText(entry);
|
|
48
|
+
return part ? [part] : [];
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (Array.isArray(row.extra)) {
|
|
52
|
+
out.extra = row.extra.flatMap((entry) => {
|
|
53
|
+
const part = asDumpText(entry);
|
|
54
|
+
return part ? [part] : [];
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function asDisplay(value: unknown): NamespaceDisplay['display'] | undefined {
|
|
61
|
+
const row = asRecord(value);
|
|
62
|
+
if (!row) return undefined;
|
|
63
|
+
const name = asString(row.name);
|
|
64
|
+
if (!name) return undefined;
|
|
65
|
+
const tooltipMods: Record<string, DumpText[]> = {};
|
|
66
|
+
const modsRow = asRecord(row.tooltipMods);
|
|
67
|
+
if (modsRow) {
|
|
68
|
+
for (const [mod, entries] of Object.entries(modsRow)) {
|
|
69
|
+
if (!Array.isArray(entries)) continue;
|
|
70
|
+
tooltipMods[mod] = entries.flatMap((entry) => {
|
|
71
|
+
const text = asDumpText(entry);
|
|
72
|
+
return text ? [text] : [];
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
name,
|
|
78
|
+
tooltip: Array.isArray(row.tooltip)
|
|
79
|
+
? row.tooltip.flatMap((entry) => {
|
|
80
|
+
const text = asDumpText(entry);
|
|
81
|
+
return text ? [text] : [];
|
|
82
|
+
})
|
|
83
|
+
: [],
|
|
84
|
+
tooltipMods,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function asNamespaces(
|
|
89
|
+
value: unknown,
|
|
90
|
+
): Record<string, NamespaceDisplay> | undefined {
|
|
91
|
+
const row = asRecord(value);
|
|
92
|
+
if (!row) return undefined;
|
|
93
|
+
const out: Record<string, NamespaceDisplay> = {};
|
|
94
|
+
for (const [ns, entry] of Object.entries(row)) {
|
|
95
|
+
if (!ns || ns.includes('/') || ns.includes('\\') || ns.includes('..')) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const nsRow = asRecord(entry);
|
|
99
|
+
const display = asDisplay(nsRow?.display);
|
|
100
|
+
if (!display) continue;
|
|
101
|
+
out[ns] = {
|
|
102
|
+
display,
|
|
103
|
+
registry: asStringList(nsRow?.registry) ?? [],
|
|
104
|
+
guides: asStringList(nsRow?.guides) ?? [],
|
|
105
|
+
languages: asStringList(nsRow?.languages) ?? [],
|
|
106
|
+
quests: nsRow?.quests === true,
|
|
107
|
+
keybinds: nsRow?.keybinds === true,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function asRecipeTypeRow(value: unknown): RecipeTypeRow | undefined {
|
|
114
|
+
const row = asRecord(value);
|
|
115
|
+
if (!row) return undefined;
|
|
116
|
+
const id = asString(row.id);
|
|
117
|
+
const count = asNumber(row.count);
|
|
118
|
+
const filesRow = asRecord(row.files);
|
|
119
|
+
if (!id || count === undefined || !filesRow) return undefined;
|
|
120
|
+
const files: Record<string, string> = {};
|
|
121
|
+
for (const [ns, path] of Object.entries(filesRow)) {
|
|
122
|
+
if (typeof path === 'string' && path) files[ns] = path;
|
|
123
|
+
}
|
|
124
|
+
return { id, count, files };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function asIndexes(value: unknown): DumpIndexes | undefined {
|
|
128
|
+
const row = asRecord(value);
|
|
129
|
+
if (!row) return undefined;
|
|
130
|
+
const recipesRow = asRecord(row.recipes);
|
|
131
|
+
if (!recipesRow || !Array.isArray(recipesRow.types)) return undefined;
|
|
132
|
+
const types = recipesRow.types.flatMap((entry) => {
|
|
133
|
+
const typeRow = asRecipeTypeRow(entry);
|
|
134
|
+
return typeRow ? [typeRow] : [];
|
|
135
|
+
});
|
|
136
|
+
const quests: Record<string, string> = {};
|
|
137
|
+
const questsRow = asRecord(row.quests);
|
|
138
|
+
if (questsRow) {
|
|
139
|
+
for (const [ns, path] of Object.entries(questsRow)) {
|
|
140
|
+
if (typeof path === 'string' && path) quests[ns] = path;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
recipes: { types },
|
|
145
|
+
quests,
|
|
146
|
+
used: asStringList(row.used) ?? [],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function parseDumpManifest(value: unknown): DumpManifest | undefined {
|
|
151
|
+
const row = asRecord(value);
|
|
152
|
+
if (!row) return undefined;
|
|
153
|
+
if (asString(row.schema) !== 'manifest') return undefined;
|
|
154
|
+
const producer = asProducer(row.producer);
|
|
155
|
+
const indexes = asIndexes(row.indexes);
|
|
156
|
+
const namespaces = asNamespaces(row.namespaces);
|
|
157
|
+
if (!producer || !indexes || !namespaces) return undefined;
|
|
158
|
+
return {
|
|
159
|
+
schema: 'manifest',
|
|
160
|
+
producer,
|
|
161
|
+
pack: asPackInfo(row.pack) ?? null,
|
|
162
|
+
languages: asStringList(row.languages),
|
|
163
|
+
layout: asString(row.layout) ?? 'v1',
|
|
164
|
+
namespaces,
|
|
165
|
+
indexes,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { DumpManifest } from '@terrafirmagreg-team/dump/lib/dump/manifest/dto';
|
|
2
|
+
|
|
3
|
+
export type GuideBookRef = {
|
|
4
|
+
modId: string;
|
|
5
|
+
bookPath: string;
|
|
6
|
+
bookId: string;
|
|
7
|
+
dir: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type DumpMod = {
|
|
11
|
+
namespace: string;
|
|
12
|
+
keybindsFile: string | null;
|
|
13
|
+
items: string | null;
|
|
14
|
+
blocks: string | null;
|
|
15
|
+
fluids: string | null;
|
|
16
|
+
entities: string | null;
|
|
17
|
+
tabs: string | null;
|
|
18
|
+
groups: string | null;
|
|
19
|
+
recipeCategories: string | null;
|
|
20
|
+
tags: string | null;
|
|
21
|
+
recipeIndex: string | null;
|
|
22
|
+
questData: string | null;
|
|
23
|
+
books: GuideBookRef[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type DumpIndex = {
|
|
27
|
+
root: string;
|
|
28
|
+
manifest: DumpManifest;
|
|
29
|
+
mods: DumpMod[];
|
|
30
|
+
};
|
|
31
|
+
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DumpManifest,
|
|
3
|
+
NamespaceDisplay,
|
|
4
|
+
} from '@terrafirmagreg-team/dump/lib/dump/manifest/dto';
|
|
5
|
+
import type { DumpIndex, DumpMod, GuideBookRef } from '@terrafirmagreg-team/dump/lib/dump/mods/dto';
|
|
6
|
+
|
|
7
|
+
function registryRel(
|
|
8
|
+
meta: NamespaceDisplay,
|
|
9
|
+
file: string,
|
|
10
|
+
): string | null {
|
|
11
|
+
return meta.registry.includes(file) ? `registry/${file}.json` : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function loadBooks(
|
|
15
|
+
namespace: string,
|
|
16
|
+
guides: string[],
|
|
17
|
+
): GuideBookRef[] {
|
|
18
|
+
const books: GuideBookRef[] = [];
|
|
19
|
+
for (const name of guides) {
|
|
20
|
+
if (!name || name.includes('..') || name.includes('/')) continue;
|
|
21
|
+
books.push({
|
|
22
|
+
modId: namespace,
|
|
23
|
+
bookPath: name,
|
|
24
|
+
bookId: `${namespace}:${name}`,
|
|
25
|
+
dir: `data/${namespace}/guides/${name}`,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return books;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function loadMod(
|
|
32
|
+
namespace: string,
|
|
33
|
+
meta: NamespaceDisplay,
|
|
34
|
+
): DumpMod {
|
|
35
|
+
return {
|
|
36
|
+
namespace,
|
|
37
|
+
keybindsFile: meta.keybinds ? 'keybinds.json' : null,
|
|
38
|
+
items: registryRel(meta, 'items'),
|
|
39
|
+
blocks: registryRel(meta, 'blocks'),
|
|
40
|
+
fluids: registryRel(meta, 'fluids'),
|
|
41
|
+
entities: registryRel(meta, 'entities'),
|
|
42
|
+
tabs: registryRel(meta, 'tabs'),
|
|
43
|
+
groups: registryRel(meta, 'groups'),
|
|
44
|
+
recipeCategories: registryRel(meta, 'recipe_categories'),
|
|
45
|
+
tags: registryRel(meta, 'tags'),
|
|
46
|
+
recipeIndex: registryRel(meta, 'recipes'),
|
|
47
|
+
questData: meta.quests ? `data/${namespace}/quests/data.json` : null,
|
|
48
|
+
books: loadBooks(namespace, meta.guides),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function dumpIndexFromManifest(
|
|
53
|
+
manifest: DumpManifest,
|
|
54
|
+
root: string,
|
|
55
|
+
): DumpIndex {
|
|
56
|
+
const mods = Object.keys(manifest.namespaces)
|
|
57
|
+
.sort((a, b) => a.localeCompare(b))
|
|
58
|
+
.flatMap((namespace) => {
|
|
59
|
+
const row = manifest.namespaces[namespace];
|
|
60
|
+
return row ? [loadMod(namespace, row)] : [];
|
|
61
|
+
});
|
|
62
|
+
return { root, manifest, mods };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const REGISTRY_FIELD = {
|
|
66
|
+
items: 'items',
|
|
67
|
+
blocks: 'blocks',
|
|
68
|
+
fluids: 'fluids',
|
|
69
|
+
entities: 'entities',
|
|
70
|
+
tabs: 'tabs',
|
|
71
|
+
groups: 'groups',
|
|
72
|
+
recipe_categories: 'recipeCategories',
|
|
73
|
+
tags: 'tags',
|
|
74
|
+
recipes: 'recipeIndex',
|
|
75
|
+
} as const satisfies Record<string, keyof DumpMod>;
|
|
76
|
+
|
|
77
|
+
export function dumpRegistryRel(
|
|
78
|
+
dump: DumpIndex | null | undefined,
|
|
79
|
+
file: string,
|
|
80
|
+
): string | undefined {
|
|
81
|
+
const field = REGISTRY_FIELD[file as keyof typeof REGISTRY_FIELD];
|
|
82
|
+
if (!field) return undefined;
|
|
83
|
+
return dump?.mods.some((mod) => mod[field] != null)
|
|
84
|
+
? `registry/${file}.json`
|
|
85
|
+
: undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function dumpModRegistryRel(
|
|
89
|
+
dump: DumpIndex | null | undefined,
|
|
90
|
+
namespace: string,
|
|
91
|
+
file: string,
|
|
92
|
+
): string | undefined {
|
|
93
|
+
const field = REGISTRY_FIELD[file as keyof typeof REGISTRY_FIELD];
|
|
94
|
+
const mod = dump?.mods.find((m) => m.namespace === namespace);
|
|
95
|
+
return field && mod?.[field] ? `registry/${file}.json` : undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { asRecord, asString } from '@terrafirmagreg-team/dump/lib/dump/contract/read';
|
|
2
|
+
import { dumpLocation } from '@terrafirmagreg-team/dump/lib/dump/paths';
|
|
3
|
+
import { createDatasetCache, dumpReader } from '@terrafirmagreg-team/dump/lib/dump/reader';
|
|
4
|
+
import { parseDumpManifest } from '@terrafirmagreg-team/dump/lib/dump/manifest/parse';
|
|
5
|
+
import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
|
|
6
|
+
import { dumpIndexFromManifest } from '@terrafirmagreg-team/dump/lib/dump/mods/index';
|
|
7
|
+
import type { DumpIndex } from '@terrafirmagreg-team/dump/lib/dump/mods/dto';
|
|
8
|
+
|
|
9
|
+
const dumpCache = createDatasetCache(
|
|
10
|
+
async (dataset: DatasetRef): Promise<DumpIndex | null> => {
|
|
11
|
+
const reader = await dumpReader(dataset);
|
|
12
|
+
const manifest = parseDumpManifest(await reader.getJson('manifest.json'));
|
|
13
|
+
if (!manifest) return null;
|
|
14
|
+
return dumpIndexFromManifest(
|
|
15
|
+
manifest,
|
|
16
|
+
dumpLocation(dataset)?.root ?? dataset.id,
|
|
17
|
+
);
|
|
18
|
+
},
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
export function loadDatasetDump(dataset: DatasetRef): Promise<DumpIndex | null> {
|
|
22
|
+
return dumpCache(dataset);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function discoverDumpDatasets(
|
|
26
|
+
datasetIds: readonly string[],
|
|
27
|
+
): Promise<DatasetRef[]> {
|
|
28
|
+
const refs = await Promise.all(
|
|
29
|
+
datasetIds.map(async (id) => {
|
|
30
|
+
const versions = asRecord(
|
|
31
|
+
await dumpReader({ id }).then((reader) => reader.getJson('versions.json')),
|
|
32
|
+
);
|
|
33
|
+
const latest = asString(versions?.latest);
|
|
34
|
+
if (!versions || !latest) return [];
|
|
35
|
+
const manifest = await dumpReader({ id }).then((reader) =>
|
|
36
|
+
reader.getJson('manifest.json'),
|
|
37
|
+
);
|
|
38
|
+
if (!manifest) return [];
|
|
39
|
+
const listed = Array.isArray(versions.versions)
|
|
40
|
+
? versions.versions.filter(
|
|
41
|
+
(row): row is string => typeof row === 'string',
|
|
42
|
+
)
|
|
43
|
+
: [];
|
|
44
|
+
const published = await Promise.all(
|
|
45
|
+
listed.map(async (version): Promise<DatasetRef | null> => {
|
|
46
|
+
const ref = { id, version };
|
|
47
|
+
const ok = await dumpReader(ref).then((reader) =>
|
|
48
|
+
reader.getJson('manifest.json'),
|
|
49
|
+
);
|
|
50
|
+
return ok ? ref : null;
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
53
|
+
return [{ id }, ...published.filter((row): row is DatasetRef => row !== null)];
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
return refs.flat().sort((a, b) =>
|
|
57
|
+
a.id.localeCompare(b.id) || (a.version ?? '').localeCompare(b.version ?? ''),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|