diff --git a/.gitignore b/.gitignore index 6a50bc2..768cf8f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ node_modules/ package-lock.json dist/ dist_debug/ -ccIDE.wheel \ No newline at end of file +ccIDE.wheel diff --git a/advremote.lua b/advremote.lua deleted file mode 100644 index 54d58fa..0000000 --- a/advremote.lua +++ /dev/null @@ -1,50 +0,0 @@ -local ws = assert(http.websocket("ws://127.0.0.1:5133")) -print("connected to server") - -local id -local isrunning = true - -function exitcheck() - while true do - local event = os.pullEventRaw("terminate") - if event == "terminate" then - print("Exiting...") - isrunning = false - ws.close() - break - end - end -end - -function main() - while isrunning do - print("ready") - local message, error = ws.receive() - if message then - print("Received message:", message) - if message == "ping" then - ws.send("pong") - elseif message == "sendcode" then - local file = io.open("main.lua", "w") - print("waiting for code") - local filedata, error = ws.receive() - file:write(filedata) - file:close() - elseif message == "runcode" then - id = multishell.launch({}, "main.lua") - multishell.setTitle(id, "Code") - multishell.setFocus(id) - elseif message == "exit" then - print("Exiting...") - break - end - - else - print("WebSocket error:", error) - break - end - end -end - -parallel.waitForAny(exitcheck, main) -print("Exited") \ No newline at end of file diff --git a/package.json b/package.json index afe123d..e92219e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ccide", - "version": "1.5", + "version": "1.6", "description": "ComputerCraft mod virtual lua IDE", "main": "index.js", "scripts": { diff --git a/readme.md b/readme.md index a0d1bb3..8df968f 100644 --- a/readme.md +++ b/readme.md @@ -6,34 +6,18 @@ special thank for [ccblockly](https://github.com/Mirka1405/ccblockly) for idea  # Quick Start -1. install nodejs and git -2. git clone cd to this project -3. Install dependency use `npm install .` +1. install nodejs and git. +2. git clone and cd to this project . +3. Install dependency use `npm install .`. 4. To run this IDE use `npm run dev` or if you using windows you can use `run.bat` to run it. -5. Done! ## Install Remote code into computercraft it very simple! to install Remote code. -### Run from URL -for advanced computer/pocket/turtle ``` -wget run https://raw.githubusercontent.com/DPSoftware-Foundation/ccIDE/main/advremote.lua -``` -for non advance computer/pocket/turtle -``` -wget run https://raw.githubusercontent.com/DPSoftware-Foundation/ccIDE/main/remote.lua -``` -### Download and Run -for advanced computer/pocket/turtle -``` -wget https://raw.githubusercontent.com/DPSoftware-Foundation/ccIDE/main/advremote.lua advremote.lua -advremote -``` -for non advanced computer/pocket/turtle -``` -wget https://raw.githubusercontent.com/DPSoftware-Foundation/ccIDE/main/remote.lua remote.lua -remote +wget https://raw.githubusercontent.com/DPSoftware-Foundation/ccIDE/main/startup.lua ``` +And restart the computer. + If error "Domain not permitted" try [this solution](https://github.com/cc-tweaked/CC-Tweaked/discussions/626#discussioncomment-241924). ## official support library, peripheral and module function @@ -150,7 +134,7 @@ https://github.com/user-attachments/assets/195231d4-8fd8-4101-8068-70bc038a5c4f https://github.com/user-attachments/assets/8f114cfa-d87c-47d0-a670-a13dc975ab06 # For adapting in other project -This project is based for every block based IDE from DPSoftware Foundation +This project is for every block based IDE from DPSoftware Foundation. # License This project is licensed under the [GPL v3 License](https://github.com/DPSoftware-Foundation/ccIDE/blob/main/LICENSE). diff --git a/remote.lua b/remote.lua deleted file mode 100644 index d5597ae..0000000 --- a/remote.lua +++ /dev/null @@ -1,48 +0,0 @@ -local ws = assert(http.websocket("ws://127.0.0.1:5133")) -print("connected to server") - -local id -local isrunning = true - -function exitcheck() - while true do - local event = os.pullEventRaw("terminate") - if event == "terminate" then - print("Exiting...") - isrunning = false - ws.close() - break - end - end -end - -function main() - while isrunning do - print("ready") - local message, error = ws.receive() - if message then - print("Received message:", message) - if message == "ping" then - ws.send("pong") - elseif message == "sendcode" then - local file = io.open("main.lua", "w") - print("waiting for code") - local filedata, error = ws.receive() - file:write(filedata) - file:close() - elseif message == "runcode" then - shell.run("main") - elseif message == "exit" then - print("Exiting...") - break - end - - else - print("WebSocket error:", error) - break - end - end -end - -parallel.waitForAny(exitcheck, main) -print("Exited") \ No newline at end of file diff --git a/src/ccRemote.js b/src/ccRemote.js index 79a4ddc..75fb1fc 100644 --- a/src/ccRemote.js +++ b/src/ccRemote.js @@ -7,105 +7,258 @@ class CCRemote { host: ip }); - console.log("Remote server is started"); + this.clients = new Map(); // Map to track client data (numeric IDs) + this.clientIdCounter = 0; // Counter for numeric client IDs - fireNotify("Computer isn't connect", "warning", "https://github.com/DPSoftware-Foundation/ccIDE#install-remote-code-into-computercraft") + console.log("Remote server is started"); + fireNotify( + "Remote server started, waiting for clients to connect...", + "info" + ); this.socket.on('connection', (ws) => { - document.getElementById("navbar-button-computer-disconnect").disabled = false; - document.getElementById("navbar-button-computer-run").disabled = false; + const clientId = this.clientIdCounter++; + this.clients.set(clientId, { ws, isAlive: true, name: `Client-${clientId}`, clientInfo: null }); - fireNotify("Computer connected", "success") + if (this.clients.size >= 0) { + document.getElementById("navbar-button-computer-disconnect").disabled = false; + document.getElementById("navbar-button-computer-run").disabled = false; + } - console.log('WebSocket connection established.'); + console.log(`Client ${clientId} connected.`); + fireNotify(`Client ${clientId} connected`, "success"); + + // Send "info" command to the client immediately upon connection + this.sendCommandToClient(clientId, 'info', true); - // Set up heartbeat - ws.isAlive = true; ws.on('pong', () => { - ws.isAlive = true; + this.clients.get(clientId).isAlive = true; }); ws.on('message', (message) => { - console.log(`Received message => ${message}`); + console.log(`Message from Client ${clientId}: ${message}`); + // Handle info response + try { + const data = JSON.parse(message); + + if (data.OSVersion && typeof data.OSVersion === 'string' && + data.Name && typeof data.Name === 'string' && + typeof data.ID === 'number' && + typeof data.uptime === 'number' && + typeof data.Type === 'number') { + + // Update clientInfo with the received data + this.clients.get(clientId).clientInfo = data; + console.log(`Client ${clientId} info updated:`, data); + } else { + console.log(`Invalid data format from Client ${clientId}:`, data); + } + } catch (error) { + console.error(`Error parsing message from Client ${clientId}:`, error); + } }); ws.on('close', () => { - document.getElementById("navbar-button-computer-disconnect").disabled = true; - document.getElementById("navbar-button-computer-run").disabled = true; + console.log(`Client ${clientId} disconnected.`); + this.clients.delete(clientId); + fireNotify(`Client ${clientId} disconnected`, "warning"); - fireNotify("Computer disconnected", "warning") + if (this.clients.size === 0) { + document.getElementById("navbar-button-computer-disconnect").disabled = true; + document.getElementById("navbar-button-computer-run").disabled = true; + } }); ws.on('error', (error) => { - console.error('Client error:', error); + console.error(`Client ${clientId} error:`, error); + fireNotify(`Error with Client ${clientId}: ${error.message}`, "error"); }); }); // Ping clients every 30 seconds to check if they are alive const interval = setInterval(() => { - this.socket.clients.forEach((ws) => { - if (ws.isAlive === false) { - console.log('Client did not respond to ping, terminating connection.'); - return ws.terminate(); + for (const [clientId, clientData] of this.clients.entries()) { + if (clientData.isAlive === false) { + console.log(`Client ${clientId} did not respond to ping, terminating connection.`); + fireNotify(`Client ${clientId} did not respond to ping, disconnected`, "warning"); + clientData.ws.terminate(); + this.clients.delete(clientId); + continue; } - ws.isAlive = false; - ws.ping(); - }); - }, 1000); + clientData.isAlive = false; + clientData.ws.ping(); + } + }, 30000); this.socket.on('close', () => { clearInterval(interval); console.log('WebSocket server closed.'); + fireNotify("WebSocket server closed.", "info"); }); this.socket.on('error', (error) => { console.error('WebSocket server error:', error); + fireNotify(`WebSocket server error: ${error.message}`, "error"); }); } - isClientConnect() { - for (const client of this.socket.clients) { - if (client.readyState === WebSocket.OPEN) { - return true; + // List all connected clients with their IDs, names, and clientInfo + // List all connected clients with their IDs, names, and clientInfo + async listClients() { + const clientList = []; + + // Create an array of promises for fetching client info + const infoPromises = []; + + // Loop over each client and trigger 'info' command to get the latest information + for (const [clientId, clientData] of this.clients.entries()) { + if (clientData.ws.readyState === WebSocket.OPEN) { + const infoPromise = new Promise((resolve) => { + // Set a timeout to handle cases where no response is received + const timeout = setTimeout(() => { + resolve({ id: clientId, name: clientData.name, info: null }); + }, 5000); // Wait 5 seconds for a response + + // Listen for the response from the client + const messageHandler = (message) => { + try { + const data = JSON.parse(message); + if (data.OSVersion && typeof data.OSVersion === 'string' && + data.Name && typeof data.Name === 'string' && + typeof data.ID === 'number' && + typeof data.uptime === 'number' && + typeof data.Type === 'number') { + + // Resolve with the valid data + resolve({ id: clientId, name: clientData.name, info: data }); + } else { + resolve({ id: clientId, name: clientData.name, info: null }); + } + } catch (error) { + resolve({ id: clientId, name: clientData.name, info: null }); + } + }; + + // Send the "info" command to the client + this.sendCommandToClient(clientId, 'info', true); + + // Attach the handler for this specific client + clientData.ws.on('message', messageHandler); + }); + + // Add the promise to the array + infoPromises.push(infoPromise); } } - return false; + + // Wait for all promises to resolve (all client info to be updated) + const resolvedClientInfo = await Promise.all(infoPromises); + + // Construct the final list of clients with updated information + for (const clientInfo of resolvedClientInfo) { + clientList.push(clientInfo); + } + + return clientList; } - sendCommand(command) { - this.socket.clients.forEach((client) => { - if (client.readyState === WebSocket.OPEN) { - client.send(command); - } - }); + // Check if any client is connected + isClientConnect() { + return this.clients.size > 0; } - sendCode(Code) { - this.socket.clients.forEach((client) => { - if (client.readyState === WebSocket.OPEN) { - client.send("sendcode"); + // Send code to all clients + sendCode(code) { + this.clients.forEach((clientData, clientId) => { + if (clientData.ws.readyState === WebSocket.OPEN) { + clientData.ws.send("sendcode"); setTimeout(() => { - client.send(Code); + clientData.ws.send(code); }, 500); } }); } + // Send code to a specific client + sendCodeToClient(clientId, code) { + const client = this.clients.get(clientId); + if (!client || client.ws.readyState !== WebSocket.OPEN) { + console.error(`Client ${clientId} is not available or not connected.`); + fireNotify(`Client ${clientId} is not connected.`, "error"); + return; + } + + client.ws.send("sendcode"); + setTimeout(() => { + client.ws.send(code); + }, 500); + } + + // Run code on all clients runCode() { - this.socket.clients.forEach((client) => { - if (client.readyState === WebSocket.OPEN) { - client.send("runcode"); + this.clients.forEach((clientData, clientId) => { + if (clientData.ws.readyState === WebSocket.OPEN) { + clientData.ws.send("runcode"); } }); } + // Run code on a specific client + runCodeOnClient(clientId) { + const client = this.clients.get(clientId); + if (!client || client.ws.readyState !== WebSocket.OPEN) { + console.error(`Client ${clientId} is not available or not connected.`); + fireNotify(`Client ${clientId} is not connected.`, "error"); + return; + } + + client.ws.send("runcode"); + } + + // Disconnect all clients disconnectAllClients() { - this.socket.clients.forEach((client) => { - if (client.readyState === WebSocket.OPEN) { - client.close(); + this.clients.forEach((clientData, clientId) => { + if (clientData.ws.readyState === WebSocket.OPEN) { + clientData.ws.send("exit"); + clientData.ws.close(); + fireNotify(`Client ${clientId} disconnected`, "warning"); } }); + this.clients.clear(); + } + + // Send a command to a specific client + sendCommandToClient(clientId, command, expectResponse = false) { + const client = this.clients.get(clientId); + if (!client || client.ws.readyState !== WebSocket.OPEN) { + console.error(`Client ${clientId} is not available or not connected.`); + fireNotify(`Client ${clientId} is not connected.`, "error"); + return; + } + + client.ws.send(command); + + if (expectResponse) { + client.ws.on('message', (response) => { + console.log(`Response from Client ${clientId}: ${response}`); + }); + } + } + + // Send a command to all clients + sendCommandToAll(command, expectResponse = false) { + for (const [clientId, clientData] of this.clients.entries()) { + if (clientData.ws.readyState === WebSocket.OPEN) { + clientData.ws.send(command); + + if (expectResponse) { + clientData.ws.on('message', (response) => { + console.log(`Response from Client ${clientId}: ${response}`); + }); + } + } + } } } diff --git a/src/codegen.js b/src/codegen.js index deef436..0285e99 100644 --- a/src/codegen.js +++ b/src/codegen.js @@ -9,6 +9,16 @@ const circles = document.querySelectorAll(".circle"); let upcurrentActive = 1; let uploadError = false; // Flag to track if there's an error +const defineicon = { + 5: path.join(__dirname, '..', 'assets', 'basic_computer.png'), + 6: path.join(__dirname, '..', 'assets', 'adv_computer.png'), + 7: path.join(__dirname, '..', 'assets', 'command_computer.png'), + 2: path.join(__dirname, '..', 'assets', 'pocket_computer.png'), + 4: path.join(__dirname, '..', 'assets', 'adv_pocket_computer.png'), + 1: path.join(__dirname, '..', 'assets', 'turtle.png'), + 3: path.join(__dirname, '..', 'assets', 'adv_turtle.png') +} + const uploadUpdateProgress = () => { circles.forEach((circle, index) => { if (index < upcurrentActive) { @@ -34,7 +44,6 @@ const uploadUpdateProgress = () => { }; function clientexit() { - ccInstance.sendCommand("exit") ccInstance.disconnectAllClients(); } @@ -42,15 +51,17 @@ function gencodeonly() { return luaGenerator.workspaceToCode(workspace); } +let selectedClientId = null; + async function gencode() { console.log("Starting generate code") document.getElementById('upload-popup').style.display = 'block'; upcurrentActive = 1; uploadError = false; uploadUpdateProgress(); - // compile/convert code - - console.log("Generating code...") + + // Compile/convert code + console.log("Generating code..."); upcurrentActive++; uploadUpdateProgress(); document.getElementById('upload-status').textContent = "Generating code"; @@ -61,37 +72,108 @@ async function gencode() { uploadError = true; uploadUpdateProgress(); document.getElementById('upload-status').textContent = e; - return + return; } upcurrentActive++; uploadUpdateProgress(); + if (ccInstance.isClientConnect()) { - // upload to computercraft with remote - console.log("Uploading code to machine...") - document.getElementById('upload-status').textContent = "Uploading code to machine"; - ccInstance.sendCode(code); - await delay(500) + const clients = await ccInstance.listClients(); + document.getElementById('upload-status').innerHTML = ""; - // execute with remote - console.log("Executing code in machine...") - document.getElementById('upload-status').textContent = "Executing code"; - upcurrentActive++; - uploadUpdateProgress(); - ccInstance.runCode(); + const ClientLists = document.createElement('div'); + ClientLists.classList.add("library-content"); + + clients.forEach(client => { + console.log(`Info:`, client.info); + + const ClientItem = document.createElement('div'); + ClientItem.classList.add('library-item', 'overflow-auto', 'library-container'); + ClientItem.setAttribute('data-clientid', client.id); + + const img = document.createElement('img'); + img.classList.add('libimage'); + img.src = defineicon[client.info.Type]; + ClientItem.appendChild(img); + + const ClientDetails = document.createElement('div'); + ClientDetails.classList.add('library-details'); + + const title = document.createElement('h3'); + title.textContent = client.info.Name; + ClientDetails.appendChild(title); + + const description = document.createElement('p'); + description.innerHTML = `OS: ${client.info.OSVersion} | ID: ${client.info.ID} | Uptime: ${client.info.uptime}s`; + ClientDetails.appendChild(description); + + ClientItem.appendChild(ClientDetails); + + // Add event listener for user selection + ClientItem.addEventListener('click', () => { + selectedClientId = client.id; // Correctly store the client ID + ClientItem.classList.add('selected'); // Optional: Style selected item + }); + + ClientLists.appendChild(ClientItem); + }); + + const title = document.createElement("h3"); + title.textContent = "Upload to machine"; + document.getElementById('upload-status').appendChild(title); + + document.getElementById('upload-status').appendChild(ClientLists); + + // Wait for user selection + const waitForSelection = () => new Promise((resolve, reject) => { + const checkSelection = setInterval(() => { + if (selectedClientId !== null) { + clearInterval(checkSelection); + resolve(selectedClientId); + } + }, 100); // Check every 100ms if a client is selected + }); + + // Wait until the user selects a client + try { + const selectedClientId = await waitForSelection(); + console.log(`Selected client: ${selectedClientId}`); + + document.getElementById('upload-status').innerHTML = ""; + + // Now proceed with uploading code to the selected client + console.log("Uploading code to machine..."); + document.getElementById('upload-status').textContent = "Uploading code to machine"; + ccInstance.sendCodeToClient(selectedClientId, code); + await delay(500); + + // Execute with remote + console.log("Executing code in machine..."); + document.getElementById('upload-status').textContent = "Executing code"; + upcurrentActive++; + uploadUpdateProgress(); + ccInstance.runCodeOnClient(selectedClientId); + } catch (error) { + console.error("Error waiting for selection:", error); + uploadError = true; + uploadUpdateProgress(); + document.getElementById('upload-status').innerHTML = `Error selecting client.`; + } } else { - console.log("Machine is not connected") + console.log("Machine is not connected"); uploadError = true; uploadUpdateProgress(); - document.getElementById('upload-status').innerHTML = `Please Connect Computer to IDE.\nInstruction: Install Remote code into computercraft in github. (Please press SHIFT or CTRL and click)`; - return + document.getElementById('upload-status').innerHTML = `Please Connect Computer to IDE.\nInstruction: Install Remote code into computercraft in github.`; + return; } - // done! - console.log("Run code done!") + // Done! + console.log("Run code done!"); document.getElementById('upload-status').textContent = "Done!"; - await delay(1000) + await delay(1000); document.getElementById('upload-popup').style.animation = 'fadeOut 0.3s ease'; // Apply fade-out animation + selectedClientId = null; setTimeout(function() { document.getElementById('upload-popup').style.display = 'none'; // Hide popup after animation completes document.getElementById('upload-popup').style.animation = ''; // Reset animation property diff --git a/src/index.html b/src/index.html index 3fa3c2b..2a1ef12 100644 --- a/src/index.html +++ b/src/index.html @@ -54,8 +54,8 @@
-