minecraft-inventory 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cache/blockRenderer.ts +31 -7
- package/src/components/InventoryOverlay/InventoryOverlay.tsx +12 -2
- package/src/components/InventoryWindow/EntityDisplay.tsx +50 -16
- package/src/components/InventoryWindow/InventoryWindow.tsx +10 -2
- package/src/components/InventoryWindow/defaultEntityImages.ts +13 -0
- package/src/components/ItemCanvas/ItemCanvas.tsx +1 -1
- package/src/components/JEI/JEI.tsx +5 -1
- package/src/components/Slot/Slot.tsx +24 -3
- package/src/connector/mineflayer.ts +357 -63
- package/src/connector/types.ts +1 -0
- package/src/context/InventoryContext.tsx +10 -1
- package/src/registry/inventories.ts +3 -0
- package/src/types.ts +5 -0
package/package.json
CHANGED
|
@@ -59,20 +59,44 @@ export function renderBlockIcon(
|
|
|
59
59
|
ctx.imageSmoothingEnabled = false
|
|
60
60
|
ctx.clearRect(0, 0, OUTPUT_SIZE, OUTPUT_SIZE)
|
|
61
61
|
|
|
62
|
-
const s = OUTPUT_SIZE /
|
|
62
|
+
const s = OUTPUT_SIZE / 2 - 2 // face size with padding (14px at 32px canvas)
|
|
63
|
+
const ox = (OUTPUT_SIZE - 2 * s) / 2 // horizontal offset to center
|
|
64
|
+
const oy = (OUTPUT_SIZE - 2 * s) / 2 // vertical offset to center
|
|
63
65
|
const [tx, ty, tw, th] = top
|
|
64
66
|
const [lx, ly, lw, lh] = left
|
|
65
67
|
const [rx, ry, rw, rh] = right
|
|
66
68
|
|
|
67
|
-
//
|
|
69
|
+
// Enable smoothing for isometric transforms (better diagonal edges)
|
|
70
|
+
ctx.imageSmoothingEnabled = true
|
|
71
|
+
ctx.imageSmoothingQuality = 'high'
|
|
72
|
+
|
|
73
|
+
// Isometric cube using affine transforms.
|
|
74
|
+
// Face vertices for a cube centered in OUTPUT_SIZE × OUTPUT_SIZE:
|
|
75
|
+
// Top: (s+ox, oy) → (2s+ox, s/2+oy) → (s+ox, s+oy) → (ox, s/2+oy)
|
|
76
|
+
// Left: (ox, s/2+oy) → (s+ox, s+oy) → (s+ox, 2s+oy) → (ox, 3s/2+oy)
|
|
77
|
+
// Right: (s+ox, s+oy) → (2s+ox, s/2+oy) → (2s+ox, 3s/2+oy) → (s+ox, 2s+oy)
|
|
78
|
+
|
|
79
|
+
// Top face
|
|
68
80
|
ctx.save()
|
|
69
|
-
ctx.
|
|
70
|
-
ctx.
|
|
71
|
-
ctx.drawImage(image, tx, ty, tw, th, -4 * s, -4 * s, 8 * s, 8 * s)
|
|
81
|
+
ctx.setTransform(1, 0.5, -1, 0.5, s + ox, oy)
|
|
82
|
+
ctx.drawImage(image, tx, ty, tw, th, 0, 0, s, s)
|
|
72
83
|
ctx.restore()
|
|
73
84
|
|
|
74
|
-
|
|
75
|
-
ctx.
|
|
85
|
+
// Left face (darkened)
|
|
86
|
+
ctx.save()
|
|
87
|
+
ctx.setTransform(1, 0.5, 0, 1, ox, s / 2 + oy)
|
|
88
|
+
ctx.drawImage(image, lx, ly, lw, lh, 0, 0, s, s)
|
|
89
|
+
ctx.fillStyle = 'rgba(0, 0, 0, 0.4)'
|
|
90
|
+
ctx.fillRect(0, 0, s, s)
|
|
91
|
+
ctx.restore()
|
|
92
|
+
|
|
93
|
+
// Right face (slightly darkened)
|
|
94
|
+
ctx.save()
|
|
95
|
+
ctx.setTransform(1, -0.5, 0, 1, s + ox, s + oy)
|
|
96
|
+
ctx.drawImage(image, rx, ry, rw, rh, 0, 0, s, s)
|
|
97
|
+
ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'
|
|
98
|
+
ctx.fillRect(0, 0, s, s)
|
|
99
|
+
ctx.restore()
|
|
76
100
|
|
|
77
101
|
const dataUrl = canvas.toDataURL('image/png')
|
|
78
102
|
resolve(dataUrl)
|
|
@@ -52,6 +52,8 @@ export interface InventoryOverlayProps {
|
|
|
52
52
|
debugBounds?: boolean
|
|
53
53
|
/** Show red debug outline around the inventory background */
|
|
54
54
|
showDebug?: boolean
|
|
55
|
+
/** When true, entity display area shows layout debug bounds instead of the default image. */
|
|
56
|
+
entityDisplayDebug?: boolean
|
|
55
57
|
/** Override entity display rendering. Pass a function returning JSX, or null to hide. */
|
|
56
58
|
renderEntity?: ((width: number, height: number) => React.ReactNode) | null
|
|
57
59
|
/** Hide the "INV" version watermark (opt-out) */
|
|
@@ -80,6 +82,7 @@ export function InventoryOverlay({
|
|
|
80
82
|
children,
|
|
81
83
|
debugBounds = false,
|
|
82
84
|
showDebug = false,
|
|
85
|
+
entityDisplayDebug = false,
|
|
83
86
|
renderEntity,
|
|
84
87
|
noWatermark = false,
|
|
85
88
|
}: InventoryOverlayProps) {
|
|
@@ -242,7 +245,14 @@ export function InventoryOverlay({
|
|
|
242
245
|
onPushFrame={handleRecipePushFrame}
|
|
243
246
|
/>
|
|
244
247
|
) : (
|
|
245
|
-
<InventoryWindow
|
|
248
|
+
<InventoryWindow
|
|
249
|
+
type={type}
|
|
250
|
+
title={title}
|
|
251
|
+
properties={properties}
|
|
252
|
+
showDebug={showDebug}
|
|
253
|
+
entityDisplayDebug={entityDisplayDebug}
|
|
254
|
+
renderEntity={renderEntity}
|
|
255
|
+
/>
|
|
246
256
|
)}
|
|
247
257
|
</div>
|
|
248
258
|
</div>
|
|
@@ -288,7 +298,7 @@ export function InventoryOverlay({
|
|
|
288
298
|
lineHeight: 1,
|
|
289
299
|
}}
|
|
290
300
|
>
|
|
291
|
-
INV 0.1.
|
|
301
|
+
INV 0.1.8
|
|
292
302
|
</a>
|
|
293
303
|
)}
|
|
294
304
|
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { useScale } from '../../context/ScaleContext'
|
|
3
|
-
import type { EntityDisplayArea } from '../../types'
|
|
3
|
+
import type { EntityDisplayArea, InventoryTypeDefinition } from '../../types'
|
|
4
|
+
import { ENTITY_PLACEHOLDER_IMAGES } from './defaultEntityImages'
|
|
4
5
|
|
|
5
6
|
interface EntityDisplayProps {
|
|
6
7
|
area: EntityDisplayArea
|
|
8
|
+
placeholder: NonNullable<InventoryTypeDefinition['entityPlaceholder']>
|
|
9
|
+
/** When true, draw layout debug bounds instead of the default image (ignored if renderEntity is set). */
|
|
10
|
+
debug?: boolean
|
|
7
11
|
renderEntity?: ((width: number, height: number) => React.ReactNode) | null
|
|
8
12
|
}
|
|
9
13
|
|
|
10
|
-
export function EntityDisplay({
|
|
14
|
+
export function EntityDisplay({
|
|
15
|
+
area,
|
|
16
|
+
placeholder,
|
|
17
|
+
debug = false,
|
|
18
|
+
renderEntity,
|
|
19
|
+
}: EntityDisplayProps) {
|
|
11
20
|
const { scale } = useScale()
|
|
12
21
|
|
|
13
22
|
if (renderEntity === null) return null
|
|
@@ -15,6 +24,44 @@ export function EntityDisplay({ area, renderEntity }: EntityDisplayProps) {
|
|
|
15
24
|
const w = area.width * scale
|
|
16
25
|
const h = area.height * scale
|
|
17
26
|
|
|
27
|
+
let content: React.ReactNode
|
|
28
|
+
if (renderEntity) {
|
|
29
|
+
content = renderEntity(w, h)
|
|
30
|
+
} else if (debug) {
|
|
31
|
+
content = (
|
|
32
|
+
<div
|
|
33
|
+
className="mc-inv-entity-display-placeholder"
|
|
34
|
+
style={{
|
|
35
|
+
width: '100%',
|
|
36
|
+
height: '100%',
|
|
37
|
+
background: 'rgba(255, 0, 0, 0.15)',
|
|
38
|
+
border: '1px solid rgba(255, 0, 0, 0.4)',
|
|
39
|
+
boxSizing: 'border-box',
|
|
40
|
+
}}
|
|
41
|
+
/>
|
|
42
|
+
)
|
|
43
|
+
} else {
|
|
44
|
+
const image = ENTITY_PLACEHOLDER_IMAGES[placeholder]
|
|
45
|
+
if (!image) return null
|
|
46
|
+
content = (
|
|
47
|
+
<img
|
|
48
|
+
src={ENTITY_PLACEHOLDER_IMAGES[placeholder]}
|
|
49
|
+
alt=""
|
|
50
|
+
draggable={false}
|
|
51
|
+
className="mc-inv-entity-display-image"
|
|
52
|
+
style={{
|
|
53
|
+
display: 'block',
|
|
54
|
+
width: '100%',
|
|
55
|
+
height: '100%',
|
|
56
|
+
objectFit: 'contain',
|
|
57
|
+
pointerEvents: 'none',
|
|
58
|
+
userSelect: 'none',
|
|
59
|
+
imageRendering: 'pixelated',
|
|
60
|
+
}}
|
|
61
|
+
/>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
18
65
|
return (
|
|
19
66
|
<div
|
|
20
67
|
className="mc-inv-entity-display"
|
|
@@ -27,20 +74,7 @@ export function EntityDisplay({ area, renderEntity }: EntityDisplayProps) {
|
|
|
27
74
|
overflow: 'hidden',
|
|
28
75
|
}}
|
|
29
76
|
>
|
|
30
|
-
{
|
|
31
|
-
renderEntity(w, h)
|
|
32
|
-
) : (
|
|
33
|
-
<div
|
|
34
|
-
className="mc-inv-entity-display-placeholder"
|
|
35
|
-
style={{
|
|
36
|
-
width: '100%',
|
|
37
|
-
height: '100%',
|
|
38
|
-
background: 'rgba(255, 0, 0, 0.15)',
|
|
39
|
-
border: '1px solid rgba(255, 0, 0, 0.4)',
|
|
40
|
-
boxSizing: 'border-box',
|
|
41
|
-
}}
|
|
42
|
-
/>
|
|
43
|
-
)}
|
|
77
|
+
{content}
|
|
44
78
|
</div>
|
|
45
79
|
)
|
|
46
80
|
}
|
|
@@ -22,6 +22,8 @@ interface InventoryWindowProps {
|
|
|
22
22
|
style?: React.CSSProperties
|
|
23
23
|
enableKeyboardShortcuts?: boolean
|
|
24
24
|
showDebug?: boolean
|
|
25
|
+
/** When true, entity slot shows layout debug bounds instead of the default placeholder image. */
|
|
26
|
+
entityDisplayDebug?: boolean
|
|
25
27
|
/** Override entity display rendering. Pass a function returning JSX, or null to hide. */
|
|
26
28
|
renderEntity?: ((width: number, height: number) => React.ReactNode) | null
|
|
27
29
|
}
|
|
@@ -35,6 +37,7 @@ export function InventoryWindow({
|
|
|
35
37
|
style,
|
|
36
38
|
enableKeyboardShortcuts = true,
|
|
37
39
|
showDebug = false,
|
|
40
|
+
entityDisplayDebug = false,
|
|
38
41
|
renderEntity,
|
|
39
42
|
}: InventoryWindowProps) {
|
|
40
43
|
const def = getInventoryType(type)
|
|
@@ -104,8 +107,13 @@ export function InventoryWindow({
|
|
|
104
107
|
))}
|
|
105
108
|
|
|
106
109
|
{/* Entity display area */}
|
|
107
|
-
{def.entityDisplay && (
|
|
108
|
-
<EntityDisplay
|
|
110
|
+
{def.entityDisplay && def.entityPlaceholder && (
|
|
111
|
+
<EntityDisplay
|
|
112
|
+
area={def.entityDisplay}
|
|
113
|
+
placeholder={def.entityPlaceholder}
|
|
114
|
+
debug={entityDisplayDebug}
|
|
115
|
+
renderEntity={renderEntity}
|
|
116
|
+
/>
|
|
109
117
|
)}
|
|
110
118
|
|
|
111
119
|
{/* Progress bars */}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import playerImage from '../../assets/entities/player.png'
|
|
2
|
+
import horseImage from '../../assets/entities/horse.png'
|
|
3
|
+
import llamaImage from '../../assets/entities/llama.png'
|
|
4
|
+
import { InventoryTypeDefinition } from '../../types'
|
|
5
|
+
|
|
6
|
+
export const ENTITY_PLACEHOLDER_IMAGES: Record<
|
|
7
|
+
NonNullable<InventoryTypeDefinition['entityPlaceholder']>,
|
|
8
|
+
string | undefined
|
|
9
|
+
> = {
|
|
10
|
+
player: playerImage,
|
|
11
|
+
horse: horseImage,
|
|
12
|
+
llama: llamaImage,
|
|
13
|
+
}
|
|
@@ -153,7 +153,7 @@ export const ItemCanvas = memo(function ItemCanvas({
|
|
|
153
153
|
: '#55ff55'
|
|
154
154
|
|
|
155
155
|
// Font size for count: ~38% of slot size, min 7px, shadow offset = 1 scaled pixel
|
|
156
|
-
const countFontSize = Math.max(7, Math.round(renderSize * 0.
|
|
156
|
+
const countFontSize = Math.max(7, Math.round(renderSize * 0.5))
|
|
157
157
|
const shadow = Math.max(1, Math.round(pixelSize))
|
|
158
158
|
|
|
159
159
|
return (
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useState, useMemo, useCallback, useEffect, useLayoutEffect, useRef } from 'react'
|
|
2
|
-
import type { ItemStack, RecipeGuide, RecipeNavFrame } from '../../types'
|
|
2
|
+
import type { ItemStack, RecipeGuide, RecipeNavFrame, BlockTextureRender } from '../../types'
|
|
3
3
|
import { useScale } from '../../context/ScaleContext'
|
|
4
4
|
import { useInventoryContext } from '../../context/InventoryContext'
|
|
5
5
|
import { Slot } from '../Slot'
|
|
@@ -11,6 +11,8 @@ export interface JEIItem {
|
|
|
11
11
|
displayName: string
|
|
12
12
|
count?: number
|
|
13
13
|
metadata?: number
|
|
14
|
+
texture?: string
|
|
15
|
+
blockTexture?: BlockTextureRender
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
interface JEIProps {
|
|
@@ -328,6 +330,8 @@ export function JEI({
|
|
|
328
330
|
count: jeiItem.count ?? 1,
|
|
329
331
|
metadata: jeiItem.metadata,
|
|
330
332
|
displayName: jeiItem.displayName,
|
|
333
|
+
texture: jeiItem.texture,
|
|
334
|
+
blockTexture: jeiItem.blockTexture,
|
|
331
335
|
}
|
|
332
336
|
const slotIndex = -(page * itemsPerPage + i + 1)
|
|
333
337
|
const isFav = favorites.has(getItemKey(jeiItem))
|
|
@@ -46,6 +46,7 @@ export function Slot({
|
|
|
46
46
|
startDrag,
|
|
47
47
|
addDragSlot,
|
|
48
48
|
endDrag,
|
|
49
|
+
cancelDrag,
|
|
49
50
|
hoveredSlot,
|
|
50
51
|
setHoveredSlot,
|
|
51
52
|
activeNumberKey,
|
|
@@ -53,12 +54,14 @@ export function Slot({
|
|
|
53
54
|
setPKeyActive,
|
|
54
55
|
focusedSlot,
|
|
55
56
|
setFocusedSlot,
|
|
57
|
+
dragEndedRef,
|
|
56
58
|
} = useInventoryContext()
|
|
57
59
|
|
|
58
60
|
const { contentSize } = useScale()
|
|
59
61
|
const isMobile = useMobile()
|
|
60
62
|
const slotRef = useRef<HTMLDivElement>(null)
|
|
61
63
|
const labelRef = useRef<HTMLDivElement>(null)
|
|
64
|
+
const lastClickTimeRef = useRef(0)
|
|
62
65
|
const [mobileTouchPos, setMobileTouchPos] = useState({ x: 0, y: 0 })
|
|
63
66
|
const [showTooltip, setShowTooltip] = useState(false)
|
|
64
67
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
|
@@ -122,16 +125,19 @@ export function Slot({
|
|
|
122
125
|
(e: React.MouseEvent) => {
|
|
123
126
|
if (isMobile || disabled) return
|
|
124
127
|
e.preventDefault()
|
|
128
|
+
dragEndedRef.current = false
|
|
125
129
|
const button = e.button === 2 ? 'right' : e.button === 1 ? 'middle' : 'left'
|
|
126
130
|
if (button === 'middle') {
|
|
127
131
|
sendAction({ type: 'click', slotIndex: index, button: 'middle', mode: 'middle' })
|
|
128
132
|
return
|
|
129
133
|
}
|
|
130
134
|
if (heldItem && (button === 'left' || button === 'right')) {
|
|
135
|
+
// Don't start drag during double-click sequence
|
|
136
|
+
if (Date.now() - lastClickTimeRef.current < 400) return
|
|
131
137
|
startDrag(index, button)
|
|
132
138
|
}
|
|
133
139
|
},
|
|
134
|
-
[isMobile, disabled, heldItem, index, sendAction, startDrag],
|
|
140
|
+
[isMobile, disabled, heldItem, index, sendAction, startDrag, dragEndedRef],
|
|
135
141
|
)
|
|
136
142
|
|
|
137
143
|
const handleMouseUp = useCallback(
|
|
@@ -145,6 +151,11 @@ export function Slot({
|
|
|
145
151
|
return
|
|
146
152
|
}
|
|
147
153
|
|
|
154
|
+
// Suppress spurious mouseUp events that fire after a drag ends.
|
|
155
|
+
// The browser can dispatch extra mouseUp events after endDrag resets isDragging;
|
|
156
|
+
// without this guard they fall through to the click path below.
|
|
157
|
+
if (dragEndedRef.current) return
|
|
158
|
+
|
|
148
159
|
// Focus/swap logic — active in P mode OR when a slot is already focused
|
|
149
160
|
if (button === 'left' && (pKeyActive || focusedSlot !== null)) {
|
|
150
161
|
if (pKeyActive) setPKeyActive(false)
|
|
@@ -162,6 +173,15 @@ export function Slot({
|
|
|
162
173
|
return
|
|
163
174
|
}
|
|
164
175
|
|
|
176
|
+
// Suppress the second mouseup of a double-click to prevent it from
|
|
177
|
+
// putting the item back before the dblclick event fires mode=6.
|
|
178
|
+
const now = Date.now()
|
|
179
|
+
if (button === 'left' && now - lastClickTimeRef.current < 400) {
|
|
180
|
+
lastClickTimeRef.current = 0
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
lastClickTimeRef.current = now
|
|
184
|
+
|
|
165
185
|
const mode = e.shiftKey ? 'shift' : 'normal'
|
|
166
186
|
if (onClickOverride) {
|
|
167
187
|
onClickOverride(button, mode)
|
|
@@ -175,20 +195,21 @@ export function Slot({
|
|
|
175
195
|
}
|
|
176
196
|
if (isDragging) endDrag()
|
|
177
197
|
},
|
|
178
|
-
[isMobile, disabled, isDragging, dragSlots.length, sendAction, index, endDrag, onClickOverride, resultSlot, heldItem, item, pKeyActive, setPKeyActive, focusedSlot, setFocusedSlot],
|
|
198
|
+
[isMobile, disabled, isDragging, dragSlots.length, sendAction, index, endDrag, onClickOverride, resultSlot, heldItem, item, pKeyActive, setPKeyActive, focusedSlot, setFocusedSlot, dragEndedRef],
|
|
179
199
|
)
|
|
180
200
|
|
|
181
201
|
const handleDoubleClick = useCallback(
|
|
182
202
|
(e: React.MouseEvent) => {
|
|
183
203
|
if (isMobile || disabled) return
|
|
184
204
|
e.preventDefault()
|
|
205
|
+
cancelDrag()
|
|
185
206
|
if (onClickOverride) {
|
|
186
207
|
onClickOverride('left', 'double')
|
|
187
208
|
} else {
|
|
188
209
|
sendAction({ type: 'click', slotIndex: index, button: 'left', mode: 'double' })
|
|
189
210
|
}
|
|
190
211
|
},
|
|
191
|
-
[isMobile, disabled, sendAction, index, onClickOverride],
|
|
212
|
+
[isMobile, disabled, sendAction, index, onClickOverride, cancelDrag],
|
|
192
213
|
)
|
|
193
214
|
|
|
194
215
|
const handleContextMenu = useCallback((e: React.MouseEvent) => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { InventoryAction, InventoryWindowState, PlayerState, SlotState, ItemStack } from '../types'
|
|
2
2
|
import type { InventoryConnector, ConnectorListener, ConnectorEvent, MineflayerBot } from './types'
|
|
3
|
+
import { getInventoryType } from '../registry'
|
|
3
4
|
|
|
4
5
|
type RawSlot = { type: number; count: number; metadata?: number; nbt?: unknown }
|
|
5
6
|
|
|
@@ -37,6 +38,20 @@ export interface MineflayerConnectorOptions {
|
|
|
37
38
|
* ```
|
|
38
39
|
*/
|
|
39
40
|
itemMapper?: (raw: RawSlot, mapped: ItemStack) => ItemStack
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* When true, the connector only tracks the player inventory (window 0) and never
|
|
44
|
+
* emits windowOpen/windowUpdate/windowClose events. Use for HUD hotbar that must
|
|
45
|
+
* always show player hotbar slots regardless of open container windows.
|
|
46
|
+
*/
|
|
47
|
+
hotbarOnly?: boolean
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Optional title formatter. Called with the raw window title from the server
|
|
51
|
+
* (may be a JSON text component string, NBT object, or plain text).
|
|
52
|
+
* Return a human-readable string for display.
|
|
53
|
+
*/
|
|
54
|
+
formatTitle?: (rawTitle: any) => string
|
|
40
55
|
}
|
|
41
56
|
|
|
42
57
|
function makeSlotConverter(itemMapper?: MineflayerConnectorOptions['itemMapper']) {
|
|
@@ -82,6 +97,9 @@ interface MineflayerBotExtended extends MineflayerBot {
|
|
|
82
97
|
supportFeature?(name: string): boolean
|
|
83
98
|
_client?: {
|
|
84
99
|
write(packet: string, data: unknown): void
|
|
100
|
+
on?(event: string, listener: (...args: any[]) => void): void
|
|
101
|
+
off?(event: string, listener: (...args: any[]) => void): void
|
|
102
|
+
removeListener?(event: string, listener: (...args: any[]) => void): void
|
|
85
103
|
writeChannel?(channel: string, data: unknown): void
|
|
86
104
|
registerChannel?(channel: string, schema: unknown): void
|
|
87
105
|
}
|
|
@@ -118,35 +136,148 @@ export function createMineflayerConnector(bot: MineflayerBot, options?: Mineflay
|
|
|
118
136
|
const listeners = new Set<ConnectorListener>()
|
|
119
137
|
const ext = bot as MineflayerBotExtended
|
|
120
138
|
const convert = makeSlotConverter(options?.itemMapper)
|
|
139
|
+
const hotbarOnly = options?.hotbarOnly ?? false
|
|
140
|
+
const formatTitle = options?.formatTitle
|
|
141
|
+
|
|
142
|
+
// Track window properties (furnace progress, enchant levels, etc.)
|
|
143
|
+
const windowProperties: Record<string, number> = {}
|
|
144
|
+
let currentWindowType: string | null = null
|
|
145
|
+
|
|
146
|
+
// Track stateId for raw packet sending (drag operations bypass bot.clickWindow)
|
|
147
|
+
let dragStateId = typeof (bot as any)._stateId === 'number' ? (bot as any)._stateId : -1
|
|
148
|
+
// True only while drag packets are being written; guards trackState against
|
|
149
|
+
// stale server responses overwriting our predicted stateId mid-sequence.
|
|
150
|
+
let isDraggingRaw = false
|
|
151
|
+
|
|
152
|
+
// Resolve the Item class (prismarine-item) for converting notch-format items.
|
|
153
|
+
// We extract it lazily from the first non-null slot item's constructor.
|
|
154
|
+
let ItemClass: { fromNotch(notch: unknown): unknown } | null = null
|
|
155
|
+
function getItemClass(): typeof ItemClass {
|
|
156
|
+
if (ItemClass) return ItemClass
|
|
157
|
+
const win = bot.currentWindow ?? bot.inventory
|
|
158
|
+
for (const slot of win.slots) {
|
|
159
|
+
if (slot) {
|
|
160
|
+
ItemClass = slot.constructor as any
|
|
161
|
+
return ItemClass
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return null
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const rawPacketListeners: Array<[string, (...args: any[]) => void]> = []
|
|
168
|
+
if (ext._client?.on) {
|
|
169
|
+
const trackState = (packet: any) => {
|
|
170
|
+
if (packet.stateId != null) {
|
|
171
|
+
if (isDraggingRaw) {
|
|
172
|
+
// During active drag, only accept higher stateIds to prevent
|
|
173
|
+
// stale server responses from reverting our predicted value.
|
|
174
|
+
if (packet.stateId > dragStateId) {
|
|
175
|
+
dragStateId = packet.stateId
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
dragStateId = packet.stateId
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
ext._client.on('window_items' as any, trackState)
|
|
183
|
+
ext._client.on('set_slot' as any, trackState)
|
|
184
|
+
rawPacketListeners.push(['window_items', trackState], ['set_slot', trackState])
|
|
185
|
+
|
|
186
|
+
// Mineflayer drops set_slot with windowId=-1 (cursor updates) because the window
|
|
187
|
+
// lookup fails. Intercept these to keep selectedItem in sync after raw packet ops.
|
|
188
|
+
const onRawSetSlot = (packet: any) => {
|
|
189
|
+
if (packet.windowId !== -1 || packet.slot !== -1) return
|
|
190
|
+
const win = bot.currentWindow ?? bot.inventory
|
|
191
|
+
const IC = getItemClass()
|
|
192
|
+
if (IC && packet.item) {
|
|
193
|
+
;(win as any).selectedItem = IC.fromNotch(packet.item) ?? null
|
|
194
|
+
} else {
|
|
195
|
+
;(win as any).selectedItem = null
|
|
196
|
+
}
|
|
197
|
+
emit({ type: 'heldItemChange', item: convert((win as any).selectedItem) })
|
|
198
|
+
}
|
|
199
|
+
ext._client.on('set_slot' as any, onRawSetSlot)
|
|
200
|
+
rawPacketListeners.push(['set_slot', onRawSetSlot])
|
|
201
|
+
|
|
202
|
+
// Mineflayer processes window_items slots but ignores the carriedItem field.
|
|
203
|
+
// Intercept to update selectedItem from the server's cursor state.
|
|
204
|
+
// Always call scheduleSlotUpdate() so UI gets corrected state even when mineflayer's
|
|
205
|
+
// handler fires first with a stale selectedItem (drag-flash fix).
|
|
206
|
+
const onRawWindowItems = (packet: any) => {
|
|
207
|
+
if (packet.carriedItem != null) {
|
|
208
|
+
const win = packet.windowId === 0 ? bot.inventory : (bot.currentWindow ?? bot.inventory)
|
|
209
|
+
const IC = getItemClass()
|
|
210
|
+
if (IC) {
|
|
211
|
+
;(win as any).selectedItem = IC.fromNotch(packet.carriedItem) ?? null
|
|
212
|
+
} else {
|
|
213
|
+
;(win as any).selectedItem = null
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
scheduleSlotUpdate()
|
|
217
|
+
}
|
|
218
|
+
ext._client.on('window_items' as any, onRawWindowItems)
|
|
219
|
+
rawPacketListeners.push(['window_items', onRawWindowItems])
|
|
220
|
+
}
|
|
121
221
|
|
|
122
222
|
function emit(event: ConnectorEvent) {
|
|
123
223
|
listeners.forEach((l) => l(event))
|
|
124
224
|
}
|
|
125
225
|
|
|
226
|
+
/** Build a reverse map from dataSlot index → property name for the current window type. */
|
|
227
|
+
function getDataSlotMap(type: string): Record<number, string> | null {
|
|
228
|
+
const typeDef = getInventoryType(type)
|
|
229
|
+
if (!typeDef?.properties) return null
|
|
230
|
+
const map: Record<number, string> = {}
|
|
231
|
+
for (const [name, def] of Object.entries(typeDef.properties)) {
|
|
232
|
+
map[def.dataSlot] = name
|
|
233
|
+
}
|
|
234
|
+
return map
|
|
235
|
+
}
|
|
236
|
+
|
|
126
237
|
/**
|
|
127
238
|
* Builds a window state from the currently open window, OR from `bot.inventory`
|
|
128
239
|
* when no container is open (exposing the player's own inventory as a synthetic
|
|
129
240
|
* 'player' window with windowId = 0).
|
|
130
241
|
*/
|
|
131
242
|
function buildWindowState(): InventoryWindowState | null {
|
|
132
|
-
|
|
243
|
+
// In hotbarOnly mode, always build a player-like state (never the container itself)
|
|
244
|
+
const win = hotbarOnly ? null : bot.currentWindow
|
|
133
245
|
if (win) {
|
|
134
|
-
|
|
246
|
+
const title = formatTitle ? formatTitle(win.title) : win.title
|
|
247
|
+
const state: InventoryWindowState = {
|
|
135
248
|
windowId: win.id,
|
|
136
249
|
type: win.type ?? 'unknown',
|
|
137
|
-
title
|
|
250
|
+
title,
|
|
138
251
|
slots: botSlotsToSlotStates(win.slots, convert),
|
|
139
|
-
heldItem: convert(
|
|
252
|
+
heldItem: convert(win.selectedItem),
|
|
140
253
|
}
|
|
254
|
+
if (Object.keys(windowProperties).length > 0) {
|
|
255
|
+
state.properties = { ...windowProperties }
|
|
256
|
+
}
|
|
257
|
+
return state
|
|
258
|
+
}
|
|
259
|
+
// No open container (or hotbarOnly) — expose the player inventory.
|
|
260
|
+
// When a container IS open in hotbarOnly mode, mineflayer doesn't update
|
|
261
|
+
// bot.inventory.slots in real time — read player slots from the container
|
|
262
|
+
// window instead (using inventoryStart offset).
|
|
263
|
+
const containerWin = hotbarOnly ? bot.currentWindow : null
|
|
264
|
+
const invStart: number | null = containerWin ? (containerWin as any).inventoryStart ?? null : null
|
|
265
|
+
const readSlot = (playerSlotIndex: number) => {
|
|
266
|
+
if (containerWin && invStart != null) {
|
|
267
|
+
// Map player inventory index to container window index
|
|
268
|
+
const containerIndex = playerSlotIndex - 9 + invStart
|
|
269
|
+
return convert(containerWin.slots[containerIndex])
|
|
270
|
+
}
|
|
271
|
+
return convert(bot.inventory.slots[playerSlotIndex])
|
|
141
272
|
}
|
|
142
|
-
|
|
273
|
+
|
|
143
274
|
const invSlots: SlotState[] = []
|
|
144
275
|
// Slots 0–8: crafting/armour — leave empty (not accessible from bot.inventory directly)
|
|
145
276
|
for (let i = 0; i < 9; i++) invSlots.push({ index: i, item: null })
|
|
146
277
|
// Slots 9–35: main inventory
|
|
147
|
-
for (let i = 9; i <= 35; i++) invSlots.push({ index: i, item:
|
|
278
|
+
for (let i = 9; i <= 35; i++) invSlots.push({ index: i, item: readSlot(i) })
|
|
148
279
|
// Slots 36–44: hotbar
|
|
149
|
-
for (let i = 36; i <= 44; i++) invSlots.push({ index: i, item:
|
|
280
|
+
for (let i = 36; i <= 44; i++) invSlots.push({ index: i, item: readSlot(i) })
|
|
150
281
|
// Slot 45: offhand
|
|
151
282
|
invSlots.push({ index: 45, item: convert(bot.inventory.slots[45]) })
|
|
152
283
|
return {
|
|
@@ -154,7 +285,7 @@ export function createMineflayerConnector(bot: MineflayerBot, options?: Mineflay
|
|
|
154
285
|
type: 'player',
|
|
155
286
|
title: undefined,
|
|
156
287
|
slots: invSlots,
|
|
157
|
-
heldItem: convert(bot.
|
|
288
|
+
heldItem: convert((bot.inventory as any).selectedItem ?? null),
|
|
158
289
|
}
|
|
159
290
|
}
|
|
160
291
|
|
|
@@ -178,14 +309,91 @@ export function createMineflayerConnector(bot: MineflayerBot, options?: Mineflay
|
|
|
178
309
|
const onSetSlot = () => {
|
|
179
310
|
const state = buildWindowState()
|
|
180
311
|
if (state) emit({ type: 'windowUpdate', state })
|
|
181
|
-
|
|
182
|
-
|
|
312
|
+
emit({ type: 'playerUpdate', state: buildPlayerState() })
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const onHeldItemChanged = () => {
|
|
316
|
+
emit({ type: 'playerUpdate', state: buildPlayerState() })
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Mineflayer emits 'setSlot:${windowId}' (e.g. 'setSlot:0'), not plain 'setSlot'.
|
|
320
|
+
// Always listen on window 0 (player inventory) and dynamically track container windows.
|
|
321
|
+
// Also listen for 'heldItemChanged' to track active hotbar slot changes.
|
|
322
|
+
let currentWindowSlotEvent: string | null = null
|
|
323
|
+
let currentWindowItemsEvent: string | null = null
|
|
324
|
+
|
|
325
|
+
const onWindowOpenInternal = () => {
|
|
326
|
+
const win = bot.currentWindow
|
|
327
|
+
if (win) {
|
|
328
|
+
currentWindowSlotEvent = `setSlot:${win.id}`
|
|
329
|
+
currentWindowItemsEvent = `setWindowItems:${win.id}`
|
|
330
|
+
bot.on(currentWindowSlotEvent as any, onSetSlot)
|
|
331
|
+
bot.on(currentWindowItemsEvent as any, scheduleSlotUpdate)
|
|
332
|
+
// Reset properties and resolve window type for property mapping
|
|
333
|
+
for (const key of Object.keys(windowProperties)) delete windowProperties[key]
|
|
334
|
+
currentWindowType = win.type ?? null
|
|
335
|
+
;(win as any).on('updateSlot', scheduleSlotUpdate)
|
|
336
|
+
}
|
|
337
|
+
if (!hotbarOnly) onWindowOpen()
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const onWindowCloseInternal = () => {
|
|
341
|
+
const closingWin = bot.currentWindow
|
|
342
|
+
if (closingWin) {
|
|
343
|
+
;(closingWin as any).off('updateSlot', scheduleSlotUpdate)
|
|
344
|
+
}
|
|
345
|
+
if (currentWindowSlotEvent) {
|
|
346
|
+
bot.off(currentWindowSlotEvent as any, onSetSlot)
|
|
347
|
+
currentWindowSlotEvent = null
|
|
348
|
+
}
|
|
349
|
+
if (currentWindowItemsEvent) {
|
|
350
|
+
bot.off(currentWindowItemsEvent as any, scheduleSlotUpdate)
|
|
351
|
+
currentWindowItemsEvent = null
|
|
352
|
+
}
|
|
353
|
+
currentWindowType = null
|
|
354
|
+
for (const key of Object.keys(windowProperties)) delete windowProperties[key]
|
|
355
|
+
if (!hotbarOnly) onWindowClose()
|
|
356
|
+
// In hotbar mode, emit a windowUpdate after close so the hotbar
|
|
357
|
+
// re-syncs from bot.inventory (now freshly copied back by mineflayer)
|
|
358
|
+
if (hotbarOnly) scheduleSlotUpdate()
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Handle craft_progress_bar packets (furnace progress, enchant levels, etc.)
|
|
362
|
+
const onCraftProgressBar = (packet: { windowId: number; property: number; value: number }) => {
|
|
363
|
+
const win = bot.currentWindow
|
|
364
|
+
if (!win || packet.windowId !== win.id || !currentWindowType) return
|
|
365
|
+
const slotMap = getDataSlotMap(currentWindowType)
|
|
366
|
+
if (!slotMap) return
|
|
367
|
+
const propName = slotMap[packet.property]
|
|
368
|
+
if (propName) {
|
|
369
|
+
windowProperties[propName] = packet.value
|
|
370
|
+
const state = buildWindowState()
|
|
371
|
+
if (state) emit({ type: 'windowUpdate', state })
|
|
183
372
|
}
|
|
184
373
|
}
|
|
185
374
|
|
|
186
|
-
|
|
187
|
-
bot
|
|
188
|
-
|
|
375
|
+
// Listen to Window-level slot changes to catch optimistic acceptClick updates
|
|
376
|
+
// that don't fire bot-level events (needed for multi-connector architecture)
|
|
377
|
+
let slotUpdateScheduled = false
|
|
378
|
+
const scheduleSlotUpdate = () => {
|
|
379
|
+
if (slotUpdateScheduled) return
|
|
380
|
+
slotUpdateScheduled = true
|
|
381
|
+
queueMicrotask(() => {
|
|
382
|
+
slotUpdateScheduled = false
|
|
383
|
+
onSetSlot()
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
;(bot.inventory as any).on('updateSlot', scheduleSlotUpdate)
|
|
388
|
+
|
|
389
|
+
bot.on('windowOpen', onWindowOpenInternal)
|
|
390
|
+
bot.on('windowClose', onWindowCloseInternal)
|
|
391
|
+
bot.on('setSlot:0' as any, onSetSlot)
|
|
392
|
+
bot.on('setWindowItems:0' as any, scheduleSlotUpdate)
|
|
393
|
+
bot.on('heldItemChanged' as any, onHeldItemChanged)
|
|
394
|
+
if (!hotbarOnly && ext._client) {
|
|
395
|
+
ext._client.on?.('craft_progress_bar' as any, onCraftProgressBar as any)
|
|
396
|
+
}
|
|
189
397
|
|
|
190
398
|
async function openPlayerInventory() {
|
|
191
399
|
const vehicle = bot.vehicle
|
|
@@ -255,57 +463,123 @@ export function createMineflayerConnector(bot: MineflayerBot, options?: Mineflay
|
|
|
255
463
|
openPlayerInventory,
|
|
256
464
|
|
|
257
465
|
sendAction: async (action: InventoryAction) => {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const win = bot.currentWindow
|
|
265
|
-
|
|
266
|
-
if (action.type === 'trade' && win) {
|
|
267
|
-
if (ext.trade && isVillagerWindow(win)) {
|
|
268
|
-
await ext.trade(win, action.tradeIndex, 1)
|
|
269
|
-
} else if (isVillagerWindow(win)) {
|
|
270
|
-
await win.trade(action.tradeIndex, 1)
|
|
466
|
+
try {
|
|
467
|
+
// Hotbar "open inventory" button — delegates to openPlayerInventory()
|
|
468
|
+
if (action.type === 'open-inventory') {
|
|
469
|
+
await openPlayerInventory()
|
|
470
|
+
return
|
|
271
471
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if (
|
|
285
|
-
|
|
286
|
-
|
|
472
|
+
|
|
473
|
+
const win = bot.currentWindow
|
|
474
|
+
|
|
475
|
+
if (action.type === 'trade' && win) {
|
|
476
|
+
if (ext.trade && isVillagerWindow(win)) {
|
|
477
|
+
await ext.trade(win, action.tradeIndex, 1)
|
|
478
|
+
} else if (isVillagerWindow(win)) {
|
|
479
|
+
await win.trade(action.tradeIndex, 1)
|
|
480
|
+
}
|
|
481
|
+
return
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (action.type === 'enchant' && win && isEnchantmentTableWindow(win)) {
|
|
485
|
+
await win.enchant(action.enchantIndex)
|
|
486
|
+
return
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (action.type === 'rename' && win && isAnvilWindow(win)) {
|
|
490
|
+
const w = win as { slots?: unknown[]; findInventoryItem?: (id: number) => unknown; rename: (item: unknown, name: string) => Promise<void> }
|
|
491
|
+
const inputSlot = w.slots?.[0] as { type?: number; metadata?: number; count?: number; nbt?: unknown } | null
|
|
492
|
+
const item = inputSlot?.type ? (w.findInventoryItem?.(inputSlot.type) ?? inputSlot) : null
|
|
493
|
+
if (item) await w.rename(item, action.text)
|
|
494
|
+
return
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (action.type === 'beacon' && win && isBeaconWindow(win)) {
|
|
498
|
+
if (typeof win.setBeaconEffects === 'function') {
|
|
499
|
+
await win.setBeaconEffects(action.primaryEffect, action.secondaryEffect)
|
|
500
|
+
} else if (ext._client) {
|
|
501
|
+
ext._client.write('beacon_effect', {
|
|
502
|
+
primaryEffect: action.primaryEffect,
|
|
503
|
+
secondaryEffect: action.secondaryEffect,
|
|
504
|
+
})
|
|
505
|
+
}
|
|
506
|
+
return
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (action.type === 'click' && action.mode === 'double') {
|
|
510
|
+
// bot.clickWindow() throws for mode=6 (prismarine-windows doubleClick is unimplemented).
|
|
511
|
+
// Send raw window_click packet directly, same approach as drag (mode=5).
|
|
512
|
+
if (!ext._client) return
|
|
513
|
+
const windowId = bot.currentWindow ? bot.currentWindow.id : 0
|
|
287
514
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
515
|
+
ext._client.write('window_click', {
|
|
516
|
+
windowId,
|
|
517
|
+
stateId: dragStateId,
|
|
518
|
+
slot: action.slotIndex,
|
|
519
|
+
mouseButton: 0,
|
|
520
|
+
mode: 6,
|
|
521
|
+
changedSlots: [],
|
|
522
|
+
cursorItem: { present: false } as any,
|
|
295
523
|
})
|
|
524
|
+
dragStateId++
|
|
525
|
+
return
|
|
296
526
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
527
|
+
|
|
528
|
+
if (action.type === 'click') {
|
|
529
|
+
const [mouseButton, mode] = modeFromAction(action)
|
|
530
|
+
await bot.clickWindow(action.slotIndex, mouseButton, mode)
|
|
531
|
+
onSetSlot()
|
|
532
|
+
} else if (action.type === 'drag') {
|
|
533
|
+
// bot.clickWindow() throws for mode=5 (prismarine-windows dragClick is unimplemented).
|
|
534
|
+
// Send raw window_click packets directly via _client.write.
|
|
535
|
+
if (!ext._client) return
|
|
536
|
+
const isRight = action.button === 'right'
|
|
537
|
+
const startButton = isRight ? 4 : 0
|
|
538
|
+
const slotButton = isRight ? 5 : 1
|
|
539
|
+
const endButton = isRight ? 6 : 2
|
|
540
|
+
const windowId = bot.currentWindow ? bot.currentWindow.id : 0
|
|
541
|
+
const cursorItem = { present: false } as any
|
|
542
|
+
|
|
543
|
+
isDraggingRaw = true
|
|
544
|
+
const writeClick = (slot: number, mouseButton: number) => {
|
|
545
|
+
ext._client!.write('window_click', {
|
|
546
|
+
windowId,
|
|
547
|
+
stateId: dragStateId,
|
|
548
|
+
slot,
|
|
549
|
+
mouseButton,
|
|
550
|
+
mode: 5,
|
|
551
|
+
changedSlots: [],
|
|
552
|
+
cursorItem,
|
|
553
|
+
})
|
|
554
|
+
dragStateId++
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
writeClick(-999, startButton)
|
|
558
|
+
for (const slot of action.slots) {
|
|
559
|
+
writeClick(slot, slotButton)
|
|
560
|
+
}
|
|
561
|
+
writeClick(-999, endButton)
|
|
562
|
+
isDraggingRaw = false
|
|
563
|
+
} else if (action.type === 'drop') {
|
|
564
|
+
await bot.clickWindow(action.slotIndex, action.all ? 1 : 0, 4)
|
|
565
|
+
onSetSlot()
|
|
566
|
+
} else if (action.type === 'close') {
|
|
567
|
+
if (win) {
|
|
568
|
+
bot.closeWindow(win)
|
|
569
|
+
} else {
|
|
570
|
+
// Player inventory (synthetic) — send close_window so server drops cursor items
|
|
571
|
+
if (ext._client) {
|
|
572
|
+
ext._client.write('close_window', { windowId: 0 })
|
|
573
|
+
}
|
|
574
|
+
;(bot.inventory as any).selectedItem = null
|
|
575
|
+
}
|
|
576
|
+
} else if (action.type === 'hotbar-swap') {
|
|
577
|
+
await bot.clickWindow(action.slotIndex, action.hotbarSlot, 2)
|
|
578
|
+
onSetSlot()
|
|
579
|
+
}
|
|
580
|
+
} catch (err) {
|
|
581
|
+
const detail = 'slotIndex' in action ? ` slot=${(action as any).slotIndex}` : ''
|
|
582
|
+
console.error(`[minecraft-inventory] sendAction "${action.type}"${detail} failed:`, err)
|
|
309
583
|
}
|
|
310
584
|
},
|
|
311
585
|
|
|
@@ -318,9 +592,29 @@ export function createMineflayerConnector(bot: MineflayerBot, options?: Mineflay
|
|
|
318
592
|
listeners.add(listener)
|
|
319
593
|
return () => {
|
|
320
594
|
listeners.delete(listener)
|
|
321
|
-
bot.off('windowOpen',
|
|
322
|
-
bot.off('windowClose',
|
|
323
|
-
bot.off('setSlot', onSetSlot)
|
|
595
|
+
bot.off('windowOpen', onWindowOpenInternal)
|
|
596
|
+
bot.off('windowClose', onWindowCloseInternal)
|
|
597
|
+
bot.off('setSlot:0' as any, onSetSlot)
|
|
598
|
+
bot.off('setWindowItems:0' as any, scheduleSlotUpdate)
|
|
599
|
+
bot.off('heldItemChanged' as any, onHeldItemChanged)
|
|
600
|
+
if (ext._client?.off) {
|
|
601
|
+
if (!hotbarOnly) {
|
|
602
|
+
ext._client.off('craft_progress_bar' as any, onCraftProgressBar as any)
|
|
603
|
+
}
|
|
604
|
+
for (const [event, listener] of rawPacketListeners) {
|
|
605
|
+
ext._client.off(event as any, listener as any)
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (currentWindowSlotEvent) {
|
|
609
|
+
bot.off(currentWindowSlotEvent as any, onSetSlot)
|
|
610
|
+
}
|
|
611
|
+
if (currentWindowItemsEvent) {
|
|
612
|
+
bot.off(currentWindowItemsEvent as any, scheduleSlotUpdate)
|
|
613
|
+
}
|
|
614
|
+
;(bot.inventory as any).off('updateSlot', scheduleSlotUpdate)
|
|
615
|
+
if (bot.currentWindow) {
|
|
616
|
+
;(bot.currentWindow as any).off('updateSlot', scheduleSlotUpdate)
|
|
617
|
+
}
|
|
324
618
|
}
|
|
325
619
|
},
|
|
326
620
|
}
|
package/src/connector/types.ts
CHANGED
|
@@ -28,6 +28,7 @@ export interface MineflayerBotWindow {
|
|
|
28
28
|
type: string
|
|
29
29
|
title?: string
|
|
30
30
|
slots: Array<{ type: number; count: number; metadata?: number; nbt?: unknown } | null>
|
|
31
|
+
selectedItem?: { type: number; count: number; metadata?: number; nbt?: unknown } | null
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
/** Villager window from bot.openVillager() — has trade() */
|
|
@@ -42,6 +42,9 @@ export interface InventoryContextValue {
|
|
|
42
42
|
/** Pending first digit for P-key slot number entry */
|
|
43
43
|
pKeyDigit: string
|
|
44
44
|
setPKeyDigit: (d: string) => void
|
|
45
|
+
/** Ref set to true when a drag just ended; cleared on next mouseDown.
|
|
46
|
+
* Used by Slot to suppress spurious click events that fire after endDrag. */
|
|
47
|
+
dragEndedRef: React.MutableRefObject<boolean>
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
const InventoryContext = createContext<InventoryContextValue | null>(null)
|
|
@@ -90,6 +93,10 @@ export function InventoryProvider({ connector, children, noDragSpread = false }:
|
|
|
90
93
|
const dragButtonRef = useRef(dragButton)
|
|
91
94
|
dragButtonRef.current = dragButton
|
|
92
95
|
|
|
96
|
+
// Set to true when endDrag fires; cleared on the next mouseDown in Slot.
|
|
97
|
+
// Prevents spurious mouseUp events from sending unwanted clicks after a drag.
|
|
98
|
+
const dragEndedRef = useRef(false)
|
|
99
|
+
|
|
93
100
|
useEffect(() => {
|
|
94
101
|
if (!connector) return
|
|
95
102
|
setWindowState(connector.getWindowState())
|
|
@@ -159,6 +166,7 @@ export function InventoryProvider({ connector, children, noDragSpread = false }:
|
|
|
159
166
|
|
|
160
167
|
const startDrag = useCallback((slotIndex: number, button: 'left' | 'right') => {
|
|
161
168
|
if (noDragSpread) return
|
|
169
|
+
dragEndedRef.current = false
|
|
162
170
|
setIsDragging(true)
|
|
163
171
|
setDragButton(button)
|
|
164
172
|
setDragSlots([slotIndex])
|
|
@@ -175,6 +183,7 @@ export function InventoryProvider({ connector, children, noDragSpread = false }:
|
|
|
175
183
|
}, [computeDragPreview, dragButton, heldItem, windowState])
|
|
176
184
|
|
|
177
185
|
const endDrag = useCallback(() => {
|
|
186
|
+
dragEndedRef.current = true
|
|
178
187
|
setDragSlots((slots) => {
|
|
179
188
|
const button = dragButtonRef.current
|
|
180
189
|
const held = heldItemRef.current
|
|
@@ -196,7 +205,6 @@ export function InventoryProvider({ connector, children, noDragSpread = false }:
|
|
|
196
205
|
})
|
|
197
206
|
if (compatibleSlots.length > 0) {
|
|
198
207
|
const perSlot = Math.floor(held.count / compatibleSlots.length)
|
|
199
|
-
// Vanilla behavior: if perSlot=0 (more slots than items), nothing is distributed
|
|
200
208
|
if (perSlot > 0) {
|
|
201
209
|
let totalPlaced = 0
|
|
202
210
|
for (const idx of compatibleSlots) {
|
|
@@ -302,6 +310,7 @@ export function InventoryProvider({ connector, children, noDragSpread = false }:
|
|
|
302
310
|
setFocusedSlot,
|
|
303
311
|
pKeyDigit,
|
|
304
312
|
setPKeyDigit,
|
|
313
|
+
dragEndedRef,
|
|
305
314
|
}
|
|
306
315
|
|
|
307
316
|
return <InventoryContext.Provider value={value}>{children}</InventoryContext.Provider>
|
|
@@ -95,6 +95,7 @@ export const inventoryDefinitions = makeInventoryDefinitions({
|
|
|
95
95
|
backgroundWidth: 176,
|
|
96
96
|
backgroundHeight: 166,
|
|
97
97
|
entityDisplay: { x: 26, y: 8, width: 49, height: 70 },
|
|
98
|
+
entityPlaceholder: 'player',
|
|
98
99
|
slots: [
|
|
99
100
|
// Result (0)
|
|
100
101
|
{ x: 154, y: 28, group: 'result', resultSlot: true },
|
|
@@ -552,6 +553,7 @@ export const inventoryDefinitions = makeInventoryDefinitions({
|
|
|
552
553
|
backgroundWidth: 176,
|
|
553
554
|
backgroundHeight: 166,
|
|
554
555
|
entityDisplay: { x: 26, y: 18, width: 52, height: 52 },
|
|
556
|
+
entityPlaceholder: 'horse',
|
|
555
557
|
slots: [
|
|
556
558
|
// Saddle (0); slot 1 (horse armor) is absent for donkeys — gap is intentional
|
|
557
559
|
{ x: 8, y: 18, group: 'saddle', label: 'Saddle' },
|
|
@@ -568,6 +570,7 @@ export const inventoryDefinitions = makeInventoryDefinitions({
|
|
|
568
570
|
backgroundWidth: 176,
|
|
569
571
|
backgroundHeight: 166,
|
|
570
572
|
entityDisplay: { x: 26, y: 18, width: 52, height: 52 },
|
|
573
|
+
entityPlaceholder: 'llama',
|
|
571
574
|
slots: [
|
|
572
575
|
{ x: 8, y: 18, group: 'saddle', label: 'Carpet' },
|
|
573
576
|
...gridSlots(5, 3, 80, 18, 'chest').map((s, i) => i === 0 ? { ...s, index: 2 } : s),
|
package/src/types.ts
CHANGED
|
@@ -177,6 +177,11 @@ export interface InventoryTypeDefinition {
|
|
|
177
177
|
extraData?: Record<string, unknown>
|
|
178
178
|
/** Bounds for the entity preview area (e.g. player model, horse model). */
|
|
179
179
|
entityDisplay?: EntityDisplayArea
|
|
180
|
+
/**
|
|
181
|
+
* Bundled placeholder image for the entity panel when `renderEntity` is not set.
|
|
182
|
+
* Maps to files under `src/assets/entities/`.
|
|
183
|
+
*/
|
|
184
|
+
entityPlaceholder?: 'player' | 'horse' | 'llama'
|
|
180
185
|
/**
|
|
181
186
|
* When set, InventoryBackground will canvas-stitch the background texture:
|
|
182
187
|
* top (title + N container rows) + bottom (player inventory section) from
|