minecraft-inventory 0.1.5 → 0.1.6
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/README.md +16 -3
- package/package.json +1 -1
- package/src/bundledTexturesConfig.ts +126 -0
- package/src/cache/blockRenderer.ts +103 -0
- package/src/cache/textureCache.ts +10 -0
- package/src/components/InventoryOverlay/InventoryOverlay.tsx +1 -1
- package/src/components/InventoryWindow/InventoryBackground.tsx +1 -1
- package/src/components/ItemCanvas/ItemCanvas.tsx +103 -17
- package/src/components/JEI/JEI.module.css +2 -0
- package/src/components/JEI/JEI.tsx +2 -3
- package/src/components/Text/MessageFormattedString.tsx +1 -0
- package/src/components/Tooltip/Tooltip.tsx +1 -2
- package/src/connector/mineflayer.ts +15 -3
- package/src/generated/localTextures.ts +66 -51
- package/src/index.tsx +18 -0
- package/src/registry/inventories.ts +9 -7
- package/src/types.ts +32 -0
package/README.md
CHANGED
|
@@ -262,16 +262,16 @@ pnpm add mc-assets # optional peer dependency
|
|
|
262
262
|
|
|
263
263
|
```tsx
|
|
264
264
|
import { TextureProvider, InventoryOverlay } from 'minecraft-inventory'
|
|
265
|
-
import {
|
|
265
|
+
import { localBundledTexturesConfig } from './bundledTexturesConfig'
|
|
266
266
|
|
|
267
267
|
// GUI container backgrounds (chest, furnace, etc.) load from bundled mc-assets;
|
|
268
268
|
// item/block textures still use the default remote URLs unless you override them.
|
|
269
|
-
<TextureProvider config={
|
|
269
|
+
<TextureProvider config={localBundledTexturesConfig}>
|
|
270
270
|
<InventoryOverlay type="chest" ... />
|
|
271
271
|
</TextureProvider>
|
|
272
272
|
```
|
|
273
273
|
|
|
274
|
-
The generated file
|
|
274
|
+
The generated file (`src/generated/localTextures.ts`) exports `bundledTextureMap`, `allTexturePaths`, and `allContainerPaths`. Use `createBundledTexturesConfig({ remoteFallback: true, bundledTextureMap? })` to get a config with `getGuiTextureUrl`, `setOverride(path, image)`, `clearOverrides()`, `setRemoteFallback(enabled)`, and `resetRenderedSlots()`. Pass short paths to `setOverride` (e.g. `gui/sprites/container/anvil/text_field_disabled.png`) — the version prefix exists only for the texture import generator. Call `resetRenderedSlots()` after multiple `setOverride` calls to invalidate cached textures so slots re-request them. `localBundledTexturesConfig` is the default instance. Re-run `pnpm gen:textures` after changing [inventory types](#adding-new-inventory-types).
|
|
275
275
|
|
|
276
276
|
---
|
|
277
277
|
|
|
@@ -546,9 +546,18 @@ interface ItemStack {
|
|
|
546
546
|
lore?: string[]
|
|
547
547
|
durability?: number // Current durability value
|
|
548
548
|
maxDurability?: number // Max durability (renders bar when < max)
|
|
549
|
+
textureKey?: string // Override texture path for getItemTextureUrl
|
|
550
|
+
texture?: string | HTMLImageElement // Direct texture (bypasses URL lookup)
|
|
551
|
+
blockTexture?: BlockTextureRender // Isometric block icon from face slices
|
|
552
|
+
debugKey?: string
|
|
549
553
|
}
|
|
550
554
|
```
|
|
551
555
|
|
|
556
|
+
**Texture overrides (mineflayer `itemMapper`):**
|
|
557
|
+
- `texture: string` — URL or data URL, fetched and cached.
|
|
558
|
+
- `texture: HTMLImageElement` — Preloaded image, used directly.
|
|
559
|
+
- `blockTexture: { source, top, left, right }` — Composite an isometric block icon from three face slices. Each face has `slice: [x, y, w, h]` in source texture pixels. Uses a pool of aux canvases (not recreated per slot).
|
|
560
|
+
|
|
552
561
|
---
|
|
553
562
|
|
|
554
563
|
## Project Structure
|
|
@@ -613,6 +622,10 @@ Each Minecraft window type has a fixed slot layout defined by the server protoco
|
|
|
613
622
|
- [`prismarine-windows`](https://github.com/PrismarineJS/prismarine-windows) for JS slot maps
|
|
614
623
|
- Minecraft source: `net/minecraft/world/inventory/` for the canonical layout
|
|
615
624
|
|
|
625
|
+
### Memory leak note (Tooltip / MessageFormattedString)
|
|
626
|
+
|
|
627
|
+
If opening tooltips increases memory without returning to baseline, see [docs/MEMORY_LEAK_ANALYSIS.md](docs/MEMORY_LEAK_ANALYSIS.md). The likely cause is `filter:blur(2px)` on obfuscated (§k) text creating retained compositor layers.
|
|
628
|
+
|
|
616
629
|
### Connector protocol
|
|
617
630
|
|
|
618
631
|
When implementing a custom connector, actions have these shapes:
|
package/package.json
CHANGED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized config for bundled GUI textures. Uses maps from generated localTextures.
|
|
3
|
+
* Supports overrides via short paths (e.g. "gui/sprites/container/anvil/text_field_disabled.png"),
|
|
4
|
+
* remote fallback toggle, and cache invalidation after overrides.
|
|
5
|
+
*
|
|
6
|
+
* Version prefix (e.g. "1.21.11/textures/") in bundled keys exists only for the texture
|
|
7
|
+
* import generator — setOverride accepts short paths without it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
bundledTextureMap,
|
|
12
|
+
allTexturePaths,
|
|
13
|
+
allContainerPaths,
|
|
14
|
+
} from './generated/localTextures'
|
|
15
|
+
import type { TextureConfig } from './context/TextureContext'
|
|
16
|
+
import { clearTextureCache } from './cache/textureCache'
|
|
17
|
+
|
|
18
|
+
const MC_ASSETS_REMOTE =
|
|
19
|
+
'https://raw.githubusercontent.com/zardoy/mc-assets/refs/heads/gh-pages'
|
|
20
|
+
|
|
21
|
+
/** Normalize path to short form: "gui/..." (strip version prefix if present). */
|
|
22
|
+
function toShortPath(path: string): string {
|
|
23
|
+
const m = path.match(/^\d[\d.]+\/textures\/(.+)$/)
|
|
24
|
+
return m ? m[1] : path
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Default version for building mc-assets URLs when path is short. */
|
|
28
|
+
const DEFAULT_VERSION = '1.21.11'
|
|
29
|
+
|
|
30
|
+
export interface BundledTexturesConfigOptions {
|
|
31
|
+
/** When true (default), unknown paths fall back to remote mc-assets URL. */
|
|
32
|
+
remoteFallback?: boolean
|
|
33
|
+
/** Override the bundled texture map (e.g. for custom builds). */
|
|
34
|
+
bundledTextureMap?: Record<string, string | undefined>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface BundledTexturesConfig extends Pick<TextureConfig, 'getGuiTextureUrl'> {
|
|
38
|
+
/**
|
|
39
|
+
* Override a texture. Pass short path (e.g. "gui/sprites/container/anvil/text_field_disabled.png")
|
|
40
|
+
* and image URL. Version prefix is only for the import generator — not needed here.
|
|
41
|
+
*/
|
|
42
|
+
setOverride(path: string, image: string): void
|
|
43
|
+
/** Remove all texture overrides. */
|
|
44
|
+
clearOverrides(): void
|
|
45
|
+
/** Enable or disable remote fallback for unknown paths. */
|
|
46
|
+
setRemoteFallback(enabled: boolean): void
|
|
47
|
+
/**
|
|
48
|
+
* Invalidate cached textures so slots re-request them. Call after multiple setOverride
|
|
49
|
+
* calls when you want UI to immediately reflect new overrides.
|
|
50
|
+
*/
|
|
51
|
+
resetRenderedSlots(): void
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build a reverse map: shortPath -> bundled URL.
|
|
56
|
+
*/
|
|
57
|
+
function buildShortPathToBundled(
|
|
58
|
+
bundled: Record<string, string | undefined>,
|
|
59
|
+
): Map<string, string> {
|
|
60
|
+
const map = new Map<string, string>()
|
|
61
|
+
for (const [key, url] of Object.entries(bundled)) {
|
|
62
|
+
if (!url) continue
|
|
63
|
+
const short = toShortPath(key)
|
|
64
|
+
if (!map.has(short)) map.set(short, url)
|
|
65
|
+
}
|
|
66
|
+
return map
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Create a bundled textures config with overrides and optional remote fallback.
|
|
71
|
+
* Pass the returned config to <TextureProvider config={...}>.
|
|
72
|
+
*/
|
|
73
|
+
export function createBundledTexturesConfig(
|
|
74
|
+
options?: BundledTexturesConfigOptions,
|
|
75
|
+
): BundledTexturesConfig {
|
|
76
|
+
let remoteFallbackEnabled = options?.remoteFallback ?? true
|
|
77
|
+
const overrides = new Map<string, string>()
|
|
78
|
+
const bundled = options?.bundledTextureMap ?? bundledTextureMap
|
|
79
|
+
const shortToBundled = buildShortPathToBundled(bundled)
|
|
80
|
+
|
|
81
|
+
function getGuiTextureUrl(path: string): string {
|
|
82
|
+
const shortPath = toShortPath(path)
|
|
83
|
+
|
|
84
|
+
const override = overrides.get(shortPath)
|
|
85
|
+
if (override) return override
|
|
86
|
+
|
|
87
|
+
const bundledUrl = bundled[path] ?? shortToBundled.get(shortPath)
|
|
88
|
+
if (bundledUrl) return bundledUrl
|
|
89
|
+
|
|
90
|
+
if (
|
|
91
|
+
remoteFallbackEnabled &&
|
|
92
|
+
path.endsWith('.png') &&
|
|
93
|
+
path.includes('/textures/')
|
|
94
|
+
) {
|
|
95
|
+
return `${MC_ASSETS_REMOTE}/${path}`
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (remoteFallbackEnabled && shortPath.endsWith('.png')) {
|
|
99
|
+
const versioned = `${DEFAULT_VERSION}/textures/${shortPath}`
|
|
100
|
+
return `${MC_ASSETS_REMOTE}/${versioned}`
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return path
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
getGuiTextureUrl,
|
|
108
|
+
setOverride(path: string, image: string) {
|
|
109
|
+
overrides.set(toShortPath(path), image)
|
|
110
|
+
},
|
|
111
|
+
clearOverrides() {
|
|
112
|
+
overrides.clear()
|
|
113
|
+
},
|
|
114
|
+
setRemoteFallback(enabled: boolean) {
|
|
115
|
+
remoteFallbackEnabled = enabled
|
|
116
|
+
},
|
|
117
|
+
resetRenderedSlots() {
|
|
118
|
+
clearTextureCache()
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Default config instance for <TextureProvider config={localBundledTexturesConfig}>. */
|
|
124
|
+
export const localBundledTexturesConfig = createBundledTexturesConfig()
|
|
125
|
+
|
|
126
|
+
export { allTexturePaths, allContainerPaths }
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pool of offscreen canvases for block-face compositing.
|
|
3
|
+
* Reuses a fixed set of canvases instead of creating new ones per render.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const POOL_SIZE = 8
|
|
7
|
+
const OUTPUT_SIZE = 32
|
|
8
|
+
|
|
9
|
+
const pool: HTMLCanvasElement[] = []
|
|
10
|
+
const inUse = new Set<HTMLCanvasElement>()
|
|
11
|
+
|
|
12
|
+
function getCanvas(): HTMLCanvasElement {
|
|
13
|
+
const free = pool.find((c) => !inUse.has(c))
|
|
14
|
+
if (free) {
|
|
15
|
+
inUse.add(free)
|
|
16
|
+
return free
|
|
17
|
+
}
|
|
18
|
+
if (pool.length < POOL_SIZE) {
|
|
19
|
+
const c = document.createElement('canvas')
|
|
20
|
+
c.width = OUTPUT_SIZE
|
|
21
|
+
c.height = OUTPUT_SIZE
|
|
22
|
+
pool.push(c)
|
|
23
|
+
inUse.add(c)
|
|
24
|
+
return c
|
|
25
|
+
}
|
|
26
|
+
const c = pool[pool.length - 1]
|
|
27
|
+
inUse.add(c)
|
|
28
|
+
return c
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function releaseCanvas(c: HTMLCanvasElement): void {
|
|
32
|
+
inUse.delete(c)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Slice rect [x, y, width, height] */
|
|
36
|
+
type Slice = [number, number, number, number]
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Composite top/left/right block faces into an isometric-style icon.
|
|
40
|
+
* Uses a pool of canvases; not recreated each time.
|
|
41
|
+
*/
|
|
42
|
+
export function renderBlockIcon(
|
|
43
|
+
source: HTMLImageElement | string,
|
|
44
|
+
top: Slice,
|
|
45
|
+
left: Slice,
|
|
46
|
+
right: Slice,
|
|
47
|
+
): Promise<string> {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const isUrl = typeof source === 'string'
|
|
50
|
+
const img = isUrl ? new Image() : (source as HTMLImageElement)
|
|
51
|
+
|
|
52
|
+
const draw = (image: HTMLImageElement) => {
|
|
53
|
+
let canvas: HTMLCanvasElement | null = null
|
|
54
|
+
try {
|
|
55
|
+
canvas = getCanvas()
|
|
56
|
+
const ctx = canvas.getContext('2d')
|
|
57
|
+
if (!ctx) throw new Error('No 2d context')
|
|
58
|
+
|
|
59
|
+
ctx.imageSmoothingEnabled = false
|
|
60
|
+
ctx.clearRect(0, 0, OUTPUT_SIZE, OUTPUT_SIZE)
|
|
61
|
+
|
|
62
|
+
const s = OUTPUT_SIZE / 16
|
|
63
|
+
const [tx, ty, tw, th] = top
|
|
64
|
+
const [lx, ly, lw, lh] = left
|
|
65
|
+
const [rx, ry, rw, rh] = right
|
|
66
|
+
|
|
67
|
+
// Isometric layout: top face tilted, left and right as sides
|
|
68
|
+
ctx.save()
|
|
69
|
+
ctx.translate(8 * s, 2 * s)
|
|
70
|
+
ctx.rotate(-Math.PI / 4)
|
|
71
|
+
ctx.drawImage(image, tx, ty, tw, th, -4 * s, -4 * s, 8 * s, 8 * s)
|
|
72
|
+
ctx.restore()
|
|
73
|
+
|
|
74
|
+
ctx.drawImage(image, lx, ly, lw, lh, 0, 12 * s, 10 * s, 10 * s)
|
|
75
|
+
ctx.drawImage(image, rx, ry, rw, rh, 12 * s, 12 * s, 10 * s, 10 * s)
|
|
76
|
+
|
|
77
|
+
const dataUrl = canvas.toDataURL('image/png')
|
|
78
|
+
resolve(dataUrl)
|
|
79
|
+
} catch (e) {
|
|
80
|
+
reject(e)
|
|
81
|
+
} finally {
|
|
82
|
+
if (canvas) releaseCanvas(canvas)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (isUrl) {
|
|
87
|
+
img.onload = () => draw(img)
|
|
88
|
+
img.onerror = () =>
|
|
89
|
+
reject(new Error('Failed to load block texture'))
|
|
90
|
+
img.crossOrigin = 'anonymous'
|
|
91
|
+
img.src = source
|
|
92
|
+
} else if (
|
|
93
|
+
(img).complete &&
|
|
94
|
+
(img).naturalWidth > 0
|
|
95
|
+
) {
|
|
96
|
+
draw(img)
|
|
97
|
+
} else {
|
|
98
|
+
img.onload = () => draw(img)
|
|
99
|
+
img.onerror = () =>
|
|
100
|
+
reject(new Error('Block texture image failed'))
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
}
|
|
@@ -45,6 +45,16 @@ export function isTextureFailed(url: string): boolean {
|
|
|
45
45
|
return failedUrls.has(url)
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Clear all cached texture data URLs and failed state.
|
|
50
|
+
* Call after setOverride in bundledTexturesConfig so slots re-request textures.
|
|
51
|
+
*/
|
|
52
|
+
export function clearTextureCache(): void {
|
|
53
|
+
dataUrlCache.clear()
|
|
54
|
+
failedUrls.clear()
|
|
55
|
+
inflight.clear()
|
|
56
|
+
}
|
|
57
|
+
|
|
48
58
|
/**
|
|
49
59
|
* Hook that resolves a URL to a cached base64 data URL.
|
|
50
60
|
* Returns:
|
|
@@ -37,7 +37,7 @@ export function InventoryBackground({
|
|
|
37
37
|
const { scale } = useScale()
|
|
38
38
|
|
|
39
39
|
const bgUrl = textures.getGuiTextureUrl(definition.backgroundTexture)
|
|
40
|
-
const isStitched = definition.containerRows != null && definition.containerRows
|
|
40
|
+
const isStitched = definition.containerRows != null && definition.containerRows <= 6
|
|
41
41
|
|
|
42
42
|
const w = definition.backgroundWidth * scale
|
|
43
43
|
const h = definition.backgroundHeight * scale
|
|
@@ -3,6 +3,7 @@ import type { ItemStack } from '../../types'
|
|
|
3
3
|
import { useTextures } from '../../context/TextureContext'
|
|
4
4
|
import { useScale } from '../../context/ScaleContext'
|
|
5
5
|
import { useDataUrl, isTextureFailed } from '../../cache/textureCache'
|
|
6
|
+
import { renderBlockIcon } from '../../cache/blockRenderer'
|
|
6
7
|
|
|
7
8
|
interface ItemCanvasProps {
|
|
8
9
|
item: ItemStack
|
|
@@ -22,28 +23,68 @@ function getDurabilityColor(current: number, max: number): string {
|
|
|
22
23
|
return '#ff5555'
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
className,
|
|
32
|
-
style,
|
|
33
|
-
}: ItemCanvasProps) {
|
|
26
|
+
/** Resolve item texture source: direct override, block render, or URL lookup. */
|
|
27
|
+
function useItemTextureSrc(item: ItemStack): {
|
|
28
|
+
src: string | null
|
|
29
|
+
failed: boolean
|
|
30
|
+
loading: boolean
|
|
31
|
+
} {
|
|
34
32
|
const textures = useTextures()
|
|
35
|
-
const { contentSize, pixelSize } = useScale()
|
|
36
|
-
const renderSize = size ?? contentSize
|
|
37
33
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
34
|
+
// Block texture: always run effect for blockTexture case
|
|
35
|
+
const blockConfig = item.blockTexture ?? null
|
|
36
|
+
const [blockDataUrl, setBlockDataUrl] = useState<string | null>(null)
|
|
37
|
+
const [blockFailed, setBlockFailed] = useState(false)
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (!blockConfig) return
|
|
41
|
+
let cancelled = false
|
|
42
|
+
renderBlockIcon(
|
|
43
|
+
blockConfig.source,
|
|
44
|
+
blockConfig.top.slice,
|
|
45
|
+
blockConfig.left.slice,
|
|
46
|
+
blockConfig.right.slice,
|
|
47
|
+
)
|
|
48
|
+
.then((url) => {
|
|
49
|
+
if (!cancelled) setBlockDataUrl(url)
|
|
50
|
+
})
|
|
51
|
+
.catch(() => {
|
|
52
|
+
if (!cancelled) setBlockFailed(true)
|
|
53
|
+
})
|
|
54
|
+
return () => {
|
|
55
|
+
cancelled = true
|
|
56
|
+
}
|
|
57
|
+
}, [
|
|
58
|
+
blockConfig?.source,
|
|
59
|
+
blockConfig ? String(blockConfig.top.slice) : '',
|
|
60
|
+
blockConfig ? String(blockConfig.left.slice) : '',
|
|
61
|
+
blockConfig ? String(blockConfig.right.slice) : '',
|
|
62
|
+
])
|
|
63
|
+
|
|
64
|
+
// Reset block state when switching away from blockTexture
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (!blockConfig) {
|
|
67
|
+
setBlockDataUrl(null)
|
|
68
|
+
setBlockFailed(false)
|
|
69
|
+
}
|
|
70
|
+
}, [blockConfig])
|
|
71
|
+
|
|
72
|
+
// Direct texture (string) - use cache
|
|
73
|
+
const directUrl =
|
|
74
|
+
typeof item.texture === 'string' ? item.texture : null
|
|
75
|
+
const directDataUrl = useDataUrl(directUrl)
|
|
41
76
|
|
|
42
|
-
//
|
|
43
|
-
const
|
|
77
|
+
// Default URL lookup
|
|
78
|
+
const primaryUrl = blockConfig ? '' : (typeof item.texture === 'string' ? '' : textures.getItemTextureUrl(item))
|
|
79
|
+
const fallbackUrl =
|
|
80
|
+
!blockConfig &&
|
|
81
|
+
!item.textureKey &&
|
|
82
|
+
item.name
|
|
83
|
+
? textures.getBlockTextureUrl(item)
|
|
84
|
+
: null
|
|
85
|
+
const primaryDataUrl = useDataUrl(primaryUrl || null)
|
|
44
86
|
const primaryFailed = primaryDataUrl === null || isTextureFailed(primaryUrl)
|
|
45
87
|
|
|
46
|
-
// Load fallback only once primary is known to have failed
|
|
47
88
|
const [loadFallback, setLoadFallback] = useState(false)
|
|
48
89
|
useEffect(() => {
|
|
49
90
|
if (primaryFailed && fallbackUrl) setLoadFallback(true)
|
|
@@ -51,11 +92,56 @@ export const ItemCanvas = memo(function ItemCanvas({
|
|
|
51
92
|
}, [primaryFailed, fallbackUrl])
|
|
52
93
|
const fallbackDataUrl = useDataUrl(loadFallback ? fallbackUrl : null)
|
|
53
94
|
|
|
95
|
+
// Resolve final src by priority
|
|
96
|
+
if (typeof item.texture === 'string') {
|
|
97
|
+
return {
|
|
98
|
+
src: directDataUrl ?? null,
|
|
99
|
+
failed: directDataUrl === null || isTextureFailed(item.texture),
|
|
100
|
+
loading: directDataUrl === undefined,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (item.texture instanceof HTMLImageElement) {
|
|
104
|
+
const img = item.texture
|
|
105
|
+
return {
|
|
106
|
+
src: img.complete && img.naturalWidth > 0 ? img.src : null,
|
|
107
|
+
failed: false,
|
|
108
|
+
loading: !img.complete,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (blockConfig) {
|
|
112
|
+
return {
|
|
113
|
+
src: blockDataUrl,
|
|
114
|
+
failed: blockFailed,
|
|
115
|
+
loading: !blockDataUrl && !blockFailed,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
54
119
|
const src = loadFallback ? fallbackDataUrl : primaryDataUrl
|
|
55
120
|
const failed = loadFallback
|
|
56
121
|
? (fallbackDataUrl === null || isTextureFailed(fallbackUrl ?? ''))
|
|
57
122
|
: primaryFailed && !fallbackUrl
|
|
58
123
|
|
|
124
|
+
return {
|
|
125
|
+
src: src ?? null,
|
|
126
|
+
failed,
|
|
127
|
+
loading: src === undefined,
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Renders a single item: texture as <img>, count as <span>, durability as CSS bars. */
|
|
132
|
+
export const ItemCanvas = memo(function ItemCanvas({
|
|
133
|
+
item,
|
|
134
|
+
size,
|
|
135
|
+
noCount = false,
|
|
136
|
+
noDurability = false,
|
|
137
|
+
className,
|
|
138
|
+
style,
|
|
139
|
+
}: ItemCanvasProps) {
|
|
140
|
+
const { contentSize, pixelSize } = useScale()
|
|
141
|
+
const renderSize = size ?? contentSize
|
|
142
|
+
|
|
143
|
+
const { src, failed, loading } = useItemTextureSrc(item)
|
|
144
|
+
|
|
59
145
|
const hasDurability =
|
|
60
146
|
!noDurability &&
|
|
61
147
|
item.durability !== undefined &&
|
|
@@ -96,6 +96,7 @@ export function JEI({
|
|
|
96
96
|
|
|
97
97
|
const ro = new ResizeObserver((entries) => {
|
|
98
98
|
for (const entry of entries) {
|
|
99
|
+
// console.log('got size', entry.target.className, entry.contentRect.width, entry.contentRect.height)
|
|
99
100
|
if (entry.target === (root as unknown as Element)) {
|
|
100
101
|
sizes.rootW = entry.contentRect.width
|
|
101
102
|
} else if (entry.target === (grid as unknown as Element)) {
|
|
@@ -240,8 +241,6 @@ export function JEI({
|
|
|
240
241
|
className="mc-inv-jei-header"
|
|
241
242
|
style={{
|
|
242
243
|
padding: `${padding}px`,
|
|
243
|
-
background: '#c6c6c6',
|
|
244
|
-
// border: `${scale}px solid #555555`,
|
|
245
244
|
flexShrink: 0,
|
|
246
245
|
}}
|
|
247
246
|
>
|
|
@@ -284,7 +283,7 @@ export function JEI({
|
|
|
284
283
|
>
|
|
285
284
|
◀
|
|
286
285
|
</button>
|
|
287
|
-
<span className="mc-inv-jei-page-counter" style={{ flex: 1, textAlign: 'center', color: '#
|
|
286
|
+
<span className="mc-inv-jei-page-counter" style={{ flex: 1, textAlign: 'center', color: '#ffffff' }}>
|
|
288
287
|
{page + 1} / {Math.max(1, totalPages)}
|
|
289
288
|
</span>
|
|
290
289
|
<button
|
|
@@ -16,6 +16,7 @@ const CODE_COLORS: Record<string, string> = {
|
|
|
16
16
|
function parseSectionCodes(text: string): MessageFormatPart[] {
|
|
17
17
|
const parts: MessageFormatPart[] = []
|
|
18
18
|
const regex = /§([0-9a-fk-orA-FK-OR])|([^§]+)/g
|
|
19
|
+
regex.lastIndex = 0
|
|
19
20
|
let color: string | undefined
|
|
20
21
|
let bold = false, italic = false, underlined = false
|
|
21
22
|
let strikethrough = false, obfuscated = false
|
|
@@ -85,8 +85,7 @@ export function Tooltip({ item, visible }: TooltipProps) {
|
|
|
85
85
|
fontSize: fs,
|
|
86
86
|
padding: pad,
|
|
87
87
|
gap: gap2,
|
|
88
|
-
|
|
89
|
-
maxWidth: Math.round(220 * scale),
|
|
88
|
+
width: 'max-content',
|
|
90
89
|
// Start invisible; applyPosition sets visibility after measuring dimensions
|
|
91
90
|
visibility: 'hidden',
|
|
92
91
|
pointerEvents: 'none',
|
|
@@ -10,19 +10,31 @@ export interface MineflayerConnectorOptions {
|
|
|
10
10
|
/**
|
|
11
11
|
* Custom item mapper called for every slot conversion from raw mineflayer data to
|
|
12
12
|
* {@link ItemStack}. Receives the raw slot data and the default-mapped stack.
|
|
13
|
-
* Return a modified stack to override fields (e.g. `name`, `textureKey`, `displayName
|
|
14
|
-
* or return the second argument unchanged to use the default mapping.
|
|
13
|
+
* Return a modified stack to override fields (e.g. `name`, `textureKey`, `displayName`,
|
|
14
|
+
* `texture`, `blockTexture`), or return the second argument unchanged to use the default mapping.
|
|
15
15
|
*
|
|
16
16
|
* @example
|
|
17
17
|
* ```ts
|
|
18
18
|
* createMineflayerConnector(bot, {
|
|
19
19
|
* itemMapper: (raw, mapped) => ({
|
|
20
20
|
* ...mapped,
|
|
21
|
-
* // Override texture for specific numeric type IDs:
|
|
22
21
|
* textureKey: raw.type === 438 ? 'item/potion_water' : mapped.textureKey,
|
|
23
22
|
* }),
|
|
24
23
|
* })
|
|
25
24
|
* ```
|
|
25
|
+
*
|
|
26
|
+
* @example Block texture with isometric face slices
|
|
27
|
+
* ```ts
|
|
28
|
+
* itemMapper: (raw, mapped) => ({
|
|
29
|
+
* ...mapped,
|
|
30
|
+
* blockTexture: {
|
|
31
|
+
* source: blockAtlasUrl,
|
|
32
|
+
* top: { slice: [0, 0, 16, 16] },
|
|
33
|
+
* left: { slice: [16, 0, 16, 16] },
|
|
34
|
+
* right: { slice: [32, 0, 16, 16] },
|
|
35
|
+
* },
|
|
36
|
+
* })
|
|
37
|
+
* ```
|
|
26
38
|
*/
|
|
27
39
|
itemMapper?: (raw: RawSlot, mapped: ItemStack) => ItemStack
|
|
28
40
|
}
|
|
@@ -28,6 +28,72 @@ import _gui_widgets from 'mc-assets/dist/other-textures/1.15/gui/widgets.png'
|
|
|
28
28
|
import _gui_sprites_container_anvil_text_field from 'mc-assets/dist/other-textures/latest/gui/sprites/container/anvil/text_field.png'
|
|
29
29
|
import _gui_sprites_container_anvil_text_field_disabled from 'mc-assets/dist/other-textures/latest/gui/sprites/container/anvil/text_field_disabled.png'
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Versioned texture path → bundled asset URL (or undefined for remote fallback).
|
|
33
|
+
* Keys are full mc-assets paths e.g. "1.21.11/textures/gui/container/inventory.png"
|
|
34
|
+
*/
|
|
35
|
+
export const bundledTextureMap: Record<string, string | undefined> = {
|
|
36
|
+
'1.21.11/textures/gui/container/inventory.png': _gui_container_inventory,
|
|
37
|
+
'1.21.11/textures/gui/container/shulker_box.png': _gui_container_shulker_box,
|
|
38
|
+
'1.21.11/textures/gui/container/generic_54.png': _gui_container_generic_54,
|
|
39
|
+
'1.21.11/textures/gui/container/crafting_table.png': _gui_container_crafting_table,
|
|
40
|
+
'1.21.11/textures/gui/container/furnace.png': _gui_container_furnace,
|
|
41
|
+
'1.21.11/textures/gui/container/blast_furnace.png': _gui_container_blast_furnace,
|
|
42
|
+
'1.21.11/textures/gui/container/smoker.png': _gui_container_smoker,
|
|
43
|
+
'1.21.11/textures/gui/container/brewing_stand.png': _gui_container_brewing_stand,
|
|
44
|
+
'1.21.11/textures/gui/container/anvil.png': _gui_container_anvil,
|
|
45
|
+
'1.21.11/textures/gui/container/grindstone.png': _gui_container_grindstone,
|
|
46
|
+
'1.21.11/textures/gui/container/enchanting_table.png': _gui_container_enchanting_table,
|
|
47
|
+
'1.21.11/textures/gui/container/smithing.png': _gui_container_smithing,
|
|
48
|
+
'1.16.4/textures/gui/container/smithing.png': _gui_container_smithing_2,
|
|
49
|
+
'1.21.11/textures/gui/container/hopper.png': _gui_container_hopper,
|
|
50
|
+
'1.21.11/textures/gui/container/dispenser.png': _gui_container_dispenser,
|
|
51
|
+
'1.21.11/textures/gui/container/beacon.png': _gui_container_beacon,
|
|
52
|
+
'1.21.11/textures/gui/container/horse.png': _gui_container_horse,
|
|
53
|
+
'1.14/textures/gui/container/villager2.png': _gui_container_villager2,
|
|
54
|
+
'1.21.11/textures/gui/container/cartography_table.png': _gui_container_cartography_table,
|
|
55
|
+
'1.21.11/textures/gui/container/loom.png': _gui_container_loom,
|
|
56
|
+
'1.21.11/textures/gui/container/stonecutter.png': _gui_container_stonecutter,
|
|
57
|
+
'1.21.11/textures/gui/container/crafter.png': _gui_container_crafter,
|
|
58
|
+
'1.21.11/textures/gui/container/creative_inventory/tab_items.png': _gui_container_creative_inventory_tab_items,
|
|
59
|
+
'1.15/textures/gui/widgets.png': _gui_widgets,
|
|
60
|
+
'1.21.11/textures/gui/sprites/container/anvil/text_field.png': _gui_sprites_container_anvil_text_field,
|
|
61
|
+
'1.21.11/textures/gui/sprites/container/anvil/text_field_disabled.png': _gui_sprites_container_anvil_text_field_disabled,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* All texture paths without version prefix (e.g. "gui/container/inventory.png").
|
|
67
|
+
* Same set as bundledTextureMap keys with version stripped.
|
|
68
|
+
*/
|
|
69
|
+
export const allTexturePaths: readonly string[] = [
|
|
70
|
+
'gui/container/inventory.png',
|
|
71
|
+
'gui/container/shulker_box.png',
|
|
72
|
+
'gui/container/generic_54.png',
|
|
73
|
+
'gui/container/crafting_table.png',
|
|
74
|
+
'gui/container/furnace.png',
|
|
75
|
+
'gui/container/blast_furnace.png',
|
|
76
|
+
'gui/container/smoker.png',
|
|
77
|
+
'gui/container/brewing_stand.png',
|
|
78
|
+
'gui/container/anvil.png',
|
|
79
|
+
'gui/container/grindstone.png',
|
|
80
|
+
'gui/container/enchanting_table.png',
|
|
81
|
+
'gui/container/smithing.png',
|
|
82
|
+
'gui/container/hopper.png',
|
|
83
|
+
'gui/container/dispenser.png',
|
|
84
|
+
'gui/container/beacon.png',
|
|
85
|
+
'gui/container/horse.png',
|
|
86
|
+
'gui/container/villager2.png',
|
|
87
|
+
'gui/container/cartography_table.png',
|
|
88
|
+
'gui/container/loom.png',
|
|
89
|
+
'gui/container/stonecutter.png',
|
|
90
|
+
'gui/container/crafter.png',
|
|
91
|
+
'gui/container/creative_inventory/tab_items.png',
|
|
92
|
+
'gui/widgets.png',
|
|
93
|
+
'gui/sprites/container/anvil/text_field.png',
|
|
94
|
+
'gui/sprites/container/anvil/text_field_disabled.png',
|
|
95
|
+
]
|
|
96
|
+
|
|
31
97
|
/**
|
|
32
98
|
* Maps each inventory type name to its texture path (version prefix stripped).
|
|
33
99
|
*/
|
|
@@ -68,54 +134,3 @@ export const allContainerPaths: Record<string, string> = {
|
|
|
68
134
|
creative: 'gui/container/creative_inventory/tab_items.png',
|
|
69
135
|
hotbar: 'gui/widgets.png',
|
|
70
136
|
}
|
|
71
|
-
|
|
72
|
-
// Internal: versioned texture key → bundled asset URL (or undefined → remote fallback)
|
|
73
|
-
const _map: Record<string, string | undefined> = {
|
|
74
|
-
'1.21.11/textures/gui/container/inventory.png': _gui_container_inventory,
|
|
75
|
-
'1.21.11/textures/gui/container/shulker_box.png': _gui_container_shulker_box,
|
|
76
|
-
'1.21.11/textures/gui/container/generic_54.png': _gui_container_generic_54,
|
|
77
|
-
'1.21.11/textures/gui/container/crafting_table.png': _gui_container_crafting_table,
|
|
78
|
-
'1.21.11/textures/gui/container/furnace.png': _gui_container_furnace,
|
|
79
|
-
'1.21.11/textures/gui/container/blast_furnace.png': _gui_container_blast_furnace,
|
|
80
|
-
'1.21.11/textures/gui/container/smoker.png': _gui_container_smoker,
|
|
81
|
-
'1.21.11/textures/gui/container/brewing_stand.png': _gui_container_brewing_stand,
|
|
82
|
-
'1.21.11/textures/gui/container/anvil.png': _gui_container_anvil,
|
|
83
|
-
'1.21.11/textures/gui/container/grindstone.png': _gui_container_grindstone,
|
|
84
|
-
'1.21.11/textures/gui/container/enchanting_table.png': _gui_container_enchanting_table,
|
|
85
|
-
'1.21.11/textures/gui/container/smithing.png': _gui_container_smithing,
|
|
86
|
-
'1.16.4/textures/gui/container/smithing.png': _gui_container_smithing_2,
|
|
87
|
-
'1.21.11/textures/gui/container/hopper.png': _gui_container_hopper,
|
|
88
|
-
'1.21.11/textures/gui/container/dispenser.png': _gui_container_dispenser,
|
|
89
|
-
'1.21.11/textures/gui/container/beacon.png': _gui_container_beacon,
|
|
90
|
-
'1.21.11/textures/gui/container/horse.png': _gui_container_horse,
|
|
91
|
-
'1.14/textures/gui/container/villager2.png': _gui_container_villager2,
|
|
92
|
-
'1.21.11/textures/gui/container/cartography_table.png': _gui_container_cartography_table,
|
|
93
|
-
'1.21.11/textures/gui/container/loom.png': _gui_container_loom,
|
|
94
|
-
'1.21.11/textures/gui/container/stonecutter.png': _gui_container_stonecutter,
|
|
95
|
-
'1.21.11/textures/gui/container/crafter.png': _gui_container_crafter,
|
|
96
|
-
'1.21.11/textures/gui/container/creative_inventory/tab_items.png': _gui_container_creative_inventory_tab_items,
|
|
97
|
-
'1.15/textures/gui/widgets.png': _gui_widgets,
|
|
98
|
-
'1.21.11/textures/gui/sprites/container/anvil/text_field.png': _gui_sprites_container_anvil_text_field,
|
|
99
|
-
'1.21.11/textures/gui/sprites/container/anvil/text_field_disabled.png': _gui_sprites_container_anvil_text_field_disabled,
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Partial TextureConfig that resolves inventory GUI textures from locally bundled
|
|
104
|
-
* mc-assets assets instead of remote GitHub URLs.
|
|
105
|
-
*
|
|
106
|
-
* Pass to `<TextureProvider config={localTexturesConfig}>` to use offline/bundled assets.
|
|
107
|
-
*
|
|
108
|
-
* Unknown paths (not bundled) fall back to the mc-assets remote URL automatically.
|
|
109
|
-
*/
|
|
110
|
-
const _MC_ASSETS_REMOTE = "https://raw.githubusercontent.com/zardoy/mc-assets/refs/heads/gh-pages"
|
|
111
|
-
export const localTexturesConfig = {
|
|
112
|
-
getGuiTextureUrl(path: string): string {
|
|
113
|
-
const local = _map[path] as string | undefined
|
|
114
|
-
if (local) return local
|
|
115
|
-
// Fall back to remote mc-assets URL for paths not in the bundle
|
|
116
|
-
if (path.endsWith('.png') && path.includes('/textures/')) {
|
|
117
|
-
return `${_MC_ASSETS_REMOTE}/${path}`
|
|
118
|
-
}
|
|
119
|
-
return path
|
|
120
|
-
},
|
|
121
|
-
}
|
package/src/index.tsx
CHANGED
|
@@ -67,4 +67,22 @@ export type {
|
|
|
67
67
|
RecipeGuide,
|
|
68
68
|
RecipeNavFrame,
|
|
69
69
|
EntityDisplayArea,
|
|
70
|
+
TextureSlice,
|
|
71
|
+
BlockFaceSlice,
|
|
72
|
+
BlockTextureRender,
|
|
70
73
|
} from './types'
|
|
74
|
+
|
|
75
|
+
// Bundled textures config
|
|
76
|
+
export {
|
|
77
|
+
createBundledTexturesConfig,
|
|
78
|
+
localBundledTexturesConfig,
|
|
79
|
+
allTexturePaths,
|
|
80
|
+
allContainerPaths,
|
|
81
|
+
} from './bundledTexturesConfig'
|
|
82
|
+
export type {
|
|
83
|
+
BundledTexturesConfig,
|
|
84
|
+
BundledTexturesConfigOptions,
|
|
85
|
+
} from './bundledTexturesConfig'
|
|
86
|
+
|
|
87
|
+
// Texture cache (for resetRenderedSlots / manual invalidation)
|
|
88
|
+
export { clearTextureCache } from './cache/textureCache'
|
|
@@ -140,7 +140,7 @@ export const inventoryDefinitions = makeInventoryDefinitions({
|
|
|
140
140
|
containerRows: 1,
|
|
141
141
|
slots: [
|
|
142
142
|
...gridSlots(9, 1, 8, 18, 'container'),
|
|
143
|
-
...playerInv(
|
|
143
|
+
...playerInv(50), // 1*18 + 30 + 2px offset below
|
|
144
144
|
],
|
|
145
145
|
},
|
|
146
146
|
|
|
@@ -153,7 +153,7 @@ export const inventoryDefinitions = makeInventoryDefinitions({
|
|
|
153
153
|
containerRows: 2,
|
|
154
154
|
slots: [
|
|
155
155
|
...gridSlots(9, 2, 8, 18, 'container'),
|
|
156
|
-
...playerInv(
|
|
156
|
+
...playerInv(68), // 2*18 + 30 + 2px offset below
|
|
157
157
|
],
|
|
158
158
|
},
|
|
159
159
|
|
|
@@ -164,10 +164,10 @@ export const inventoryDefinitions = makeInventoryDefinitions({
|
|
|
164
164
|
backgroundWidth: 176,
|
|
165
165
|
backgroundHeight: 168, // 3*18 + 114
|
|
166
166
|
containerRows: 3,
|
|
167
|
-
playerInventoryOffset: { x: 8, y:
|
|
167
|
+
playerInventoryOffset: { x: 8, y: 82 },
|
|
168
168
|
slots: [
|
|
169
169
|
...gridSlots(9, 3, 8, 18, 'container'),
|
|
170
|
-
...playerInv(
|
|
170
|
+
...playerInv(86), // 3*18 + 30 + 2px offset below
|
|
171
171
|
],
|
|
172
172
|
},
|
|
173
173
|
|
|
@@ -180,7 +180,7 @@ export const inventoryDefinitions = makeInventoryDefinitions({
|
|
|
180
180
|
containerRows: 4,
|
|
181
181
|
slots: [
|
|
182
182
|
...gridSlots(9, 4, 8, 18, 'container'),
|
|
183
|
-
...playerInv(
|
|
183
|
+
...playerInv(104), // 4*18 + 30 + 2px offset below
|
|
184
184
|
],
|
|
185
185
|
},
|
|
186
186
|
|
|
@@ -193,7 +193,7 @@ export const inventoryDefinitions = makeInventoryDefinitions({
|
|
|
193
193
|
containerRows: 5,
|
|
194
194
|
slots: [
|
|
195
195
|
...gridSlots(9, 5, 8, 18, 'container'),
|
|
196
|
-
...playerInv(
|
|
196
|
+
...playerInv(122), // 5*18 + 30 + 2px offset below
|
|
197
197
|
],
|
|
198
198
|
},
|
|
199
199
|
|
|
@@ -204,6 +204,7 @@ export const inventoryDefinitions = makeInventoryDefinitions({
|
|
|
204
204
|
backgroundWidth: 176,
|
|
205
205
|
backgroundHeight: 222, // full 6-row texture, no stitching needed (N*18+114 = 222)
|
|
206
206
|
playerInventoryOffset: { x: 8, y: 140 },
|
|
207
|
+
containerRows: 6,
|
|
207
208
|
slots: [
|
|
208
209
|
...gridSlots(9, 6, 8, 18, 'container'),
|
|
209
210
|
...playerInv(140), // matches large_chest
|
|
@@ -216,7 +217,8 @@ export const inventoryDefinitions = makeInventoryDefinitions({
|
|
|
216
217
|
backgroundTexture: '1.21.11/textures/gui/container/generic_54.png',
|
|
217
218
|
backgroundWidth: 176,
|
|
218
219
|
backgroundHeight: 222,
|
|
219
|
-
|
|
220
|
+
containerRows: 6,
|
|
221
|
+
playerInventoryOffset: { x: 8, y: 138 },
|
|
220
222
|
slots: [
|
|
221
223
|
...gridSlots(9, 6, 8, 18, 'container'),
|
|
222
224
|
...playerInv(140),
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
/** Slice rect [x, y, width, height] in source texture pixels. */
|
|
2
|
+
export type TextureSlice = [x:number, y:number, width:number, height:number]
|
|
3
|
+
|
|
4
|
+
/** Block face slice for isometric block rendering. */
|
|
5
|
+
export interface BlockFaceSlice {
|
|
6
|
+
slice: TextureSlice
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Block-style render config. When set, ItemCanvas uses an aux canvas to composite
|
|
11
|
+
* top/left/right faces into an isometric icon instead of a single texture.
|
|
12
|
+
* Useful for blocks in mineflayer connector where block texture atlas faces differ.
|
|
13
|
+
*/
|
|
14
|
+
export interface BlockTextureRender {
|
|
15
|
+
/** Source texture URL or preloaded HTMLImageElement. */
|
|
16
|
+
source: string | HTMLImageElement
|
|
17
|
+
top: BlockFaceSlice
|
|
18
|
+
left: BlockFaceSlice
|
|
19
|
+
right: BlockFaceSlice
|
|
20
|
+
}
|
|
21
|
+
|
|
1
22
|
export interface ItemStack {
|
|
2
23
|
type: number
|
|
3
24
|
count: number
|
|
@@ -19,6 +40,17 @@ export interface ItemStack {
|
|
|
19
40
|
* Example: `"item/dye_black"` or `"entity/spider/spider"`
|
|
20
41
|
*/
|
|
21
42
|
textureKey?: string
|
|
43
|
+
/**
|
|
44
|
+
* Direct texture override. When set, bypasses getItemTextureUrl.
|
|
45
|
+
* - `string`: URL (data URL or http) used as img src.
|
|
46
|
+
* - `HTMLImageElement`: preloaded image, drawn directly.
|
|
47
|
+
*/
|
|
48
|
+
texture?: string | HTMLImageElement
|
|
49
|
+
/**
|
|
50
|
+
* Block-style isometric render. When set, ItemCanvas uses a canvas pool to composite
|
|
51
|
+
* top/left/right face slices into the icon. Use in mineflayer itemMapper for blocks.
|
|
52
|
+
*/
|
|
53
|
+
blockTexture?: BlockTextureRender
|
|
22
54
|
/**
|
|
23
55
|
* Arbitrary debug identifier exposed as a `data-debug` attribute on the slot element.
|
|
24
56
|
* The mineflayer connector sets this to `"<type>:<metadata>"` by default, making it easy
|