@terrafirmagreg-team/game 0.0.0-stage → 0.1.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 +58 -3
- package/src/components/entry/atlas-sprite.tsx +57 -0
- package/src/components/entry/copy-id-menu.tsx +55 -0
- package/src/components/entry/entry.tsx +229 -0
- package/src/components/entry/frame.tsx +32 -0
- package/src/components/entry/group.tsx +180 -0
- package/src/components/entry/icon/tabs.tsx +118 -0
- package/src/components/entry/icon.tsx +187 -0
- package/src/components/entry/list.tsx +70 -0
- package/src/components/entry/mc-text.tsx +92 -0
- package/src/components/entry/stack-count.tsx +14 -0
- package/src/components/entry/tooltip.tsx +129 -0
- package/src/components/mdx/item-icon.tsx +69 -0
- package/src/components/preview-icon.tsx +25 -0
- package/src/hooks/use-dump-location.ts +18 -0
- package/src/hooks/use-entry-context.ts +35 -0
- package/src/hooks/use-icon-source.ts +297 -0
- package/src/hooks/use-lang-map.ts +18 -0
- package/src/lib/dump/assets/atlas/dto.ts +49 -0
- package/src/lib/dump/assets/atlas/lookup.ts +206 -0
- package/src/lib/dump/assets/atlas/sprites.ts +130 -0
- package/src/lib/dump/assets/lang/load.ts +17 -0
- package/src/lib/dump/assets/lang/names.ts +244 -0
- package/src/lib/dump/assets/urls.ts +143 -0
- package/src/lib/dump/client/atlases.ts +83 -0
- package/src/lib/dump/client/dump.ts +188 -0
- package/src/lib/dump/client/groups.ts +31 -0
- package/src/lib/dump/client/keybinds.ts +11 -0
- package/src/lib/dump/client/mods.ts +16 -0
- package/src/lib/dump/client/registry.ts +125 -0
- package/src/lib/dump/client/tabs.ts +27 -0
- package/src/lib/dump/client/tags.ts +64 -0
- package/src/lib/dump/contract/kind.ts +61 -0
- package/src/lib/dump/contract/read.ts +77 -0
- package/src/lib/dump/contract/ref.ts +262 -0
- package/src/lib/dump/contract/text.ts +86 -0
- package/src/lib/dump/data/registry/dto.ts +56 -0
- package/src/lib/dump/data/registry/parse.ts +291 -0
- package/src/lib/dump/data/tab/dto.ts +13 -0
- package/src/lib/dump/data/tab/parse.ts +26 -0
- package/src/lib/dump/data/tag/dto.ts +7 -0
- package/src/lib/dump/data/tag/parse.ts +57 -0
- package/src/lib/dump/gen/data/guides/book.ts +56 -0
- package/src/lib/dump/gen/data/guides/category.ts +30 -0
- package/src/lib/dump/gen/data/guides/entry.ts +48 -0
- package/src/lib/dump/gen/data/models/model.ts +3 -0
- package/src/lib/dump/gen/data/quests/chapter.ts +126 -0
- package/src/lib/dump/gen/data/quests/chapter_groups.ts +11 -0
- package/src/lib/dump/gen/data/quests/data.ts +40 -0
- package/src/lib/dump/gen/data/recipes/recipe.ts +78 -0
- package/src/lib/dump/gen/index.ts +20 -0
- package/src/lib/dump/gen/keybinds.ts +3 -0
- package/src/lib/dump/gen/lang.ts +3 -0
- package/src/lib/dump/gen/manifest.ts +59 -0
- package/src/lib/dump/gen/registry/blocks.ts +60 -0
- package/src/lib/dump/gen/registry/entities.ts +49 -0
- package/src/lib/dump/gen/registry/fluids.ts +48 -0
- package/src/lib/dump/gen/registry/groups.ts +14 -0
- package/src/lib/dump/gen/registry/items.ts +51 -0
- package/src/lib/dump/gen/registry/recipe_categories.ts +64 -0
- package/src/lib/dump/gen/registry/recipes.ts +3 -0
- package/src/lib/dump/gen/registry/tabs.ts +46 -0
- package/src/lib/dump/gen/registry/tags.ts +11 -0
- package/src/lib/dump/json-reader.ts +231 -0
- package/src/lib/dump/manifest/dto.ts +11 -0
- package/src/lib/dump/manifest/parse.ts +167 -0
- package/src/lib/dump/mods/dto.ts +40 -0
- package/src/lib/dump/mods/index.ts +81 -0
- package/src/lib/dump/mods/load.ts +61 -0
- package/src/lib/dump/paths.ts +171 -0
- package/src/lib/dump/reader.ts +15 -0
- package/src/lib/entry/display.ts +4 -0
- package/src/lib/entry/entry.ts +57 -0
- package/src/lib/entry/formula.ts +226 -0
- package/src/lib/entry/href.ts +148 -0
- package/src/lib/entry/icon-load.ts +195 -0
- package/src/lib/entry/icon.ts +10 -0
- package/src/lib/entry/tooltip.ts +60 -0
- package/src/lib/preview.ts +18 -0
- package/src/lib/search.ts +31 -0
- package/src/lib/snbt.ts +246 -0
- package/src/locales/en_us.json +9 -0
- package/src/locales/ru_ru.json +9 -0
- package/src/locales.ts +5 -0
- package/src/types/entry.ts +52 -0
- package/styles/fonts.css +15 -0
- package/styles/tokens.css +15 -0
- package/styles.css +2 -0
- package/README.md +0 -3
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { asSpriteEntry } from '@terrafirmagreg-team/game/lib/dump/data/registry/parse';
|
|
2
|
+
import { asDisplay } from '@terrafirmagreg-team/game/lib/dump/contract/text';
|
|
3
|
+
import { asNbt } from '@terrafirmagreg-team/game/lib/dump/contract/ref';
|
|
4
|
+
import { asRecord, asString } from '@terrafirmagreg-team/game/lib/dump/contract/read';
|
|
5
|
+
import type {
|
|
6
|
+
AtlasKind,
|
|
7
|
+
AtlasTint,
|
|
8
|
+
IconAtlas,
|
|
9
|
+
} from '@terrafirmagreg-team/game/lib/dump/assets/atlas/dto';
|
|
10
|
+
import type { DumpRegistryKind } from '@terrafirmagreg-team/game/lib/dump/data/registry/dto';
|
|
11
|
+
import type { TaggedReference } from '@terrafirmagreg-team/game/lib/dump/contract/ref';
|
|
12
|
+
import type { SpriteEntry } from '@terrafirmagreg-team/game/lib/dump/gen';
|
|
13
|
+
|
|
14
|
+
const TINTS = new Set(['none', 'grass', 'foliage', 'water', 'custom']);
|
|
15
|
+
|
|
16
|
+
function asTint(raw: unknown): AtlasTint | undefined {
|
|
17
|
+
return typeof raw === 'string' && TINTS.has(raw) ? (raw as AtlasTint) : undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function asTintColor(raw: unknown): string | undefined {
|
|
21
|
+
if (typeof raw === 'number' && Number.isFinite(raw)) {
|
|
22
|
+
return `#${(raw & 0xffffff).toString(16).padStart(6, '0')}`;
|
|
23
|
+
}
|
|
24
|
+
if (typeof raw !== 'string' || !raw) return undefined;
|
|
25
|
+
if (/^#[0-9a-f]{6}$/i.test(raw)) return raw;
|
|
26
|
+
if (/^#[0-9a-f]{8}$/i.test(raw)) return `#${raw.slice(-6)}`;
|
|
27
|
+
if (/^0x[0-9a-f]{6,8}$/i.test(raw)) return `#${raw.slice(-6)}`;
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const STACK_KIND: Record<DumpRegistryKind, TaggedReference['kind']> = {
|
|
32
|
+
items: 'item',
|
|
33
|
+
blocks: 'block',
|
|
34
|
+
fluids: 'fluid',
|
|
35
|
+
entities: 'entity',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function pushSprite(
|
|
39
|
+
atlas: IconAtlas,
|
|
40
|
+
bySrc: Map<string, number>,
|
|
41
|
+
ref: TaggedReference,
|
|
42
|
+
sprite: SpriteEntry,
|
|
43
|
+
langKey?: string,
|
|
44
|
+
): void {
|
|
45
|
+
let page = bySrc.get(sprite.image);
|
|
46
|
+
if (page === undefined) {
|
|
47
|
+
atlas.pages.push({ image: sprite.image, w: sprite.pw, h: sprite.ph });
|
|
48
|
+
page = atlas.pages.length - 1;
|
|
49
|
+
bySrc.set(sprite.image, page);
|
|
50
|
+
}
|
|
51
|
+
const tint = asTint(sprite.tint);
|
|
52
|
+
const tintColor = asTintColor(sprite.tintColor);
|
|
53
|
+
atlas.sprites.push({
|
|
54
|
+
ref,
|
|
55
|
+
page,
|
|
56
|
+
x: sprite.x,
|
|
57
|
+
y: sprite.y,
|
|
58
|
+
w: sprite.w,
|
|
59
|
+
h: sprite.h,
|
|
60
|
+
...(tint ? { tint } : {}),
|
|
61
|
+
...(tintColor ? { tintColor } : {}),
|
|
62
|
+
...(langKey ? { langKey } : {}),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function iconAtlasFromEntries(
|
|
67
|
+
value: unknown,
|
|
68
|
+
dataset: string,
|
|
69
|
+
modId: string,
|
|
70
|
+
kind: DumpRegistryKind,
|
|
71
|
+
atlasKind: AtlasKind,
|
|
72
|
+
): IconAtlas | null {
|
|
73
|
+
const entries = asRecord(value);
|
|
74
|
+
if (!entries) return null;
|
|
75
|
+
const stackKind = STACK_KIND[kind];
|
|
76
|
+
const atlas: IconAtlas = { sprites: [], pages: [], modId, dataset, kind: atlasKind };
|
|
77
|
+
const bySrc = new Map<string, number>();
|
|
78
|
+
for (const [key, raw] of Object.entries(entries)) {
|
|
79
|
+
const row = asRecord(raw);
|
|
80
|
+
if (!row) continue;
|
|
81
|
+
const id = asString(row.id) ?? key;
|
|
82
|
+
const langKey = asDisplay(row.display)?.name;
|
|
83
|
+
const sprite = asSpriteEntry(row.sprite);
|
|
84
|
+
if (sprite) pushSprite(atlas, bySrc, { id, kind: stackKind }, sprite, langKey);
|
|
85
|
+
const variants = row.variants;
|
|
86
|
+
if (!Array.isArray(variants)) continue;
|
|
87
|
+
for (const raw2 of variants) {
|
|
88
|
+
const variant = asRecord(raw2);
|
|
89
|
+
const variantSprite = asSpriteEntry(variant?.sprite);
|
|
90
|
+
if (!variantSprite) continue;
|
|
91
|
+
const tag = asNbt(variant?.tag);
|
|
92
|
+
pushSprite(
|
|
93
|
+
atlas,
|
|
94
|
+
bySrc,
|
|
95
|
+
{ id, kind: stackKind, ...(tag !== undefined ? { tag } : {}) },
|
|
96
|
+
variantSprite,
|
|
97
|
+
asDisplay(variant?.display)?.name ?? langKey,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return atlas.sprites.length || atlas.pages.length ? atlas : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function mergeAtlases(
|
|
105
|
+
parts: Array<IconAtlas | null>,
|
|
106
|
+
dataset: string,
|
|
107
|
+
modId: string,
|
|
108
|
+
kind: AtlasKind,
|
|
109
|
+
): IconAtlas | null {
|
|
110
|
+
const sources = parts.flatMap((part) => (part ? [part] : []));
|
|
111
|
+
if (!sources.length) return null;
|
|
112
|
+
const atlas: IconAtlas = { sprites: [], pages: [], modId, dataset, kind };
|
|
113
|
+
const bySrc = new Map<string, number>();
|
|
114
|
+
for (const source of sources) {
|
|
115
|
+
const pageMap = new Map<number, number>();
|
|
116
|
+
for (const [index, page] of source.pages.entries()) {
|
|
117
|
+
let mapped = bySrc.get(page.image);
|
|
118
|
+
if (mapped === undefined) {
|
|
119
|
+
atlas.pages.push({ image: page.image, w: page.w, h: page.h });
|
|
120
|
+
mapped = atlas.pages.length - 1;
|
|
121
|
+
bySrc.set(page.image, mapped);
|
|
122
|
+
}
|
|
123
|
+
pageMap.set(index, mapped);
|
|
124
|
+
}
|
|
125
|
+
for (const sprite of source.sprites) {
|
|
126
|
+
atlas.sprites.push({ ...sprite, page: pageMap.get(sprite.page) ?? 0 });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return atlas.sprites.length || atlas.pages.length ? atlas : null;
|
|
130
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
import { clientDumpLang } from '@terrafirmagreg-team/game/lib/dump/client/dump';
|
|
3
|
+
import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
creativeTabName,
|
|
7
|
+
isTechnicalId,
|
|
8
|
+
langString,
|
|
9
|
+
resolveName,
|
|
10
|
+
} from '@terrafirmagreg-team/game/lib/dump/assets/lang/names';
|
|
11
|
+
|
|
12
|
+
export function loadMergedLangMap(
|
|
13
|
+
dataset: string,
|
|
14
|
+
lang: Lang,
|
|
15
|
+
): Promise<Record<string, string>> {
|
|
16
|
+
return clientDumpLang(dataset, lang);
|
|
17
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import {
|
|
2
|
+
nbtAt,
|
|
3
|
+
parseStackId,
|
|
4
|
+
stackIdBase,
|
|
5
|
+
type DumpNbt,
|
|
6
|
+
} from '@terrafirmagreg-team/game/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
|
+
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AtlasPage,
|
|
3
|
+
IconAtlas,
|
|
4
|
+
} from '@terrafirmagreg-team/game/lib/dump/assets/atlas/dto';
|
|
5
|
+
import {
|
|
6
|
+
cleanDumpRel,
|
|
7
|
+
dumpLocation,
|
|
8
|
+
dumpRelPublicUrl,
|
|
9
|
+
dumpStorageUrl,
|
|
10
|
+
splitDumpId,
|
|
11
|
+
type DumpLocation,
|
|
12
|
+
} from '@terrafirmagreg-team/game/lib/dump/paths';
|
|
13
|
+
|
|
14
|
+
export { dumpRelPublicUrl } from '@terrafirmagreg-team/game/lib/dump/paths';
|
|
15
|
+
|
|
16
|
+
export function texturePublicUrl(
|
|
17
|
+
dataset: string,
|
|
18
|
+
textureId: string,
|
|
19
|
+
): string | undefined {
|
|
20
|
+
const parts = splitDumpId(textureId.trim());
|
|
21
|
+
if (!parts) return undefined;
|
|
22
|
+
const path = parts.path.startsWith('textures/')
|
|
23
|
+
? parts.path
|
|
24
|
+
: `textures/${parts.path}`;
|
|
25
|
+
const rel = `${path.replace(/\.(png|webp)$/i, '')}.webp`;
|
|
26
|
+
return dumpRelPublicUrl(dataset, `assets/${parts.ns}/${rel}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function modelPublicUrl(
|
|
30
|
+
dataset: string,
|
|
31
|
+
namespace: string,
|
|
32
|
+
rel: string | null | undefined,
|
|
33
|
+
): string | undefined {
|
|
34
|
+
const cleaned = typeof rel === 'string' ? cleanDumpRel(rel) : null;
|
|
35
|
+
return cleaned
|
|
36
|
+
? dumpRelPublicUrl(dataset, `assets/${namespace}/${cleaned}`)
|
|
37
|
+
: undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function recipeImageUrl(
|
|
41
|
+
dataset: string,
|
|
42
|
+
recipe: { modId: string; image?: string },
|
|
43
|
+
): string | undefined {
|
|
44
|
+
const rel = recipe.image ? cleanDumpRel(recipe.image) : undefined;
|
|
45
|
+
if (!rel) return undefined;
|
|
46
|
+
if (rel.startsWith('assets/')) return dumpRelPublicUrl(dataset, rel);
|
|
47
|
+
if (!recipe.modId) return undefined;
|
|
48
|
+
return dumpRelPublicUrl(dataset, `assets/${recipe.modId}/${rel}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function atlasPagePublicUrl(
|
|
52
|
+
dataset: string,
|
|
53
|
+
atlas: IconAtlas,
|
|
54
|
+
page: AtlasPage,
|
|
55
|
+
): string | undefined {
|
|
56
|
+
const src = cleanDumpRel(page.image);
|
|
57
|
+
if (!src || !atlas.modId) return undefined;
|
|
58
|
+
return dumpRelPublicUrl(dataset, `assets/${atlas.modId}/${src}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function pathDir(path: string): string {
|
|
62
|
+
const slash = path.lastIndexOf('/');
|
|
63
|
+
return slash >= 0 ? path.slice(0, slash + 1) : '';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function relativePath(fromDir: string, toPath: string): string {
|
|
67
|
+
const a = fromDir.split('/').filter(Boolean);
|
|
68
|
+
const b = toPath.split('/').filter(Boolean);
|
|
69
|
+
let i = 0;
|
|
70
|
+
while (i < a.length && i < b.length && a[i] === b[i]) i += 1;
|
|
71
|
+
return '../'.repeat(a.length - i) + b.slice(i).join('/');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function resolveAssetRel(baseDir: string, relUri: string): string | null {
|
|
75
|
+
const out: string[] = [];
|
|
76
|
+
for (const seg of `${baseDir}${relUri}`.split('/')) {
|
|
77
|
+
if (seg === '..') {
|
|
78
|
+
if (!out.pop()) return null;
|
|
79
|
+
} else if (seg !== '.' && seg !== '') {
|
|
80
|
+
out.push(seg);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return out.length ? out.join('/') : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const reverseFileIndexes = new WeakMap<
|
|
87
|
+
Record<string, string>,
|
|
88
|
+
Map<string, string>
|
|
89
|
+
>();
|
|
90
|
+
function reverseFileIndex(files: Record<string, string>): Map<string, string> {
|
|
91
|
+
let map = reverseFileIndexes.get(files);
|
|
92
|
+
if (!map) {
|
|
93
|
+
map = new Map(Object.entries(files).map(([rel, key]) => [key, rel]));
|
|
94
|
+
reverseFileIndexes.set(files, map);
|
|
95
|
+
}
|
|
96
|
+
return map;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function dumpRelForStorageRel(
|
|
100
|
+
loc: DumpLocation,
|
|
101
|
+
storageRel: string,
|
|
102
|
+
): string | undefined {
|
|
103
|
+
if (loc.files) return reverseFileIndex(loc.files).get(storageRel);
|
|
104
|
+
return storageRel.startsWith(`${loc.root}/`)
|
|
105
|
+
? storageRel.slice(loc.root.length + 1)
|
|
106
|
+
: undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function createDumpAssetResolver(
|
|
110
|
+
dataset: string,
|
|
111
|
+
): (resourceUrl: string, modelUrl: string) => string | undefined {
|
|
112
|
+
|
|
113
|
+
let prefix: string | null = null;
|
|
114
|
+
return (resourceUrl, modelUrl) => {
|
|
115
|
+
const loc = dumpLocation(dataset);
|
|
116
|
+
if (!loc) return undefined;
|
|
117
|
+
const base =
|
|
118
|
+
typeof window === 'undefined' ? 'http://x.invalid' : location.origin;
|
|
119
|
+
prefix ??= new URL(dumpStorageUrl(''), base).href;
|
|
120
|
+
let resourcePath: string;
|
|
121
|
+
let modelAbs: URL;
|
|
122
|
+
try {
|
|
123
|
+
modelAbs = new URL(modelUrl, base);
|
|
124
|
+
const resource = new URL(resourceUrl, modelAbs);
|
|
125
|
+
if (resource.origin !== modelAbs.origin) return undefined;
|
|
126
|
+
resourcePath = resource.pathname;
|
|
127
|
+
} catch {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
const modelHref = modelAbs.href;
|
|
131
|
+
if (!modelHref.startsWith(prefix)) return undefined;
|
|
132
|
+
const modelRel = dumpRelForStorageRel(
|
|
133
|
+
loc,
|
|
134
|
+
modelHref.slice(prefix.length),
|
|
135
|
+
);
|
|
136
|
+
if (!modelRel) return undefined;
|
|
137
|
+
const rel = relativePath(pathDir(modelAbs.pathname), resourcePath);
|
|
138
|
+
if (!rel || !rel.includes('/')) return undefined;
|
|
139
|
+
const dumpRel = resolveAssetRel(pathDir(modelRel), rel);
|
|
140
|
+
if (!dumpRel) return undefined;
|
|
141
|
+
return dumpRelPublicUrl(dataset, dumpRel);
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { AtlasBundle, IconAtlas } from '@terrafirmagreg-team/game/lib/dump/assets/atlas/dto';
|
|
2
|
+
import {
|
|
3
|
+
iconAtlasFromEntries,
|
|
4
|
+
mergeAtlases,
|
|
5
|
+
} from '@terrafirmagreg-team/game/lib/dump/assets/atlas/sprites';
|
|
6
|
+
import {
|
|
7
|
+
clientDumpJson,
|
|
8
|
+
loadClientManifest,
|
|
9
|
+
} from '@terrafirmagreg-team/game/lib/dump/client/dump';
|
|
10
|
+
import { asRecord } from '@terrafirmagreg-team/game/lib/dump/contract/read';
|
|
11
|
+
import { createAsyncCache } from '@terrafirmagreg-team/ui/lib/utils/async-cache';
|
|
12
|
+
|
|
13
|
+
async function modEntries(
|
|
14
|
+
dataset: string,
|
|
15
|
+
modId: string,
|
|
16
|
+
kind: string,
|
|
17
|
+
): Promise<Record<string, unknown> | undefined> {
|
|
18
|
+
const entries = asRecord(
|
|
19
|
+
await clientDumpJson(dataset, `registry/${kind}.json`, 'high'),
|
|
20
|
+
);
|
|
21
|
+
if (!entries) return undefined;
|
|
22
|
+
const prefix = `${modId}:`;
|
|
23
|
+
const out: Record<string, unknown> = {};
|
|
24
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
25
|
+
if (key.startsWith(prefix)) out[key] = value;
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const loadClientModAtlas = createAsyncCache(
|
|
31
|
+
async (
|
|
32
|
+
dataset: string,
|
|
33
|
+
modId: string,
|
|
34
|
+
kind: 'items' | 'blocks',
|
|
35
|
+
): Promise<IconAtlas | null> => {
|
|
36
|
+
const manifest = await loadClientManifest(dataset);
|
|
37
|
+
const listed = manifest?.namespaces[modId];
|
|
38
|
+
if (!listed) return null;
|
|
39
|
+
const has = (file: string) => listed.registry.includes(file);
|
|
40
|
+
if (kind === 'blocks') {
|
|
41
|
+
if (!has('blocks')) return null;
|
|
42
|
+
return iconAtlasFromEntries(
|
|
43
|
+
await modEntries(dataset, modId, 'blocks'),
|
|
44
|
+
dataset,
|
|
45
|
+
modId,
|
|
46
|
+
'blocks',
|
|
47
|
+
'block',
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return mergeAtlases(
|
|
51
|
+
[
|
|
52
|
+
has('items')
|
|
53
|
+
? iconAtlasFromEntries(
|
|
54
|
+
await modEntries(dataset, modId, 'items'),
|
|
55
|
+
dataset,
|
|
56
|
+
modId,
|
|
57
|
+
'items',
|
|
58
|
+
'item',
|
|
59
|
+
)
|
|
60
|
+
: null,
|
|
61
|
+
has('fluids')
|
|
62
|
+
? iconAtlasFromEntries(
|
|
63
|
+
await modEntries(dataset, modId, 'fluids'),
|
|
64
|
+
dataset,
|
|
65
|
+
modId,
|
|
66
|
+
'fluids',
|
|
67
|
+
'item',
|
|
68
|
+
)
|
|
69
|
+
: null,
|
|
70
|
+
],
|
|
71
|
+
dataset,
|
|
72
|
+
modId,
|
|
73
|
+
'item',
|
|
74
|
+
);
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
export const loadClientModAtlases = createAsyncCache(
|
|
79
|
+
async (dataset: string, modId: string): Promise<AtlasBundle> => ({
|
|
80
|
+
items: await loadClientModAtlas(dataset, modId, 'items'),
|
|
81
|
+
blocks: await loadClientModAtlas(dataset, modId, 'blocks'),
|
|
82
|
+
}),
|
|
83
|
+
);
|