dougbot/bridge/lib/mineflayer/lib/bedrock/workstations/cartography.mts
roberts 8f616598fd Fix chat, brain stability, MariaDB reconnect, suppress warnings
- Listen on raw 'text' packet for Bedrock chat (pattern-based chat event
  doesn't fire reliably on Bedrock)
- Brain: add safety reset for stuck pending_status flag
- MariaDB: add retry-on-disconnect for all query methods
- Suppress harmless punycode deprecation warning from Node.js
- Add mineflayer-bedrock lib packages (mineflayer, prismarine-chunk,
  prismarine-registry) for movement support
- Exclude minecraft-data from git (278MB, installed via npm)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 12:33:17 -05:00

206 lines
6.9 KiB
TypeScript

/**
* Cartography Table Workstation - Map operations for Bedrock protocol
*
* Container IDs from packet captures:
* - cartography_input: slot 12 (main map)
* - cartography_additional: slot 13 (paper/empty map/glass pane)
*
* Pattern from captures:
* optional + consume(cartography_input:12) + consume(cartography_additional:13) +
* take(creative_output → cursor)
*
* Operations:
* - Clone map: map + empty_map → 2 identical maps
* - Extend map: map + paper → larger map
* - Lock map: map + glass_pane → locked (non-updating) map
*/
import type { Item } from 'prismarine-item';
import type { Block } from 'prismarine-block';
import type { Window } from 'prismarine-windows';
import type { BedrockBot } from '../../../index.js';
import { actions, getNextItemStackRequestId, getStackId, sendRequest, waitForResponse, ContainerIds, cursor } from '../item-stack-actions.mts';
import { findItemInAllSlots, CraftingSlots } from '../crafting-core.mts';
import { twoStepTransfer } from '../container.mts';
// ============================================================================
// Constants
// ============================================================================
export const CartographySlots = {
INPUT: 12,
ADDITIONAL: 13,
} as const;
// ============================================================================
// Cartography Table Interface
// ============================================================================
export interface CartographyTable {
window: Window;
/** Put map in input slot */
putMap: (itemType: number | string, metadata: number | null) => Promise<void>;
/** Put paper in additional slot (for extending) */
putPaper: () => Promise<void>;
/** Put empty map in additional slot (for cloning) */
putEmptyMap: () => Promise<void>;
/** Put glass pane in additional slot (for locking) */
putGlassPane: () => Promise<void>;
/** Execute the cartography operation (clone/extend/lock) */
craft: () => Promise<void>;
/** Take result from cartography table */
takeResult: () => Promise<Item | null>;
/** Get current map item */
mapItem: () => Item | null;
/** Get current additional item */
additionalItem: () => Item | null;
/** Close the cartography table */
close: () => void;
}
// ============================================================================
// Implementation
// ============================================================================
/**
* Open a cartography table block and return interface for map operations
*/
export async function openCartographyTable(bot: BedrockBot, cartographyBlock: Block): Promise<CartographyTable> {
const window = await bot.openBlock(cartographyBlock);
bot.logger.debug(`Opened cartography table window: ${window?.id}`);
// Track stack IDs of placed items
let mapStackId = 0;
let additionalStackId = 0;
async function putItem(containerId: string, slot: number, itemType: number | string, metadata: number | null): Promise<number> {
const foundItem = findItemInAllSlots(bot, itemType, metadata);
if (!foundItem) {
throw new Error(`Item ${itemType} not found in inventory`);
}
const slotIndex = foundItem.slot;
const stackId = getStackId(foundItem);
const result = await twoStepTransfer(
bot,
{ containerId: ContainerIds.HOTBAR_AND_INVENTORY, slot: slotIndex, stackId },
{ containerId, slot, stackId: 0 },
1
);
if (!result.success) {
throw new Error(`Failed to place ${itemType} in cartography table`);
}
return result.cursorStackId;
}
return {
window,
async putMap(itemType: number | string, metadata: number | null): Promise<void> {
mapStackId = await putItem(ContainerIds.CARTOGRAPHY_INPUT, CartographySlots.INPUT, itemType, metadata);
},
async putPaper(): Promise<void> {
additionalStackId = await putItem(ContainerIds.CARTOGRAPHY_ADDITIONAL, CartographySlots.ADDITIONAL, 'paper', null);
},
async putEmptyMap(): Promise<void> {
additionalStackId = await putItem(ContainerIds.CARTOGRAPHY_ADDITIONAL, CartographySlots.ADDITIONAL, 'empty_map', null);
},
async putGlassPane(): Promise<void> {
additionalStackId = await putItem(ContainerIds.CARTOGRAPHY_ADDITIONAL, CartographySlots.ADDITIONAL, 'glass_pane', null);
},
async craft(): Promise<void> {
// Get current stack IDs from window slots
const mapItem = window.slots[CartographySlots.INPUT];
const addItem = window.slots[CartographySlots.ADDITIONAL];
if (!mapItem || !addItem) {
throw new Error('Map and additional item required in cartography table');
}
const currentMapStackId = getStackId(mapItem) || mapStackId;
const currentAdditionalStackId = getStackId(addItem) || additionalStackId;
// Need to wait a bit for server to register the placed items
await new Promise((r) => setTimeout(r, 200));
const requestId = getNextItemStackRequestId();
// Pattern from packet captures:
// optional + consume(input) + consume(additional) + take(output)
// Note: Cartography uses 'optional' instead of craft_recipe
const actionList: any[] = [
{
type_id: 'optional',
filtered_string_index: 0,
},
{
type_id: 'consume',
count: 1,
source: {
slot_type: { container_id: ContainerIds.CARTOGRAPHY_INPUT },
slot: CartographySlots.INPUT,
stack_id: currentMapStackId,
},
},
{
type_id: 'consume',
count: 1,
source: {
slot_type: { container_id: ContainerIds.CARTOGRAPHY_ADDITIONAL },
slot: CartographySlots.ADDITIONAL,
stack_id: currentAdditionalStackId,
},
},
{
type_id: 'take',
count: 2, // Clone produces 2 maps
source: {
slot_type: { container_id: ContainerIds.CREATIVE_OUTPUT },
slot: CraftingSlots.CREATIVE_OUTPUT_SLOT,
stack_id: requestId,
},
destination: {
slot_type: { container_id: ContainerIds.CURSOR },
slot: 0,
stack_id: 0,
},
},
];
bot.logger.debug(`Cartography craft: mapStackId=${currentMapStackId}, additionalStackId=${currentAdditionalStackId}`);
sendRequest(bot, requestId, actionList);
if (!(await waitForResponse(bot, requestId))) {
throw new Error('Cartography table craft failed');
}
// Put result away from cursor
await bot.putAway(0);
},
async takeResult(): Promise<Item | null> {
// Result is already in cursor after craft, just put away
await bot.putAway(0);
return null;
},
mapItem(): Item | null {
return window.slots[CartographySlots.INPUT] || null;
},
additionalItem(): Item | null {
return window.slots[CartographySlots.ADDITIONAL] || null;
},
close() {
bot.closeWindow(window);
},
};
}