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 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 { localTexturesConfig } from './generated/localTextures'
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={localTexturesConfig}>
269
+ <TextureProvider config={localBundledTexturesConfig}>
270
270
  <InventoryOverlay type="chest" ... />
271
271
  </TextureProvider>
272
272
  ```
273
273
 
274
- The generated file imports every `backgroundTexture` from your registry, resolves each to `node_modules/mc-assets/dist/other-textures/<version>/...` (or `latest/` if that version is missing), and exports `allContainerPaths` (inventory name short path) plus `localTexturesConfig.getGuiTextureUrl(path)` for use with `<TextureProvider config={...}>`. Re-run `pnpm gen:textures` after changing [inventory types](#adding-new-inventory-types).
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecraft-inventory",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "release": {
@@ -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:
@@ -288,7 +288,7 @@ export function InventoryOverlay({
288
288
  lineHeight: 1,
289
289
  }}
290
290
  >
291
- INV 0.0.0
291
+ INV 0.1.6
292
292
  </a>
293
293
  )}
294
294
 
@@ -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 < 6
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
- /** Renders a single item: texture as <img>, count as <span>, durability as CSS bars. */
26
- export const ItemCanvas = memo(function ItemCanvas({
27
- item,
28
- size,
29
- noCount = false,
30
- noDurability = false,
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
- const primaryUrl = textures.getItemTextureUrl(item)
39
- // Skip the block-texture fallback when a textureKey override is already in use
40
- const fallbackUrl = !item.textureKey && item.name ? textures.getBlockTextureUrl(item) : null
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
- // Load primary URL as cached data URL
43
- const primaryDataUrl = useDataUrl(primaryUrl)
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 &&
@@ -1,6 +1,8 @@
1
1
  .jei {
2
2
  font-family: 'Minecraft', monospace;
3
3
  image-rendering: pixelated;
4
+ height: 100%;
5
+ border-radius: 1px;
4
6
  }
5
7
 
6
8
  .searchInput::placeholder {
@@ -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: '#404040' }}>
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
- minWidth: Math.round(80 * scale),
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(48), // 1*18 + 30
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(66), // 2*18 + 30
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: 84 },
167
+ playerInventoryOffset: { x: 8, y: 82 },
168
168
  slots: [
169
169
  ...gridSlots(9, 3, 8, 18, 'container'),
170
- ...playerInv(84), // 3*18 + 30
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(102), // 4*18 + 30
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(120), // 5*18 + 30
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
- playerInventoryOffset: { x: 8, y: 140 },
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