@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.
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
package/package.json CHANGED
@@ -1,6 +1,31 @@
1
1
  {
2
2
  "name": "@terrafirmagreg-team/dump",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.2.0",
4
+ "type": "module",
5
+ "description": "Game-dump readers and contracts",
6
+ "license": "LGPL-3.0-only",
7
+ "exports": {
8
+ "./lib/*": [
9
+ "./src/lib/*.ts",
10
+ "./src/lib/*/index.ts"
11
+ ]
12
+ },
13
+ "files": [
14
+ "src"
15
+ ],
16
+ "dependencies": {
17
+ "@terrafirmagreg-team/data": "1.2.0",
18
+ "@terrafirmagreg-team/sites": "1.2.0",
19
+ "@terrafirmagreg-team/translate": "1.2.0",
20
+ "aws4fetch": "^1.0.20",
21
+ "fumadocs-core": "16.17.8"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/TerraFirmaGreg-Team/Web-Foundation.git",
26
+ "directory": "packages/dump"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ }
6
31
  }
@@ -0,0 +1,51 @@
1
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
2
+ import type { DumpNbt, TaggedReference } from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
3
+
4
+ export type AtlasPage = {
5
+ image: string;
6
+ w: number;
7
+ h: number;
8
+ };
9
+
10
+ export type AtlasTint = 'none' | 'grass' | 'foliage' | 'water' | 'custom';
11
+
12
+ export type AtlasSprite = {
13
+ ref: TaggedReference;
14
+ page: number;
15
+ x: number;
16
+ y: number;
17
+ w: number;
18
+ h: number;
19
+ tint?: AtlasTint;
20
+ tintColor?: string;
21
+ langKey?: string;
22
+ };
23
+
24
+ export type AtlasKind = 'item' | 'block';
25
+
26
+ export type IconAtlas = {
27
+ sprites: AtlasSprite[];
28
+ pages: AtlasPage[];
29
+ modId: string;
30
+ dataset: DatasetRef;
31
+ kind: AtlasKind;
32
+ };
33
+
34
+ export type AtlasBundle = {
35
+ items: IconAtlas | null;
36
+ blocks: IconAtlas | null;
37
+ };
38
+
39
+ export type SpriteQuery = {
40
+ kind?: 'item' | 'block' | 'fluid';
41
+ id: string;
42
+ tag?: DumpNbt;
43
+ };
44
+
45
+ export type AtlasLookup = {
46
+ atlas: IconAtlas;
47
+ sprite: AtlasSprite;
48
+ page: AtlasPage;
49
+ spriteId: string;
50
+ };
51
+
@@ -0,0 +1,207 @@
1
+ import type {
2
+ AtlasLookup,
3
+ AtlasSprite,
4
+ IconAtlas,
5
+ SpriteQuery,
6
+ } from '@terrafirmagreg-team/dump/lib/dump/assets/atlas/dto';
7
+ import {
8
+ asTaggedReference,
9
+ nbtSpecificity,
10
+ parseStackId,
11
+ } from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
12
+ import type { DumpNbt, TaggedReference } from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
13
+ import { formatNbt } from '@terrafirmagreg-team/dump/lib/snbt';
14
+
15
+ export { atlasPagePublicUrl } from '@terrafirmagreg-team/dump/lib/dump/assets/urls';
16
+
17
+ function asStackKind(
18
+ kind: string | undefined,
19
+ ): SpriteQuery['kind'] | undefined {
20
+ return kind === 'item' || kind === 'block' || kind === 'fluid' ? kind : undefined;
21
+ }
22
+
23
+ function asSpriteQuery(
24
+ value: string | TaggedReference | SpriteQuery | null | undefined,
25
+ kind?: SpriteQuery['kind'],
26
+ ): SpriteQuery | null {
27
+ if (value == null) return null;
28
+ if (typeof value === 'string') {
29
+ const trimmed = value.trim();
30
+ if (!trimmed) return null;
31
+ const parsed = parseStackId(trimmed);
32
+ if (!parsed.id) return null;
33
+ return {
34
+ id: parsed.id,
35
+ ...(kind ? { kind } : {}),
36
+ ...(parsed.nbt !== undefined ? { tag: parsed.nbt } : {}),
37
+ };
38
+ }
39
+ const tagged = asTaggedReference(value);
40
+ if (tagged) {
41
+ const stackKind = asStackKind(tagged.kind);
42
+ if (!stackKind) return null;
43
+ return {
44
+ kind: kind ?? stackKind,
45
+ id: tagged.id,
46
+ ...(tagged.tag !== undefined ? { tag: tagged.tag } : {}),
47
+ };
48
+ }
49
+ if (typeof value === 'object' && typeof value.id === 'string' && value.id) {
50
+ const resolvedKind = kind ?? asStackKind(value.kind);
51
+ return {
52
+ id: value.id,
53
+ ...(resolvedKind ? { kind: resolvedKind } : {}),
54
+ ...(value.tag !== undefined ? { tag: value.tag } : {}),
55
+ };
56
+ }
57
+ return null;
58
+ }
59
+
60
+ function spriteScore(
61
+ ref: TaggedReference,
62
+ query: SpriteQuery,
63
+ preferVariant: boolean,
64
+ ): number {
65
+ if (ref.id !== query.id) return -1;
66
+ if (query.kind && ref.kind !== query.kind) return -1;
67
+ if (query.tag === undefined) {
68
+ if (ref.tag === undefined) return 500;
69
+ return preferVariant ? 600 : -1;
70
+ }
71
+ if (ref.tag === undefined) return 100;
72
+ return nbtSpecificity(ref.tag, query.tag);
73
+ }
74
+
75
+ type SpriteBucket = {
76
+ all: AtlasSprite[];
77
+ untagged: AtlasSprite[];
78
+ tagged: AtlasSprite[];
79
+ byTag: Map<string, AtlasSprite[]>;
80
+ maxTagKeys: number;
81
+ };
82
+
83
+ const atlasIndex = new WeakMap<IconAtlas, Map<string, SpriteBucket>>();
84
+
85
+ const EXACT_SAFE_MAX_KEYS = 99;
86
+
87
+ function nbtTopKeys(tag: DumpNbt | undefined): number {
88
+ return tag !== null && typeof tag === 'object' && !Array.isArray(tag)
89
+ ? Object.keys(tag ?? {}).length
90
+ : 0;
91
+ }
92
+
93
+ function spriteBuckets(atlas: IconAtlas): Map<string, SpriteBucket> {
94
+ let byId = atlasIndex.get(atlas);
95
+ if (byId) return byId;
96
+ byId = new Map();
97
+ for (const sprite of atlas.sprites) {
98
+ const bucket =
99
+ byId.get(sprite.ref.id) ??
100
+ (() => {
101
+ const created: SpriteBucket = {
102
+ all: [],
103
+ untagged: [],
104
+ tagged: [],
105
+ byTag: new Map(),
106
+ maxTagKeys: 0,
107
+ };
108
+ byId.set(sprite.ref.id, created);
109
+ return created;
110
+ })();
111
+ bucket.all.push(sprite);
112
+ if (sprite.ref.tag === undefined) {
113
+ bucket.untagged.push(sprite);
114
+ continue;
115
+ }
116
+ bucket.tagged.push(sprite);
117
+ const keyCount = nbtTopKeys(sprite.ref.tag);
118
+ if (keyCount > bucket.maxTagKeys) bucket.maxTagKeys = keyCount;
119
+ const key = formatNbt(sprite.ref.tag);
120
+ if (key !== undefined) {
121
+ const list = bucket.byTag.get(key);
122
+ if (list) list.push(sprite);
123
+ else bucket.byTag.set(key, [sprite]);
124
+ }
125
+ }
126
+ atlasIndex.set(atlas, byId);
127
+ return byId;
128
+ }
129
+
130
+ function spriteHit(
131
+ atlas: IconAtlas,
132
+ sprite: AtlasSprite,
133
+ ): AtlasLookup | null {
134
+ const page = atlas.pages[sprite.page];
135
+ return page ? { atlas, sprite, page, spriteId: sprite.ref.id } : null;
136
+ }
137
+
138
+ function lookupSprite(
139
+ atlas: IconAtlas | null | undefined,
140
+ query?: string | TaggedReference | SpriteQuery | null,
141
+ kind?: SpriteQuery['kind'],
142
+ preferVariant = false,
143
+ ): AtlasLookup | null {
144
+ if (!atlas || query == null) return null;
145
+ const resolved = asSpriteQuery(query, kind);
146
+ if (!resolved) return null;
147
+ const bucket = spriteBuckets(atlas).get(resolved.id);
148
+ if (!bucket) return null;
149
+ if (resolved.tag === undefined) {
150
+ const first = preferVariant ? bucket.tagged : bucket.untagged;
151
+ const second = preferVariant ? bucket.untagged : bucket.tagged;
152
+ for (const list of [first, second]) {
153
+ for (const sprite of list) {
154
+ if (resolved.kind && sprite.ref.kind !== resolved.kind) continue;
155
+ const hit = spriteHit(atlas, sprite);
156
+ if (hit) return hit;
157
+ }
158
+ }
159
+ return null;
160
+ }
161
+ if (bucket.maxTagKeys <= EXACT_SAFE_MAX_KEYS) {
162
+ const exacts = bucket.byTag.get(formatNbt(resolved.tag) ?? '');
163
+ if (exacts) {
164
+ for (const sprite of exacts) {
165
+ if (resolved.kind && sprite.ref.kind !== resolved.kind) continue;
166
+ const hit = spriteHit(atlas, sprite);
167
+ if (hit) return hit;
168
+ }
169
+ }
170
+ }
171
+ let best: AtlasLookup | null = null;
172
+ let bestScore = -1;
173
+ for (const sprite of bucket.all) {
174
+ const score = spriteScore(sprite.ref, resolved, preferVariant);
175
+ if (score <= bestScore) continue;
176
+ const hit = spriteHit(atlas, sprite);
177
+ if (!hit) continue;
178
+ best = hit;
179
+ bestScore = score;
180
+ }
181
+ return best;
182
+ }
183
+
184
+ export function lookupSpriteInBundle(
185
+ bundle: { items: IconAtlas | null; blocks: IconAtlas | null } | null | undefined,
186
+ query?: string | TaggedReference | SpriteQuery | null,
187
+ kind?: SpriteQuery['kind'],
188
+ preferVariant = false,
189
+ ): AtlasLookup | null {
190
+ if (!bundle) return null;
191
+ const resolved = asSpriteQuery(query, kind);
192
+ if (!resolved) return null;
193
+ if (resolved.kind === 'block') {
194
+ return (
195
+ lookupSprite(bundle.blocks, resolved, undefined, preferVariant) ??
196
+ lookupSprite(bundle.items, resolved, undefined, preferVariant)
197
+ );
198
+ }
199
+ if (resolved.kind === 'item' || resolved.kind === 'fluid') {
200
+ return lookupSprite(bundle.items, resolved, undefined, preferVariant);
201
+ }
202
+ return (
203
+ lookupSprite(bundle.items, resolved, undefined, preferVariant) ??
204
+ lookupSprite(bundle.blocks, resolved, undefined, preferVariant)
205
+ );
206
+ }
207
+
@@ -0,0 +1,132 @@
1
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
2
+ import { asSpriteEntry } from '@terrafirmagreg-team/dump/lib/dump/data/registry/parse';
3
+ import { asDisplay } from '@terrafirmagreg-team/dump/lib/dump/contract/text';
4
+ import { asNbt } from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
5
+ import { asRecord, asString } from '@terrafirmagreg-team/dump/lib/dump/contract/read';
6
+ import type {
7
+ AtlasKind,
8
+ AtlasTint,
9
+ IconAtlas,
10
+ } from '@terrafirmagreg-team/dump/lib/dump/assets/atlas/dto';
11
+ import type { DumpRegistryKind } from '@terrafirmagreg-team/dump/lib/dump/data/registry/dto';
12
+ import type { TaggedReference } from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
13
+ import type { SpriteEntry } from '@terrafirmagreg-team/dump/lib/dump/gen';
14
+
15
+ const TINTS = new Set(['none', 'grass', 'foliage', 'water', 'custom']);
16
+
17
+ function asTint(raw: unknown): AtlasTint | undefined {
18
+ return typeof raw === 'string' && TINTS.has(raw) ? (raw as AtlasTint) : undefined;
19
+ }
20
+
21
+ function asTintColor(raw: unknown): string | undefined {
22
+ if (typeof raw === 'number' && Number.isFinite(raw)) {
23
+ return `#${(raw & 0xffffff).toString(16).padStart(6, '0')}`;
24
+ }
25
+ if (typeof raw !== 'string' || !raw) return undefined;
26
+ if (/^#[0-9a-f]{6}$/i.test(raw)) return raw;
27
+ if (/^#[0-9a-f]{8}$/i.test(raw)) return `#${raw.slice(-6)}`;
28
+ if (/^0x[0-9a-f]{6,8}$/i.test(raw)) return `#${raw.slice(-6)}`;
29
+ return undefined;
30
+ }
31
+
32
+ const STACK_KIND: Record<DumpRegistryKind, TaggedReference['kind']> = {
33
+ items: 'item',
34
+ blocks: 'block',
35
+ fluids: 'fluid',
36
+ entities: 'entity',
37
+ };
38
+
39
+ function pushSprite(
40
+ atlas: IconAtlas,
41
+ bySrc: Map<string, number>,
42
+ ref: TaggedReference,
43
+ sprite: SpriteEntry,
44
+ langKey?: string,
45
+ ): void {
46
+ let page = bySrc.get(sprite.image);
47
+ if (page === undefined) {
48
+ atlas.pages.push({ image: sprite.image, w: sprite.pw, h: sprite.ph });
49
+ page = atlas.pages.length - 1;
50
+ bySrc.set(sprite.image, page);
51
+ }
52
+ const tint = asTint(sprite.tint);
53
+ const tintColor = asTintColor(sprite.tintColor);
54
+ atlas.sprites.push({
55
+ ref,
56
+ page,
57
+ x: sprite.x,
58
+ y: sprite.y,
59
+ w: sprite.w,
60
+ h: sprite.h,
61
+ ...(tint ? { tint } : {}),
62
+ ...(tintColor ? { tintColor } : {}),
63
+ ...(langKey ? { langKey } : {}),
64
+ });
65
+ }
66
+
67
+ export function iconAtlasFromEntries(
68
+ value: unknown,
69
+ dataset: DatasetRef,
70
+ modId: string,
71
+ kind: DumpRegistryKind,
72
+ atlasKind: AtlasKind,
73
+ ): IconAtlas | null {
74
+ const entries = asRecord(value);
75
+ if (!entries) return null;
76
+ const stackKind = STACK_KIND[kind];
77
+ const atlas: IconAtlas = { sprites: [], pages: [], modId, dataset, kind: atlasKind };
78
+ const bySrc = new Map<string, number>();
79
+ for (const [key, raw] of Object.entries(entries)) {
80
+ const row = asRecord(raw);
81
+ if (!row) continue;
82
+ const id = asString(row.id) ?? key;
83
+ const langKey = asDisplay(row.display)?.name;
84
+ const sprite = asSpriteEntry(row.sprite);
85
+ if (sprite) pushSprite(atlas, bySrc, { id, kind: stackKind }, sprite, langKey);
86
+ const variants = row.variants;
87
+ if (!Array.isArray(variants)) continue;
88
+ for (const raw2 of variants) {
89
+ const variant = asRecord(raw2);
90
+ const variantSprite = asSpriteEntry(variant?.sprite);
91
+ if (!variantSprite) continue;
92
+ const tag = asNbt(variant?.tag);
93
+ pushSprite(
94
+ atlas,
95
+ bySrc,
96
+ { id, kind: stackKind, ...(tag !== undefined ? { tag } : {}) },
97
+ variantSprite,
98
+ asDisplay(variant?.display)?.name ?? langKey,
99
+ );
100
+ }
101
+ }
102
+ return atlas.sprites.length || atlas.pages.length ? atlas : null;
103
+ }
104
+
105
+ export function mergeAtlases(
106
+ parts: Array<IconAtlas | null>,
107
+ dataset: DatasetRef,
108
+ modId: string,
109
+ kind: AtlasKind,
110
+ ): IconAtlas | null {
111
+ const sources = parts.flatMap((part) => (part ? [part] : []));
112
+ if (!sources.length) return null;
113
+ const atlas: IconAtlas = { sprites: [], pages: [], modId, dataset, kind };
114
+ const bySrc = new Map<string, number>();
115
+ for (const source of sources) {
116
+ const pageMap = new Map<number, number>();
117
+ for (const [index, page] of source.pages.entries()) {
118
+ let mapped = bySrc.get(page.image);
119
+ if (mapped === undefined) {
120
+ atlas.pages.push({ image: page.image, w: page.w, h: page.h });
121
+ mapped = atlas.pages.length - 1;
122
+ bySrc.set(page.image, mapped);
123
+ }
124
+ pageMap.set(index, mapped);
125
+ }
126
+ for (const sprite of source.sprites) {
127
+ atlas.sprites.push({ ...sprite, page: pageMap.get(sprite.page) ?? 0 });
128
+ }
129
+ }
130
+ return atlas.sprites.length || atlas.pages.length ? atlas : null;
131
+ }
132
+
@@ -0,0 +1,19 @@
1
+ import 'server-only';
2
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
3
+ import { clientDumpLang } from '@terrafirmagreg-team/dump/lib/dump/client/dump';
4
+ import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
5
+
6
+ export {
7
+ creativeTabName,
8
+ isTechnicalId,
9
+ langString,
10
+ resolveName,
11
+ } from '@terrafirmagreg-team/dump/lib/dump/assets/lang/names';
12
+
13
+ export function loadMergedLangMap(
14
+ dataset: DatasetRef,
15
+ lang: Lang,
16
+ ): Promise<Record<string, string>> {
17
+ return clientDumpLang(dataset, lang);
18
+ }
19
+
@@ -0,0 +1,244 @@
1
+ import {
2
+ nbtAt,
3
+ parseStackId,
4
+ stackIdBase,
5
+ type DumpNbt,
6
+ } from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
7
+
8
+ function unwrapLangKey(raw: string): string {
9
+ const value = raw.trim();
10
+ return value.startsWith('{') && value.endsWith('}') && value.length > 2
11
+ ? value.slice(1, -1).trim()
12
+ : value;
13
+ }
14
+
15
+ export function langString(
16
+ map: Record<string, string>,
17
+ key: string | undefined,
18
+ ): string | undefined {
19
+ if (!key) return undefined;
20
+ const bare = unwrapLangKey(key);
21
+ return map[key] ?? map[bare];
22
+ }
23
+
24
+ function applyFormat(template: string, args: string[]): string {
25
+ let sequential = 0;
26
+ return template.replace(
27
+ /%(?:(\d+)\$)?([A-Za-z%])/g,
28
+ (_, n: string | undefined, conv: string) => {
29
+ if (conv === '%') return '%';
30
+ if (conv === 'n') return '\n';
31
+ const index = n ? Number(n) - 1 : sequential++;
32
+ return args[index] ?? '';
33
+ },
34
+ );
35
+ }
36
+
37
+ export function customStackName(
38
+ map: Record<string, string>,
39
+ compactId: string,
40
+ ): string | undefined {
41
+ const raw = nbtAt(parseStackId(compactId).nbt, 'display', 'Name');
42
+ if (typeof raw !== 'string' || !raw) return undefined;
43
+ try {
44
+ const node: unknown = JSON.parse(raw);
45
+ if (node && typeof node === 'object') {
46
+ return dumpTextString(map, node as Parameters<typeof dumpTextString>[1]);
47
+ }
48
+ if (typeof node === 'string') return node;
49
+ } catch {
50
+
51
+ }
52
+ return raw;
53
+ }
54
+
55
+ export function dumpTextString(
56
+ map: Record<string, string>,
57
+ text: {
58
+ translate?: string;
59
+ text?: string;
60
+ keybind?: string;
61
+ with?: unknown[];
62
+ extra?: unknown[];
63
+ },
64
+ keybinds?: Record<string, string>,
65
+ ): string {
66
+ const parts: string[] = [];
67
+ if (text.translate) {
68
+ const template = langString(map, text.translate) ?? text.translate;
69
+ const args = Array.isArray(text.with)
70
+ ? text.with.map((item) =>
71
+ item && typeof item === 'object'
72
+ ? dumpTextString(map, item as typeof text, keybinds)
73
+ : String(item ?? ''),
74
+ )
75
+ : [];
76
+ parts.push(args.length ? applyFormat(template, args) : template);
77
+ } else if (text.text !== undefined) {
78
+ parts.push(text.text);
79
+ } else if (text.keybind) {
80
+ parts.push(
81
+ keybinds?.[text.keybind] ?? langString(map, text.keybind) ?? text.keybind,
82
+ );
83
+ }
84
+ if (Array.isArray(text.extra)) {
85
+ for (const item of text.extra) {
86
+ if (item && typeof item === 'object') {
87
+ parts.push(dumpTextString(map, item as typeof text, keybinds));
88
+ }
89
+ }
90
+ }
91
+ return parts.join('');
92
+ }
93
+
94
+ export function dumpTextLines(
95
+ map: Record<string, string>,
96
+ lines: Array<{
97
+ translate?: string;
98
+ text?: string;
99
+ keybind?: string;
100
+ with?: unknown[];
101
+ extra?: unknown[];
102
+ }>,
103
+ keybinds?: Record<string, string>,
104
+ ): string[] {
105
+
106
+ return lines.map((line) => dumpTextString(map, line, keybinds));
107
+ }
108
+
109
+ export function resolveName(
110
+ map: Record<string, string>,
111
+ id: string,
112
+ langKey?: string,
113
+ ): string {
114
+ if (langKey) {
115
+ return langString(map, langKey) ?? langKey;
116
+ }
117
+ return langString(map, id) ?? id;
118
+ }
119
+
120
+ export function stripMcFormat(value: string): string {
121
+ return value.replace(/§x(?:§[0-9a-f]){6}|§[0-9a-fk-or]/gi, '');
122
+ }
123
+
124
+ const KIND_LANG_PREFIXES: Record<string, readonly string[]> = {
125
+ item: ['item', 'block'],
126
+ block: ['block', 'item'],
127
+ fluid: ['fluid', 'fluid_type', 'block'],
128
+ entity: ['entity'],
129
+ itemTag: ['tag.item'],
130
+ blockTag: ['tag.block', 'tag.item'],
131
+ fluidTag: ['tag.fluid'],
132
+ entityTag: ['tag.entity'],
133
+ };
134
+
135
+ const DEFAULT_LANG_PREFIXES = ['item', 'block', 'fluid', 'fluid_type', 'entity'];
136
+ const TAG_LANG_PREFIXES = ['tag.item', 'tag.block', 'tag.fluid', 'tag.entity'];
137
+
138
+ function stackLangKeys(
139
+ kind: string | undefined,
140
+ id: string,
141
+ typeKeys = true,
142
+ ): string[] {
143
+ const raw = parseStackId(id).id;
144
+ const hashed = raw.startsWith('#');
145
+ const resource = hashed ? raw.slice(1) : raw;
146
+ if (!resource.includes(':')) return [];
147
+ const [ns, path] = resource.split(':', 2);
148
+ const flat = path.replaceAll('/', '.');
149
+ const prefixes = hashed
150
+ ? ((kind ? KIND_LANG_PREFIXES[kind] : undefined) ?? TAG_LANG_PREFIXES)
151
+ : (KIND_LANG_PREFIXES[kind ?? ''] ?? DEFAULT_LANG_PREFIXES);
152
+ return [
153
+ ...prefixes.map((prefix) => `${prefix}.${ns}.${flat}`),
154
+ ...(typeKeys ? [`${ns}.type.${flat}.name`, `${ns}.type.${flat}`] : []),
155
+ ];
156
+ }
157
+
158
+ function nbtLangKeys(nbt: DumpNbt | undefined): string[] {
159
+ if (!nbt || typeof nbt !== 'object' || Array.isArray(nbt)) return [];
160
+ const keys: string[] = [];
161
+ for (const [field, value] of Object.entries(nbt)) {
162
+ if (!field.endsWith('Id') || field.length <= 2) continue;
163
+ if (typeof value !== 'string' || !value.includes(':')) continue;
164
+ const [ns, path] = value.split(':', 2);
165
+ const flat = path.replaceAll('/', '.');
166
+ const stem = field
167
+ .slice(0, -2)
168
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
169
+ .toLowerCase();
170
+ if (!stem) continue;
171
+ keys.push(`${ns}.${stem}.${flat}`, `${ns}.${stem}.${flat}.name`);
172
+ }
173
+ return keys;
174
+ }
175
+
176
+ export function stackDisplay(
177
+ langMap: Record<string, string> | undefined,
178
+ compactId: string,
179
+ kind?: string,
180
+ langKey?: string,
181
+ ): { name: string; localized: boolean } {
182
+ const parsed = parseStackId(compactId);
183
+ if (!langMap) return { name: langKey ?? parsed.id, localized: false };
184
+ const custom = customStackName(langMap, compactId);
185
+ if (custom) return { name: stripMcFormat(custom), localized: true };
186
+ const canonical = stackLangKeys(kind, parsed.id, false);
187
+ const candidates: { key: string | undefined; localized: boolean }[] = [
188
+ { key: compactId, localized: true },
189
+ { key: langKey, localized: true },
190
+ ...nbtLangKeys(parsed.nbt).map((key) => ({ key, localized: true })),
191
+ ...canonical.map((key) => ({ key, localized: true })),
192
+ ...stackLangKeys(kind, parsed.id, true)
193
+ .slice(canonical.length)
194
+ .map((key) => ({ key, localized: false })),
195
+ { key: parsed.id, localized: false },
196
+ ];
197
+ for (const { key, localized } of candidates) {
198
+ if (!key) continue;
199
+ const value = langString(langMap, key);
200
+ if (value !== undefined) {
201
+ return { name: stripMcFormat(value), localized };
202
+ }
203
+ }
204
+ return {
205
+ name: humanizeKey(langKey ?? stackIdBase(compactId)),
206
+ localized: false,
207
+ };
208
+ }
209
+
210
+ export function stackDisplayName(
211
+ langMap: Record<string, string> | undefined,
212
+ compactId: string,
213
+ kind?: string,
214
+ langKey?: string,
215
+ ): string {
216
+ return stackDisplay(langMap, compactId, kind, langKey).name;
217
+ }
218
+
219
+ export function humanizeKey(key: string): string {
220
+ const value = unwrapLangKey(key);
221
+ const segment = value.includes(':')
222
+ ? (value.split(':', 2)[1] ?? value)
223
+ : (value.split('.').pop() ?? value);
224
+ const words = segment
225
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
226
+ .split(/[_.\-/]+/)
227
+ .filter(Boolean);
228
+ if (!words.length) return key;
229
+ return words
230
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
231
+ .join(' ');
232
+ }
233
+
234
+ export function creativeTabName(
235
+ map: Record<string, string>,
236
+ nameKey?: string,
237
+ ): string {
238
+ return langString(map, nameKey) ?? humanizeKey(nameKey ?? '');
239
+ }
240
+
241
+ export function isTechnicalId(map: Record<string, string>, id: string): boolean {
242
+ return resolveName(map, id) === id;
243
+ }
244
+