Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 1 addition & 2 deletions settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
65 changes: 62 additions & 3 deletions src/mindcraft/mindserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><rect width="100%" height="100%" fill="#444"/><text x="50%" y="55%" font-size="12" fill="#bbb" text-anchor="middle">?</text></svg>');
} catch (e) {
res.setHeader('Content-Type', 'image/svg+xml');
res.status(500).send('<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><rect width="100%" height="100%" fill="#444"/><text x="50%" y="55%" font-size="12" fill="#bbb" text-anchor="middle">!</text></svg>');
}
});

// Socket.io connection handling
io.on('connection', (socket) => {
let curAgentName = null;
Expand Down Expand Up @@ -191,18 +250,18 @@ export function createMindServer(host_public = false, port = 8080) {
// wait 2 seconds
setTimeout(() => {
console.log('Exiting MindServer');
process.exit(0);
globalThis.process.exit(0);
}, 2000);

});

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);
}
Expand Down
161 changes: 152 additions & 9 deletions src/mindcraft/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -386,6 +449,64 @@ <h2 id="agentSettingsTitle" style="margin:0;">Agent Settings</h2>
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('<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><rect width="100%" height="100%" fill="#444"/><text x="50%" y="55%" font-size="12" fill="#bbb" text-anchor="middle">?</text></svg>');
}
}
function prettyItemName(name) {
return String(name || '').replace(/_/g, ' ');
}

function iconHTMLForItem(itemName, title, agentName) {
const cands = buildIconUrlCandidates(itemName, agentName);
const icon = cands[0];
return `
<div class="armor-slot" title="${title}">
<img class="inv-img" src="${icon}" data-url-index="0" data-url-list="${cands.join('||')}" onerror="fallbackItemIcon(this)" alt="${title}">
</div>
`;
}
function emptySlotHTML(label) {
return `<div class="armor-slot empty" title="${label}: none"><span class="armor-placeholder">-</span></div>`;
}

const statusEl = document.getElementById('msStatus');
function updateStatus(connected) {
if (!statusEl) return;
Expand Down Expand Up @@ -581,21 +702,43 @@ <h2 id="agentSettingsTitle" style="margin:0;">Agent Settings</h2>
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 = `<div class="armor-grid">${parts.join('')}</div>`;
}
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]) => `<div class="cell">${k}: ${v}</div>`).join('') :
'<div class="cell">(empty)</div>';
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 `
<div class="inv-item" title="${title}">
<img class="inv-img" src="${icon}" data-url-index="0" data-url-list="${iconCandidates.join('||')}" onerror="fallbackItemIcon(this)" alt="${title}">
<div class="inv-count">${v}</div>
<div class="inv-name">${title}</div>
</div>
`;
})
.join('');
} else {
invGrid.classList.add('empty');
invGrid.innerHTML = '<div class="cell">(empty)</div>';
}
}
}
});
Expand Down