@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,25 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { DynamicIcon, type IconName } from 'lucide-react/dynamic';
|
|
4
|
+
import { EntryIcon } from '@terrafirmagreg-team/game/components/entry/icon';
|
|
5
|
+
import type { ReferenceKind } from '@terrafirmagreg-team/game/lib/dump/contract/ref';
|
|
6
|
+
import type { PreviewIcon } from '@terrafirmagreg-team/ui/types/preview';
|
|
7
|
+
|
|
8
|
+
export function PagePreviewIcon({ icon }: { icon: PreviewIcon }) {
|
|
9
|
+
if (icon.type === 'entry') {
|
|
10
|
+
return (
|
|
11
|
+
<EntryIcon
|
|
12
|
+
reference={{ kind: icon.kind as ReferenceKind, id: icon.id }}
|
|
13
|
+
dataset={icon.dataset}
|
|
14
|
+
size={16}
|
|
15
|
+
missing="placeholder"
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
return (
|
|
20
|
+
<DynamicIcon
|
|
21
|
+
name={icon.name as IconName}
|
|
22
|
+
className="size-4 shrink-0"
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useSyncExternalStore } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
dumpLocation,
|
|
6
|
+
subscribeDumpLocations,
|
|
7
|
+
type DumpLocation,
|
|
8
|
+
} from '@terrafirmagreg-team/game/lib/dump/paths';
|
|
9
|
+
|
|
10
|
+
export function useDumpLocation(
|
|
11
|
+
dataset: string | undefined,
|
|
12
|
+
): DumpLocation | undefined {
|
|
13
|
+
return useSyncExternalStore(
|
|
14
|
+
subscribeDumpLocations,
|
|
15
|
+
() => (dataset ? dumpLocation(dataset) : undefined),
|
|
16
|
+
() => undefined,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
createElement,
|
|
6
|
+
useContext,
|
|
7
|
+
useMemo,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
|
|
11
|
+
|
|
12
|
+
type EntryContextValue = {
|
|
13
|
+
dataset: string;
|
|
14
|
+
lang?: Lang;
|
|
15
|
+
langMap?: Record<string, string>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const EntryContext = createContext<EntryContextValue | null>(null);
|
|
19
|
+
|
|
20
|
+
export function EntryProvider({
|
|
21
|
+
dataset,
|
|
22
|
+
lang,
|
|
23
|
+
langMap,
|
|
24
|
+
children,
|
|
25
|
+
}: EntryContextValue & { children: ReactNode }) {
|
|
26
|
+
const value = useMemo(
|
|
27
|
+
() => ({ dataset, lang, langMap }),
|
|
28
|
+
[dataset, lang, langMap],
|
|
29
|
+
);
|
|
30
|
+
return createElement(EntryContext.Provider, { value }, children);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function useEntryContext(): EntryContextValue | null {
|
|
34
|
+
return useContext(EntryContext);
|
|
35
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useAsyncResult } from '@terrafirmagreg-team/ui/hooks/use-async-value';
|
|
4
|
+
import { useCycleIndex } from '@terrafirmagreg-team/ui/hooks/use-cycle-index';
|
|
5
|
+
import {
|
|
6
|
+
classifyRefId,
|
|
7
|
+
isTagKind,
|
|
8
|
+
parseStackId,
|
|
9
|
+
tagKindOf,
|
|
10
|
+
} from '@terrafirmagreg-team/game/lib/dump/contract/ref';
|
|
11
|
+
import { texturePublicUrl } from '@terrafirmagreg-team/game/lib/dump/assets/urls';
|
|
12
|
+
import { useDumpLocation } from '@terrafirmagreg-team/game/hooks/use-dump-location';
|
|
13
|
+
import { lookupSpriteInBundle } from '@terrafirmagreg-team/game/lib/dump/assets/atlas/lookup';
|
|
14
|
+
import type { AtlasLookup } from '@terrafirmagreg-team/game/lib/dump/assets/atlas/dto';
|
|
15
|
+
import {
|
|
16
|
+
blockItemId,
|
|
17
|
+
fetchAtlases,
|
|
18
|
+
fetchEntityTexture,
|
|
19
|
+
fetchTagMembers,
|
|
20
|
+
peekBlockItem,
|
|
21
|
+
peekEntityTexture,
|
|
22
|
+
peekTagMembers,
|
|
23
|
+
} from '@terrafirmagreg-team/game/lib/entry/icon-load';
|
|
24
|
+
import { loadClientModAtlases } from '@terrafirmagreg-team/game/lib/dump/client/atlases';
|
|
25
|
+
import { stackKindOf } from '@terrafirmagreg-team/game/lib/entry/icon';
|
|
26
|
+
import type {
|
|
27
|
+
DumpNbt,
|
|
28
|
+
ReferenceKind,
|
|
29
|
+
TaggedReference,
|
|
30
|
+
} from '@terrafirmagreg-team/game/lib/dump/contract/ref';
|
|
31
|
+
|
|
32
|
+
export const ENTRY_CYCLE_MS = 1500;
|
|
33
|
+
|
|
34
|
+
export type IconSource = {
|
|
35
|
+
|
|
36
|
+
rawId: string;
|
|
37
|
+
|
|
38
|
+
cycleRef: TaggedReference | null;
|
|
39
|
+
|
|
40
|
+
lookup: AtlasLookup | null;
|
|
41
|
+
|
|
42
|
+
missingLookup: AtlasLookup | null;
|
|
43
|
+
missingPending: boolean;
|
|
44
|
+
|
|
45
|
+
imgSrc: string | undefined;
|
|
46
|
+
|
|
47
|
+
sourceKey: string;
|
|
48
|
+
|
|
49
|
+
pending: boolean;
|
|
50
|
+
awaitingAtlas: boolean;
|
|
51
|
+
awaitingTexture: boolean;
|
|
52
|
+
usesAtlas: boolean;
|
|
53
|
+
isEntity: boolean;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export function useIconSource({
|
|
57
|
+
dataset,
|
|
58
|
+
reference,
|
|
59
|
+
id,
|
|
60
|
+
kind,
|
|
61
|
+
tag,
|
|
62
|
+
missing,
|
|
63
|
+
preferVariant,
|
|
64
|
+
}: {
|
|
65
|
+
dataset?: string;
|
|
66
|
+
reference?: TaggedReference;
|
|
67
|
+
id?: string;
|
|
68
|
+
kind?: ReferenceKind;
|
|
69
|
+
tag?: DumpNbt;
|
|
70
|
+
missing: 'placeholder' | 'item' | 'hide';
|
|
71
|
+
preferVariant?: boolean;
|
|
72
|
+
}): IconSource {
|
|
73
|
+
const ref = reference ?? (id ? classifyRefId(id, kind) : undefined);
|
|
74
|
+
const refKind = ref?.kind;
|
|
75
|
+
const rawId = ref?.id ?? id ?? '';
|
|
76
|
+
const nbt = tag ?? ref?.tag;
|
|
77
|
+
const tagMemberKind = tagKindOf(refKind);
|
|
78
|
+
const tagKey =
|
|
79
|
+
dataset && rawId && isTagKind(refKind)
|
|
80
|
+
? `${dataset}/${tagMemberKind ?? 'item'}/${rawId}`
|
|
81
|
+
: null;
|
|
82
|
+
const tagMembersPeek = tagKey
|
|
83
|
+
? peekTagMembers(dataset!, rawId, tagMemberKind ?? 'item')
|
|
84
|
+
: undefined;
|
|
85
|
+
const tagHit = useAsyncResult(
|
|
86
|
+
tagKey && tagMembersPeek === undefined
|
|
87
|
+
? async () => ({
|
|
88
|
+
key: tagKey,
|
|
89
|
+
members: await fetchTagMembers(
|
|
90
|
+
dataset!,
|
|
91
|
+
rawId,
|
|
92
|
+
tagMemberKind ?? 'item',
|
|
93
|
+
),
|
|
94
|
+
})
|
|
95
|
+
: false,
|
|
96
|
+
[dataset, rawId, tagMemberKind, tagKey, tagMembersPeek],
|
|
97
|
+
);
|
|
98
|
+
const tagMembers = !tagKey
|
|
99
|
+
? undefined
|
|
100
|
+
: tagMembersPeek !== undefined
|
|
101
|
+
? tagMembersPeek
|
|
102
|
+
: tagHit.pending
|
|
103
|
+
? undefined
|
|
104
|
+
: tagHit.value?.key === tagKey
|
|
105
|
+
? (tagHit.value.members ?? null)
|
|
106
|
+
: null;
|
|
107
|
+
const cycleRefs =
|
|
108
|
+
tagMembers && tagMembers.length > 1
|
|
109
|
+
? tagMembers.map((member) => ({
|
|
110
|
+
kind: (tagMemberKind ?? 'item') as ReferenceKind,
|
|
111
|
+
id: member,
|
|
112
|
+
}))
|
|
113
|
+
: null;
|
|
114
|
+
const [cycleIndex] = useCycleIndex(cycleRefs?.length ?? 0, {
|
|
115
|
+
enabled: cycleRefs !== null,
|
|
116
|
+
intervalMs: ENTRY_CYCLE_MS,
|
|
117
|
+
});
|
|
118
|
+
const itemId = !rawId
|
|
119
|
+
? ''
|
|
120
|
+
: tagKey
|
|
121
|
+
? tagMembers === undefined
|
|
122
|
+
? undefined
|
|
123
|
+
: tagMembers === null
|
|
124
|
+
? null
|
|
125
|
+
: (tagMembers[0] ?? null)
|
|
126
|
+
: rawId;
|
|
127
|
+
const memberKind =
|
|
128
|
+
isTagKind(refKind) && itemId && !itemId.startsWith('#')
|
|
129
|
+
? (tagMemberKind ?? 'item')
|
|
130
|
+
: refKind;
|
|
131
|
+
const blockKey =
|
|
132
|
+
dataset && memberKind === 'block' && itemId && !itemId.startsWith('#')
|
|
133
|
+
? `${dataset}/${itemId}`
|
|
134
|
+
: null;
|
|
135
|
+
const blockPeek = blockKey ? peekBlockItem(dataset!, itemId!) : undefined;
|
|
136
|
+
const blockHit = useAsyncResult(
|
|
137
|
+
blockKey && blockPeek === undefined
|
|
138
|
+
? async () => ({
|
|
139
|
+
key: blockKey,
|
|
140
|
+
id: await blockItemId(dataset!, itemId!),
|
|
141
|
+
})
|
|
142
|
+
: false,
|
|
143
|
+
[dataset, itemId, blockKey, blockPeek],
|
|
144
|
+
);
|
|
145
|
+
const blockResolved = !blockKey
|
|
146
|
+
? undefined
|
|
147
|
+
: blockPeek !== undefined
|
|
148
|
+
? blockPeek
|
|
149
|
+
: blockHit.pending
|
|
150
|
+
? undefined
|
|
151
|
+
: blockHit.value?.key === blockKey
|
|
152
|
+
? (blockHit.value.id ?? null)
|
|
153
|
+
: null;
|
|
154
|
+
const atlasId = blockKey ? (blockResolved ?? undefined) : itemId;
|
|
155
|
+
const displayKind = blockKey && blockResolved ? 'item' : memberKind;
|
|
156
|
+
const resolvedId = atlasId ?? rawId;
|
|
157
|
+
const isTexture = displayKind === 'texture';
|
|
158
|
+
const isEntity = displayKind === 'entity';
|
|
159
|
+
const parsed = parseStackId(resolvedId.replace(/^#/, ''));
|
|
160
|
+
const spriteId = parsed.id;
|
|
161
|
+
const eggId =
|
|
162
|
+
isEntity && spriteId && !spriteId.startsWith('#')
|
|
163
|
+
? `${spriteId}_spawn_egg`
|
|
164
|
+
: undefined;
|
|
165
|
+
const usesAtlas =
|
|
166
|
+
Boolean(dataset && rawId) &&
|
|
167
|
+
(displayKind === 'item' ||
|
|
168
|
+
displayKind === 'block' ||
|
|
169
|
+
displayKind === 'fluid' ||
|
|
170
|
+
Boolean(eggId));
|
|
171
|
+
const modId = spriteId.includes(':')
|
|
172
|
+
? spriteId.slice(0, spriteId.indexOf(':'))
|
|
173
|
+
: '';
|
|
174
|
+
const atlasNeedsLoad =
|
|
175
|
+
dataset && usesAtlas && modId && spriteId && !spriteId.startsWith('#');
|
|
176
|
+
const atlasPeek = atlasNeedsLoad
|
|
177
|
+
? loadClientModAtlases.peek(dataset, modId)
|
|
178
|
+
: undefined;
|
|
179
|
+
const atlasBundle =
|
|
180
|
+
atlasPeek && (atlasPeek.items || atlasPeek.blocks) ? atlasPeek : null;
|
|
181
|
+
const atlases = useAsyncResult(
|
|
182
|
+
atlasNeedsLoad && atlasPeek === undefined
|
|
183
|
+
? () => fetchAtlases(dataset, modId)
|
|
184
|
+
: false,
|
|
185
|
+
[atlasNeedsLoad, atlasPeek, dataset, modId],
|
|
186
|
+
);
|
|
187
|
+
const atlasValue = atlasPeek === undefined ? atlases.value : atlasBundle;
|
|
188
|
+
const sourceKey = `${dataset}:${displayKind}:${resolvedId}`;
|
|
189
|
+
const dumpLoc = useDumpLocation(dataset);
|
|
190
|
+
const texturePeek =
|
|
191
|
+
dataset && isTexture && rawId
|
|
192
|
+
? texturePublicUrl(dataset, rawId)
|
|
193
|
+
: dataset && isEntity && resolvedId
|
|
194
|
+
? peekEntityTexture(dataset, resolvedId, nbt)
|
|
195
|
+
: null;
|
|
196
|
+
const textureSrc = useAsyncResult(
|
|
197
|
+
dataset && isTexture && rawId
|
|
198
|
+
? false
|
|
199
|
+
: dataset && isEntity && resolvedId && texturePeek === undefined
|
|
200
|
+
? async () => ({
|
|
201
|
+
key: sourceKey,
|
|
202
|
+
url:
|
|
203
|
+
(await fetchEntityTexture(dataset, resolvedId, nbt)) ?? undefined,
|
|
204
|
+
})
|
|
205
|
+
: false,
|
|
206
|
+
[
|
|
207
|
+
isTexture,
|
|
208
|
+
isEntity,
|
|
209
|
+
dataset,
|
|
210
|
+
rawId,
|
|
211
|
+
resolvedId,
|
|
212
|
+
sourceKey,
|
|
213
|
+
nbt,
|
|
214
|
+
texturePeek,
|
|
215
|
+
],
|
|
216
|
+
);
|
|
217
|
+
const resolvedTexture =
|
|
218
|
+
texturePeek !== undefined
|
|
219
|
+
? (texturePeek ?? undefined)
|
|
220
|
+
: textureSrc.value?.key === sourceKey
|
|
221
|
+
? textureSrc.value.url
|
|
222
|
+
: undefined;
|
|
223
|
+
const wantMissingItem =
|
|
224
|
+
missing === 'item' && dataset !== undefined && usesAtlas;
|
|
225
|
+
const missingPeek = wantMissingItem
|
|
226
|
+
? loadClientModAtlases.peek(dataset, 'ftbquests')
|
|
227
|
+
: undefined;
|
|
228
|
+
const missingBundleValue =
|
|
229
|
+
missingPeek !== undefined
|
|
230
|
+
? missingPeek.items || missingPeek.blocks
|
|
231
|
+
? missingPeek
|
|
232
|
+
: null
|
|
233
|
+
: undefined;
|
|
234
|
+
const missingBundle = useAsyncResult(
|
|
235
|
+
wantMissingItem && missingPeek === undefined
|
|
236
|
+
? () => fetchAtlases(dataset, 'ftbquests')
|
|
237
|
+
: false,
|
|
238
|
+
[wantMissingItem, missingPeek, dataset],
|
|
239
|
+
);
|
|
240
|
+
const missingLookup = wantMissingItem
|
|
241
|
+
? lookupSpriteInBundle(
|
|
242
|
+
missingBundleValue === undefined
|
|
243
|
+
? missingBundle.value
|
|
244
|
+
: missingBundleValue,
|
|
245
|
+
{
|
|
246
|
+
id: 'ftbquests:missing_item',
|
|
247
|
+
kind: 'item',
|
|
248
|
+
},
|
|
249
|
+
)
|
|
250
|
+
: null;
|
|
251
|
+
|
|
252
|
+
const lookupKind = stackKindOf(displayKind) ?? (eggId ? 'item' : undefined);
|
|
253
|
+
const lookup = usesAtlas
|
|
254
|
+
? lookupSpriteInBundle(
|
|
255
|
+
atlasValue,
|
|
256
|
+
{
|
|
257
|
+
id: eggId ?? spriteId,
|
|
258
|
+
...(lookupKind ? { kind: lookupKind } : {}),
|
|
259
|
+
...(nbt !== undefined
|
|
260
|
+
? { tag: nbt }
|
|
261
|
+
: parsed.nbt !== undefined
|
|
262
|
+
? { tag: parsed.nbt }
|
|
263
|
+
: {}),
|
|
264
|
+
},
|
|
265
|
+
lookupKind,
|
|
266
|
+
preferVariant,
|
|
267
|
+
)
|
|
268
|
+
: null;
|
|
269
|
+
const pending =
|
|
270
|
+
(isTagKind(refKind) && itemId === undefined) ||
|
|
271
|
+
(blockKey !== null && blockResolved === undefined);
|
|
272
|
+
const awaitingAtlas =
|
|
273
|
+
usesAtlas &&
|
|
274
|
+
atlasId != null &&
|
|
275
|
+
!atlasId.startsWith('#') &&
|
|
276
|
+
atlases.pending;
|
|
277
|
+
const awaitingTexture =
|
|
278
|
+
(isTexture || isEntity) &&
|
|
279
|
+
texturePeek === undefined &&
|
|
280
|
+
(textureSrc.pending || (isTexture && !dumpLoc));
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
rawId,
|
|
284
|
+
cycleRef:
|
|
285
|
+
cycleRefs && !pending ? (cycleRefs[cycleIndex] ?? cycleRefs[0]!) : null,
|
|
286
|
+
lookup: lookup ?? null,
|
|
287
|
+
missingLookup: missingLookup ?? null,
|
|
288
|
+
missingPending: missingBundle.pending,
|
|
289
|
+
imgSrc: isTexture || isEntity ? resolvedTexture : undefined,
|
|
290
|
+
sourceKey,
|
|
291
|
+
pending,
|
|
292
|
+
awaitingAtlas,
|
|
293
|
+
awaitingTexture,
|
|
294
|
+
usesAtlas,
|
|
295
|
+
isEntity,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useAsyncResult } from '@terrafirmagreg-team/ui/hooks/use-async-value';
|
|
4
|
+
import { dumpReader } from '@terrafirmagreg-team/game/lib/dump/reader';
|
|
5
|
+
|
|
6
|
+
export function useLangMap(
|
|
7
|
+
dataset: string | undefined,
|
|
8
|
+
lang: string | undefined,
|
|
9
|
+
enabled = true,
|
|
10
|
+
): Record<string, string> | undefined {
|
|
11
|
+
const { value, pending } = useAsyncResult(
|
|
12
|
+
enabled && dataset && lang
|
|
13
|
+
? () => dumpReader(dataset).then((reader) => reader.getLang(lang))
|
|
14
|
+
: false,
|
|
15
|
+
[dataset, lang, enabled],
|
|
16
|
+
);
|
|
17
|
+
return pending ? undefined : value;
|
|
18
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { DumpNbt, TaggedReference } from '@terrafirmagreg-team/game/lib/dump/contract/ref';
|
|
2
|
+
|
|
3
|
+
export type AtlasPage = {
|
|
4
|
+
image: string;
|
|
5
|
+
w: number;
|
|
6
|
+
h: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type AtlasTint = 'none' | 'grass' | 'foliage' | 'water' | 'custom';
|
|
10
|
+
|
|
11
|
+
export type AtlasSprite = {
|
|
12
|
+
ref: TaggedReference;
|
|
13
|
+
page: number;
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
w: number;
|
|
17
|
+
h: number;
|
|
18
|
+
tint?: AtlasTint;
|
|
19
|
+
tintColor?: string;
|
|
20
|
+
langKey?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type AtlasKind = 'item' | 'block';
|
|
24
|
+
|
|
25
|
+
export type IconAtlas = {
|
|
26
|
+
sprites: AtlasSprite[];
|
|
27
|
+
pages: AtlasPage[];
|
|
28
|
+
modId: string;
|
|
29
|
+
dataset: string;
|
|
30
|
+
kind: AtlasKind;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type AtlasBundle = {
|
|
34
|
+
items: IconAtlas | null;
|
|
35
|
+
blocks: IconAtlas | null;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type SpriteQuery = {
|
|
39
|
+
kind?: 'item' | 'block' | 'fluid';
|
|
40
|
+
id: string;
|
|
41
|
+
tag?: DumpNbt;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type AtlasLookup = {
|
|
45
|
+
atlas: IconAtlas;
|
|
46
|
+
sprite: AtlasSprite;
|
|
47
|
+
page: AtlasPage;
|
|
48
|
+
spriteId: string;
|
|
49
|
+
};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AtlasLookup,
|
|
3
|
+
AtlasSprite,
|
|
4
|
+
IconAtlas,
|
|
5
|
+
SpriteQuery,
|
|
6
|
+
} from '@terrafirmagreg-team/game/lib/dump/assets/atlas/dto';
|
|
7
|
+
import {
|
|
8
|
+
asTaggedReference,
|
|
9
|
+
nbtSpecificity,
|
|
10
|
+
parseStackId,
|
|
11
|
+
} from '@terrafirmagreg-team/game/lib/dump/contract/ref';
|
|
12
|
+
import type { DumpNbt, TaggedReference } from '@terrafirmagreg-team/game/lib/dump/contract/ref';
|
|
13
|
+
import { formatNbt } from '@terrafirmagreg-team/game/lib/snbt';
|
|
14
|
+
|
|
15
|
+
export { atlasPagePublicUrl } from '@terrafirmagreg-team/game/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
|
+
}
|