minecraft-inventory 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -3
- package/package.json +1 -1
- package/src/bundledTexturesConfig.ts +126 -0
- package/src/cache/blockRenderer.ts +127 -0
- package/src/cache/textureCache.ts +10 -0
- package/src/components/InventoryOverlay/InventoryOverlay.tsx +12 -2
- package/src/components/InventoryWindow/EntityDisplay.tsx +50 -16
- package/src/components/InventoryWindow/InventoryBackground.tsx +1 -1
- package/src/components/InventoryWindow/InventoryWindow.tsx +10 -2
- package/src/components/InventoryWindow/defaultEntityImages.ts +13 -0
- package/src/components/ItemCanvas/ItemCanvas.tsx +103 -17
- package/src/components/JEI/JEI.module.css +2 -0
- package/src/components/JEI/JEI.tsx +2 -3
- package/src/components/Slot/Slot.tsx +24 -3
- package/src/components/Text/MessageFormattedString.tsx +1 -0
- package/src/components/Tooltip/Tooltip.tsx +1 -2
- package/src/connector/mineflayer.ts +372 -66
- package/src/connector/types.ts +1 -0
- package/src/context/InventoryContext.tsx +10 -1
- package/src/generated/localTextures.ts +66 -51
- package/src/index.tsx +18 -0
- package/src/registry/inventories.ts +12 -7
- package/src/types.ts +37 -0
|
@@ -96,6 +96,7 @@ export function JEI({
|
|
|
96
96
|
|
|
97
97
|
const ro = new ResizeObserver((entries) => {
|
|
98
98
|
for (const entry of entries) {
|
|
99
|
+
// console.log('got size', entry.target.className, entry.contentRect.width, entry.contentRect.height)
|
|
99
100
|
if (entry.target === (root as unknown as Element)) {
|
|
100
101
|
sizes.rootW = entry.contentRect.width
|
|
101
102
|
} else if (entry.target === (grid as unknown as Element)) {
|
|
@@ -240,8 +241,6 @@ export function JEI({
|
|
|
240
241
|
className="mc-inv-jei-header"
|
|
241
242
|
style={{
|
|
242
243
|
padding: `${padding}px`,
|
|
243
|
-
background: '#c6c6c6',
|
|
244
|
-
// border: `${scale}px solid #555555`,
|
|
245
244
|
flexShrink: 0,
|
|
246
245
|
}}
|
|
247
246
|
>
|
|
@@ -284,7 +283,7 @@ export function JEI({
|
|
|
284
283
|
>
|
|
285
284
|
◀
|
|
286
285
|
</button>
|
|
287
|
-
<span className="mc-inv-jei-page-counter" style={{ flex: 1, textAlign: 'center', color: '#
|
|
286
|
+
<span className="mc-inv-jei-page-counter" style={{ flex: 1, textAlign: 'center', color: '#ffffff' }}>
|
|
288
287
|
{page + 1} / {Math.max(1, totalPages)}
|
|
289
288
|
</span>
|
|
290
289
|
<button
|
|
@@ -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) => {
|
|
@@ -16,6 +16,7 @@ const CODE_COLORS: Record<string, string> = {
|
|
|
16
16
|
function parseSectionCodes(text: string): MessageFormatPart[] {
|
|
17
17
|
const parts: MessageFormatPart[] = []
|
|
18
18
|
const regex = /§([0-9a-fk-orA-FK-OR])|([^§]+)/g
|
|
19
|
+
regex.lastIndex = 0
|
|
19
20
|
let color: string | undefined
|
|
20
21
|
let bold = false, italic = false, underlined = false
|
|
21
22
|
let strikethrough = false, obfuscated = false
|
|
@@ -85,8 +85,7 @@ export function Tooltip({ item, visible }: TooltipProps) {
|
|
|
85
85
|
fontSize: fs,
|
|
86
86
|
padding: pad,
|
|
87
87
|
gap: gap2,
|
|
88
|
-
|
|
89
|
-
maxWidth: Math.round(220 * scale),
|
|
88
|
+
width: 'max-content',
|
|
90
89
|
// Start invisible; applyPosition sets visibility after measuring dimensions
|
|
91
90
|
visibility: 'hidden',
|
|
92
91
|
pointerEvents: 'none',
|
|
@@ -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
|
|
|
@@ -10,21 +11,47 @@ export interface MineflayerConnectorOptions {
|
|
|
10
11
|
/**
|
|
11
12
|
* Custom item mapper called for every slot conversion from raw mineflayer data to
|
|
12
13
|
* {@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.
|
|
14
|
+
* Return a modified stack to override fields (e.g. `name`, `textureKey`, `displayName`,
|
|
15
|
+
* `texture`, `blockTexture`), or return the second argument unchanged to use the default mapping.
|
|
15
16
|
*
|
|
16
17
|
* @example
|
|
17
18
|
* ```ts
|
|
18
19
|
* createMineflayerConnector(bot, {
|
|
19
20
|
* itemMapper: (raw, mapped) => ({
|
|
20
21
|
* ...mapped,
|
|
21
|
-
* // Override texture for specific numeric type IDs:
|
|
22
22
|
* textureKey: raw.type === 438 ? 'item/potion_water' : mapped.textureKey,
|
|
23
23
|
* }),
|
|
24
24
|
* })
|
|
25
25
|
* ```
|
|
26
|
+
*
|
|
27
|
+
* @example Block texture with isometric face slices
|
|
28
|
+
* ```ts
|
|
29
|
+
* itemMapper: (raw, mapped) => ({
|
|
30
|
+
* ...mapped,
|
|
31
|
+
* blockTexture: {
|
|
32
|
+
* source: blockAtlasUrl,
|
|
33
|
+
* top: { slice: [0, 0, 16, 16] },
|
|
34
|
+
* left: { slice: [16, 0, 16, 16] },
|
|
35
|
+
* right: { slice: [32, 0, 16, 16] },
|
|
36
|
+
* },
|
|
37
|
+
* })
|
|
38
|
+
* ```
|
|
26
39
|
*/
|
|
27
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
|
|
28
55
|
}
|
|
29
56
|
|
|
30
57
|
function makeSlotConverter(itemMapper?: MineflayerConnectorOptions['itemMapper']) {
|
|
@@ -70,6 +97,9 @@ interface MineflayerBotExtended extends MineflayerBot {
|
|
|
70
97
|
supportFeature?(name: string): boolean
|
|
71
98
|
_client?: {
|
|
72
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
|
|
73
103
|
writeChannel?(channel: string, data: unknown): void
|
|
74
104
|
registerChannel?(channel: string, schema: unknown): void
|
|
75
105
|
}
|
|
@@ -106,35 +136,148 @@ export function createMineflayerConnector(bot: MineflayerBot, options?: Mineflay
|
|
|
106
136
|
const listeners = new Set<ConnectorListener>()
|
|
107
137
|
const ext = bot as MineflayerBotExtended
|
|
108
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
|
+
}
|
|
109
221
|
|
|
110
222
|
function emit(event: ConnectorEvent) {
|
|
111
223
|
listeners.forEach((l) => l(event))
|
|
112
224
|
}
|
|
113
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
|
+
|
|
114
237
|
/**
|
|
115
238
|
* Builds a window state from the currently open window, OR from `bot.inventory`
|
|
116
239
|
* when no container is open (exposing the player's own inventory as a synthetic
|
|
117
240
|
* 'player' window with windowId = 0).
|
|
118
241
|
*/
|
|
119
242
|
function buildWindowState(): InventoryWindowState | null {
|
|
120
|
-
|
|
243
|
+
// In hotbarOnly mode, always build a player-like state (never the container itself)
|
|
244
|
+
const win = hotbarOnly ? null : bot.currentWindow
|
|
121
245
|
if (win) {
|
|
122
|
-
|
|
246
|
+
const title = formatTitle ? formatTitle(win.title) : win.title
|
|
247
|
+
const state: InventoryWindowState = {
|
|
123
248
|
windowId: win.id,
|
|
124
249
|
type: win.type ?? 'unknown',
|
|
125
|
-
title
|
|
250
|
+
title,
|
|
126
251
|
slots: botSlotsToSlotStates(win.slots, convert),
|
|
127
|
-
heldItem: convert(
|
|
252
|
+
heldItem: convert(win.selectedItem),
|
|
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])
|
|
128
270
|
}
|
|
271
|
+
return convert(bot.inventory.slots[playerSlotIndex])
|
|
129
272
|
}
|
|
130
|
-
|
|
273
|
+
|
|
131
274
|
const invSlots: SlotState[] = []
|
|
132
275
|
// Slots 0–8: crafting/armour — leave empty (not accessible from bot.inventory directly)
|
|
133
276
|
for (let i = 0; i < 9; i++) invSlots.push({ index: i, item: null })
|
|
134
277
|
// Slots 9–35: main inventory
|
|
135
|
-
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) })
|
|
136
279
|
// Slots 36–44: hotbar
|
|
137
|
-
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) })
|
|
138
281
|
// Slot 45: offhand
|
|
139
282
|
invSlots.push({ index: 45, item: convert(bot.inventory.slots[45]) })
|
|
140
283
|
return {
|
|
@@ -142,7 +285,7 @@ export function createMineflayerConnector(bot: MineflayerBot, options?: Mineflay
|
|
|
142
285
|
type: 'player',
|
|
143
286
|
title: undefined,
|
|
144
287
|
slots: invSlots,
|
|
145
|
-
heldItem: convert(bot.
|
|
288
|
+
heldItem: convert((bot.inventory as any).selectedItem ?? null),
|
|
146
289
|
}
|
|
147
290
|
}
|
|
148
291
|
|
|
@@ -166,14 +309,91 @@ export function createMineflayerConnector(bot: MineflayerBot, options?: Mineflay
|
|
|
166
309
|
const onSetSlot = () => {
|
|
167
310
|
const state = buildWindowState()
|
|
168
311
|
if (state) emit({ type: 'windowUpdate', state })
|
|
169
|
-
|
|
170
|
-
|
|
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)
|
|
171
336
|
}
|
|
337
|
+
if (!hotbarOnly) onWindowOpen()
|
|
172
338
|
}
|
|
173
339
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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 })
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
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
|
+
}
|
|
177
397
|
|
|
178
398
|
async function openPlayerInventory() {
|
|
179
399
|
const vehicle = bot.vehicle
|
|
@@ -243,57 +463,123 @@ export function createMineflayerConnector(bot: MineflayerBot, options?: Mineflay
|
|
|
243
463
|
openPlayerInventory,
|
|
244
464
|
|
|
245
465
|
sendAction: async (action: InventoryAction) => {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const win = bot.currentWindow
|
|
253
|
-
|
|
254
|
-
if (action.type === 'trade' && win) {
|
|
255
|
-
if (ext.trade && isVillagerWindow(win)) {
|
|
256
|
-
await ext.trade(win, action.tradeIndex, 1)
|
|
257
|
-
} else if (isVillagerWindow(win)) {
|
|
258
|
-
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
|
|
259
471
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if (
|
|
273
|
-
|
|
274
|
-
|
|
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
|
|
275
514
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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,
|
|
283
523
|
})
|
|
524
|
+
dragStateId++
|
|
525
|
+
return
|
|
284
526
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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)
|
|
297
583
|
}
|
|
298
584
|
},
|
|
299
585
|
|
|
@@ -306,9 +592,29 @@ export function createMineflayerConnector(bot: MineflayerBot, options?: Mineflay
|
|
|
306
592
|
listeners.add(listener)
|
|
307
593
|
return () => {
|
|
308
594
|
listeners.delete(listener)
|
|
309
|
-
bot.off('windowOpen',
|
|
310
|
-
bot.off('windowClose',
|
|
311
|
-
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
|
+
}
|
|
312
618
|
}
|
|
313
619
|
},
|
|
314
620
|
}
|