minecraft-inventory 0.1.4 → 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.
@@ -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
@@ -45,6 +45,7 @@ export function Notes({
45
45
  const [notes, setNotes] = useState<Note[]>([])
46
46
  const [draft, setDraft] = useState('')
47
47
  const [loading, setLoading] = useState(true)
48
+ const [showInput, setShowInput] = useState(false)
48
49
 
49
50
  // Load notes on mount
50
51
  useEffect(() => {
@@ -104,6 +105,14 @@ export function Notes({
104
105
  setDraft('')
105
106
  }, [draft, notes, saveNotes])
106
107
 
108
+ const handleAddButtonClick = useCallback(() => {
109
+ if (!showInput) {
110
+ setShowInput(true)
111
+ } else {
112
+ addNote()
113
+ }
114
+ }, [showInput, addNote])
115
+
107
116
  const removeNote = useCallback((id: number) => {
108
117
  const next = notes.filter((n) => n.id !== id)
109
118
  saveNotes(next)
@@ -142,7 +151,7 @@ export function Notes({
142
151
  </div>
143
152
  ) : notes.length === 0 ? (
144
153
  <div style={{ color: '#888', fontSize: Math.round(6 * scale), textAlign: 'center', padding: `${4 * scale}px` }}>
145
- No tasks yet
154
+ {/* No tasks yet */}
146
155
  </div>
147
156
  ) : (
148
157
  notes.map((note) => (
@@ -187,50 +196,72 @@ export function Notes({
187
196
  boxSizing: 'border-box',
188
197
  // borderTop: `${scale}px solid #555555`,
189
198
  }}>
190
- <textarea
191
- value={draft}
192
- rows={2}
193
- onChange={(e) => {
194
- // Strip newlines — behave like a single-line input but with word wrap
195
- setDraft(e.target.value.replace(/[\r\n]/g, ''))
196
- }}
197
- onKeyDown={(e) => {
198
- if (e.code === 'Enter') { e.preventDefault(); addNote() }
199
- }}
200
- placeholder="Add task…"
201
- style={{
202
- flex: 1,
203
- minWidth: 0,
204
- resize: 'none',
205
- background: '#8b8b8b',
206
- border: `${scale}px solid #373737`,
207
- color: '#fff',
208
- fontSize: Math.round(6 * scale),
209
- padding: `${scale}px`,
210
- fontFamily: 'inherit',
211
- outline: 'none',
212
- lineHeight: 1.4,
213
- boxSizing: 'border-box',
214
- overflowY: 'hidden',
215
- }}
216
- />
217
- <button
218
- onClick={addNote}
219
- disabled={!draft.trim()}
220
- style={{
221
- background: '#404040',
222
- color: '#fff',
223
- border: `${scale}px solid #666`,
224
- cursor: draft.trim() ? 'pointer' : 'not-allowed',
225
- fontSize: Math.round(6 * scale),
226
- padding: `${scale}px ${2 * scale}px`,
227
- fontFamily: 'inherit',
228
- flexShrink: 0,
229
- opacity: draft.trim() ? 1 : 0.5,
230
- }}
231
- >
232
- +
233
- </button>
199
+ {showInput && (
200
+ <textarea
201
+ value={draft}
202
+ rows={2}
203
+ autoFocus
204
+ onChange={(e) => {
205
+ setDraft(e.target.value.replace(/[\r\n]/g, ''))
206
+ }}
207
+ onKeyDown={(e) => {
208
+ if (e.code === 'Enter') { e.preventDefault(); addNote() }
209
+ if (e.code === 'Escape') { setShowInput(false); setDraft('') }
210
+ }}
211
+ placeholder="Add task…"
212
+ style={{
213
+ flex: 1,
214
+ minWidth: 0,
215
+ resize: 'none',
216
+ background: '#8b8b8b',
217
+ border: `${scale}px solid #373737`,
218
+ color: '#fff',
219
+ fontSize: Math.round(6 * scale),
220
+ padding: `${scale}px`,
221
+ fontFamily: 'inherit',
222
+ outline: 'none',
223
+ lineHeight: 1.4,
224
+ boxSizing: 'border-box',
225
+ overflowY: 'hidden',
226
+ }}
227
+ />
228
+ )}
229
+ <div style={{ display: 'flex', flexDirection: 'column', gap: scale, flexShrink: 0 }}>
230
+ {showInput && (
231
+ <button
232
+ onClick={() => { setShowInput(false); setDraft('') }}
233
+ title="Hide"
234
+ style={{
235
+ background: '#5a3a3a',
236
+ color: '#ccc',
237
+ border: `${scale}px solid #666`,
238
+ cursor: 'pointer',
239
+ fontSize: Math.round(6 * scale),
240
+ padding: `${scale}px ${2 * scale}px`,
241
+ fontFamily: 'inherit',
242
+ lineHeight: 1,
243
+ }}
244
+ >
245
+ ×
246
+ </button>
247
+ )}
248
+ <button
249
+ onClick={handleAddButtonClick}
250
+ disabled={showInput && !draft.trim()}
251
+ style={{
252
+ background: '#404040',
253
+ color: '#fff',
254
+ border: `${scale}px solid #666`,
255
+ cursor: showInput && !draft.trim() ? 'not-allowed' : 'pointer',
256
+ fontSize: Math.round(6 * scale),
257
+ padding: `${scale}px ${2 * scale}px`,
258
+ fontFamily: 'inherit',
259
+ opacity: showInput && !draft.trim() ? 0.5 : 1,
260
+ }}
261
+ >
262
+ +
263
+ </button>
264
+ </div>
234
265
  </div>
235
266
  </div>
236
267
  )
@@ -244,6 +244,9 @@ export function Slot({
244
244
  const touch = e.changedTouches[0]
245
245
  if (Math.abs(touch.clientX - start.x) > 10 || Math.abs(touch.clientY - start.y) > 10) return
246
246
  e.stopPropagation()
247
+ // Prevent the browser from firing a synthetic click after touchEnd.
248
+ // Without this, the click bubbles to the inventory window div which clears focusedSlot.
249
+ e.preventDefault()
247
250
 
248
251
  if (pKeyActive) setPKeyActive(false)
249
252
 
@@ -319,6 +322,9 @@ export function Slot({
319
322
  .filter(Boolean)
320
323
  .join(' ')}
321
324
  tabIndex={index >= 0 ? 0 : undefined}
325
+ data-slot={index}
326
+ data-debug={item?.debugKey ?? undefined}
327
+ data-texture={item?.textureKey ?? undefined}
322
328
  style={{
323
329
  width: renderSize,
324
330
  height: renderSize,
@@ -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',
@@ -1,5 +1,6 @@
1
1
  import type { InventoryAction, InventoryWindowState, PlayerState, SlotState, ItemStack } from '../types'
2
2
  import type { InventoryConnector, ConnectorListener, ConnectorEvent } from './types'
3
+ import { isItemEqual, getMaxStackSize } from '../utils/isItemEqual'
3
4
 
4
5
  export interface ActionLogEntry {
5
6
  id: number
@@ -154,11 +155,75 @@ export function createDemoConnector(options: DemoConnectorOptions): InventoryCon
154
155
  }
155
156
  } else if (action.mode === 'shift' && slotState?.item) {
156
157
  // Simulate shift-click: just log it
158
+ } else if (action.mode === 'double') {
159
+ // Double-click collect: pick up all matching items from other slots up to maxStack
160
+ const held = windowState.heldItem
161
+ if (held) {
162
+ const maxStack = getMaxStackSize(held)
163
+ let remaining = maxStack - held.count
164
+ for (let i = 0; i < slots.length && remaining > 0; i++) {
165
+ const s = slots[i]
166
+ if (!s.item || !isItemEqual(s.item, held)) continue
167
+ const take = Math.min(s.item.count, remaining)
168
+ slots[i] = { ...s, item: s.item.count - take > 0 ? { ...s.item, count: s.item.count - take } : null }
169
+ remaining -= take
170
+ }
171
+ windowState = { ...windowState, slots, heldItem: { ...held, count: maxStack - remaining } }
172
+ }
157
173
  }
158
174
 
159
175
  emit({ type: 'windowUpdate', state: windowState })
160
176
  }
161
177
 
178
+ // Demo: simulate drag/spread behavior
179
+ if (action.type === 'drag' && windowState && windowState.heldItem) {
180
+ const held = windowState.heldItem
181
+ const maxStack = getMaxStackSize(held)
182
+ const slots = [...windowState.slots]
183
+
184
+ if (action.button === 'left') {
185
+ // Vanilla left-drag: distribute perSlot items evenly, remainder stays in cursor
186
+ const compatibleSlots = action.slots.filter((idx) => {
187
+ const existing = slots.find((s) => s.index === idx)?.item
188
+ return !existing || isItemEqual(existing, held)
189
+ })
190
+ if (compatibleSlots.length > 0) {
191
+ const perSlot = Math.floor(held.count / compatibleSlots.length)
192
+ // If perSlot=0 (more slots than items), nothing is distributed
193
+ if (perSlot > 0) {
194
+ let totalPlaced = 0
195
+ for (const idx of compatibleSlots) {
196
+ const si = slots.findIndex((s) => s.index === idx)
197
+ const existingCount = si >= 0 ? (slots[si].item?.count ?? 0) : 0
198
+ const add = Math.min(perSlot, maxStack - existingCount)
199
+ totalPlaced += add
200
+ const newCount = existingCount + add
201
+ if (si >= 0) slots[si] = { ...slots[si], item: { ...held, count: newCount } }
202
+ else slots.push({ index: idx, item: { ...held, count: newCount } })
203
+ }
204
+ const remaining = held.count - totalPlaced
205
+ windowState = { ...windowState, slots, heldItem: remaining > 0 ? { ...held, count: remaining } : null }
206
+ }
207
+ }
208
+ } else {
209
+ // Place 1 item per slot (right-click drag)
210
+ let remaining = held.count
211
+ for (const idx of action.slots) {
212
+ if (remaining <= 0) break
213
+ const si = slots.findIndex((s) => s.index === idx)
214
+ const existing = si >= 0 ? slots[si].item : null
215
+ if (existing && !isItemEqual(existing, held)) continue
216
+ const existingCount = existing?.count ?? 0
217
+ const newCount = Math.min(existingCount + 1, maxStack)
218
+ if (si >= 0) slots[si] = { ...slots[si], item: { ...held, count: newCount } }
219
+ else slots.push({ index: idx, item: { ...held, count: newCount } })
220
+ remaining--
221
+ }
222
+ windowState = { ...windowState, slots, heldItem: remaining > 0 ? { ...held, count: remaining } : null }
223
+ }
224
+ emit({ type: 'windowUpdate', state: windowState })
225
+ }
226
+
162
227
  if (action.type === 'drop' && windowState) {
163
228
  const slots = [...windowState.slots]
164
229
  const slotState = slots.find((s) => s.index === action.slotIndex)
@@ -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
  }
@@ -35,6 +47,9 @@ function makeSlotConverter(itemMapper?: MineflayerConnectorOptions['itemMapper']
35
47
  count: slot.count,
36
48
  metadata: slot.metadata,
37
49
  nbt: slot.nbt as Record<string, unknown> | undefined,
50
+ // Default debug key: "<type>:<metadata>" — visible as data-debug on slot elements.
51
+ // Override via itemMapper if needed.
52
+ debugKey: slot.metadata ? `${slot.type}:${slot.metadata}` : String(slot.type),
38
53
  }
39
54
  return itemMapper ? itemMapper(slot, mapped) : mapped
40
55
  }
@@ -137,12 +137,11 @@ export function InventoryProvider({ connector, children, noDragSpread = false }:
137
137
  })
138
138
  if (compatibleSlots.length === 0) return preview
139
139
  const perSlot = Math.floor(held.count / compatibleSlots.length)
140
- let remainder = held.count % compatibleSlots.length
140
+ // Vanilla behavior: if perSlot=0 (more slots than items), nothing is distributed
141
+ if (perSlot === 0) return preview
141
142
  for (const idx of compatibleSlots) {
142
143
  const existingCount = ws?.slots.find((s) => s.index === idx)?.item?.count ?? 0
143
- const add = perSlot + (remainder > 0 ? 1 : 0)
144
- if (remainder > 0) remainder--
145
- const total = Math.min(existingCount + add, maxStack)
144
+ const total = Math.min(existingCount + perSlot, maxStack)
146
145
  preview.set(idx, { count: total })
147
146
  }
148
147
  } else {
@@ -197,21 +196,26 @@ export function InventoryProvider({ connector, children, noDragSpread = false }:
197
196
  })
198
197
  if (compatibleSlots.length > 0) {
199
198
  const perSlot = Math.floor(held.count / compatibleSlots.length)
200
- let remainder = held.count % compatibleSlots.length
201
- for (const idx of compatibleSlots) {
202
- const existingIdx = newSlots.findIndex((s) => s.index === idx)
203
- const existingCount = existingIdx >= 0 ? (newSlots[existingIdx].item?.count ?? 0) : 0
204
- const add = perSlot + (remainder > 0 ? 1 : 0)
205
- if (remainder > 0) remainder--
206
- const newCount = Math.min(existingCount + add, maxStack)
207
- if (existingIdx >= 0) {
208
- newSlots[existingIdx] = { index: idx, item: { ...held, count: newCount } }
209
- } else {
210
- newSlots.push({ index: idx, item: { ...held, count: newCount } })
199
+ // Vanilla behavior: if perSlot=0 (more slots than items), nothing is distributed
200
+ if (perSlot > 0) {
201
+ let totalPlaced = 0
202
+ for (const idx of compatibleSlots) {
203
+ const existingIdx = newSlots.findIndex((s) => s.index === idx)
204
+ const existingCount = existingIdx >= 0 ? (newSlots[existingIdx].item?.count ?? 0) : 0
205
+ const add = Math.min(perSlot, maxStack - existingCount)
206
+ totalPlaced += add
207
+ const newCount = existingCount + add
208
+ if (existingIdx >= 0) {
209
+ newSlots[existingIdx] = { index: idx, item: { ...held, count: newCount } }
210
+ } else {
211
+ newSlots.push({ index: idx, item: { ...held, count: newCount } })
212
+ }
211
213
  }
214
+ if (ws) setWindowState({ ...ws, slots: newSlots })
215
+ const remaining = held.count - totalPlaced
216
+ if (remaining > 0) setHeldItemState({ ...held, count: remaining })
217
+ else setHeldItemState(null)
212
218
  }
213
- if (ws) setWindowState({ ...ws, slots: newSlots })
214
- setHeldItemState(null)
215
219
  }
216
220
  } else {
217
221
  // Right-click drag: place 1 per slot
@@ -256,6 +260,19 @@ export function InventoryProvider({ connector, children, noDragSpread = false }:
256
260
  [windowState],
257
261
  )
258
262
 
263
+ // Expose state to window for easier debugging from browser DevTools.
264
+ // Access via: window.__mcInv
265
+ useEffect(() => {
266
+ ;(window as unknown as Record<string, unknown>).__mcInv = {
267
+ get windowState() { return windowStateRef.current },
268
+ get heldItem() { return heldItemRef.current },
269
+ get dragSlots() { return dragSlots },
270
+ get isDragging() { return isDragging },
271
+ get focusedSlot() { return focusedSlot },
272
+ get pKeyActive() { return pKeyActive },
273
+ }
274
+ })
275
+
259
276
  const value: InventoryContextValue = {
260
277
  windowState,
261
278
  playerState,
@@ -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
@@ -49,7 +49,7 @@ export type {
49
49
  export type { ActionLogEntry, DemoConnectorOptions } from './connector/demo'
50
50
 
51
51
  // Registry
52
- export { registerInventoryType, getInventoryType, getAllInventoryTypes } from './registry'
52
+ export { registerInventoryType, registerTypeAlias, getInventoryType, getAllInventoryTypes } from './registry'
53
53
  export type { InventoryTypeDefinition, WindowType } from './registry'
54
54
 
55
55
  // Types
@@ -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'
@@ -5,12 +5,41 @@ const registry = new Map<string, InventoryTypeDefinition>(
5
5
  Object.entries(inventoryDefinitions),
6
6
  )
7
7
 
8
+ /**
9
+ * Maps alternative names/aliases to canonical inventory type names.
10
+ * Both the alias and the canonical name resolve to the same definition.
11
+ *
12
+ * Add entries here to support shorthand or legacy type identifiers.
13
+ */
14
+ const typeAliases: Record<string, string> = {
15
+ // Shorthand aliases
16
+ crafting3x3: 'crafting_table',
17
+ crafting: 'crafting_table',
18
+ chest: 'chest', // already canonical, listed for clarity
19
+ // Protocol-level minecraft: prefix stripping is handled in getInventoryType
20
+ }
21
+
8
22
  export function registerInventoryType(def: InventoryTypeDefinition): void {
9
23
  registry.set(def.name, def)
10
24
  }
11
25
 
26
+ /**
27
+ * Resolve an inventory type name or alias to its definition.
28
+ * Handles:
29
+ * - Exact matches (e.g. "crafting_table")
30
+ * - Aliases defined in `typeAliases` (e.g. "crafting3x3" → "crafting_table")
31
+ * - "minecraft:" namespace prefix (e.g. "minecraft:generic_9x3" → "generic_9x3")
32
+ */
12
33
  export function getInventoryType(name: string): InventoryTypeDefinition | undefined {
13
- return registry.get(name)
34
+ // Strip "minecraft:" namespace prefix if present
35
+ const stripped = name.startsWith('minecraft:') ? name.slice('minecraft:'.length) : name
36
+ // Resolve alias if defined
37
+ const canonical = typeAliases[stripped] ?? stripped
38
+ return registry.get(canonical)
39
+ }
40
+
41
+ export function registerTypeAlias(alias: string, canonical: string): void {
42
+ typeAliases[alias] = canonical
14
43
  }
15
44
 
16
45
  export function getAllInventoryTypes(): InventoryTypeDefinition[] {