@terrafirmagreg-team/dump 0.0.0-stage → 1.2.1

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.
Files changed (57) hide show
  1. package/package.json +28 -3
  2. package/src/lib/dump/assets/atlas/dto.ts +51 -0
  3. package/src/lib/dump/assets/atlas/lookup.ts +207 -0
  4. package/src/lib/dump/assets/atlas/sprites.ts +132 -0
  5. package/src/lib/dump/assets/lang/load.ts +19 -0
  6. package/src/lib/dump/assets/lang/names.ts +244 -0
  7. package/src/lib/dump/assets/urls.ts +145 -0
  8. package/src/lib/dump/client/atlases.ts +85 -0
  9. package/src/lib/dump/client/dump.ts +132 -0
  10. package/src/lib/dump/client/groups.ts +27 -0
  11. package/src/lib/dump/client/keybinds.ts +16 -0
  12. package/src/lib/dump/client/mods.ts +18 -0
  13. package/src/lib/dump/client/registry.ts +121 -0
  14. package/src/lib/dump/client/tabs.ts +23 -0
  15. package/src/lib/dump/client/tags.ts +59 -0
  16. package/src/lib/dump/contract/kind.ts +62 -0
  17. package/src/lib/dump/contract/read.ts +76 -0
  18. package/src/lib/dump/contract/ref.ts +260 -0
  19. package/src/lib/dump/contract/text.ts +101 -0
  20. package/src/lib/dump/data/registry/dto.ts +57 -0
  21. package/src/lib/dump/data/registry/parse.ts +291 -0
  22. package/src/lib/dump/data/tab/dto.ts +14 -0
  23. package/src/lib/dump/data/tab/parse.ts +27 -0
  24. package/src/lib/dump/data/tag/dto.ts +8 -0
  25. package/src/lib/dump/data/tag/parse.ts +58 -0
  26. package/src/lib/dump/gen/data/guides/book.ts +57 -0
  27. package/src/lib/dump/gen/data/guides/category.ts +31 -0
  28. package/src/lib/dump/gen/data/guides/entry.ts +49 -0
  29. package/src/lib/dump/gen/data/models/model.ts +4 -0
  30. package/src/lib/dump/gen/data/quests/chapter.ts +127 -0
  31. package/src/lib/dump/gen/data/quests/chapter_groups.ts +12 -0
  32. package/src/lib/dump/gen/data/quests/data.ts +41 -0
  33. package/src/lib/dump/gen/data/recipes/recipe.ts +79 -0
  34. package/src/lib/dump/gen/index.ts +21 -0
  35. package/src/lib/dump/gen/keybinds.ts +4 -0
  36. package/src/lib/dump/gen/lang.ts +4 -0
  37. package/src/lib/dump/gen/manifest.ts +60 -0
  38. package/src/lib/dump/gen/registry/blocks.ts +61 -0
  39. package/src/lib/dump/gen/registry/entities.ts +50 -0
  40. package/src/lib/dump/gen/registry/fluids.ts +49 -0
  41. package/src/lib/dump/gen/registry/groups.ts +15 -0
  42. package/src/lib/dump/gen/registry/items.ts +52 -0
  43. package/src/lib/dump/gen/registry/recipe_categories.ts +65 -0
  44. package/src/lib/dump/gen/registry/recipes.ts +4 -0
  45. package/src/lib/dump/gen/registry/tabs.ts +47 -0
  46. package/src/lib/dump/gen/registry/tags.ts +12 -0
  47. package/src/lib/dump/json-reader.ts +254 -0
  48. package/src/lib/dump/manifest/dto.ts +12 -0
  49. package/src/lib/dump/manifest/parse.ts +168 -0
  50. package/src/lib/dump/mods/dto.ts +31 -0
  51. package/src/lib/dump/mods/index.ts +97 -0
  52. package/src/lib/dump/mods/load.ts +60 -0
  53. package/src/lib/dump/paths.ts +172 -0
  54. package/src/lib/dump/reader.ts +31 -0
  55. package/src/lib/dump-search.ts +125 -0
  56. package/src/lib/snbt.ts +247 -0
  57. package/README.md +0 -3
@@ -0,0 +1,145 @@
1
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
2
+ import type {
3
+ AtlasPage,
4
+ IconAtlas,
5
+ } from '@terrafirmagreg-team/dump/lib/dump/assets/atlas/dto';
6
+ import {
7
+ cleanDumpRel,
8
+ dumpLocation,
9
+ dumpRelPublicUrl,
10
+ dumpStorageUrl,
11
+ splitDumpId,
12
+ type DumpLocation,
13
+ } from '@terrafirmagreg-team/dump/lib/dump/paths';
14
+
15
+ export { dumpRelPublicUrl } from '@terrafirmagreg-team/dump/lib/dump/paths';
16
+
17
+ export function texturePublicUrl(
18
+ dataset: DatasetRef,
19
+ textureId: string,
20
+ ): string | undefined {
21
+ const parts = splitDumpId(textureId.trim());
22
+ if (!parts) return undefined;
23
+ const path = parts.path.startsWith('textures/')
24
+ ? parts.path
25
+ : `textures/${parts.path}`;
26
+ const rel = `${path.replace(/\.(png|webp)$/i, '')}.webp`;
27
+ return dumpRelPublicUrl(dataset, `assets/${parts.ns}/${rel}`);
28
+ }
29
+
30
+ export function modelPublicUrl(
31
+ dataset: DatasetRef,
32
+ namespace: string,
33
+ rel: string | null | undefined,
34
+ ): string | undefined {
35
+ const cleaned = typeof rel === 'string' ? cleanDumpRel(rel) : null;
36
+ return cleaned
37
+ ? dumpRelPublicUrl(dataset, `assets/${namespace}/${cleaned}`)
38
+ : undefined;
39
+ }
40
+
41
+ export function recipeImageUrl(
42
+ dataset: DatasetRef,
43
+ recipe: { modId: string; image?: string },
44
+ ): string | undefined {
45
+ const rel = recipe.image ? cleanDumpRel(recipe.image) : undefined;
46
+ if (!rel) return undefined;
47
+ if (rel.startsWith('assets/')) return dumpRelPublicUrl(dataset, rel);
48
+ if (!recipe.modId) return undefined;
49
+ return dumpRelPublicUrl(dataset, `assets/${recipe.modId}/${rel}`);
50
+ }
51
+
52
+ export function atlasPagePublicUrl(
53
+ dataset: DatasetRef,
54
+ atlas: IconAtlas,
55
+ page: AtlasPage,
56
+ ): string | undefined {
57
+ const src = cleanDumpRel(page.image);
58
+ if (!src || !atlas.modId) return undefined;
59
+ return dumpRelPublicUrl(dataset, `assets/${atlas.modId}/${src}`);
60
+ }
61
+
62
+ function pathDir(path: string): string {
63
+ const slash = path.lastIndexOf('/');
64
+ return slash >= 0 ? path.slice(0, slash + 1) : '';
65
+ }
66
+
67
+ function relativePath(fromDir: string, toPath: string): string {
68
+ const a = fromDir.split('/').filter(Boolean);
69
+ const b = toPath.split('/').filter(Boolean);
70
+ let i = 0;
71
+ while (i < a.length && i < b.length && a[i] === b[i]) i += 1;
72
+ return '../'.repeat(a.length - i) + b.slice(i).join('/');
73
+ }
74
+
75
+ function resolveAssetRel(baseDir: string, relUri: string): string | null {
76
+ const out: string[] = [];
77
+ for (const seg of `${baseDir}${relUri}`.split('/')) {
78
+ if (seg === '..') {
79
+ if (!out.pop()) return null;
80
+ } else if (seg !== '.' && seg !== '') {
81
+ out.push(seg);
82
+ }
83
+ }
84
+ return out.length ? out.join('/') : null;
85
+ }
86
+
87
+ const reverseFileIndexes = new WeakMap<
88
+ Record<string, string>,
89
+ Map<string, string>
90
+ >();
91
+ function reverseFileIndex(files: Record<string, string>): Map<string, string> {
92
+ let map = reverseFileIndexes.get(files);
93
+ if (!map) {
94
+ map = new Map(Object.entries(files).map(([rel, key]) => [key, rel]));
95
+ reverseFileIndexes.set(files, map);
96
+ }
97
+ return map;
98
+ }
99
+
100
+ function dumpRelForStorageRel(
101
+ loc: DumpLocation,
102
+ storageRel: string,
103
+ ): string | undefined {
104
+ if (loc.files) return reverseFileIndex(loc.files).get(storageRel);
105
+ return storageRel.startsWith(`${loc.root}/`)
106
+ ? storageRel.slice(loc.root.length + 1)
107
+ : undefined;
108
+ }
109
+
110
+ export function createDumpAssetResolver(
111
+ dataset: DatasetRef,
112
+ ): (resourceUrl: string, modelUrl: string) => string | undefined {
113
+
114
+ let prefix: string | null = null;
115
+ return (resourceUrl, modelUrl) => {
116
+ const loc = dumpLocation(dataset);
117
+ if (!loc) return undefined;
118
+ const base =
119
+ typeof window === 'undefined' ? 'http://x.invalid' : location.origin;
120
+ prefix ??= new URL(dumpStorageUrl(''), base).href;
121
+ let resourcePath: string;
122
+ let modelAbs: URL;
123
+ try {
124
+ modelAbs = new URL(modelUrl, base);
125
+ const resource = new URL(resourceUrl, modelAbs);
126
+ if (resource.origin !== modelAbs.origin) return undefined;
127
+ resourcePath = resource.pathname;
128
+ } catch {
129
+ return undefined;
130
+ }
131
+ const modelHref = modelAbs.href;
132
+ if (!modelHref.startsWith(prefix)) return undefined;
133
+ const modelRel = dumpRelForStorageRel(
134
+ loc,
135
+ modelHref.slice(prefix.length),
136
+ );
137
+ if (!modelRel) return undefined;
138
+ const rel = relativePath(pathDir(modelAbs.pathname), resourcePath);
139
+ if (!rel || !rel.includes('/')) return undefined;
140
+ const dumpRel = resolveAssetRel(pathDir(modelRel), rel);
141
+ if (!dumpRel) return undefined;
142
+ return dumpRelPublicUrl(dataset, dumpRel);
143
+ };
144
+ }
145
+
@@ -0,0 +1,85 @@
1
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
2
+ import type { AtlasBundle, IconAtlas } from '@terrafirmagreg-team/dump/lib/dump/assets/atlas/dto';
3
+ import {
4
+ iconAtlasFromEntries,
5
+ mergeAtlases,
6
+ } from '@terrafirmagreg-team/dump/lib/dump/assets/atlas/sprites';
7
+ import {
8
+ clientDumpJson,
9
+ loadClientManifest,
10
+ } from '@terrafirmagreg-team/dump/lib/dump/client/dump';
11
+ import { asRecord } from '@terrafirmagreg-team/dump/lib/dump/contract/read';
12
+ import { createDatasetCache } from '@terrafirmagreg-team/dump/lib/dump/reader';
13
+
14
+ async function modEntries(
15
+ dataset: DatasetRef,
16
+ modId: string,
17
+ kind: string,
18
+ ): Promise<Record<string, unknown> | undefined> {
19
+ const entries = asRecord(
20
+ await clientDumpJson(dataset, `registry/${kind}.json`, 'high'),
21
+ );
22
+ if (!entries) return undefined;
23
+ const prefix = `${modId}:`;
24
+ const out: Record<string, unknown> = {};
25
+ for (const [key, value] of Object.entries(entries)) {
26
+ if (key.startsWith(prefix)) out[key] = value;
27
+ }
28
+ return out;
29
+ }
30
+
31
+ const loadClientModAtlas = createDatasetCache(
32
+ async (
33
+ dataset: DatasetRef,
34
+ modId: string,
35
+ kind: 'items' | 'blocks',
36
+ ): Promise<IconAtlas | null> => {
37
+ const manifest = await loadClientManifest(dataset);
38
+ const listed = manifest?.namespaces[modId];
39
+ if (!listed) return null;
40
+ const has = (file: string) => listed.registry.includes(file);
41
+ if (kind === 'blocks') {
42
+ if (!has('blocks')) return null;
43
+ return iconAtlasFromEntries(
44
+ await modEntries(dataset, modId, 'blocks'),
45
+ dataset,
46
+ modId,
47
+ 'blocks',
48
+ 'block',
49
+ );
50
+ }
51
+ return mergeAtlases(
52
+ [
53
+ has('items')
54
+ ? iconAtlasFromEntries(
55
+ await modEntries(dataset, modId, 'items'),
56
+ dataset,
57
+ modId,
58
+ 'items',
59
+ 'item',
60
+ )
61
+ : null,
62
+ has('fluids')
63
+ ? iconAtlasFromEntries(
64
+ await modEntries(dataset, modId, 'fluids'),
65
+ dataset,
66
+ modId,
67
+ 'fluids',
68
+ 'item',
69
+ )
70
+ : null,
71
+ ],
72
+ dataset,
73
+ modId,
74
+ 'item',
75
+ );
76
+ },
77
+ );
78
+
79
+ export const loadClientModAtlases = createDatasetCache(
80
+ async (dataset: DatasetRef, modId: string): Promise<AtlasBundle> => ({
81
+ items: await loadClientModAtlas(dataset, modId, 'items'),
82
+ blocks: await loadClientModAtlas(dataset, modId, 'blocks'),
83
+ }),
84
+ );
85
+
@@ -0,0 +1,132 @@
1
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
2
+ import { dumpReader } from '@terrafirmagreg-team/dump/lib/dump/reader';
3
+ import { DUMP_POINTER_TTL } from '@terrafirmagreg-team/dump/lib/dump/json-reader';
4
+ import { dumpRelPublicUrl, splitDumpId } from '@terrafirmagreg-team/dump/lib/dump/paths';
5
+ import { stackIdBase } from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
6
+ import { asRecord } from '@terrafirmagreg-team/dump/lib/dump/contract/read';
7
+ import {
8
+ modelPublicUrl,
9
+ texturePublicUrl,
10
+ } from '@terrafirmagreg-team/dump/lib/dump/assets/urls';
11
+ import { createAsyncCache } from '@terrafirmagreg-team/data/lib/async-cache';
12
+ import { loadDatasetDump } from '@terrafirmagreg-team/dump/lib/dump/mods/load';
13
+ import {
14
+ dumpModRegistryRel,
15
+ dumpRegistryRel,
16
+ } from '@terrafirmagreg-team/dump/lib/dump/mods/index';
17
+ import type { DumpManifest } from '@terrafirmagreg-team/dump/lib/dump/manifest/dto';
18
+
19
+ export function loadClientManifest(
20
+ dataset: DatasetRef,
21
+ ): Promise<DumpManifest | null> {
22
+ return loadDatasetDump(dataset).then((dump) => dump?.manifest ?? null);
23
+ }
24
+
25
+ export type DatasetVersions = { latest?: string; versions: string[] };
26
+
27
+ export const loadClientVersions = createAsyncCache(
28
+ async (datasetId: string): Promise<DatasetVersions | null> => {
29
+ const reader = await dumpReader({ id: datasetId });
30
+ const raw = await reader.getJson('versions.json');
31
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
32
+ const row = raw as Record<string, unknown>;
33
+ const versions = Array.isArray(row.versions)
34
+ ? row.versions.filter((v): v is string => typeof v === 'string')
35
+ : [];
36
+ if (!versions.length) return null;
37
+ return {
38
+ latest: typeof row.latest === 'string' ? row.latest : undefined,
39
+ versions,
40
+ };
41
+ },
42
+ undefined,
43
+ DUMP_POINTER_TTL,
44
+ );
45
+
46
+ export function clientDumpJson(
47
+ dataset: DatasetRef,
48
+ rel: string | null | undefined,
49
+ priority?: 'high',
50
+ ): Promise<unknown | undefined> {
51
+ if (!rel) return Promise.resolve(undefined);
52
+ return dumpReader(dataset).then((reader) => reader.getJson(rel, priority));
53
+ }
54
+
55
+ export async function clientRegistryEntries(
56
+ dataset: DatasetRef,
57
+ file: string,
58
+ priority?: 'high',
59
+ ): Promise<Record<string, unknown> | undefined> {
60
+ const rel = dumpRegistryRel(await loadDatasetDump(dataset), file);
61
+ return rel
62
+ ? asRecord(await clientDumpJson(dataset, rel, priority))
63
+ : undefined;
64
+ }
65
+
66
+ export function clientDumpLang(
67
+ dataset: DatasetRef,
68
+ lang: string,
69
+ ): Promise<Record<string, string>> {
70
+ return dumpReader(dataset).then((reader) => reader.getLang(lang));
71
+ }
72
+
73
+ export function clientDumpLangRaw(
74
+ dataset: DatasetRef,
75
+ lang: string,
76
+ ): Promise<Record<string, string>> {
77
+ return dumpReader(dataset).then((reader) => reader.getLangRaw(lang));
78
+ }
79
+
80
+ export async function clientRegistryDataFileRel(
81
+ dataset: DatasetRef,
82
+ ns: string,
83
+ file: string,
84
+ ): Promise<string | undefined> {
85
+ return dumpModRegistryRel(await loadDatasetDump(dataset), ns, file);
86
+ }
87
+
88
+ function stackNs(id: string): string | undefined {
89
+ const ns = stackIdBase(id).split(':', 1)[0];
90
+ return ns || undefined;
91
+ }
92
+
93
+ export function clientTagFileRel(
94
+ dataset: DatasetRef,
95
+ tagId: string,
96
+ ): Promise<string | undefined> {
97
+ const ns = stackNs(tagId.startsWith('#') ? tagId.slice(1) : tagId);
98
+ return ns
99
+ ? clientRegistryDataFileRel(dataset, ns, 'tags')
100
+ : Promise.resolve(undefined);
101
+ }
102
+
103
+ export function dumpTextureUrl(
104
+ dataset: DatasetRef,
105
+ textureId: string,
106
+ ): string | undefined {
107
+ return texturePublicUrl(dataset, textureId);
108
+ }
109
+
110
+ export async function dumpModelUrl(
111
+ dataset: DatasetRef,
112
+ modelId: string,
113
+ ): Promise<string | undefined> {
114
+ const parts = splitDumpId(modelId.trim());
115
+ if (!parts) return undefined;
116
+ const index = await clientDumpJson(
117
+ dataset,
118
+ `data/${parts.ns}/models/multiblock.json`,
119
+ );
120
+ const rel =
121
+ index && typeof index === 'object' && !Array.isArray(index)
122
+ ? (index as Record<string, unknown>)[modelId]
123
+ : undefined;
124
+ return modelPublicUrl(
125
+ dataset,
126
+ parts.ns,
127
+ typeof rel === 'string' ? rel : undefined,
128
+ );
129
+ }
130
+
131
+ export { dumpRelPublicUrl };
132
+
@@ -0,0 +1,27 @@
1
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
2
+ import { asRecord, asStringList } from '@terrafirmagreg-team/dump/lib/dump/contract/read';
3
+ import { asDumpText, type DumpText } from '@terrafirmagreg-team/dump/lib/dump/contract/text';
4
+ import { clientRegistryEntries } from '@terrafirmagreg-team/dump/lib/dump/client/dump';
5
+ import { createDatasetCache } from '@terrafirmagreg-team/dump/lib/dump/reader';
6
+
7
+ type RegistryGroupRow = {
8
+ id: string;
9
+ name?: DumpText;
10
+ members: string[];
11
+ };
12
+
13
+ export const loadClientRegistryGroups = createDatasetCache(
14
+ async (dataset: DatasetRef): Promise<RegistryGroupRow[]> => {
15
+ const out: RegistryGroupRow[] = [];
16
+ const entries = await clientRegistryEntries(dataset, 'groups');
17
+ for (const [id, raw] of Object.entries(entries ?? {})) {
18
+ const row = asRecord(raw);
19
+ const members = asStringList(row?.members);
20
+ if (!id || members.length <= 1) continue;
21
+ const name = asDumpText(row?.name);
22
+ out.push({ id, ...(name ? { name } : {}), members });
23
+ }
24
+ return out;
25
+ },
26
+ );
27
+
@@ -0,0 +1,16 @@
1
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
2
+ import { asStringMap } from '@terrafirmagreg-team/dump/lib/dump/contract/read';
3
+ import { clientDumpJson } from '@terrafirmagreg-team/dump/lib/dump/client/dump';
4
+ import { loadDatasetDump } from '@terrafirmagreg-team/dump/lib/dump/mods/load';
5
+ import { createDatasetCache } from '@terrafirmagreg-team/dump/lib/dump/reader';
6
+
7
+ export const loadClientKeybinds = createDatasetCache(
8
+ async (dataset: DatasetRef): Promise<Record<string, string>> => {
9
+ const dump = await loadDatasetDump(dataset);
10
+ const file = dump?.mods.find((mod) => mod.keybindsFile)?.keybindsFile;
11
+ return file
12
+ ? (asStringMap(await clientDumpJson(dataset, file, 'high')) ?? {})
13
+ : {};
14
+ },
15
+ );
16
+
@@ -0,0 +1,18 @@
1
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
2
+ import { langString } from '@terrafirmagreg-team/dump/lib/dump/assets/lang/names';
3
+ import { clientDumpLang, loadClientManifest } from '@terrafirmagreg-team/dump/lib/dump/client/dump';
4
+ import { createDatasetCache } from '@terrafirmagreg-team/dump/lib/dump/reader';
5
+
6
+ export const loadClientModNames = createDatasetCache(
7
+ async (dataset: DatasetRef, lang: string): Promise<Record<string, string>> => {
8
+ const manifest = await loadClientManifest(dataset);
9
+ const map = await clientDumpLang(dataset, lang);
10
+ const out: Record<string, string> = {};
11
+ for (const [ns, entry] of Object.entries(manifest?.namespaces ?? {})) {
12
+ const key = entry?.display?.name;
13
+ out[ns] = langString(map, key) ?? key ?? ns;
14
+ }
15
+ return out;
16
+ },
17
+ );
18
+
@@ -0,0 +1,121 @@
1
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
2
+ import { asIdTable, EDGE_TABLE_FIELD } from '@terrafirmagreg-team/dump/lib/dump/contract/read';
3
+ import { asEntry, displayOf } from '@terrafirmagreg-team/dump/lib/dump/data/registry/parse';
4
+ import { parseStackId } from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
5
+ import type { DumpDisplay, DumpText } from '@terrafirmagreg-team/dump/lib/dump/contract/text';
6
+ import type {
7
+ DumpBlockEntry,
8
+ DumpEntityEntry,
9
+ DumpFluidEntry,
10
+ DumpItemEntry,
11
+ DumpRegistryEntry,
12
+ DumpRegistryKind,
13
+ } from '@terrafirmagreg-team/dump/lib/dump/data/registry/dto';
14
+ import { clientRegistryEntries } from '@terrafirmagreg-team/dump/lib/dump/client/dump';
15
+ import { createDatasetCache } from '@terrafirmagreg-team/dump/lib/dump/reader';
16
+
17
+ type RegistryByKind = {
18
+ items: DumpItemEntry;
19
+ blocks: DumpBlockEntry;
20
+ fluids: DumpFluidEntry;
21
+ entities: DumpEntityEntry;
22
+ };
23
+
24
+ const KIND_FILE: Record<DumpRegistryKind, string> = {
25
+ items: 'items',
26
+ blocks: 'blocks',
27
+ fluids: 'fluids',
28
+ entities: 'entities',
29
+ };
30
+
31
+ const clientRegistry = createDatasetCache(
32
+ async (
33
+ dataset: DatasetRef,
34
+ kind: DumpRegistryKind,
35
+ ): Promise<Map<string, DumpRegistryEntry>> => {
36
+ const out = new Map<string, DumpRegistryEntry>();
37
+ const file = KIND_FILE[kind];
38
+ const entries = await clientRegistryEntries(dataset, file);
39
+ const ids = asIdTable(entries);
40
+ for (const [id, raw] of Object.entries(entries ?? {})) {
41
+ if (id === EDGE_TABLE_FIELD) continue;
42
+ const entry = asEntry(raw, kind, id, ids);
43
+ if (!entry) continue;
44
+ if (!out.has(id)) out.set(id, entry);
45
+ if (entry.id !== id && !out.has(entry.id)) {
46
+ out.set(entry.id, entry);
47
+ }
48
+ }
49
+ return out;
50
+ },
51
+ );
52
+
53
+ export function loadClientRegistry<K extends DumpRegistryKind>(
54
+ dataset: DatasetRef,
55
+ kind: K,
56
+ ): Promise<Map<string, RegistryByKind[K]>> {
57
+ return clientRegistry(dataset, kind) as Promise<Map<string, RegistryByKind[K]>>;
58
+ }
59
+
60
+ export function resolveClientStackDisplay(
61
+ compactId: string,
62
+ items: Map<string, DumpItemEntry>,
63
+ fluids: Map<string, DumpFluidEntry>,
64
+ ): DumpDisplay | undefined {
65
+ const parsed = parseStackId(compactId);
66
+ const item = items.get(parsed.id);
67
+ if (item) return displayOf(item, parsed.nbt);
68
+ const fluid = fluids.get(parsed.id);
69
+ if (fluid) return displayOf(fluid, parsed.nbt);
70
+ return undefined;
71
+ }
72
+
73
+ const SINGULAR_KIND: Record<string, DumpRegistryKind> = {
74
+ item: 'items',
75
+ block: 'blocks',
76
+ fluid: 'fluids',
77
+ entity: 'entities',
78
+ };
79
+
80
+ export function clientRegistryKindFor(kind: string): DumpRegistryKind {
81
+ return SINGULAR_KIND[kind] ?? (kind as DumpRegistryKind);
82
+ }
83
+
84
+ export async function loadClientRegistryOrder(
85
+ dataset: DatasetRef,
86
+ kind: string,
87
+ ): Promise<string[]> {
88
+ const entries = await loadClientRegistry(dataset, clientRegistryKindFor(kind));
89
+ return [...entries.values()]
90
+ .flatMap((entry) => (entry.order != null ? [entry] : []))
91
+ .sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.id.localeCompare(b.id))
92
+ .map((entry) => entry.id);
93
+ }
94
+
95
+ export async function loadClientRegistryTooltips(
96
+ dataset: DatasetRef,
97
+ kind: string,
98
+ ): Promise<Record<string, DumpText[]> | undefined> {
99
+ const entries = await loadClientRegistry(dataset, clientRegistryKindFor(kind));
100
+ const out: Record<string, DumpText[]> = {};
101
+ for (const entry of entries.values()) {
102
+ const tooltip = entry.display?.tooltip;
103
+ if (tooltip?.length) out[entry.id] = tooltip;
104
+ }
105
+ return Object.keys(out).length ? out : undefined;
106
+ }
107
+
108
+ export async function loadClientRegistryTooltipsMod(
109
+ dataset: DatasetRef,
110
+ kind: string,
111
+ mod: string,
112
+ ): Promise<Record<string, DumpText[]> | undefined> {
113
+ const entries = await loadClientRegistry(dataset, clientRegistryKindFor(kind));
114
+ const out: Record<string, DumpText[]> = {};
115
+ for (const entry of entries.values()) {
116
+ const lines = entry.display?.tooltipMods?.[mod];
117
+ if (lines?.length) out[entry.id] = lines;
118
+ }
119
+ return Object.keys(out).length ? out : undefined;
120
+ }
121
+
@@ -0,0 +1,23 @@
1
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
2
+ import { asTab, asTabItems } from '@terrafirmagreg-team/dump/lib/dump/data/tab/parse';
3
+ import type { DumpCreativeTab, DumpCreativeTabs } from '@terrafirmagreg-team/dump/lib/dump/data/tab/dto';
4
+ import { clientRegistryEntries } from '@terrafirmagreg-team/dump/lib/dump/client/dump';
5
+ import { createDatasetCache } from '@terrafirmagreg-team/dump/lib/dump/reader';
6
+
7
+ export const loadClientTabs = createDatasetCache(
8
+ async (dataset: DatasetRef): Promise<DumpCreativeTabs> => {
9
+ const tabs: DumpCreativeTab[] = [];
10
+ const members: Record<string, string[]> = {};
11
+ const entries = await clientRegistryEntries(dataset, 'tabs');
12
+ for (const raw of Object.values(entries ?? {})) {
13
+ const tab = asTab(raw);
14
+ if (!tab) continue;
15
+ tabs.push(tab);
16
+ const items = asTabItems(raw);
17
+ if (items.length) members[tab.id] = items;
18
+ }
19
+ tabs.sort((a, b) => a.order - b.order || a.id.localeCompare(b.id));
20
+ return { tabs, members };
21
+ },
22
+ );
23
+
@@ -0,0 +1,59 @@
1
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
2
+ import { asIdTable, EDGE_TABLE_FIELD } from '@terrafirmagreg-team/dump/lib/dump/contract/read';
3
+ import { asTag } from '@terrafirmagreg-team/dump/lib/dump/data/tag/parse';
4
+ import type { DumpTag } from '@terrafirmagreg-team/dump/lib/dump/data/tag/dto';
5
+ import { clientRegistryEntries } from '@terrafirmagreg-team/dump/lib/dump/client/dump';
6
+ import { tagHashId, tagIdForms } from '@terrafirmagreg-team/dump/lib/dump/paths';
7
+ import { createDatasetCache } from '@terrafirmagreg-team/dump/lib/dump/reader';
8
+
9
+ function indexKeys(tag: DumpTag, manifestKey: string): string[] {
10
+ return [...new Set([manifestKey, ...tagIdForms(tag.id)])];
11
+ }
12
+
13
+ const loadClientTags = createDatasetCache(async (dataset: DatasetRef) => {
14
+ const map = new Map<string, DumpTag>();
15
+ const entries = await clientRegistryEntries(dataset, 'tags');
16
+ const ids = asIdTable(entries);
17
+ for (const [manifestKey, raw] of Object.entries(entries ?? {})) {
18
+ if (manifestKey === EDGE_TABLE_FIELD) continue;
19
+ const tag = asTag(raw, manifestKey, ids);
20
+ if (!tag) continue;
21
+ for (const alias of indexKeys(tag, manifestKey)) {
22
+ if (!map.has(alias)) map.set(alias, tag);
23
+ }
24
+ }
25
+ return map;
26
+ });
27
+
28
+ export async function resolveClientTag(
29
+ dataset: DatasetRef,
30
+ id: string,
31
+ ): Promise<DumpTag | undefined> {
32
+ const tags = await loadClientTags(dataset);
33
+ for (const form of tagIdForms(id)) {
34
+ const hit = tags.get(form);
35
+ if (hit) return hit;
36
+ }
37
+ return undefined;
38
+ }
39
+
40
+ const MEMBER_REGISTRY: Record<string, string> = {
41
+ item: 'items',
42
+ block: 'blocks',
43
+ fluid: 'fluids',
44
+ entity: 'entities',
45
+ };
46
+
47
+ export async function loadClientTagMembers(
48
+ dataset: DatasetRef,
49
+ kind: string,
50
+ ): Promise<Record<string, string[]> | undefined> {
51
+ const registry = MEMBER_REGISTRY[kind] ?? kind;
52
+ const out: Record<string, string[]> = {};
53
+ for (const tag of (await loadClientTags(dataset)).values()) {
54
+ if (tag.registry !== registry || !tag.members.length) continue;
55
+ out[tagHashId(tag.id)] ??= tag.members;
56
+ }
57
+ return Object.keys(out).length ? out : undefined;
58
+ }
59
+
@@ -0,0 +1,62 @@
1
+ import {
2
+ asTaggedReference,
3
+ stackIdFromRef,
4
+ type ReferenceKind,
5
+ type TaggedReference,
6
+ } from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
7
+ import { asRecord, asString } from '@terrafirmagreg-team/dump/lib/dump/contract/read';
8
+
9
+ const LINK_KINDS = [
10
+ 'item',
11
+ 'block',
12
+ 'fluid',
13
+ 'entity',
14
+ 'tag',
15
+ 'texture',
16
+ 'recipe',
17
+ 'quest',
18
+ 'guide',
19
+ ] as const;
20
+
21
+ export type ResourceKind = (typeof LINK_KINDS)[number];
22
+
23
+ export function asResourceKind(value: unknown): ResourceKind | undefined {
24
+ return typeof value === 'string' &&
25
+ (LINK_KINDS as readonly string[]).includes(value)
26
+ ? (value as ResourceKind)
27
+ : undefined;
28
+ }
29
+
30
+ export function referenceKindFromResource(
31
+ kind: ResourceKind | undefined,
32
+ ): ReferenceKind | undefined {
33
+ switch (kind) {
34
+ case 'tag':
35
+ return 'itemTag';
36
+ case 'item':
37
+ case 'block':
38
+ case 'fluid':
39
+ case 'entity':
40
+ case 'texture':
41
+ case 'recipe':
42
+ case 'quest':
43
+ case 'guide':
44
+ return kind;
45
+ default:
46
+ return undefined;
47
+ }
48
+ }
49
+
50
+ export function referenceFrom(value: unknown): TaggedReference | undefined {
51
+ const tagged = asTaggedReference(value);
52
+ if (tagged) return { kind: tagged.kind, id: stackIdFromRef(tagged) };
53
+ const row = asRecord(value);
54
+ const tagId = asString(row?.tag);
55
+ if (tagId && row?.id === undefined && row?.kind === undefined) {
56
+ return { kind: 'itemTag', id: `#${tagId}` };
57
+ }
58
+ const kind = referenceKindFromResource(asResourceKind(row?.kind));
59
+ const id = asString(row?.id);
60
+ return kind && id ? { kind, id } : undefined;
61
+ }
62
+