minecraft-inventory 0.1.24 → 0.1.26
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 +6 -6
- package/package.json +1 -1
- package/src/components/InventoryOverlay/InventoryOverlay.tsx +1 -1
- package/src/components/Slot/Slot.tsx +29 -21
- package/src/connector/mineflayer.ts +184 -28
- package/src/context/InventoryContext.tsx +81 -14
- package/src/debug/README.md +69 -0
- package/src/debug/inventoryDebug.ts +240 -0
- package/src/index.tsx +17 -0
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ A flexible, scalable React library for rendering Minecraft inventory GUIs. Desig
|
|
|
9
9
|
- `<img>`-rendered item textures with automatic `items/` → `blocks/` fallback (via PrismarineJS asset mirror by default)
|
|
10
10
|
- Tooltips that follow the cursor, matching the original Minecraft style
|
|
11
11
|
- Full keyboard support: number keys (1-9) to swap hotbar, Q to drop, double-click to collect, scroll wheel to pick/place
|
|
12
|
-
- Mobile support: tap
|
|
12
|
+
- Mobile support: tap-to-focus inventory interactions plus long-press slot actions (pick half / custom amount / drop one / drop all)
|
|
13
13
|
- Optional JEI (item browser) panel on the left or right
|
|
14
14
|
- Bot connector layer — plugs into a mineflayer bot or any custom server connection
|
|
15
15
|
- Demo connector with action logging for local development
|
|
@@ -414,13 +414,13 @@ const myConnector: InventoryConnector = {
|
|
|
414
414
|
|
|
415
415
|
## Mobile Support
|
|
416
416
|
|
|
417
|
-
On touch devices, tapping a slot with no held item opens a context menu
|
|
417
|
+
On touch devices, tapping a slot uses the mobile focus/swap flow. Long-pressing a populated slot with no held item opens a context menu. The menu appears to the side in landscape or below in portrait.
|
|
418
418
|
|
|
419
419
|
Context menu options:
|
|
420
|
-
- **
|
|
421
|
-
- **
|
|
422
|
-
- **
|
|
423
|
-
- **Drop** — drops the stack from the slot
|
|
420
|
+
- **Pick Half** — picks up half and highlights the selected slot
|
|
421
|
+
- **Pick Amount…** — `window.prompt` to enter a custom quantity, remembering the last value entered
|
|
422
|
+
- **Drop One** — drops a single item from the slot (same as `Q`)
|
|
423
|
+
- **Drop All** — drops the full stack from the slot (same as `Ctrl+Q`)
|
|
424
424
|
- **Cancel**
|
|
425
425
|
|
|
426
426
|
When you have a held item, tapping a slot places/transfers it (same as left-click on desktop).
|
package/package.json
CHANGED
|
@@ -54,6 +54,8 @@ export function Slot({
|
|
|
54
54
|
setPKeyActive,
|
|
55
55
|
focusedSlot,
|
|
56
56
|
setFocusedSlot,
|
|
57
|
+
mobilePickAmount,
|
|
58
|
+
setMobilePickAmount,
|
|
57
59
|
dragEndedRef,
|
|
58
60
|
noPlaceholders,
|
|
59
61
|
} = useInventoryContext()
|
|
@@ -331,6 +333,7 @@ export function Slot({
|
|
|
331
333
|
|
|
332
334
|
if (heldItem) {
|
|
333
335
|
// When holding an item, place it (standard behavior, no focus needed)
|
|
336
|
+
if (focusedSlot !== null) setFocusedSlot(null)
|
|
334
337
|
sendAction({ type: 'click', slotIndex: index, button: 'left', mode: 'normal' })
|
|
335
338
|
return
|
|
336
339
|
}
|
|
@@ -351,28 +354,25 @@ export function Slot({
|
|
|
351
354
|
[isMobile, disabled, heldItem, sendAction, index, pKeyActive, setPKeyActive, focusedSlot, setFocusedSlot, onClickOverride, cancelLongPress, mobileMenuOpen],
|
|
352
355
|
)
|
|
353
356
|
|
|
354
|
-
const handleMobilePickAll = useCallback(() => {
|
|
355
|
-
setMobileMenuOpen(false)
|
|
356
|
-
setShowTooltip(false)
|
|
357
|
-
sendAction({ type: 'click', slotIndex: index, button: 'left', mode: 'normal' })
|
|
358
|
-
}, [sendAction, index])
|
|
359
|
-
|
|
360
357
|
const handleMobilePickHalf = useCallback(() => {
|
|
361
358
|
setMobileMenuOpen(false)
|
|
362
359
|
setShowTooltip(false)
|
|
360
|
+
setFocusedSlot(index)
|
|
363
361
|
sendAction({ type: 'click', slotIndex: index, button: 'right', mode: 'normal' })
|
|
364
|
-
}, [sendAction, index])
|
|
362
|
+
}, [sendAction, index, setFocusedSlot])
|
|
365
363
|
|
|
366
364
|
const handleMobilePickCustom = useCallback(() => {
|
|
367
365
|
if (!item) return
|
|
368
|
-
const input = window.prompt(`Pick amount (max ${item.count}):`, String(
|
|
366
|
+
const input = window.prompt(`Pick amount (max ${item.count}):`, String(mobilePickAmount))
|
|
369
367
|
const amount = parseInt(input ?? '', 10)
|
|
370
368
|
if (isNaN(amount) || amount <= 0) return
|
|
369
|
+
setMobilePickAmount(amount)
|
|
371
370
|
setMobileMenuOpen(false)
|
|
372
371
|
setShowTooltip(false)
|
|
372
|
+
setFocusedSlot(index)
|
|
373
373
|
const take = Math.min(amount, item.count)
|
|
374
374
|
if (take >= item.count) {
|
|
375
|
-
//
|
|
375
|
+
// Pick all: just left-click
|
|
376
376
|
sendAction({ type: 'click', slotIndex: index, button: 'left', mode: 'normal' })
|
|
377
377
|
} else {
|
|
378
378
|
// Pick up all, then put back (count - take) items one-by-one via right-click
|
|
@@ -381,13 +381,21 @@ export function Slot({
|
|
|
381
381
|
sendAction({ type: 'click', slotIndex: index, button: 'right', mode: 'normal' })
|
|
382
382
|
}
|
|
383
383
|
}
|
|
384
|
-
}, [item, sendAction, index])
|
|
384
|
+
}, [item, mobilePickAmount, sendAction, index, setFocusedSlot, setMobilePickAmount])
|
|
385
|
+
|
|
386
|
+
const handleMobileDropOne = useCallback(() => {
|
|
387
|
+
setMobileMenuOpen(false)
|
|
388
|
+
setShowTooltip(false)
|
|
389
|
+
setFocusedSlot(null)
|
|
390
|
+
sendAction({ type: 'drop', slotIndex: index, all: false })
|
|
391
|
+
}, [sendAction, index, setFocusedSlot])
|
|
385
392
|
|
|
386
|
-
const
|
|
393
|
+
const handleMobileDropAll = useCallback(() => {
|
|
387
394
|
setMobileMenuOpen(false)
|
|
388
395
|
setShowTooltip(false)
|
|
396
|
+
setFocusedSlot(null)
|
|
389
397
|
sendAction({ type: 'drop', slotIndex: index, all: true })
|
|
390
|
-
}, [sendAction, index])
|
|
398
|
+
}, [sendAction, index, setFocusedSlot])
|
|
391
399
|
|
|
392
400
|
const closeMobileMenu = useCallback(() => {
|
|
393
401
|
setMobileMenuOpen(false)
|
|
@@ -532,10 +540,10 @@ export function Slot({
|
|
|
532
540
|
item={item}
|
|
533
541
|
x={mobileTouchPos.x}
|
|
534
542
|
y={mobileTouchPos.y}
|
|
535
|
-
onPickAll={handleMobilePickAll}
|
|
536
543
|
onPickHalf={handleMobilePickHalf}
|
|
537
544
|
onPickCustom={handleMobilePickCustom}
|
|
538
|
-
|
|
545
|
+
onDropOne={handleMobileDropOne}
|
|
546
|
+
onDropAll={handleMobileDropAll}
|
|
539
547
|
onClose={closeMobileMenu}
|
|
540
548
|
/>
|
|
541
549
|
</>
|
|
@@ -548,14 +556,14 @@ interface MobileSlotMenuProps {
|
|
|
548
556
|
item: ItemStack
|
|
549
557
|
x: number
|
|
550
558
|
y: number
|
|
551
|
-
onPickAll(): void
|
|
552
559
|
onPickHalf(): void
|
|
553
560
|
onPickCustom(): void
|
|
554
|
-
|
|
561
|
+
onDropOne(): void
|
|
562
|
+
onDropAll(): void
|
|
555
563
|
onClose(): void
|
|
556
564
|
}
|
|
557
565
|
|
|
558
|
-
function MobileSlotMenu({ item, x, y,
|
|
566
|
+
function MobileSlotMenu({ item, x, y, onPickHalf, onPickCustom, onDropOne, onDropAll, onClose }: MobileSlotMenuProps) {
|
|
559
567
|
const { scale } = useScale()
|
|
560
568
|
const menuRef = useRef<HTMLDivElement>(null)
|
|
561
569
|
const [pos, setPos] = useState({ left: x, top: y })
|
|
@@ -600,10 +608,10 @@ function MobileSlotMenu({ item, x, y, onPickAll, onPickHalf, onPickCustom, onDro
|
|
|
600
608
|
<div className={styles.mobileMenuTitle}>
|
|
601
609
|
{item.displayName ?? item.name ?? `Item #${item.type}`} ×{item.count}
|
|
602
610
|
</div>
|
|
603
|
-
<button className={styles.mobileBtn} {...touchBtn(
|
|
604
|
-
<button className={styles.mobileBtn} {...touchBtn(
|
|
605
|
-
<button className={styles.mobileBtn} {...touchBtn(
|
|
606
|
-
<button className={[styles.mobileBtn, styles.mobileBtnDanger].join(' ')} {...touchBtn(
|
|
611
|
+
<button className={styles.mobileBtn} {...touchBtn(onPickHalf)}>Pick Half ({Math.ceil(item.count / 2)})</button>
|
|
612
|
+
<button className={styles.mobileBtn} {...touchBtn(onPickCustom)}>Pick Amount…</button>
|
|
613
|
+
<button className={[styles.mobileBtn, styles.mobileBtnDanger].join(' ')} {...touchBtn(onDropOne)}>Drop One</button>
|
|
614
|
+
<button className={[styles.mobileBtn, styles.mobileBtnDanger].join(' ')} {...touchBtn(onDropAll)}>Drop All</button>
|
|
607
615
|
<button className={styles.mobileBtn} {...touchBtn(onClose)}>Cancel</button>
|
|
608
616
|
</div>
|
|
609
617
|
)
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import type { InventoryAction, InventoryWindowState, PlayerState, SlotState, ItemStack } from '../types'
|
|
2
2
|
import type { InventoryConnector, ConnectorListener, ConnectorEvent, MineflayerBot } from './types'
|
|
3
3
|
import { getInventoryType } from '../registry'
|
|
4
|
+
import {
|
|
5
|
+
createInventoryDebugSession,
|
|
6
|
+
getActionSlotIndexes,
|
|
7
|
+
sanitizeDebugValue,
|
|
8
|
+
summarizeAction,
|
|
9
|
+
summarizeItem,
|
|
10
|
+
summarizeSlots,
|
|
11
|
+
summarizeWindowState,
|
|
12
|
+
} from '../debug/inventoryDebug'
|
|
4
13
|
|
|
5
14
|
type RawSlot = { type: number; count: number; metadata?: number; nbt?: unknown }
|
|
6
15
|
|
|
@@ -170,6 +179,24 @@ export function createMineflayerConnector(bot: MineflayerBot, options?: Mineflay
|
|
|
170
179
|
// stale server responses overwriting our predicted stateId mid-sequence.
|
|
171
180
|
let isDraggingRaw = false
|
|
172
181
|
|
|
182
|
+
const debugSession = createInventoryDebugSession(
|
|
183
|
+
hotbarOnly ? 'mineflayer-connector:hotbar' : 'mineflayer-connector',
|
|
184
|
+
() => {
|
|
185
|
+
const state = buildWindowState()
|
|
186
|
+
return {
|
|
187
|
+
windowState: state,
|
|
188
|
+
playerState: buildPlayerState(),
|
|
189
|
+
heldItem: state?.heldItem ?? null,
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
)
|
|
193
|
+
debugSession.log({
|
|
194
|
+
event: 'connector.mount',
|
|
195
|
+
data: {
|
|
196
|
+
hotbarOnly,
|
|
197
|
+
},
|
|
198
|
+
})
|
|
199
|
+
|
|
173
200
|
// Resolve the Item class (prismarine-item) for converting notch-format items.
|
|
174
201
|
// We extract it lazily from the first non-null slot item's constructor.
|
|
175
202
|
let ItemClass: { fromNotch(notch: unknown): unknown } | null = null
|
|
@@ -318,6 +345,58 @@ export function createMineflayerConnector(bot: MineflayerBot, options?: Mineflay
|
|
|
318
345
|
}
|
|
319
346
|
}
|
|
320
347
|
|
|
348
|
+
function logActionEvent(
|
|
349
|
+
event: string,
|
|
350
|
+
action: InventoryAction,
|
|
351
|
+
data?: Record<string, unknown>,
|
|
352
|
+
) {
|
|
353
|
+
const state = buildWindowState()
|
|
354
|
+
const slotIndexes = getActionSlotIndexes(action)
|
|
355
|
+
debugSession.log({
|
|
356
|
+
event,
|
|
357
|
+
windowId: state?.windowId,
|
|
358
|
+
windowType: state?.type,
|
|
359
|
+
action: summarizeAction(action),
|
|
360
|
+
stateId: dragStateId,
|
|
361
|
+
slots: state ? summarizeSlots(state.slots, slotIndexes) : undefined,
|
|
362
|
+
heldItem: summarizeItem(state?.heldItem),
|
|
363
|
+
data: data ? sanitizeDebugValue(data) : summarizeWindowState(state, slotIndexes),
|
|
364
|
+
})
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function logPacketWrite(packet: string, params: unknown) {
|
|
368
|
+
const state = buildWindowState()
|
|
369
|
+
const paramsObject = params && typeof params === 'object' ? params as Record<string, unknown> : {}
|
|
370
|
+
debugSession.log({
|
|
371
|
+
event: 'connector.packetWrite',
|
|
372
|
+
windowId: typeof paramsObject.windowId === 'number' ? paramsObject.windowId : state?.windowId,
|
|
373
|
+
windowType: state?.type,
|
|
374
|
+
stateId: typeof paramsObject.stateId === 'number' ? paramsObject.stateId : dragStateId,
|
|
375
|
+
data: {
|
|
376
|
+
packet,
|
|
377
|
+
params: sanitizeDebugValue(params),
|
|
378
|
+
},
|
|
379
|
+
})
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function logHelperIntent(action: InventoryAction, helper: string, params: Record<string, unknown>) {
|
|
383
|
+
const state = buildWindowState()
|
|
384
|
+
const slotIndexes = getActionSlotIndexes(action)
|
|
385
|
+
debugSession.log({
|
|
386
|
+
event: 'connector.helperIntent',
|
|
387
|
+
windowId: state?.windowId,
|
|
388
|
+
windowType: state?.type,
|
|
389
|
+
action: summarizeAction(action),
|
|
390
|
+
stateId: dragStateId,
|
|
391
|
+
slots: state ? summarizeSlots(state.slots, slotIndexes) : undefined,
|
|
392
|
+
heldItem: summarizeItem(state?.heldItem),
|
|
393
|
+
data: {
|
|
394
|
+
helper,
|
|
395
|
+
params: sanitizeDebugValue(params),
|
|
396
|
+
},
|
|
397
|
+
})
|
|
398
|
+
}
|
|
399
|
+
|
|
321
400
|
/** Compute anvil cost client-side if callback is provided and window is anvil. */
|
|
322
401
|
function tryComputeAnvilCost() {
|
|
323
402
|
if (!computeAnvilCost || !currentWindowType) return
|
|
@@ -516,26 +595,40 @@ export function createMineflayerConnector(bot: MineflayerBot, options?: Mineflay
|
|
|
516
595
|
openPlayerInventory,
|
|
517
596
|
|
|
518
597
|
sendAction: async (action: InventoryAction) => {
|
|
598
|
+
logActionEvent('connector.action.start', action)
|
|
519
599
|
try {
|
|
520
600
|
// Hotbar "open inventory" button — delegates to openPlayerInventory()
|
|
521
601
|
if (action.type === 'open-inventory') {
|
|
522
602
|
await openPlayerInventory()
|
|
603
|
+
logActionEvent('connector.action.success', action)
|
|
523
604
|
return
|
|
524
605
|
}
|
|
525
606
|
|
|
526
607
|
const win = bot.currentWindow
|
|
527
608
|
|
|
528
609
|
if (action.type === 'trade' && win) {
|
|
610
|
+
let handled = false
|
|
529
611
|
if (ext.trade && isVillagerWindow(win)) {
|
|
612
|
+
logHelperIntent(action, 'bot.trade', { tradeIndex: action.tradeIndex, count: 1 })
|
|
530
613
|
await ext.trade(win, action.tradeIndex, 1)
|
|
614
|
+
handled = true
|
|
531
615
|
} else if (isVillagerWindow(win)) {
|
|
616
|
+
logHelperIntent(action, 'window.trade', { tradeIndex: action.tradeIndex, count: 1 })
|
|
532
617
|
await win.trade(action.tradeIndex, 1)
|
|
618
|
+
handled = true
|
|
619
|
+
}
|
|
620
|
+
if (handled) {
|
|
621
|
+
logActionEvent('connector.action.success', action)
|
|
622
|
+
} else {
|
|
623
|
+
logActionEvent('connector.action.skipped', action, { reason: 'missing_trade_handler' })
|
|
533
624
|
}
|
|
534
625
|
return
|
|
535
626
|
}
|
|
536
627
|
|
|
537
628
|
if (action.type === 'enchant' && win && isEnchantmentTableWindow(win)) {
|
|
629
|
+
logHelperIntent(action, 'window.enchant', { enchantIndex: action.enchantIndex })
|
|
538
630
|
await win.enchant(action.enchantIndex)
|
|
631
|
+
logActionEvent('connector.action.success', action)
|
|
539
632
|
return
|
|
540
633
|
}
|
|
541
634
|
|
|
@@ -544,23 +637,44 @@ export function createMineflayerConnector(bot: MineflayerBot, options?: Mineflay
|
|
|
544
637
|
// because it tries to transfer items from player inventory into anvil,
|
|
545
638
|
// which fails when the user already placed items via the UI.
|
|
546
639
|
if (ext.supportFeature?.('useMCItemName')) {
|
|
547
|
-
if (!ext._client.registerChannel)
|
|
640
|
+
if (!ext._client.registerChannel) {
|
|
641
|
+
logActionEvent('connector.action.skipped', action, { reason: 'missing_registerChannel' })
|
|
642
|
+
return
|
|
643
|
+
}
|
|
548
644
|
ext._client.registerChannel('MC|ItemName', 'string')
|
|
645
|
+
logPacketWrite('MC|ItemName', action.text)
|
|
549
646
|
ext._client.writeChannel?.('MC|ItemName', action.text)
|
|
550
647
|
} else {
|
|
551
|
-
|
|
648
|
+
const packet = { name: action.text }
|
|
649
|
+
logPacketWrite('name_item', packet)
|
|
650
|
+
ext._client.write('name_item', packet)
|
|
552
651
|
}
|
|
652
|
+
logActionEvent('connector.action.success', action)
|
|
553
653
|
return
|
|
554
654
|
}
|
|
555
655
|
|
|
556
656
|
if (action.type === 'beacon' && win && isBeaconWindow(win)) {
|
|
657
|
+
let handled = false
|
|
557
658
|
if (typeof win.setBeaconEffects === 'function') {
|
|
659
|
+
logHelperIntent(action, 'window.setBeaconEffects', {
|
|
660
|
+
primaryEffect: action.primaryEffect,
|
|
661
|
+
secondaryEffect: action.secondaryEffect,
|
|
662
|
+
})
|
|
558
663
|
await win.setBeaconEffects(action.primaryEffect, action.secondaryEffect)
|
|
664
|
+
handled = true
|
|
559
665
|
} else if (ext._client) {
|
|
560
|
-
|
|
666
|
+
const packet = {
|
|
561
667
|
primaryEffect: action.primaryEffect,
|
|
562
668
|
secondaryEffect: action.secondaryEffect,
|
|
563
|
-
}
|
|
669
|
+
}
|
|
670
|
+
logPacketWrite('beacon_effect', packet)
|
|
671
|
+
ext._client.write('beacon_effect', packet)
|
|
672
|
+
handled = true
|
|
673
|
+
}
|
|
674
|
+
if (handled) {
|
|
675
|
+
logActionEvent('connector.action.success', action)
|
|
676
|
+
} else {
|
|
677
|
+
logActionEvent('connector.action.skipped', action, { reason: 'missing_beacon_handler' })
|
|
564
678
|
}
|
|
565
679
|
return
|
|
566
680
|
}
|
|
@@ -568,10 +682,13 @@ export function createMineflayerConnector(bot: MineflayerBot, options?: Mineflay
|
|
|
568
682
|
if (action.type === 'click' && action.mode === 'double') {
|
|
569
683
|
// bot.clickWindow() throws for mode=6 (prismarine-windows doubleClick is unimplemented).
|
|
570
684
|
// Send raw window_click packet directly, same approach as drag (mode=5).
|
|
571
|
-
if (!ext._client)
|
|
685
|
+
if (!ext._client) {
|
|
686
|
+
logActionEvent('connector.action.skipped', action, { reason: 'missing_client' })
|
|
687
|
+
return
|
|
688
|
+
}
|
|
572
689
|
const windowId = bot.currentWindow ? bot.currentWindow.id : 0
|
|
573
690
|
|
|
574
|
-
|
|
691
|
+
const packet = {
|
|
575
692
|
windowId,
|
|
576
693
|
stateId: dragStateId,
|
|
577
694
|
slot: action.slotIndex,
|
|
@@ -579,19 +696,31 @@ export function createMineflayerConnector(bot: MineflayerBot, options?: Mineflay
|
|
|
579
696
|
mode: 6,
|
|
580
697
|
changedSlots: [],
|
|
581
698
|
cursorItem: { present: false } as any,
|
|
582
|
-
}
|
|
699
|
+
}
|
|
700
|
+
logPacketWrite('window_click', packet)
|
|
701
|
+
ext._client.write('window_click', packet)
|
|
583
702
|
dragStateId++
|
|
703
|
+
logActionEvent('connector.action.success', action)
|
|
584
704
|
return
|
|
585
705
|
}
|
|
586
706
|
|
|
587
707
|
if (action.type === 'click') {
|
|
588
708
|
const [mouseButton, mode] = modeFromAction(action)
|
|
709
|
+
logHelperIntent(action, 'bot.clickWindow', {
|
|
710
|
+
slot: action.slotIndex,
|
|
711
|
+
mouseButton,
|
|
712
|
+
mode,
|
|
713
|
+
})
|
|
589
714
|
await bot.clickWindow(action.slotIndex, mouseButton, mode)
|
|
590
715
|
onSetSlot()
|
|
716
|
+
logActionEvent('connector.action.success', action)
|
|
591
717
|
} else if (action.type === 'drag') {
|
|
592
718
|
// bot.clickWindow() throws for mode=5 (prismarine-windows dragClick is unimplemented).
|
|
593
719
|
// Send raw window_click packets directly via _client.write.
|
|
594
|
-
if (!ext._client)
|
|
720
|
+
if (!ext._client) {
|
|
721
|
+
logActionEvent('connector.action.skipped', action, { reason: 'missing_client' })
|
|
722
|
+
return
|
|
723
|
+
}
|
|
595
724
|
const isRight = action.button === 'right'
|
|
596
725
|
const startButton = isRight ? 4 : 0
|
|
597
726
|
const slotButton = isRight ? 5 : 1
|
|
@@ -600,43 +729,68 @@ export function createMineflayerConnector(bot: MineflayerBot, options?: Mineflay
|
|
|
600
729
|
const cursorItem = { present: false } as any
|
|
601
730
|
|
|
602
731
|
isDraggingRaw = true
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
732
|
+
try {
|
|
733
|
+
const writeClick = (slot: number, mouseButton: number) => {
|
|
734
|
+
const packet = {
|
|
735
|
+
windowId,
|
|
736
|
+
stateId: dragStateId,
|
|
737
|
+
slot,
|
|
738
|
+
mouseButton,
|
|
739
|
+
mode: 5,
|
|
740
|
+
changedSlots: [],
|
|
741
|
+
cursorItem,
|
|
742
|
+
}
|
|
743
|
+
logPacketWrite('window_click', packet)
|
|
744
|
+
ext._client!.write('window_click', packet)
|
|
745
|
+
dragStateId++
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
writeClick(-999, startButton)
|
|
749
|
+
for (const slot of action.slots) {
|
|
750
|
+
writeClick(slot, slotButton)
|
|
751
|
+
}
|
|
752
|
+
writeClick(-999, endButton)
|
|
753
|
+
} finally {
|
|
754
|
+
isDraggingRaw = false
|
|
619
755
|
}
|
|
620
|
-
|
|
621
|
-
isDraggingRaw = false
|
|
756
|
+
logActionEvent('connector.action.success', action)
|
|
622
757
|
} else if (action.type === 'drop') {
|
|
758
|
+
logHelperIntent(action, 'bot.clickWindow', {
|
|
759
|
+
slot: action.slotIndex,
|
|
760
|
+
mouseButton: action.all ? 1 : 0,
|
|
761
|
+
mode: 4,
|
|
762
|
+
})
|
|
623
763
|
await bot.clickWindow(action.slotIndex, action.all ? 1 : 0, 4)
|
|
624
764
|
onSetSlot()
|
|
765
|
+
logActionEvent('connector.action.success', action)
|
|
625
766
|
} else if (action.type === 'close') {
|
|
626
767
|
if (win) {
|
|
768
|
+
logHelperIntent(action, 'bot.closeWindow', { windowId: win.id })
|
|
627
769
|
bot.closeWindow(win)
|
|
628
770
|
} else {
|
|
629
771
|
// Player inventory (synthetic) — send close_window so server drops cursor items
|
|
630
772
|
if (ext._client) {
|
|
631
|
-
|
|
773
|
+
const packet = { windowId: 0 }
|
|
774
|
+
logPacketWrite('close_window', packet)
|
|
775
|
+
ext._client.write('close_window', packet)
|
|
632
776
|
}
|
|
633
777
|
;(bot.inventory as any).selectedItem = null
|
|
634
778
|
}
|
|
779
|
+
logActionEvent('connector.action.success', action)
|
|
635
780
|
} else if (action.type === 'hotbar-swap') {
|
|
781
|
+
logHelperIntent(action, 'bot.clickWindow', {
|
|
782
|
+
slot: action.slotIndex,
|
|
783
|
+
mouseButton: action.hotbarSlot,
|
|
784
|
+
mode: 2,
|
|
785
|
+
})
|
|
636
786
|
await bot.clickWindow(action.slotIndex, action.hotbarSlot, 2)
|
|
637
787
|
onSetSlot()
|
|
788
|
+
logActionEvent('connector.action.success', action)
|
|
789
|
+
} else {
|
|
790
|
+
logActionEvent('connector.action.ignored', action, { reason: 'no_matching_handler' })
|
|
638
791
|
}
|
|
639
792
|
} catch (err) {
|
|
793
|
+
logActionEvent('connector.action.failure', action, { error: err instanceof Error ? err.message : String(err) })
|
|
640
794
|
const detail = 'slotIndex' in action ? ` slot=${(action as any).slotIndex}` : ''
|
|
641
795
|
console.error(`[minecraft-inventory] sendAction "${action.type}"${detail} failed:`, err)
|
|
642
796
|
}
|
|
@@ -674,7 +828,9 @@ export function createMineflayerConnector(bot: MineflayerBot, options?: Mineflay
|
|
|
674
828
|
if (bot.currentWindow) {
|
|
675
829
|
;(bot.currentWindow as any).off('updateSlot', scheduleSlotUpdate)
|
|
676
830
|
}
|
|
831
|
+
debugSession.log({ event: 'connector.unmount' })
|
|
832
|
+
debugSession.dispose()
|
|
677
833
|
}
|
|
678
834
|
},
|
|
679
835
|
}
|
|
680
|
-
}
|
|
836
|
+
}
|
|
@@ -2,6 +2,15 @@ import React, { createContext, useContext, useEffect, useRef, useState, useCallb
|
|
|
2
2
|
import type { InventoryWindowState, PlayerState, ItemStack, SlotState } from '../types'
|
|
3
3
|
import type { InventoryConnector } from '../connector/types'
|
|
4
4
|
import { isItemEqual, getMaxStackSize } from '../utils/isItemEqual'
|
|
5
|
+
import {
|
|
6
|
+
createInventoryDebugSession,
|
|
7
|
+
getActionSlotIndexes,
|
|
8
|
+
summarizeAction,
|
|
9
|
+
summarizeItem,
|
|
10
|
+
summarizeSlots,
|
|
11
|
+
summarizeWindowState,
|
|
12
|
+
type InventoryDebugSession,
|
|
13
|
+
} from '../debug/inventoryDebug'
|
|
5
14
|
|
|
6
15
|
export interface DragPreviewEntry {
|
|
7
16
|
count: number
|
|
@@ -44,6 +53,9 @@ export interface InventoryContextValue {
|
|
|
44
53
|
/** Pending first digit for P-key slot number entry */
|
|
45
54
|
pKeyDigit: string
|
|
46
55
|
setPKeyDigit: (d: string) => void
|
|
56
|
+
/** Last amount entered in the mobile pick-amount prompt */
|
|
57
|
+
mobilePickAmount: number
|
|
58
|
+
setMobilePickAmount: (amount: number) => void
|
|
47
59
|
/** Ref set to true when a drag just ended; cleared on next mouseDown.
|
|
48
60
|
* Used by Slot to suppress spurious click events that fire after endDrag. */
|
|
49
61
|
dragEndedRef: React.MutableRefObject<boolean>
|
|
@@ -90,6 +102,7 @@ export function InventoryProvider({ connector, children, noDragSpread = false, n
|
|
|
90
102
|
const [pKeyActive, setPKeyActive] = useState(false)
|
|
91
103
|
const [focusedSlot, setFocusedSlot] = useState<number | null>(null)
|
|
92
104
|
const [pKeyDigit, setPKeyDigit] = useState('')
|
|
105
|
+
const [mobilePickAmount, setMobilePickAmount] = useState(1)
|
|
93
106
|
|
|
94
107
|
const connectorRef = useRef(connector)
|
|
95
108
|
connectorRef.current = connector
|
|
@@ -105,8 +118,30 @@ export function InventoryProvider({ connector, children, noDragSpread = false, n
|
|
|
105
118
|
// Set to true when endDrag fires; cleared on the next mouseDown in Slot.
|
|
106
119
|
// Prevents spurious mouseUp events from sending unwanted clicks after a drag.
|
|
107
120
|
const dragEndedRef = useRef(false)
|
|
108
|
-
/** Latest full context value;
|
|
121
|
+
/** Latest full context value; used by the DevTools debug API (`globalThis.__mcInv.state`). */
|
|
109
122
|
const valueRef = useRef<InventoryContextValue | null>(null)
|
|
123
|
+
const debugSessionRef = useRef<InventoryDebugSession | null>(null)
|
|
124
|
+
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
const session = createInventoryDebugSession('inventory-provider', () => {
|
|
127
|
+
const value = valueRef.current
|
|
128
|
+
if (!value) return null
|
|
129
|
+
return {
|
|
130
|
+
windowState: value.windowState,
|
|
131
|
+
playerState: value.playerState,
|
|
132
|
+
heldItem: value.heldItem,
|
|
133
|
+
isDragging: value.isDragging,
|
|
134
|
+
dragSlots: [...value.dragSlots],
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
debugSessionRef.current = session
|
|
138
|
+
session.log({ event: 'provider.mount' })
|
|
139
|
+
return () => {
|
|
140
|
+
session.log({ event: 'provider.unmount' })
|
|
141
|
+
session.dispose()
|
|
142
|
+
if (debugSessionRef.current === session) debugSessionRef.current = null
|
|
143
|
+
}
|
|
144
|
+
}, [])
|
|
110
145
|
|
|
111
146
|
useEffect(() => {
|
|
112
147
|
if (!connector) return
|
|
@@ -115,16 +150,35 @@ export function InventoryProvider({ connector, children, noDragSpread = false, n
|
|
|
115
150
|
|
|
116
151
|
return connector.subscribe((event) => {
|
|
117
152
|
if (event.type === 'windowOpen' || event.type === 'windowUpdate') {
|
|
153
|
+
debugSessionRef.current?.log({
|
|
154
|
+
event: `connector.${event.type}`,
|
|
155
|
+
windowId: event.state.windowId,
|
|
156
|
+
windowType: event.state.type,
|
|
157
|
+
slots: summarizeSlots(event.state.slots),
|
|
158
|
+
heldItem: summarizeItem(event.state.heldItem),
|
|
159
|
+
})
|
|
118
160
|
setWindowState({ ...event.state })
|
|
119
161
|
setHeldItemState(event.state.heldItem)
|
|
120
162
|
} else if (event.type === 'windowClose') {
|
|
163
|
+
debugSessionRef.current?.log({ event: 'connector.windowClose' })
|
|
121
164
|
setWindowState(null)
|
|
122
165
|
setHeldItemState(null)
|
|
123
166
|
setIsDragging(false)
|
|
124
167
|
setDragSlots([])
|
|
125
168
|
} else if (event.type === 'playerUpdate') {
|
|
169
|
+
debugSessionRef.current?.log({
|
|
170
|
+
event: 'connector.playerUpdate',
|
|
171
|
+
slots: summarizeSlots(event.state.inventory),
|
|
172
|
+
data: {
|
|
173
|
+
activeHotbarSlot: event.state.activeHotbarSlot,
|
|
174
|
+
},
|
|
175
|
+
})
|
|
126
176
|
setPlayerState(event.state)
|
|
127
177
|
} else if (event.type === 'heldItemChange') {
|
|
178
|
+
debugSessionRef.current?.log({
|
|
179
|
+
event: 'connector.heldItemChange',
|
|
180
|
+
heldItem: summarizeItem(event.item),
|
|
181
|
+
})
|
|
128
182
|
setHeldItemState(event.item)
|
|
129
183
|
}
|
|
130
184
|
})
|
|
@@ -136,6 +190,19 @@ export function InventoryProvider({ connector, children, noDragSpread = false, n
|
|
|
136
190
|
|
|
137
191
|
const sendAction = useCallback<InventoryConnector['sendAction']>(
|
|
138
192
|
(action) => {
|
|
193
|
+
const slotIndexes = getActionSlotIndexes(action)
|
|
194
|
+
const state = valueRef.current?.windowState ?? connectorRef.current?.getWindowState() ?? null
|
|
195
|
+
debugSessionRef.current?.log({
|
|
196
|
+
event: 'ui.action',
|
|
197
|
+
windowId: state?.windowId,
|
|
198
|
+
windowType: state?.type,
|
|
199
|
+
action: summarizeAction(action),
|
|
200
|
+
slots: state ? summarizeSlots(state.slots, slotIndexes) : undefined,
|
|
201
|
+
heldItem: summarizeItem(valueRef.current?.heldItem ?? state?.heldItem),
|
|
202
|
+
data: {
|
|
203
|
+
before: summarizeWindowState(state, slotIndexes),
|
|
204
|
+
},
|
|
205
|
+
})
|
|
139
206
|
return connectorRef.current?.sendAction(action)
|
|
140
207
|
},
|
|
141
208
|
[],
|
|
@@ -210,6 +277,17 @@ export function InventoryProvider({ connector, children, noDragSpread = false, n
|
|
|
210
277
|
|
|
211
278
|
// Only send drag action if multiple slots were involved (single slot = normal click)
|
|
212
279
|
if (slots.length > 1 && button && held) {
|
|
280
|
+
debugSessionRef.current?.log({
|
|
281
|
+
event: 'ui.drag.optimisticUpdate',
|
|
282
|
+
windowId: ws?.windowId,
|
|
283
|
+
windowType: ws?.type,
|
|
284
|
+
action: summarizeAction({ type: 'drag', slots, button }),
|
|
285
|
+
slots: ws ? summarizeSlots(ws.slots, slots) : undefined,
|
|
286
|
+
heldItem: summarizeItem(held),
|
|
287
|
+
data: {
|
|
288
|
+
before: summarizeWindowState(ws, slots),
|
|
289
|
+
},
|
|
290
|
+
})
|
|
213
291
|
connectorRef.current?.sendAction({ type: 'drag', slots, button })
|
|
214
292
|
|
|
215
293
|
// Optimistic client-side update: apply item distribution immediately
|
|
@@ -320,24 +398,13 @@ export function InventoryProvider({ connector, children, noDragSpread = false, n
|
|
|
320
398
|
setFocusedSlot,
|
|
321
399
|
pKeyDigit,
|
|
322
400
|
setPKeyDigit,
|
|
401
|
+
mobilePickAmount,
|
|
402
|
+
setMobilePickAmount,
|
|
323
403
|
dragEndedRef,
|
|
324
404
|
resolveEnchantmentName,
|
|
325
405
|
}
|
|
326
406
|
|
|
327
407
|
valueRef.current = value
|
|
328
408
|
|
|
329
|
-
// Full inventory UI state on global for DevTools: globalThis.__mcInv.state (e.g. windowState, playerState, heldItem, drag state).
|
|
330
|
-
useEffect(() => {
|
|
331
|
-
const g = globalThis as any
|
|
332
|
-
g.__mcInv = {
|
|
333
|
-
get state() {
|
|
334
|
-
return valueRef.current
|
|
335
|
-
},
|
|
336
|
-
}
|
|
337
|
-
return () => {
|
|
338
|
-
delete g.__mcInv
|
|
339
|
-
}
|
|
340
|
-
}, [])
|
|
341
|
-
|
|
342
409
|
return <InventoryContext.Provider value={value}>{children}</InventoryContext.Provider>
|
|
343
410
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Inventory Debug API
|
|
2
|
+
|
|
3
|
+
Глобальный объект `__mcInv` доступен в DevTools сразу после монтирования `InventoryProvider`.
|
|
4
|
+
|
|
5
|
+
## Быстрый старт
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
// Текущее состояние инвентаря
|
|
9
|
+
__mcInv.state
|
|
10
|
+
|
|
11
|
+
// Все сессии (провайдер + коннектор)
|
|
12
|
+
__mcInv.getStates()
|
|
13
|
+
|
|
14
|
+
// Лог событий
|
|
15
|
+
__mcInv.getLogs()
|
|
16
|
+
|
|
17
|
+
// Последние N событий
|
|
18
|
+
__mcInv.getLogs().slice(-20)
|
|
19
|
+
|
|
20
|
+
// Экспорт для сравнения / передачи
|
|
21
|
+
copy(JSON.stringify(__mcInv.exportLogs(), null, 2))
|
|
22
|
+
|
|
23
|
+
// Очистить буфер
|
|
24
|
+
__mcInv.clearLogs()
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## События
|
|
28
|
+
|
|
29
|
+
| Событие | Источник | Когда |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| `connector.mount` | mineflayer-connector | Коннектор создан |
|
|
32
|
+
| `connector.unmount` | mineflayer-connector | Коннектор уничтожен |
|
|
33
|
+
| `provider.mount` | inventory-provider | Провайдер смонтирован |
|
|
34
|
+
| `provider.unmount` | inventory-provider | Провайдер размонтирован |
|
|
35
|
+
| `connector.windowOpen` | inventory-provider | Открыто новое окно |
|
|
36
|
+
| `connector.windowUpdate` | inventory-provider | Слоты обновились от сервера |
|
|
37
|
+
| `connector.windowClose` | inventory-provider | Окно закрыто |
|
|
38
|
+
| `connector.playerUpdate` | inventory-provider | Обновился инвентарь игрока |
|
|
39
|
+
| `connector.heldItemChange` | inventory-provider | Изменился предмет в руке |
|
|
40
|
+
| `ui.action` | inventory-provider | Пользователь инициировал действие |
|
|
41
|
+
| `ui.drag.optimisticUpdate` | inventory-provider | Drag завершён, применён оптимистичный апдейт |
|
|
42
|
+
| `connector.action.start` | mineflayer-connector | Начало отправки пакета |
|
|
43
|
+
| `connector.action.success` | mineflayer-connector | Пакет успешно отправлен |
|
|
44
|
+
| `connector.action.failure` | mineflayer-connector | Ошибка при отправке |
|
|
45
|
+
| `connector.action.skipped` | mineflayer-connector | Действие пропущено (нет нужного хендлера) |
|
|
46
|
+
| `connector.action.ignored` | mineflayer-connector | Неизвестный тип действия |
|
|
47
|
+
| `connector.helperIntent` | mineflayer-connector | Вызов mineflayer-хелпера (bot.clickWindow и др.) |
|
|
48
|
+
| `connector.packetWrite` | mineflayer-connector | Прямая запись пакета через `_client.write` |
|
|
49
|
+
|
|
50
|
+
## Фильтрация логов
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
// Только ошибки
|
|
54
|
+
__mcInv.getLogs().filter(e => e.event === 'connector.action.failure')
|
|
55
|
+
|
|
56
|
+
// Только действия пользователя
|
|
57
|
+
__mcInv.getLogs().filter(e => e.event.startsWith('ui.'))
|
|
58
|
+
|
|
59
|
+
// Конкретное окно
|
|
60
|
+
__mcInv.getLogs().filter(e => e.windowType === 'enchanting_table')
|
|
61
|
+
|
|
62
|
+
// Пакеты конкретного типа
|
|
63
|
+
__mcInv.getLogs().filter(e => e.event === 'connector.packetWrite' && e.data?.packet === 'window_click')
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Ограничения
|
|
67
|
+
|
|
68
|
+
- Буфер хранит последние **1000** записей. При активном геймплее старые события вытесняются — используйте `exportLogs()` своевременно.
|
|
69
|
+
- `__mcInv.state` всегда показывает состояние последней созданной сессии (обычно `inventory-provider`). Для состояния коннектора используйте `getStates()`.
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import type { InventoryAction, InventoryWindowState, ItemStack, PlayerState, SlotState } from '../types'
|
|
2
|
+
|
|
3
|
+
type JsonPrimitive = string | number | boolean | null
|
|
4
|
+
type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue }
|
|
5
|
+
|
|
6
|
+
export interface InventoryDebugLogEntry {
|
|
7
|
+
id: number
|
|
8
|
+
time: number
|
|
9
|
+
source: string
|
|
10
|
+
event: string
|
|
11
|
+
sessionId?: string
|
|
12
|
+
windowId?: number
|
|
13
|
+
windowType?: string
|
|
14
|
+
action?: JsonValue
|
|
15
|
+
stateId?: number
|
|
16
|
+
slots?: JsonValue
|
|
17
|
+
heldItem?: JsonValue
|
|
18
|
+
data?: JsonValue
|
|
19
|
+
error?: JsonValue
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface InventoryDebugState {
|
|
23
|
+
windowState: InventoryWindowState | null
|
|
24
|
+
playerState: PlayerState | null
|
|
25
|
+
heldItem: ItemStack | null
|
|
26
|
+
isDragging?: boolean
|
|
27
|
+
dragSlots?: number[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface InventoryDebugApi {
|
|
31
|
+
readonly state: InventoryDebugState | null
|
|
32
|
+
getStates(): Array<{ sessionId: string; source: string; state: InventoryDebugState | null }>
|
|
33
|
+
getLogs(): InventoryDebugLogEntry[]
|
|
34
|
+
clearLogs(): void
|
|
35
|
+
exportLogs(): {
|
|
36
|
+
exportedAt: string
|
|
37
|
+
logs: InventoryDebugLogEntry[]
|
|
38
|
+
states: JsonValue
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface ProviderRegistration {
|
|
43
|
+
sessionId: string
|
|
44
|
+
source: string
|
|
45
|
+
getState: () => InventoryDebugState | null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface InventoryDebugSession {
|
|
49
|
+
sessionId: string
|
|
50
|
+
source: string
|
|
51
|
+
log(entry: Omit<InventoryDebugLogEntry, 'id' | 'time' | 'source' | 'sessionId'>): void
|
|
52
|
+
dispose(): void
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const MAX_LOGS = 1000
|
|
56
|
+
const MAX_OBJECT_DEPTH = 4
|
|
57
|
+
const MAX_ARRAY_ITEMS = 80
|
|
58
|
+
|
|
59
|
+
let nextLogId = 1
|
|
60
|
+
let nextSessionId = 1
|
|
61
|
+
const logs: InventoryDebugLogEntry[] = []
|
|
62
|
+
const providers = new Map<string, ProviderRegistration>()
|
|
63
|
+
let activeSessionId: string | null = null
|
|
64
|
+
|
|
65
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
66
|
+
return typeof value === 'object' && value !== null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function sanitize(value: unknown, depth = 0, seen = new WeakSet<object>()): JsonValue {
|
|
70
|
+
if (value == null) return null
|
|
71
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return value
|
|
72
|
+
if (typeof value === 'bigint') return String(value)
|
|
73
|
+
if (typeof value === 'function' || typeof value === 'symbol' || typeof value === 'undefined') return null
|
|
74
|
+
if (depth >= MAX_OBJECT_DEPTH) return '[truncated]'
|
|
75
|
+
|
|
76
|
+
if (Array.isArray(value)) {
|
|
77
|
+
return value.slice(0, MAX_ARRAY_ITEMS).map((item) => sanitize(item, depth + 1, seen))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (value instanceof Uint8Array) {
|
|
81
|
+
return `[Uint8Array:${value.byteLength}]`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!isObject(value)) return String(value)
|
|
85
|
+
if (seen.has(value)) return '[circular]'
|
|
86
|
+
seen.add(value)
|
|
87
|
+
|
|
88
|
+
const output: Record<string, JsonValue> = {}
|
|
89
|
+
for (const [key, entryValue] of Object.entries(value)) {
|
|
90
|
+
if (key === 'nbt') {
|
|
91
|
+
output.hasNbt = entryValue != null
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
output[key] = sanitize(entryValue, depth + 1, seen)
|
|
95
|
+
}
|
|
96
|
+
return output
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function summarizeItem(item: ItemStack | null | undefined): JsonValue {
|
|
100
|
+
if (!item) return null
|
|
101
|
+
const summary: Record<string, JsonValue> = {
|
|
102
|
+
type: item.type,
|
|
103
|
+
count: item.count,
|
|
104
|
+
}
|
|
105
|
+
if (item.metadata !== undefined) summary.metadata = item.metadata
|
|
106
|
+
if (item.name) summary.name = item.name
|
|
107
|
+
if (item.displayName) summary.displayName = item.displayName
|
|
108
|
+
if (item.debugKey) summary.debugKey = item.debugKey
|
|
109
|
+
if (item.nbt) summary.hasNbt = true
|
|
110
|
+
if (item.enchantments?.length) summary.enchantments = sanitize(item.enchantments)
|
|
111
|
+
return summary
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function summarizeSlots(slots: SlotState[] | undefined, slotIndexes?: number[]): JsonValue {
|
|
115
|
+
if (!slots) return []
|
|
116
|
+
const filter = slotIndexes ? new Set(slotIndexes) : null
|
|
117
|
+
const selected = filter ? slots.filter((slot) => filter.has(slot.index)) : slots.filter((slot) => slot.item)
|
|
118
|
+
return selected.map((slot) => ({
|
|
119
|
+
index: slot.index,
|
|
120
|
+
item: summarizeItem(slot.item),
|
|
121
|
+
}))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function summarizeAction(action: InventoryAction): JsonValue {
|
|
125
|
+
return sanitize(action)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function sanitizeDebugValue(value: unknown): JsonValue {
|
|
129
|
+
return sanitize(value)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function summarizeWindowState(state: InventoryWindowState | null | undefined, slotIndexes?: number[]): JsonValue {
|
|
133
|
+
if (!state) return null
|
|
134
|
+
return {
|
|
135
|
+
windowId: state.windowId,
|
|
136
|
+
type: state.type,
|
|
137
|
+
title: state.title ?? null,
|
|
138
|
+
heldItem: summarizeItem(state.heldItem),
|
|
139
|
+
properties: sanitize(state.properties ?? null),
|
|
140
|
+
slots: summarizeSlots(state.slots, slotIndexes),
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function getActionSlotIndexes(action: InventoryAction): number[] | undefined {
|
|
145
|
+
if (action.type === 'click' || action.type === 'drop' || action.type === 'hotbar-swap') return [action.slotIndex]
|
|
146
|
+
if (action.type === 'drag') return [...action.slots]
|
|
147
|
+
return undefined
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function getActiveState(): InventoryDebugState | null {
|
|
151
|
+
if (activeSessionId && providers.has(activeSessionId)) {
|
|
152
|
+
return providers.get(activeSessionId)!.getState()
|
|
153
|
+
}
|
|
154
|
+
const last = [...providers.values()].at(-1)
|
|
155
|
+
return last?.getState() ?? null
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function getStates(): Array<{ sessionId: string; source: string; state: InventoryDebugState | null }> {
|
|
159
|
+
return [...providers.values()].map((provider) => ({
|
|
160
|
+
sessionId: provider.sessionId,
|
|
161
|
+
source: provider.source,
|
|
162
|
+
state: provider.getState(),
|
|
163
|
+
}))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function installGlobalApi() {
|
|
167
|
+
const g = globalThis as typeof globalThis & { __mcInv?: InventoryDebugApi }
|
|
168
|
+
if (g.__mcInv?.getLogs === getLogs) return
|
|
169
|
+
|
|
170
|
+
const api: InventoryDebugApi = {
|
|
171
|
+
get state() {
|
|
172
|
+
return getActiveState()
|
|
173
|
+
},
|
|
174
|
+
getStates,
|
|
175
|
+
getLogs,
|
|
176
|
+
clearLogs,
|
|
177
|
+
exportLogs,
|
|
178
|
+
}
|
|
179
|
+
g.__mcInv = api
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function getLogs(): InventoryDebugLogEntry[] {
|
|
183
|
+
return logs.map((entry) => ({ ...entry }))
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function clearLogs() {
|
|
187
|
+
logs.length = 0
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function exportLogs() {
|
|
191
|
+
return {
|
|
192
|
+
exportedAt: new Date().toISOString(),
|
|
193
|
+
logs: getLogs(),
|
|
194
|
+
states: sanitize(getStates()),
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function logInventoryDebug(entry: Omit<InventoryDebugLogEntry, 'id' | 'time'>): void {
|
|
199
|
+
const fullEntry: InventoryDebugLogEntry = {
|
|
200
|
+
...entry,
|
|
201
|
+
id: nextLogId++,
|
|
202
|
+
time: Date.now(),
|
|
203
|
+
}
|
|
204
|
+
logs.push(fullEntry)
|
|
205
|
+
if (logs.length > MAX_LOGS) logs.splice(0, logs.length - MAX_LOGS)
|
|
206
|
+
installGlobalApi()
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function createInventoryDebugSession(
|
|
210
|
+
source: string,
|
|
211
|
+
getState: () => InventoryDebugState | null,
|
|
212
|
+
): InventoryDebugSession {
|
|
213
|
+
installGlobalApi()
|
|
214
|
+
const sessionId = `${source}-${nextSessionId++}`
|
|
215
|
+
providers.set(sessionId, { sessionId, source, getState })
|
|
216
|
+
activeSessionId = sessionId
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
sessionId,
|
|
220
|
+
source,
|
|
221
|
+
log(entry) {
|
|
222
|
+
logInventoryDebug({
|
|
223
|
+
...entry,
|
|
224
|
+
source,
|
|
225
|
+
sessionId,
|
|
226
|
+
})
|
|
227
|
+
},
|
|
228
|
+
dispose() {
|
|
229
|
+
providers.delete(sessionId)
|
|
230
|
+
if (activeSessionId === sessionId) {
|
|
231
|
+
activeSessionId = [...providers.keys()].at(-1) ?? null
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function getInventoryDebugApi(): InventoryDebugApi {
|
|
238
|
+
installGlobalApi()
|
|
239
|
+
return (globalThis as typeof globalThis & { __mcInv: InventoryDebugApi }).__mcInv
|
|
240
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -36,6 +36,23 @@ export { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'
|
|
|
36
36
|
|
|
37
37
|
// Utilities
|
|
38
38
|
export { isItemEqual, getMaxStackSize } from './utils/isItemEqual'
|
|
39
|
+
export {
|
|
40
|
+
getInventoryDebugApi,
|
|
41
|
+
logInventoryDebug,
|
|
42
|
+
createInventoryDebugSession,
|
|
43
|
+
summarizeAction,
|
|
44
|
+
summarizeItem,
|
|
45
|
+
summarizeSlots,
|
|
46
|
+
summarizeWindowState,
|
|
47
|
+
getActionSlotIndexes,
|
|
48
|
+
sanitizeDebugValue,
|
|
49
|
+
} from './debug/inventoryDebug'
|
|
50
|
+
export type {
|
|
51
|
+
InventoryDebugApi,
|
|
52
|
+
InventoryDebugLogEntry,
|
|
53
|
+
InventoryDebugSession,
|
|
54
|
+
InventoryDebugState,
|
|
55
|
+
} from './debug/inventoryDebug'
|
|
39
56
|
|
|
40
57
|
// Connector
|
|
41
58
|
export { createMineflayerConnector } from './connector/mineflayer'
|