import type { InventoryAction, InventoryWindowState, ItemStack, PlayerState, SlotState } from '../types' type JsonPrimitive = string | number | boolean | null type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue } export interface InventoryDebugLogEntry { id: number time: number source: string event: string sessionId?: string windowId?: number windowType?: string action?: JsonValue stateId?: number slots?: JsonValue heldItem?: JsonValue data?: JsonValue error?: JsonValue } export interface InventoryDebugState { windowState: InventoryWindowState | null playerState: PlayerState | null heldItem: ItemStack | null isDragging?: boolean dragSlots?: number[] } export interface InventoryDebugApi { readonly state: InventoryDebugState | null getStates(): Array<{ sessionId: string; source: string; state: InventoryDebugState | null }> getLogs(): InventoryDebugLogEntry[] clearLogs(): void exportLogs(): { exportedAt: string logs: InventoryDebugLogEntry[] states: JsonValue } } interface ProviderRegistration { sessionId: string source: string getState: () => InventoryDebugState | null } export interface InventoryDebugSession { sessionId: string source: string log(entry: Omit): void dispose(): void } const MAX_LOGS = 1000 const MAX_OBJECT_DEPTH = 4 const MAX_ARRAY_ITEMS = 80 let nextLogId = 1 let nextSessionId = 1 const logs: InventoryDebugLogEntry[] = [] const providers = new Map() let activeSessionId: string | null = null function isObject(value: unknown): value is Record { return typeof value === 'object' && value !== null } function sanitize(value: unknown, depth = 0, seen = new WeakSet()): JsonValue { if (value == null) return null if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return value if (typeof value === 'bigint') return String(value) if (typeof value === 'function' || typeof value === 'symbol' || typeof value === 'undefined') return null if (depth >= MAX_OBJECT_DEPTH) return '[truncated]' if (Array.isArray(value)) { return value.slice(0, MAX_ARRAY_ITEMS).map((item) => sanitize(item, depth + 1, seen)) } if (value instanceof Uint8Array) { return `[Uint8Array:${value.byteLength}]` } if (!isObject(value)) return String(value) if (seen.has(value)) return '[circular]' seen.add(value) const output: Record = {} for (const [key, entryValue] of Object.entries(value)) { if (key === 'nbt') { output.hasNbt = entryValue != null continue } output[key] = sanitize(entryValue, depth + 1, seen) } return output } export function summarizeItem(item: ItemStack | null | undefined): JsonValue { if (!item) return null const summary: Record = { type: item.type, count: item.count, } if (item.metadata !== undefined) summary.metadata = item.metadata if (item.name) summary.name = item.name if (item.displayName) summary.displayName = item.displayName if (item.debugKey) summary.debugKey = item.debugKey if (item.nbt) summary.hasNbt = true if (item.enchantments?.length) summary.enchantments = sanitize(item.enchantments) return summary } export function summarizeSlots(slots: SlotState[] | undefined, slotIndexes?: number[]): JsonValue { if (!slots) return [] const filter = slotIndexes ? new Set(slotIndexes) : null const selected = filter ? slots.filter((slot) => filter.has(slot.index)) : slots.filter((slot) => slot.item) return selected.map((slot) => ({ index: slot.index, item: summarizeItem(slot.item), })) } export function summarizeAction(action: InventoryAction): JsonValue { return sanitize(action) } export function sanitizeDebugValue(value: unknown): JsonValue { return sanitize(value) } export function summarizeWindowState(state: InventoryWindowState | null | undefined, slotIndexes?: number[]): JsonValue { if (!state) return null return { windowId: state.windowId, type: state.type, title: state.title ?? null, heldItem: summarizeItem(state.heldItem), properties: sanitize(state.properties ?? null), slots: summarizeSlots(state.slots, slotIndexes), } } export function getActionSlotIndexes(action: InventoryAction): number[] | undefined { if (action.type === 'click' || action.type === 'drop' || action.type === 'hotbar-swap' || action.type === 'hotbar-select') return [action.slotIndex] if (action.type === 'drag') return [...action.slots] return undefined } function getActiveState(): InventoryDebugState | null { if (activeSessionId && providers.has(activeSessionId)) { return providers.get(activeSessionId)!.getState() } const last = [...providers.values()].at(-1) return last?.getState() ?? null } function getStates(): Array<{ sessionId: string; source: string; state: InventoryDebugState | null }> { return [...providers.values()].map((provider) => ({ sessionId: provider.sessionId, source: provider.source, state: provider.getState(), })) } function installGlobalApi() { const g = globalThis as typeof globalThis & { __mcInv?: InventoryDebugApi } if (g.__mcInv?.getLogs === getLogs) return const api: InventoryDebugApi = { get state() { return getActiveState() }, getStates, getLogs, clearLogs, exportLogs, } g.__mcInv = api } function getLogs(): InventoryDebugLogEntry[] { return logs.map((entry) => ({ ...entry })) } function clearLogs() { logs.length = 0 } function exportLogs() { return { exportedAt: new Date().toISOString(), logs: getLogs(), states: sanitize(getStates()), } } export function logInventoryDebug(entry: Omit): void { const fullEntry: InventoryDebugLogEntry = { ...entry, id: nextLogId++, time: Date.now(), } logs.push(fullEntry) if (logs.length > MAX_LOGS) logs.splice(0, logs.length - MAX_LOGS) installGlobalApi() } export function createInventoryDebugSession( source: string, getState: () => InventoryDebugState | null, ): InventoryDebugSession { installGlobalApi() const sessionId = `${source}-${nextSessionId++}` providers.set(sessionId, { sessionId, source, getState }) activeSessionId = sessionId return { sessionId, source, log(entry) { logInventoryDebug({ ...entry, source, sessionId, }) }, dispose() { providers.delete(sessionId) if (activeSessionId === sessionId) { activeSessionId = [...providers.keys()].at(-1) ?? null } }, } } export function getInventoryDebugApi(): InventoryDebugApi { installGlobalApi() return (globalThis as typeof globalThis & { __mcInv: InventoryDebugApi }).__mcInv }