diff --git a/package.json b/package.json index 7738bd1c2..9c89e3edb 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "google-translate-api-x": "^10.7.1", "groq-sdk": "^0.15.0", "minecraft-data": "^3.97.0", + "minecraft-assets": "^1.16.0", "mineflayer": "^4.33.0", "mineflayer-armor-manager": "^2.0.1", "mineflayer-auto-eat": "^3.3.6", diff --git a/settings.js b/settings.js index e59457db6..2ea4dee4d 100644 --- a/settings.js +++ b/settings.js @@ -57,7 +57,6 @@ const settings = { "block_place_delay": 0, // delay between placing blocks (ms) if using newAction. helps avoid bot being kicked by anti-cheat mechanisms on servers. "log_all_prompts": false, // log ALL prompts to file - -} +}; export default settings; diff --git a/src/mindcraft/mindserver.js b/src/mindcraft/mindserver.js index 1397553ec..a46485156 100644 --- a/src/mindcraft/mindserver.js +++ b/src/mindcraft/mindserver.js @@ -54,6 +54,65 @@ export function createMindServer(host_public = false, port = 8080) { const __dirname = path.dirname(fileURLToPath(import.meta.url)); app.use(express.static(path.join(__dirname, 'public'))); + // Texture proxy: resolve item/block textures using minecraft-assets with version fallback + app.get('/assets/item/:agent/:name.png', async (req, res) => { + try { + const agentName = req.params.agent; + const rawName = req.params.name; + const itemName = String(rawName).toLowerCase(); + const conn = agent_connections[agentName]; + const preferred = conn?.settings?.minecraft_version; + const candidates = []; + if (preferred && preferred !== 'auto') candidates.push(preferred); + candidates.push('1.21.8'); + + // Lazy import to avoid ESM/CJS conflicts + const mod = await import('minecraft-assets'); + const mcAssetsFactory = mod.default || mod; + + for (const ver of candidates) { + try { + const assets = mcAssetsFactory(ver); + // Prefer items path first, then blocks + const item = assets.items[itemName]; + const block = assets.blocks[itemName]; + const tex = assets.textureContent?.[itemName]?.texture + || (item ? assets.textureContent?.[itemName]?.texture : null) + || (block ? assets.textureContent?.[itemName]?.texture : null); + if (tex) { + // textureContent already provides a data URL in many versions + if (tex.startsWith('data:image')) { + const base64 = tex.split(',')[1]; + const img = globalThis.Buffer.from(base64, 'base64'); + res.setHeader('Content-Type', 'image/png'); + return res.end(img); + } + } + // If textureContent missing, try static path resolution inside package + // Helps with some strange blocks like Leaf Litter + const guessPaths = []; + const base = assets.directory; + guessPaths.push(path.join(base, 'items', `${itemName}.png`)); + guessPaths.push(path.join(base, 'blocks', `${itemName}.png`)); + for (const p of guessPaths) { + try { + const fsMod = await import('fs'); + const buf = fsMod.readFileSync(p); + res.setHeader('Content-Type', 'image/png'); + return res.end(buf); + } catch { /* ignore */ } + } + } catch { /* ignore */ } + } + // Not found, fallback svg + res.setHeader('Content-Type', 'image/svg+xml'); + res.status(404).send('?'); + } catch (e) { + res.setHeader('Content-Type', 'image/svg+xml'); + res.status(500).send('!'); + } + }); + // Socket.io connection handling io.on('connection', (socket) => { let curAgentName = null; @@ -191,7 +250,7 @@ export function createMindServer(host_public = false, port = 8080) { // wait 2 seconds setTimeout(() => { console.log('Exiting MindServer'); - process.exit(0); + globalThis.process.exit(0); }, 2000); }); @@ -199,10 +258,10 @@ export function createMindServer(host_public = false, port = 8080) { socket.on('send-message', (agentName, data) => { if (!agent_connections[agentName]) { console.warn(`Agent ${agentName} not in game, cannot send message via MindServer.`); - return + return; } try { - agent_connections[agentName].socket.emit('send-message', data) + agent_connections[agentName].socket.emit('send-message', data); } catch (error) { console.error('Error: ', error); } diff --git a/src/mindcraft/public/index.html b/src/mindcraft/public/index.html index b6bcdaf9e..c8ce5b800 100644 --- a/src/mindcraft/public/index.html +++ b/src/mindcraft/public/index.html @@ -165,6 +165,69 @@ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 6px; } + /* Inventory item visuals */ + .inventory-grid.empty { + display: grid; + grid-template-columns: 1fr; + color: #999; + } + .inv-item { + position: relative; + display: grid; + grid-template-rows: auto auto; + justify-items: center; + align-items: center; + gap: 4px; + background: #2f2f2f; + border: 1px solid #444; + border-radius: 6px; + padding: 8px; + box-shadow: inset 0 1px 0 rgba(255,255,255,0.03); + } + .inv-img { + width: 32px; + height: 32px; + image-rendering: pixelated; + image-rendering: crisp-edges; + } + .inv-count { + position: absolute; + right: 6px; + bottom: 6px; + background: rgba(0,0,0,0.6); + color: #fff; + font-size: 0.8em; + padding: 2px 6px; + border-radius: 10px; + } + .inv-name { + font-size: 0.85em; + color: #ccc; + text-align: center; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + /* Armor icons */ + .armor-grid { + display: flex; + align-items: center; + gap: 8px; + } + .armor-slot { + position: relative; + width: 36px; + height: 36px; + border-radius: 6px; + background: #2f2f2f; + border: 1px solid #444; + display: flex; + align-items: center; + justify-content: center; + } + .armor-slot.empty { opacity: 0.4; } + .armor-placeholder { color: #888; font-size: 0.9em; } .agent-details-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); @@ -386,6 +449,64 @@

Agent Settings

const inventoryOpen = {}; let currentAgents = []; + // Item texture helpers (use PrismarineJS minecraft-assets CDN with graceful fallbacks) + const BASE_ICON_VERSION_CANDIDATES = ['1.21.8']; + const PATH_CANDIDATES = ['items', 'blocks']; + function buildVersionCandidatesForAgent(agentName) { + const preferred = (agentSettings[agentName] && agentSettings[agentName].minecraft_version && agentSettings[agentName].minecraft_version !== 'auto') + ? [String(agentSettings[agentName].minecraft_version)] + : []; + const merged = [...preferred, ...BASE_ICON_VERSION_CANDIDATES]; + // de-duplicate while preserving order + const seen = new Set(); + const uniq = []; + for (const v of merged) { if (!seen.has(v)) { seen.add(v); uniq.push(v); } } + return uniq; + } + function getItemIconUrl(itemName, vers, pathType) { + return `https://raw.githubusercontent.com/PrismarineJS/minecraft-assets/master/data/${vers}/${pathType}/${itemName}.png`; + } + function buildIconUrlCandidates(itemName, agentName) { + // Prefer local proxy that resolves items vs blocks using minecraft-assets + const proxiedFirst = [`/assets/item/${agentName}/${itemName}.png`]; + const versList = buildVersionCandidatesForAgent(agentName); + const urls = [...proxiedFirst]; + for (const vers of versList) { + for (const p of PATH_CANDIDATES) { + urls.push(getItemIconUrl(itemName, vers, p)); + } + } + return urls; + } + function fallbackItemIcon(imgEl) { + const list = (imgEl.dataset.urlList ? imgEl.dataset.urlList.split('||') : []); + let i = Number(imgEl.dataset.urlIndex || '0'); + if (i < list.length - 1) { + i++; + imgEl.dataset.urlIndex = String(i); + imgEl.src = list[i]; + } else { + imgEl.onerror = null; + imgEl.src = 'data:image/svg+xml;utf8,' + encodeURIComponent('?'); + } + } + function prettyItemName(name) { + return String(name || '').replace(/_/g, ' '); + } + + function iconHTMLForItem(itemName, title, agentName) { + const cands = buildIconUrlCandidates(itemName, agentName); + const icon = cands[0]; + return ` +
+ ${title} +
+ `; + } + function emptySlotHTML(label) { + return `
-
`; + } + const statusEl = document.getElementById('msStatus'); function updateStatus(connected) { if (!statusEl) return; @@ -581,21 +702,43 @@

Agent Settings

const armorEl = document.getElementById(`armor-${name}`); if (armorEl && st.inventory?.equipment) { const e = st.inventory.equipment; - const armor = []; - if (e.helmet) armor.push(`head: ${e.helmet}`); - if (e.chestplate) armor.push(`chest: ${e.chestplate}`); - if (e.leggings) armor.push(`legs: ${e.leggings}`); - if (e.boots) armor.push(`feet: ${e.boots}`); - armorEl.textContent = `armor: ${armor.length ? armor.join(', ') : 'none'}`; + const parts = []; + parts.push(e.helmet ? iconHTMLForItem(e.helmet, `head: ${prettyItemName(e.helmet)}`, name) : emptySlotHTML('head')); + parts.push(e.chestplate ? iconHTMLForItem(e.chestplate, `chest: ${prettyItemName(e.chestplate)}`, name) : emptySlotHTML('chest')); + parts.push(e.leggings ? iconHTMLForItem(e.leggings, `legs: ${prettyItemName(e.leggings)}`, name) : emptySlotHTML('legs')); + parts.push(e.boots ? iconHTMLForItem(e.boots, `feet: ${prettyItemName(e.boots)}`, name) : emptySlotHTML('feet')); + // Main hand for quick glance + parts.push(e.mainHand ? iconHTMLForItem(e.mainHand, `main hand: ${prettyItemName(e.mainHand)}`, name) : emptySlotHTML('main hand')); + armorEl.innerHTML = `
${parts.join('')}
`; } if (actionEl && st.action) { actionEl.textContent = `${st.action.current || 'Idle'}`; } if (invGrid && st.inventory?.counts) { const counts = st.inventory.counts; - invGrid.innerHTML = Object.keys(counts).length ? - Object.entries(counts).map(([k, v]) => `
${k}: ${v}
`).join('') : - '
(empty)
'; + const entries = Object.entries(counts); + if (entries.length) { + invGrid.classList.remove('empty'); + invGrid.innerHTML = entries + .sort((a, b) => a[0].localeCompare(b[0])) + .map(([k, v]) => { + const title = prettyItemName(k); + const safeAttr = k.replace(/'/g, "\\'"); + const iconCandidates = buildIconUrlCandidates(k, name); + const icon = iconCandidates[0]; + return ` +
+ ${title} +
${v}
+
${title}
+
+ `; + }) + .join(''); + } else { + invGrid.classList.add('empty'); + invGrid.innerHTML = '
(empty)
'; + } } } });