@terrafirmagreg-team/game 0.1.0 → 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 (86) hide show
  1. package/package.json +15 -14
  2. package/src/components/entry/atlas-sprite.tsx +22 -35
  3. package/src/components/entry/copy-id-menu.tsx +22 -29
  4. package/src/components/entry/entry.tsx +99 -20
  5. package/src/components/entry/group.tsx +27 -46
  6. package/src/components/entry/icon.tsx +21 -27
  7. package/src/components/entry/list.tsx +15 -18
  8. package/src/components/entry/mc-text.tsx +3 -89
  9. package/src/components/entry/stack-count.tsx +12 -8
  10. package/src/components/entry/tooltip.tsx +52 -44
  11. package/src/components/mdx/item-icon.tsx +23 -15
  12. package/src/components/preview-icon.tsx +2 -1
  13. package/src/hooks/use-dump-location.ts +4 -2
  14. package/src/hooks/use-entry-context.ts +3 -1
  15. package/src/hooks/use-icon-source.ts +22 -13
  16. package/src/hooks/use-lang-map.ts +4 -2
  17. package/src/lib/entry/display.ts +2 -1
  18. package/src/lib/entry/entry.ts +5 -3
  19. package/src/lib/entry/formula.ts +1 -0
  20. package/src/lib/entry/href.ts +20 -12
  21. package/src/lib/entry/icon-load.ts +67 -31
  22. package/src/lib/entry/icon.ts +3 -2
  23. package/src/lib/entry/tooltip.ts +12 -10
  24. package/src/lib/preview.ts +4 -2
  25. package/src/lib/search.ts +18 -19
  26. package/src/locales/en_us.json +3 -7
  27. package/src/locales/ru_ru.json +3 -7
  28. package/src/locales.ts +1 -0
  29. package/src/types/entry.ts +41 -2
  30. package/styles/fonts.css +18 -2
  31. package/src/components/entry/frame.tsx +0 -32
  32. package/src/components/entry/icon/tabs.tsx +0 -118
  33. package/src/lib/dump/assets/atlas/dto.ts +0 -49
  34. package/src/lib/dump/assets/atlas/lookup.ts +0 -206
  35. package/src/lib/dump/assets/atlas/sprites.ts +0 -130
  36. package/src/lib/dump/assets/lang/load.ts +0 -17
  37. package/src/lib/dump/assets/lang/names.ts +0 -244
  38. package/src/lib/dump/assets/urls.ts +0 -143
  39. package/src/lib/dump/client/atlases.ts +0 -83
  40. package/src/lib/dump/client/dump.ts +0 -188
  41. package/src/lib/dump/client/groups.ts +0 -31
  42. package/src/lib/dump/client/keybinds.ts +0 -11
  43. package/src/lib/dump/client/mods.ts +0 -16
  44. package/src/lib/dump/client/registry.ts +0 -125
  45. package/src/lib/dump/client/tabs.ts +0 -27
  46. package/src/lib/dump/client/tags.ts +0 -64
  47. package/src/lib/dump/contract/kind.ts +0 -61
  48. package/src/lib/dump/contract/read.ts +0 -77
  49. package/src/lib/dump/contract/ref.ts +0 -262
  50. package/src/lib/dump/contract/text.ts +0 -86
  51. package/src/lib/dump/data/registry/dto.ts +0 -56
  52. package/src/lib/dump/data/registry/parse.ts +0 -291
  53. package/src/lib/dump/data/tab/dto.ts +0 -13
  54. package/src/lib/dump/data/tab/parse.ts +0 -26
  55. package/src/lib/dump/data/tag/dto.ts +0 -7
  56. package/src/lib/dump/data/tag/parse.ts +0 -57
  57. package/src/lib/dump/gen/data/guides/book.ts +0 -56
  58. package/src/lib/dump/gen/data/guides/category.ts +0 -30
  59. package/src/lib/dump/gen/data/guides/entry.ts +0 -48
  60. package/src/lib/dump/gen/data/models/model.ts +0 -3
  61. package/src/lib/dump/gen/data/quests/chapter.ts +0 -126
  62. package/src/lib/dump/gen/data/quests/chapter_groups.ts +0 -11
  63. package/src/lib/dump/gen/data/quests/data.ts +0 -40
  64. package/src/lib/dump/gen/data/recipes/recipe.ts +0 -78
  65. package/src/lib/dump/gen/index.ts +0 -20
  66. package/src/lib/dump/gen/keybinds.ts +0 -3
  67. package/src/lib/dump/gen/lang.ts +0 -3
  68. package/src/lib/dump/gen/manifest.ts +0 -59
  69. package/src/lib/dump/gen/registry/blocks.ts +0 -60
  70. package/src/lib/dump/gen/registry/entities.ts +0 -49
  71. package/src/lib/dump/gen/registry/fluids.ts +0 -48
  72. package/src/lib/dump/gen/registry/groups.ts +0 -14
  73. package/src/lib/dump/gen/registry/items.ts +0 -51
  74. package/src/lib/dump/gen/registry/recipe_categories.ts +0 -64
  75. package/src/lib/dump/gen/registry/recipes.ts +0 -3
  76. package/src/lib/dump/gen/registry/tabs.ts +0 -46
  77. package/src/lib/dump/gen/registry/tags.ts +0 -11
  78. package/src/lib/dump/json-reader.ts +0 -231
  79. package/src/lib/dump/manifest/dto.ts +0 -11
  80. package/src/lib/dump/manifest/parse.ts +0 -167
  81. package/src/lib/dump/mods/dto.ts +0 -40
  82. package/src/lib/dump/mods/index.ts +0 -81
  83. package/src/lib/dump/mods/load.ts +0 -61
  84. package/src/lib/dump/paths.ts +0 -171
  85. package/src/lib/dump/reader.ts +0 -15
  86. package/src/lib/snbt.ts +0 -246
@@ -1,16 +0,0 @@
1
- import { langString } from '@terrafirmagreg-team/game/lib/dump/assets/lang/names';
2
- import { clientDumpLang, loadClientManifest } from '@terrafirmagreg-team/game/lib/dump/client/dump';
3
- import { createAsyncCache } from '@terrafirmagreg-team/ui/lib/utils/async-cache';
4
-
5
- export const loadClientModNames = createAsyncCache(
6
- async (dataset: string, lang: string): Promise<Record<string, string>> => {
7
- const manifest = await loadClientManifest(dataset);
8
- const map = await clientDumpLang(dataset, lang);
9
- const out: Record<string, string> = {};
10
- for (const [ns, entry] of Object.entries(manifest?.namespaces ?? {})) {
11
- const key = entry?.display?.name;
12
- out[ns] = langString(map, key) ?? key ?? ns;
13
- }
14
- return out;
15
- },
16
- );
@@ -1,125 +0,0 @@
1
- import { asIdTable, EDGE_TABLE_FIELD } from '@terrafirmagreg-team/game/lib/dump/contract/read';
2
- import { asEntry, displayOf } from '@terrafirmagreg-team/game/lib/dump/data/registry/parse';
3
- import { parseStackId } from '@terrafirmagreg-team/game/lib/dump/contract/ref';
4
- import type { DumpDisplay, DumpText } from '@terrafirmagreg-team/game/lib/dump/contract/text';
5
- import type {
6
- DumpBlockEntry,
7
- DumpEntityEntry,
8
- DumpFluidEntry,
9
- DumpItemEntry,
10
- DumpRegistryEntry,
11
- DumpRegistryKind,
12
- } from '@terrafirmagreg-team/game/lib/dump/data/registry/dto';
13
- import { clientNamespaceEntries } from '@terrafirmagreg-team/game/lib/dump/client/dump';
14
- import { createAsyncCache } from '@terrafirmagreg-team/ui/lib/utils/async-cache';
15
-
16
- type RegistryByKind = {
17
- items: DumpItemEntry;
18
- blocks: DumpBlockEntry;
19
- fluids: DumpFluidEntry;
20
- entities: DumpEntityEntry;
21
- };
22
-
23
- const KIND_FILE: Record<DumpRegistryKind, string> = {
24
- items: 'items',
25
- blocks: 'blocks',
26
- fluids: 'fluids',
27
- entities: 'entities',
28
- };
29
-
30
- const clientRegistry = createAsyncCache(
31
- async (
32
- dataset: string,
33
- kind: DumpRegistryKind,
34
- ): Promise<Map<string, DumpRegistryEntry>> => {
35
- const out = new Map<string, DumpRegistryEntry>();
36
- const file = KIND_FILE[kind];
37
- const files = await clientNamespaceEntries(
38
- dataset,
39
- () => `registry/${file}.json`,
40
- (ns) => ns.registry.includes(file),
41
- );
42
- for (const { entries } of files) {
43
- const ids = asIdTable(entries);
44
- for (const [id, raw] of Object.entries(entries)) {
45
- if (id === EDGE_TABLE_FIELD) continue;
46
- const entry = asEntry(raw, kind, id, ids);
47
- if (!entry) continue;
48
- if (!out.has(id)) out.set(id, entry);
49
- if (entry.id !== id && !out.has(entry.id)) {
50
- out.set(entry.id, entry);
51
- }
52
- }
53
- }
54
- return out;
55
- },
56
- );
57
-
58
- export function loadClientRegistry<K extends DumpRegistryKind>(
59
- dataset: string,
60
- kind: K,
61
- ): Promise<Map<string, RegistryByKind[K]>> {
62
- return clientRegistry(dataset, kind) as Promise<Map<string, RegistryByKind[K]>>;
63
- }
64
-
65
- export function resolveClientStackDisplay(
66
- compactId: string,
67
- items: Map<string, DumpItemEntry>,
68
- fluids: Map<string, DumpFluidEntry>,
69
- ): DumpDisplay | undefined {
70
- const parsed = parseStackId(compactId);
71
- const item = items.get(parsed.id);
72
- if (item) return displayOf(item, parsed.nbt);
73
- const fluid = fluids.get(parsed.id);
74
- if (fluid) return displayOf(fluid, parsed.nbt);
75
- return undefined;
76
- }
77
-
78
- const SINGULAR_KIND: Record<string, DumpRegistryKind> = {
79
- item: 'items',
80
- block: 'blocks',
81
- fluid: 'fluids',
82
- entity: 'entities',
83
- };
84
-
85
- export function clientRegistryKindFor(kind: string): DumpRegistryKind {
86
- return SINGULAR_KIND[kind] ?? (kind as DumpRegistryKind);
87
- }
88
-
89
- export async function loadClientRegistryOrder(
90
- dataset: string,
91
- kind: string,
92
- ): Promise<string[]> {
93
- const entries = await loadClientRegistry(dataset, clientRegistryKindFor(kind));
94
- return [...entries.values()]
95
- .flatMap((entry) => (entry.order != null ? [entry] : []))
96
- .sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.id.localeCompare(b.id))
97
- .map((entry) => entry.id);
98
- }
99
-
100
- export async function loadClientRegistryTooltips(
101
- dataset: string,
102
- kind: string,
103
- ): Promise<Record<string, DumpText[]> | undefined> {
104
- const entries = await loadClientRegistry(dataset, clientRegistryKindFor(kind));
105
- const out: Record<string, DumpText[]> = {};
106
- for (const entry of entries.values()) {
107
- const tooltip = entry.display?.tooltip;
108
- if (tooltip?.length) out[entry.id] = tooltip;
109
- }
110
- return Object.keys(out).length ? out : undefined;
111
- }
112
-
113
- export async function loadClientRegistryTooltipsMod(
114
- dataset: string,
115
- kind: string,
116
- mod: string,
117
- ): Promise<Record<string, DumpText[]> | undefined> {
118
- const entries = await loadClientRegistry(dataset, clientRegistryKindFor(kind));
119
- const out: Record<string, DumpText[]> = {};
120
- for (const entry of entries.values()) {
121
- const lines = entry.display?.tooltipMods?.[mod];
122
- if (lines?.length) out[entry.id] = lines;
123
- }
124
- return Object.keys(out).length ? out : undefined;
125
- }
@@ -1,27 +0,0 @@
1
- import { asTab, asTabItems } from '@terrafirmagreg-team/game/lib/dump/data/tab/parse';
2
- import type { DumpCreativeTab, DumpCreativeTabs } from '@terrafirmagreg-team/game/lib/dump/data/tab/dto';
3
- import { clientNamespaceEntries } from '@terrafirmagreg-team/game/lib/dump/client/dump';
4
- import { createAsyncCache } from '@terrafirmagreg-team/ui/lib/utils/async-cache';
5
-
6
- export const loadClientTabs = createAsyncCache(
7
- async (dataset: string): Promise<DumpCreativeTabs> => {
8
- const tabs: DumpCreativeTab[] = [];
9
- const members: Record<string, string[]> = {};
10
- const files = await clientNamespaceEntries(
11
- dataset,
12
- () => 'registry/tabs.json',
13
- (ns) => ns.registry.includes('tabs'),
14
- );
15
- for (const { entries } of files) {
16
- for (const raw of Object.values(entries)) {
17
- const tab = asTab(raw);
18
- if (!tab) continue;
19
- tabs.push(tab);
20
- const items = asTabItems(raw);
21
- if (items.length) members[tab.id] = items;
22
- }
23
- }
24
- tabs.sort((a, b) => a.order - b.order || a.id.localeCompare(b.id));
25
- return { tabs, members };
26
- },
27
- );
@@ -1,64 +0,0 @@
1
- import { asIdTable, EDGE_TABLE_FIELD } from '@terrafirmagreg-team/game/lib/dump/contract/read';
2
- import { asTag } from '@terrafirmagreg-team/game/lib/dump/data/tag/parse';
3
- import type { DumpTag } from '@terrafirmagreg-team/game/lib/dump/data/tag/dto';
4
- import { clientNamespaceEntries } from '@terrafirmagreg-team/game/lib/dump/client/dump';
5
- import { tagHashId, tagIdForms } from '@terrafirmagreg-team/game/lib/dump/paths';
6
- import { createAsyncCache } from '@terrafirmagreg-team/ui/lib/utils/async-cache';
7
-
8
- function indexKeys(tag: DumpTag, manifestKey: string): string[] {
9
- return [...new Set([manifestKey, ...tagIdForms(tag.id)])];
10
- }
11
-
12
- const loadClientTags = createAsyncCache(async (dataset: string) => {
13
- const map = new Map<string, DumpTag>();
14
- const files = await clientNamespaceEntries(
15
- dataset,
16
- () => 'registry/tags.json',
17
- (ns) => ns.registry.includes('tags'),
18
- );
19
- for (const { entries } of files) {
20
- const ids = asIdTable(entries);
21
- for (const [manifestKey, raw] of Object.entries(entries)) {
22
- if (manifestKey === EDGE_TABLE_FIELD) continue;
23
- const tag = asTag(raw, manifestKey, ids);
24
- if (!tag) continue;
25
- for (const alias of indexKeys(tag, manifestKey)) {
26
- if (!map.has(alias)) map.set(alias, tag);
27
- }
28
- }
29
- }
30
- return map;
31
- });
32
-
33
- export async function resolveClientTag(
34
- dataset: string,
35
- id: string,
36
- ): Promise<DumpTag | undefined> {
37
- const tags = await loadClientTags(dataset);
38
- for (const form of tagIdForms(id)) {
39
- const hit = tags.get(form);
40
- if (hit) return hit;
41
- }
42
- return undefined;
43
- }
44
-
45
- const MEMBER_REGISTRY: Record<string, string> = {
46
- item: 'items',
47
- block: 'blocks',
48
- fluid: 'fluids',
49
- entity: 'entities',
50
- };
51
-
52
- export async function loadClientTagMembers(
53
- dataset: string,
54
- kind: string,
55
- ): Promise<Record<string, string[]> | undefined> {
56
- const registry = MEMBER_REGISTRY[kind] ?? kind;
57
- const out: Record<string, string[]> = {};
58
- for (const tag of (await loadClientTags(dataset)).values()) {
59
- if (tag.registry !== registry || !tag.members.length) continue;
60
- out[tagHashId(tag.id)] ??= tag.members;
61
- }
62
- return Object.keys(out).length ? out : undefined;
63
- }
64
-
@@ -1,61 +0,0 @@
1
- import {
2
- asTaggedReference,
3
- stackIdFromRef,
4
- type ReferenceKind,
5
- type TaggedReference,
6
- } from '@terrafirmagreg-team/game/lib/dump/contract/ref';
7
- import { asRecord, asString } from '@terrafirmagreg-team/game/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
- }
@@ -1,77 +0,0 @@
1
-
2
-
3
- export function asRecord(value: unknown): Record<string, unknown> | undefined {
4
- if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
5
- return value as Record<string, unknown>;
6
- }
7
-
8
- export function asString(value: unknown): string | undefined {
9
- return typeof value === 'string' && value ? value : undefined;
10
- }
11
-
12
- export function asNumber(value: unknown): number | undefined {
13
- return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
14
- }
15
-
16
- export function asBool(value: unknown): boolean | undefined {
17
- if (typeof value === 'boolean') return value;
18
- if (value === 1) return true;
19
- if (value === 0) return false;
20
- return undefined;
21
- }
22
-
23
- export function asStringList(value: unknown): string[] {
24
- if (!Array.isArray(value)) return [];
25
- return value.filter((item): item is string => typeof item === 'string' && Boolean(item));
26
- }
27
-
28
- export const EDGE_TABLE_FIELD = '$ids';
29
-
30
- export function asIdTable(file: unknown): string[] | undefined {
31
- const ids = asStringList(asRecord(file)?.[EDGE_TABLE_FIELD]);
32
- return ids.length ? ids : undefined;
33
- }
34
-
35
- export function asEdgeList(value: unknown, ids?: string[]): string[] {
36
- if (!Array.isArray(value)) return [];
37
- const out: string[] = [];
38
- for (const item of value) {
39
- if (typeof item === 'string') {
40
- if (item) out.push(item);
41
- continue;
42
- }
43
- if (typeof item === 'number' && Number.isInteger(item)) {
44
- const ref = ids?.[item];
45
- if (ref) out.push(ref);
46
- }
47
- }
48
- return out;
49
- }
50
-
51
- export function resolveEdgeRows(
52
- map: Record<string, unknown>,
53
- ): Record<string, Record<string, unknown>> {
54
- const ids = asIdTable(map);
55
- const out: Record<string, Record<string, unknown>> = {};
56
- for (const [key, value] of Object.entries(map)) {
57
- if (key === EDGE_TABLE_FIELD) continue;
58
- const row = asRecord(value);
59
- if (!row) continue;
60
- if (ids) {
61
- if (Array.isArray(row.usedBy)) row.usedBy = asEdgeList(row.usedBy, ids);
62
- if (Array.isArray(row.producedBy)) row.producedBy = asEdgeList(row.producedBy, ids);
63
- }
64
- out[key] = row;
65
- }
66
- return out;
67
- }
68
-
69
- export function asStringMap(value: unknown): Record<string, string> | undefined {
70
- const row = asRecord(value);
71
- if (!row) return undefined;
72
- const out: Record<string, string> = {};
73
- for (const [key, item] of Object.entries(row)) {
74
- if (typeof item === 'string') out[key] = item;
75
- }
76
- return out;
77
- }
@@ -1,262 +0,0 @@
1
-
2
-
3
- import { formatNbt, parseSnbt } from '@terrafirmagreg-team/game/lib/snbt';
4
- import { asRecord, asString } from '@terrafirmagreg-team/game/lib/dump/contract/read';
5
-
6
- const REFERENCE_KINDS = [
7
- 'item',
8
- 'block',
9
- 'fluid',
10
- 'entity',
11
- 'itemTag',
12
- 'blockTag',
13
- 'fluidTag',
14
- 'entityTag',
15
- 'texture',
16
- 'model',
17
- 'recipe',
18
- 'guide',
19
- 'quest',
20
- 'chapter',
21
- ] as const;
22
-
23
- export type ReferenceKind = (typeof REFERENCE_KINDS)[number];
24
-
25
- const TAG_KINDS = ['itemTag', 'blockTag', 'fluidTag', 'entityTag'] as const;
26
-
27
- const ICON_KINDS: readonly ReferenceKind[] = [
28
- 'item',
29
- 'block',
30
- 'fluid',
31
- 'entity',
32
- ...TAG_KINDS,
33
- 'texture',
34
- ];
35
-
36
- export type DumpNbt =
37
- | string
38
- | number
39
- | boolean
40
- | null
41
- | DumpNbt[]
42
- | { [key: string]: DumpNbt };
43
-
44
- export type TaggedReference = {
45
- kind: ReferenceKind;
46
- id: string;
47
- tag?: DumpNbt;
48
- };
49
-
50
- export type ParsedStackId = {
51
- id: string;
52
- nbt?: DumpNbt;
53
- };
54
-
55
- function asReferenceKind(value: unknown): ReferenceKind | undefined {
56
- if (typeof value !== 'string') return undefined;
57
- return (REFERENCE_KINDS as readonly string[]).includes(value)
58
- ? (value as ReferenceKind)
59
- : undefined;
60
- }
61
-
62
- export function asNbt(value: unknown): DumpNbt | undefined {
63
- if (value === null) return null;
64
- if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
65
- return value;
66
- }
67
- if (Array.isArray(value)) return value as DumpNbt[];
68
- const row = asRecord(value);
69
- if (!row || Object.keys(row).length === 0) return undefined;
70
- return row as { [key: string]: DumpNbt };
71
- }
72
-
73
- export function nbtAt(
74
- tag: DumpNbt | undefined,
75
- ...path: (string | number)[]
76
- ): DumpNbt | undefined {
77
- let cur: DumpNbt | undefined = tag;
78
- for (const key of path) {
79
- if (cur === undefined || cur === null) return undefined;
80
- if (typeof key === 'number') {
81
- if (!Array.isArray(cur)) return undefined;
82
- cur = cur[key];
83
- continue;
84
- }
85
- if (typeof cur !== 'object' || Array.isArray(cur)) return undefined;
86
- cur = cur[key];
87
- }
88
- return cur;
89
- }
90
-
91
- export function parseStackId(compact: string): ParsedStackId {
92
- const brace = compact.indexOf('{');
93
- if (brace <= 0) return { id: compact };
94
- const id = compact.slice(0, brace);
95
- if (!id) return { id: compact };
96
- try {
97
- const nbt = asNbt(parseSnbt(compact.slice(brace)));
98
- return nbt !== undefined ? { id, nbt } : { id };
99
- } catch {
100
- return { id };
101
- }
102
- }
103
-
104
- export function stackIdBase(compact: string): string {
105
- const brace = compact.indexOf('{');
106
- return brace > 0 ? compact.slice(0, brace) : compact;
107
- }
108
-
109
- export function canonicalStackId(compact: string): string {
110
- const brace = compact.indexOf('{');
111
- if (brace <= 0) return compact;
112
- const parsed = parseStackId(compact);
113
- const nbt = formatNbt(parsed.nbt);
114
- return nbt !== undefined && nbt.startsWith('{')
115
- ? `${parsed.id}${nbt}`
116
- : compact;
117
- }
118
-
119
- export function asTaggedReference(value: unknown): TaggedReference | undefined {
120
- const row = asRecord(value);
121
- if (!row) return undefined;
122
- const kind = asReferenceKind(row.kind);
123
- const id = asString(row.id);
124
- if (!kind || !id) return undefined;
125
- const tag = asNbt(row.tag);
126
- return { kind, id, ...(tag !== undefined ? { tag } : {}) };
127
- }
128
-
129
- export function isTagKind(
130
- kind: ReferenceKind | undefined,
131
- ): kind is (typeof TAG_KINDS)[number] {
132
- return kind !== undefined && (TAG_KINDS as readonly string[]).includes(kind);
133
- }
134
-
135
- export function tagKindOf(
136
- kind: ReferenceKind | undefined,
137
- ): 'item' | 'block' | 'fluid' | 'entity' | undefined {
138
- switch (kind) {
139
- case 'itemTag':
140
- return 'item';
141
- case 'blockTag':
142
- return 'block';
143
- case 'fluidTag':
144
- return 'fluid';
145
- case 'entityTag':
146
- return 'entity';
147
- default:
148
- return undefined;
149
- }
150
- }
151
-
152
- export function isIconKind(kind: ReferenceKind | undefined): boolean {
153
- return kind !== undefined && ICON_KINDS.includes(kind);
154
- }
155
-
156
- export function classifyRefId(
157
- id: string,
158
- kind?: ReferenceKind,
159
- ): TaggedReference | undefined {
160
- const trimmed = id.trim();
161
- if (!trimmed) return undefined;
162
- const parsed = parseStackId(
163
- trimmed.startsWith('#') ? trimmed.slice(1) : trimmed,
164
- );
165
- const resolved = trimmed.startsWith('#')
166
- ? (isTagKind(kind) ? kind : 'itemTag')
167
- : (kind ?? 'item');
168
- return {
169
- kind: resolved,
170
- id: parsed.id,
171
- ...(parsed.nbt !== undefined ? { tag: parsed.nbt } : {}),
172
- };
173
- }
174
-
175
- export function refId(ref: TaggedReference): string {
176
- return stackIdFromRef(ref);
177
- }
178
-
179
- export function compactRefId(value: unknown): string | undefined {
180
- if (typeof value === 'string' && value) return value;
181
- const tagged = asTaggedReference(value);
182
- return tagged ? stackIdFromRef(tagged) : undefined;
183
- }
184
-
185
- function nbtEquals(left: DumpNbt | undefined, right: DumpNbt | undefined): boolean {
186
- if (left === right) return true;
187
- if (left === undefined || right === undefined) return false;
188
- if (left === null || right === null) return left === right;
189
- if (typeof left !== typeof right) return false;
190
- if (typeof left !== 'object') return left === right;
191
- if (Array.isArray(left)) {
192
- return (
193
- Array.isArray(right) &&
194
- left.length === right.length &&
195
- left.every((value, index) => nbtEquals(value, right[index]))
196
- );
197
- }
198
- if (Array.isArray(right)) return false;
199
- const leftRow = left as { [key: string]: DumpNbt };
200
- const rightRow = right as { [key: string]: DumpNbt };
201
- const leftKeys = Object.keys(leftRow).sort();
202
- const rightKeys = Object.keys(rightRow).sort();
203
- if (leftKeys.length !== rightKeys.length) return false;
204
- return leftKeys.every(
205
- (key, index) => key === rightKeys[index] && nbtEquals(leftRow[key], rightRow[key]),
206
- );
207
- }
208
-
209
- function nbtMatches(pattern: DumpNbt | undefined, actual: DumpNbt | undefined): boolean {
210
- if (pattern === undefined) return actual === undefined;
211
- if (actual === undefined || actual === null) return pattern === actual;
212
- if (pattern === null) return actual === null;
213
- if (typeof pattern !== 'object') return pattern === actual;
214
- if (Array.isArray(pattern)) {
215
- if (!Array.isArray(actual) || pattern.length !== actual.length) return false;
216
- return pattern.every((item, index) => nbtMatches(item, actual[index]));
217
- }
218
- if (typeof actual !== 'object' || Array.isArray(actual) || actual === null) return false;
219
- const actualRow = actual as { [key: string]: DumpNbt };
220
- return Object.entries(pattern).every(([key, value]) => nbtMatches(value, actualRow[key]));
221
- }
222
-
223
- function nbtKeyCount(tag: DumpNbt | undefined): number {
224
- return tag !== null && typeof tag === 'object' && !Array.isArray(tag)
225
- ? Object.keys(tag ?? {}).length
226
- : 0;
227
- }
228
-
229
- function nbtSharedKeys(left: DumpNbt | undefined, right: DumpNbt | undefined): number {
230
- if (!left || !right) return 0;
231
- if (typeof left !== 'object' || Array.isArray(left)) return 0;
232
- if (typeof right !== 'object' || Array.isArray(right)) return 0;
233
- const rightRow = right as { [key: string]: DumpNbt };
234
- let shared = 0;
235
- for (const [key, value] of Object.entries(left)) {
236
- if (key in rightRow && nbtEquals(value, rightRow[key])) shared++;
237
- }
238
- return shared;
239
- }
240
-
241
- export function nbtSpecificity(candidate: DumpNbt, query: DumpNbt): number {
242
- if (nbtEquals(candidate, query)) return 500;
243
- if (nbtMatches(candidate, query)) return 400 + nbtKeyCount(candidate);
244
- if (nbtMatches(query, candidate)) return 300 - nbtKeyCount(candidate);
245
- const shared = nbtSharedKeys(candidate, query);
246
- return shared > 0 ? 200 + shared : -1;
247
- }
248
-
249
- export function stackIdFromRef(ref: TaggedReference): string {
250
- switch (ref.kind) {
251
- case 'itemTag':
252
- case 'blockTag':
253
- case 'fluidTag':
254
- case 'entityTag':
255
- return `#${ref.id.replace(/^#+/, '')}`;
256
- default: {
257
- const tag = formatNbt(ref.tag);
258
- return tag !== undefined && tag.startsWith('{') ? `${ref.id}${tag}` : ref.id;
259
- }
260
- }
261
- }
262
-