mirror of
https://github.com/kolbytn/mindcraft.git
synced 2025-08-27 17:33:02 +02:00
add full listeners for UI, auto open browser
This commit is contained in:
parent
28c2143e73
commit
70c34c79f9
9 changed files with 366 additions and 52 deletions
2
main.js
2
main.js
|
@ -63,7 +63,7 @@ if (process.env.LOG_ALL) {
|
|||
settings.log_all_prompts = process.env.LOG_ALL;
|
||||
}
|
||||
|
||||
Mindcraft.init(false, settings.mindserver_port);
|
||||
Mindcraft.init(false, settings.mindserver_port, settings.auto_open_browser);
|
||||
|
||||
for (let profile of settings.profiles) {
|
||||
const profile_json = JSON.parse(readFileSync(profile, 'utf8'));
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
"mineflayer-pathfinder": "^2.4.5",
|
||||
"mineflayer-pvp": "^1.3.2",
|
||||
"node-canvas-webgl": "PrismarineJS/node-canvas-webgl",
|
||||
"open": "^10.2.0",
|
||||
"openai": "^4.4.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"prismarine-item": "^1.15.0",
|
||||
|
|
|
@ -6,6 +6,7 @@ const settings = {
|
|||
|
||||
// the mindserver manages all agents and hosts the UI
|
||||
"mindserver_port": 8080,
|
||||
"auto_open_browser": true,
|
||||
|
||||
"base_profile": "assistant", // survival, assistant, creative, or god_mode
|
||||
"profiles": [
|
||||
|
|
89
src/agent/library/full_state.js
Normal file
89
src/agent/library/full_state.js
Normal file
|
@ -0,0 +1,89 @@
|
|||
import {
|
||||
getPosition,
|
||||
getBiomeName,
|
||||
getNearbyPlayerNames,
|
||||
getInventoryCounts,
|
||||
getNearbyEntityTypes,
|
||||
getNearbyBlockTypes,
|
||||
getBlockAtPosition,
|
||||
getFirstBlockAboveHead
|
||||
} from "./world.js";
|
||||
import convoManager from '../conversation.js';
|
||||
|
||||
export function getFullState(agent) {
|
||||
const bot = agent.bot;
|
||||
|
||||
const pos = getPosition(bot);
|
||||
const position = {
|
||||
x: Number(pos.x.toFixed(2)),
|
||||
y: Number(pos.y.toFixed(2)),
|
||||
z: Number(pos.z.toFixed(2))
|
||||
};
|
||||
|
||||
let weather = 'Clear';
|
||||
if (bot.thunderState > 0) weather = 'Thunderstorm';
|
||||
else if (bot.rainState > 0) weather = 'Rain';
|
||||
|
||||
let timeLabel = 'Night';
|
||||
if (bot.time.timeOfDay < 6000) timeLabel = 'Morning';
|
||||
else if (bot.time.timeOfDay < 12000) timeLabel = 'Afternoon';
|
||||
|
||||
const below = getBlockAtPosition(bot, 0, -1, 0).name;
|
||||
const legs = getBlockAtPosition(bot, 0, 0, 0).name;
|
||||
const head = getBlockAtPosition(bot, 0, 1, 0).name;
|
||||
|
||||
let players = getNearbyPlayerNames(bot);
|
||||
let bots = convoManager.getInGameAgents().filter(b => b !== agent.name);
|
||||
players = players.filter(p => !bots.includes(p));
|
||||
|
||||
const helmet = bot.inventory.slots[5];
|
||||
const chestplate = bot.inventory.slots[6];
|
||||
const leggings = bot.inventory.slots[7];
|
||||
const boots = bot.inventory.slots[8];
|
||||
|
||||
const state = {
|
||||
name: agent.name,
|
||||
gameplay: {
|
||||
position,
|
||||
dimension: bot.game.dimension,
|
||||
gamemode: bot.game.gameMode,
|
||||
health: Math.round(bot.health),
|
||||
hunger: Math.round(bot.food),
|
||||
biome: getBiomeName(bot),
|
||||
weather,
|
||||
timeOfDay: bot.time.timeOfDay,
|
||||
timeLabel
|
||||
},
|
||||
action: {
|
||||
current: agent.isIdle() ? 'Idle' : agent.actions.currentActionLabel,
|
||||
isIdle: agent.isIdle()
|
||||
},
|
||||
surroundings: {
|
||||
below,
|
||||
legs,
|
||||
head,
|
||||
firstBlockAboveHead: getFirstBlockAboveHead(bot, null, 32)
|
||||
},
|
||||
inventory: {
|
||||
counts: getInventoryCounts(bot),
|
||||
equipment: {
|
||||
helmet: helmet ? helmet.name : null,
|
||||
chestplate: chestplate ? chestplate.name : null,
|
||||
leggings: leggings ? leggings.name : null,
|
||||
boots: boots ? boots.name : null,
|
||||
mainHand: bot.heldItem ? bot.heldItem.name : null
|
||||
}
|
||||
},
|
||||
nearby: {
|
||||
humanPlayers: players,
|
||||
botPlayers: bots,
|
||||
entityTypes: getNearbyEntityTypes(bot).filter(t => t !== 'player' && t !== 'item'),
|
||||
blockTypes: getNearbyBlockTypes(bot)
|
||||
},
|
||||
modes: {
|
||||
summary: bot.modes.getMiniDocs()
|
||||
}
|
||||
};
|
||||
|
||||
return state;
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import { io } from 'socket.io-client';
|
||||
import convoManager from './conversation.js';
|
||||
import { setSettings } from './settings.js';
|
||||
import { getFullState } from './library/full_state.js';
|
||||
|
||||
// agent's individual connection to the mindserver
|
||||
// always connect to localhost
|
||||
|
@ -65,6 +66,16 @@ class MindServerProxy {
|
|||
}
|
||||
});
|
||||
|
||||
this.socket.on('get-full-state', (callback) => {
|
||||
try {
|
||||
const state = getFullState(this.agent);
|
||||
callback(state);
|
||||
} catch (error) {
|
||||
console.error('Error getting full state:', error);
|
||||
callback(null);
|
||||
}
|
||||
});
|
||||
|
||||
// Request settings and wait for response
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { createMindServer, registerAgent } from './mindserver.js';
|
||||
import { createMindServer, registerAgent, numStateListeners } from './mindserver.js';
|
||||
import { AgentProcess } from '../process/agent_process.js';
|
||||
import { getServer } from './mcserver.js';
|
||||
import open from 'open';
|
||||
|
||||
let mindserver;
|
||||
let connected = false;
|
||||
|
@ -8,7 +9,7 @@ let agent_processes = {};
|
|||
let agent_count = 0;
|
||||
let port = 8080;
|
||||
|
||||
export async function init(host_public=false, port=8080) {
|
||||
export async function init(host_public=false, port=8080, auto_open_browser=true) {
|
||||
if (connected) {
|
||||
console.error('Already initiliazed!');
|
||||
return;
|
||||
|
@ -16,6 +17,14 @@ export async function init(host_public=false, port=8080) {
|
|||
mindserver = createMindServer(host_public, port);
|
||||
port = port;
|
||||
connected = true;
|
||||
if (auto_open_browser) {
|
||||
setTimeout(() => {
|
||||
// check if browser listener is already open
|
||||
if (numStateListeners() === 0) {
|
||||
open('http://localhost:'+port);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createAgent(settings) {
|
||||
|
|
|
@ -15,6 +15,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|||
let io;
|
||||
let server;
|
||||
const agent_connections = {};
|
||||
const agent_listeners = [];
|
||||
|
||||
const settings_spec = JSON.parse(readFileSync(path.join(__dirname, 'public/settings_spec.json'), 'utf8'));
|
||||
|
||||
|
@ -23,8 +24,8 @@ class AgentConnection {
|
|||
this.socket = null;
|
||||
this.settings = settings;
|
||||
this.in_game = false;
|
||||
this.full_state = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function registerAgent(settings) {
|
||||
|
@ -114,6 +115,9 @@ export function createMindServer(host_public = false, port = 8080) {
|
|||
agent_connections[curAgentName].in_game = false;
|
||||
agentsUpdate();
|
||||
}
|
||||
if (agent_listeners.includes(socket)) {
|
||||
removeListener(socket);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('chat-message', (agentName, json) => {
|
||||
|
@ -174,6 +178,10 @@ export function createMindServer(host_public = false, port = 8080) {
|
|||
socket.on('bot-output', (agentName, message) => {
|
||||
io.emit('bot-output', agentName, message);
|
||||
});
|
||||
|
||||
socket.on('listen-to-agents', () => {
|
||||
addListener(socket);
|
||||
});
|
||||
});
|
||||
|
||||
let host = host_public ? '0.0.0.0' : 'localhost';
|
||||
|
@ -195,6 +203,42 @@ function agentsUpdate(socket) {
|
|||
socket.emit('agents-update', agents);
|
||||
}
|
||||
|
||||
|
||||
let listenerInterval = null;
|
||||
function addListener(listener_socket) {
|
||||
agent_listeners.push(listener_socket);
|
||||
if (agent_listeners.length === 1) {
|
||||
listenerInterval = setInterval(async () => {
|
||||
const states = {};
|
||||
for (let agentName in agent_connections) {
|
||||
let agent = agent_connections[agentName];
|
||||
if (agent.in_game) {
|
||||
try {
|
||||
const state = await new Promise((resolve) => {
|
||||
agent.socket.emit('get-full-state', (s) => resolve(s));
|
||||
});
|
||||
states[agentName] = state;
|
||||
} catch (e) {
|
||||
states[agentName] = { error: String(e) };
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let listener of agent_listeners) {
|
||||
listener.emit('state-update', states);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function removeListener(listener_socket) {
|
||||
agent_listeners.splice(agent_listeners.indexOf(listener_socket), 1);
|
||||
if (agent_listeners.length === 0) {
|
||||
clearInterval(listenerInterval);
|
||||
listenerInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Optional: export these if you need access to them from other files
|
||||
export const getIO = () => io;
|
||||
export const getServer = () => server;
|
||||
export const numStateListeners = () => agent_listeners.length;
|
|
@ -118,22 +118,115 @@
|
|||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.status-badge {
|
||||
font-size: 0.75em;
|
||||
margin-left: 8px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
background: #3a3a3a;
|
||||
color: #cccccc;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
.status-badge.online { color: #4CAF50; }
|
||||
.status-badge.offline { color: #f44336; }
|
||||
.title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.title-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.title-spacer { flex: 1; }
|
||||
/* Modal styles */
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.modal {
|
||||
background: #2d2d2d;
|
||||
border-radius: 8px;
|
||||
width: 80vw;
|
||||
height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
||||
}
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #3a3a3a;
|
||||
}
|
||||
.modal-close-btn {
|
||||
background: #f44336;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.modal-body {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid #3a3a3a;
|
||||
}
|
||||
.footer-left { color: #cccccc; font-style: italic; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Mindcraft</h1>
|
||||
<div class="title-row">
|
||||
<div class="title-left">
|
||||
<h1 style="margin: 0;">Mindcraft</h1>
|
||||
<span id="msStatus" class="status-badge offline">mindserver offline</span>
|
||||
</div>
|
||||
<div class="title-spacer"></div>
|
||||
</div>
|
||||
<div id="agents"></div>
|
||||
|
||||
<div id="createAgentSection" style="margin-top:20px;background:#2d2d2d;padding:20px;border-radius:8px;">
|
||||
<h2>Create Agent</h2>
|
||||
<div id="settingsForm"></div>
|
||||
<div id="profileStatus" style="margin-top:6px;font-style:italic;color:#cccccc;">Profile: Not uploaded</div>
|
||||
<div style="margin-top:10px;">
|
||||
<button id="uploadProfileBtn" class="start-btn">Upload Profile</button>
|
||||
<input type="file" id="profileFileInput" accept=".json,application/json" style="display:none">
|
||||
<button id="submitCreateAgentBtn" class="start-btn" disabled>Create Agent</button>
|
||||
<div style="margin-top:10px;">
|
||||
<button id="openCreateAgentBtn" class="start-btn">New Agent</button>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div id="createAgentModal" class="modal-backdrop">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 style="margin:0;">Create Agent</h2>
|
||||
<button id="closeCreateAgentBtn" class="modal-close-btn">Close</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="createAgentSection">
|
||||
<div id="profileStatus" style="margin:6px 0;">Profile: Not uploaded</div>
|
||||
<div id="settingsForm"></div>
|
||||
<div id="createError" style="color:#f44336;margin-top:10px;"></div>
|
||||
<input type="file" id="profileFileInput" accept=".json,application/json" style="display:none">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="footer-left" id="footerStatus">Configure settings, then upload a profile and create the agent.</div>
|
||||
<div class="footer-actions">
|
||||
<button id="uploadProfileBtn" class="start-btn">Upload Profile</button>
|
||||
<button id="submitCreateAgentBtn" class="start-btn" disabled>Create Agent</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="createError" style="color:#f44336;margin-top:10px;"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
@ -144,44 +237,82 @@
|
|||
const agentSettings = {};
|
||||
const agentLastMessage = {};
|
||||
|
||||
const statusEl = document.getElementById('msStatus');
|
||||
function updateStatus(connected) {
|
||||
if (!statusEl) return;
|
||||
if (connected) {
|
||||
statusEl.textContent = 'MindServer online';
|
||||
statusEl.classList.remove('offline');
|
||||
statusEl.classList.add('online');
|
||||
} else {
|
||||
statusEl.textContent = 'MindServer offline';
|
||||
statusEl.classList.remove('online');
|
||||
statusEl.classList.add('offline');
|
||||
}
|
||||
}
|
||||
function subscribeToState() {
|
||||
socket.emit('listen-to-agents');
|
||||
}
|
||||
// Initial status
|
||||
updateStatus(false);
|
||||
socket.on('connect', () => {
|
||||
updateStatus(true);
|
||||
subscribeToState();
|
||||
});
|
||||
socket.on('disconnect', () => {
|
||||
updateStatus(false);
|
||||
});
|
||||
socket.on('connect_error', () => {
|
||||
updateStatus(false);
|
||||
});
|
||||
|
||||
fetch('/settings_spec.json')
|
||||
.then(r => r.json())
|
||||
.then(spec => {
|
||||
settingsSpec = spec;
|
||||
const form = document.getElementById('settingsForm');
|
||||
Object.keys(spec).forEach(key => {
|
||||
if (key === 'profile') return; // profile handled via upload
|
||||
const cfg = spec[key];
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'setting-wrapper';
|
||||
const label = document.createElement('label');
|
||||
label.textContent = key;
|
||||
label.title = cfg.description || '';
|
||||
let input;
|
||||
switch (cfg.type) {
|
||||
case 'boolean':
|
||||
input = document.createElement('input');
|
||||
input.type = 'checkbox';
|
||||
input.checked = cfg.default === true;
|
||||
break;
|
||||
case 'number':
|
||||
input = document.createElement('input');
|
||||
input.type = 'number';
|
||||
input.value = cfg.default;
|
||||
break;
|
||||
default:
|
||||
input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.value = typeof cfg.default === 'object' ? JSON.stringify(cfg.default) : cfg.default;
|
||||
}
|
||||
input.title = cfg.description || '';
|
||||
input.id = `setting-${key}`;
|
||||
wrapper.appendChild(label);
|
||||
wrapper.appendChild(input);
|
||||
form.appendChild(wrapper);
|
||||
});
|
||||
buildSettingsForm();
|
||||
});
|
||||
|
||||
function buildSettingsForm() {
|
||||
const form = document.getElementById('settingsForm');
|
||||
form.innerHTML = '';
|
||||
// ensure grid for multi-column layout
|
||||
form.style.display = 'grid';
|
||||
form.style.gridTemplateColumns = 'repeat(auto-fit, minmax(320px, 1fr))';
|
||||
form.style.gap = '8px';
|
||||
Object.keys(settingsSpec).forEach(key => {
|
||||
if (key === 'profile') return; // profile handled via upload
|
||||
const cfg = settingsSpec[key];
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'setting-wrapper';
|
||||
const label = document.createElement('label');
|
||||
label.textContent = key;
|
||||
label.title = cfg.description || '';
|
||||
let input;
|
||||
switch (cfg.type) {
|
||||
case 'boolean':
|
||||
input = document.createElement('input');
|
||||
input.type = 'checkbox';
|
||||
input.checked = cfg.default === true;
|
||||
break;
|
||||
case 'number':
|
||||
input = document.createElement('input');
|
||||
input.type = 'number';
|
||||
input.value = cfg.default;
|
||||
break;
|
||||
default:
|
||||
input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.value = typeof cfg.default === 'object' ? JSON.stringify(cfg.default) : cfg.default;
|
||||
}
|
||||
input.title = cfg.description || '';
|
||||
input.id = `setting-${key}`;
|
||||
wrapper.appendChild(label);
|
||||
wrapper.appendChild(input);
|
||||
form.appendChild(wrapper);
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('uploadProfileBtn').addEventListener('click', () => {
|
||||
document.getElementById('profileFileInput').click();
|
||||
});
|
||||
|
@ -233,10 +364,22 @@
|
|||
document.getElementById('submitCreateAgentBtn').disabled = true;
|
||||
document.getElementById('profileStatus').textContent = 'Profile: Not uploaded';
|
||||
document.getElementById('createError').textContent = '';
|
||||
hideCreateAgentModal();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Modal open/close logic
|
||||
const modalBackdrop = document.getElementById('createAgentModal');
|
||||
document.getElementById('openCreateAgentBtn').addEventListener('click', () => {
|
||||
buildSettingsForm();
|
||||
modalBackdrop.style.display = 'flex';
|
||||
});
|
||||
function hideCreateAgentModal() {
|
||||
modalBackdrop.style.display = 'none';
|
||||
}
|
||||
document.getElementById('closeCreateAgentBtn').addEventListener('click', hideCreateAgentModal);
|
||||
|
||||
socket.on('bot-output', (agentName, message) => {
|
||||
agentLastMessage[agentName] = message;
|
||||
const messageDiv = document.getElementById(`lastMessage-${agentName}`);
|
||||
|
@ -245,6 +388,18 @@
|
|||
}
|
||||
});
|
||||
|
||||
// Subscribe to aggregated state updates (re-sent on each connect)
|
||||
socket.on('state-update', (states) => {
|
||||
Object.keys(states || {}).forEach(name => {
|
||||
const st = states[name];
|
||||
const el = document.getElementById(`health-${name}`);
|
||||
if (el && st && !st.error) {
|
||||
const health = st?.gameplay?.health;
|
||||
if (typeof health === 'number') el.textContent = `Health: ${health}`;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function fetchAgentSettings(name) {
|
||||
return new Promise((resolve) => {
|
||||
if (agentSettings[name]) { resolve(agentSettings[name]); return; }
|
||||
|
@ -282,6 +437,7 @@
|
|||
`}
|
||||
</div>
|
||||
</div>
|
||||
<div id="health-${agent.name}" class="last-message">Health: -</div>
|
||||
<div id="lastMessage-${agent.name}" class="last-message">${lastMessage}</div>
|
||||
${viewerHTML}
|
||||
</div>`;
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
},
|
||||
"minecraft_version": {
|
||||
"type": "string",
|
||||
"description": "The version of Minecraft to use",
|
||||
"default": "1.21.1"
|
||||
"description": "The version of Minecraft to use. Set to 'auto' to automatically detect the version.",
|
||||
"default": "auto"
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
|
@ -16,18 +16,20 @@
|
|||
},
|
||||
"port": {
|
||||
"type": "number",
|
||||
"description": "The minecraft server port to connect to",
|
||||
"description": "The minecraft server port to connect to. -1 for auto-detect.",
|
||||
"default": 55916
|
||||
},
|
||||
"auth": {
|
||||
"type": "string",
|
||||
"description": "The authentication method to use",
|
||||
"default": "offline"
|
||||
"default": "offline",
|
||||
"options": ["offline", "microsoft"]
|
||||
},
|
||||
"base_profile": {
|
||||
"type": "string",
|
||||
"description": "Allowed values: survival, assistant, creative, god_mode. Each has fine tuned settings for different game modes.",
|
||||
"default": "survival"
|
||||
"default": "survival",
|
||||
"options": ["survival", "assistant", "creative", "god_mode"]
|
||||
},
|
||||
"load_memory": {
|
||||
"type": "boolean",
|
||||
|
@ -97,7 +99,8 @@
|
|||
"show_command_syntax": {
|
||||
"type": "string",
|
||||
"description": "Whether to show \"full\" command syntax, \"shortened\" command syntax, or \"none\"",
|
||||
"default": "full"
|
||||
"default": "full",
|
||||
"options": ["full", "shortened", "none"]
|
||||
},
|
||||
"chat_bot_messages": {
|
||||
"type": "boolean",
|
||||
|
|
Loading…
Add table
Reference in a new issue